github.com/keybase/client/go@v0.0.0-20240309051027-028f7c731f8b/chat/flipmanager.go (about)

     1  package chat
     2  
     3  import (
     4  	"bytes"
     5  	"context"
     6  	"encoding/base64"
     7  	"encoding/hex"
     8  	"encoding/json"
     9  	"errors"
    10  	"fmt"
    11  	"image"
    12  	"image/color"
    13  	"image/png"
    14  	"math"
    15  	"math/big"
    16  	"sort"
    17  	"strconv"
    18  	"strings"
    19  	"sync"
    20  	"time"
    21  
    22  	lru "github.com/hashicorp/golang-lru"
    23  	"github.com/keybase/client/go/chat/flip"
    24  	"github.com/keybase/client/go/chat/globals"
    25  	"github.com/keybase/client/go/chat/storage"
    26  	"github.com/keybase/client/go/chat/types"
    27  	"github.com/keybase/client/go/chat/utils"
    28  	"github.com/keybase/client/go/libkb"
    29  	"github.com/keybase/client/go/protocol/chat1"
    30  	"github.com/keybase/client/go/protocol/gregor1"
    31  	"github.com/keybase/client/go/protocol/keybase1"
    32  	"github.com/keybase/clockwork"
    33  )
    34  
    35  type sentMessageResult struct {
    36  	MsgID chat1.MessageID
    37  	Err   error
    38  }
    39  
    40  type sentMessageListener struct {
    41  	globals.Contextified
    42  	libkb.NoopNotifyListener
    43  	utils.DebugLabeler
    44  
    45  	outboxID chat1.OutboxID
    46  	listenCh chan sentMessageResult
    47  }
    48  
    49  type startFlipSendStatus struct {
    50  	status     types.FlipSendStatus
    51  	flipConvID chat1.ConversationID
    52  }
    53  
    54  func newSentMessageListener(g *globals.Context, outboxID chat1.OutboxID) *sentMessageListener {
    55  	return &sentMessageListener{
    56  		Contextified: globals.NewContextified(g),
    57  		DebugLabeler: utils.NewDebugLabeler(g.ExternalG(), "sentMessageListener", false),
    58  		outboxID:     outboxID,
    59  		listenCh:     make(chan sentMessageResult, 10),
    60  	}
    61  }
    62  
    63  func (n *sentMessageListener) NewChatActivity(uid keybase1.UID, activity chat1.ChatActivity,
    64  	source chat1.ChatActivitySource) {
    65  	if source != chat1.ChatActivitySource_LOCAL {
    66  		return
    67  	}
    68  	st, err := activity.ActivityType()
    69  	if err != nil {
    70  		n.Debug(context.Background(), "NewChatActivity: failed to get type: %s", err)
    71  		return
    72  	}
    73  	switch st {
    74  	case chat1.ChatActivityType_INCOMING_MESSAGE:
    75  		msg := activity.IncomingMessage().Message
    76  		if msg.IsOutbox() {
    77  			return
    78  		}
    79  		if n.outboxID.Eq(msg.GetOutboxID()) {
    80  			n.listenCh <- sentMessageResult{
    81  				MsgID: msg.GetMessageID(),
    82  			}
    83  		}
    84  	case chat1.ChatActivityType_FAILED_MESSAGE:
    85  		for _, obr := range activity.FailedMessage().OutboxRecords {
    86  			if obr.OutboxID.Eq(&n.outboxID) {
    87  				n.listenCh <- sentMessageResult{
    88  					Err: errors.New("failed to send message"),
    89  				}
    90  				break
    91  			}
    92  		}
    93  	}
    94  }
    95  
    96  type flipTextMetadata struct {
    97  	LowerBound        string
    98  	ShuffleItems      []string
    99  	DeckShuffle       bool
   100  	HandCardCount     uint
   101  	HandTargets       []string
   102  	ConvMemberShuffle bool
   103  }
   104  
   105  type hostMessageInfo struct {
   106  	flipTextMetadata
   107  	ConvID chat1.ConversationID
   108  	MsgID  chat1.MessageID
   109  }
   110  
   111  type loadGameJob struct {
   112  	uid        gregor1.UID
   113  	hostConvID chat1.ConversationID
   114  	hostMsgID  chat1.MessageID
   115  	gameID     chat1.FlipGameID
   116  	flipConvID chat1.ConversationID
   117  	resCh      chan chat1.UICoinFlipStatus
   118  	errCh      chan error
   119  }
   120  
   121  type convParticipationsRateLimit struct {
   122  	count int
   123  	reset time.Time
   124  }
   125  
   126  type FlipManager struct {
   127  	globals.Contextified
   128  	utils.DebugLabeler
   129  
   130  	dealer           *flip.Dealer
   131  	visualizer       *FlipVisualizer
   132  	clock            clockwork.Clock
   133  	ri               func() chat1.RemoteInterface
   134  	started          bool
   135  	shutdownMu       sync.Mutex
   136  	shutdownCh       chan struct{}
   137  	dealerShutdownCh chan struct{}
   138  	dealerCancel     context.CancelFunc
   139  	forceCh          chan struct{}
   140  	loadGameCh       chan loadGameJob
   141  	maybeInjectCh    chan func()
   142  
   143  	deck           string
   144  	cardMap        map[string]int
   145  	cardReverseMap map[int]string
   146  
   147  	gamesMu        sync.Mutex
   148  	games          *lru.Cache
   149  	dirtyGames     map[chat1.FlipGameIDStr]chat1.FlipGameID
   150  	flipConvs      *lru.Cache
   151  	gameMsgIDs     *lru.Cache
   152  	gameOutboxIDMu sync.Mutex
   153  	gameOutboxIDs  *lru.Cache
   154  
   155  	partMu                     sync.Mutex
   156  	maxConvParticipations      int
   157  	maxConvParticipationsReset time.Duration
   158  	convParticipations         map[chat1.ConvIDStr]convParticipationsRateLimit
   159  
   160  	// testing only
   161  	testingServerClock clockwork.Clock
   162  }
   163  
   164  func NewFlipManager(g *globals.Context, ri func() chat1.RemoteInterface) *FlipManager {
   165  	games, _ := lru.New(200)
   166  	flipConvs, _ := lru.New(200)
   167  	gameMsgIDs, _ := lru.New(200)
   168  	gameOutboxIDs, _ := lru.New(200)
   169  	m := &FlipManager{
   170  		Contextified:               globals.NewContextified(g),
   171  		DebugLabeler:               utils.NewDebugLabeler(g.ExternalG(), "FlipManager", false),
   172  		ri:                         ri,
   173  		clock:                      clockwork.NewRealClock(),
   174  		games:                      games,
   175  		dirtyGames:                 make(map[chat1.FlipGameIDStr]chat1.FlipGameID),
   176  		forceCh:                    make(chan struct{}, 10),
   177  		loadGameCh:                 make(chan loadGameJob, 200),
   178  		convParticipations:         make(map[chat1.ConvIDStr]convParticipationsRateLimit),
   179  		maxConvParticipations:      1000,
   180  		maxConvParticipationsReset: 5 * time.Minute,
   181  		visualizer:                 NewFlipVisualizer(128, 80),
   182  		cardMap:                    make(map[string]int),
   183  		cardReverseMap:             make(map[int]string),
   184  		flipConvs:                  flipConvs,
   185  		gameMsgIDs:                 gameMsgIDs,
   186  		gameOutboxIDs:              gameOutboxIDs,
   187  		maybeInjectCh:              make(chan func(), 2000),
   188  	}
   189  	dealer := flip.NewDealer(m)
   190  	m.dealer = dealer
   191  	m.deck = "2♠️,3♠️,4♠️,5♠️,6♠️,7♠️,8♠️,9♠️,10♠️,J♠️,Q♠️,K♠️,A♠️,2♣️,3♣️,4♣️,5♣️,6♣️,7♣️,8♣️,9♣️,10♣️,J♣️,Q♣️,K♣️,A♣️,2♦️,3♦️,4♦️,5♦️,6♦️,7♦️,8♦️,9♦️,10♦️,J♦️,Q♦️,K♦️,A♦️,2♥️,3♥️,4♥️,5♥️,6♥️,7♥️,8♥️,9♥️,10♥️,J♥️,Q♥️,K♥️,A♥️"
   192  	for index, card := range strings.Split(m.deck, ",") {
   193  		m.cardMap[card] = index
   194  		m.cardReverseMap[index] = card
   195  	}
   196  	return m
   197  }
   198  
   199  func (m *FlipManager) Start(ctx context.Context, uid gregor1.UID) {
   200  	defer m.Trace(ctx, nil, "Start")()
   201  	m.shutdownMu.Lock()
   202  	if m.started {
   203  		m.shutdownMu.Unlock()
   204  		return
   205  	}
   206  	m.started = true
   207  	var dealerCtx context.Context
   208  	shutdownCh := make(chan struct{})
   209  	dealerShutdownCh := make(chan struct{})
   210  	m.shutdownCh = shutdownCh
   211  	m.dealerShutdownCh = dealerShutdownCh
   212  	dealerCtx, m.dealerCancel = context.WithCancel(context.Background())
   213  	m.shutdownMu.Unlock()
   214  
   215  	go func(shutdownCh chan struct{}) {
   216  		_ = m.dealer.Run(dealerCtx)
   217  		close(shutdownCh)
   218  	}(dealerShutdownCh)
   219  	go m.updateLoop(shutdownCh)
   220  	go m.notificationLoop(shutdownCh)
   221  	go m.loadGameLoop(shutdownCh)
   222  	go m.maybeInjectLoop(shutdownCh)
   223  }
   224  
   225  func (m *FlipManager) Stop(ctx context.Context) (ch chan struct{}) {
   226  	defer m.Trace(ctx, nil, "Stop")()
   227  	m.dealer.Stop()
   228  
   229  	m.shutdownMu.Lock()
   230  	defer m.shutdownMu.Unlock()
   231  	m.started = false
   232  	if m.shutdownCh != nil {
   233  		m.dealerCancel()
   234  		close(m.shutdownCh)
   235  		m.shutdownCh = nil
   236  	}
   237  	if m.dealerShutdownCh != nil {
   238  		ch = m.dealerShutdownCh
   239  		m.dealerShutdownCh = nil
   240  	} else {
   241  		ch = make(chan struct{})
   242  		close(ch)
   243  	}
   244  	return ch
   245  }
   246  
   247  func (m *FlipManager) makeBkgContext() context.Context {
   248  	ctx := context.Background()
   249  	return globals.ChatCtx(ctx, m.G(), keybase1.TLFIdentifyBehavior_CHAT_SKIP, nil, nil)
   250  }
   251  
   252  func (m *FlipManager) isHostMessageInfoMsgID(msgID chat1.MessageID) bool {
   253  	// The first message in a flip thread is metadata about the flip, which is
   254  	// message ID 2 since conversations have an initial message from creation.
   255  	return chat1.MessageID(2) == msgID
   256  }
   257  
   258  func (m *FlipManager) startMsgID() chat1.MessageID {
   259  	return chat1.MessageID(3)
   260  }
   261  
   262  func (m *FlipManager) isStartMsgID(msgID chat1.MessageID) bool {
   263  	// The first message after the host message is the flip start message,
   264  	// which will have message ID 3
   265  	return m.startMsgID() == msgID
   266  }
   267  
   268  func (m *FlipManager) getVisualizer() *FlipVisualizer {
   269  	return m.visualizer
   270  }
   271  
   272  func (m *FlipManager) notifyDirtyGames() {
   273  	m.gamesMu.Lock()
   274  	if len(m.dirtyGames) == 0 {
   275  		m.gamesMu.Unlock()
   276  		return
   277  	}
   278  	dirtyGames := m.dirtyGames
   279  	m.dirtyGames = make(map[chat1.FlipGameIDStr]chat1.FlipGameID)
   280  	m.gamesMu.Unlock()
   281  
   282  	ctx := m.makeBkgContext()
   283  	ui, err := m.G().UIRouter.GetChatUI()
   284  	if err != nil || ui == nil {
   285  		m.Debug(ctx, "notifyDirtyGames: no chat UI available for notification")
   286  		return
   287  	}
   288  	var updates []chat1.UICoinFlipStatus
   289  	m.Debug(ctx, "notifyDirtyGames: notifying about %d games", len(dirtyGames))
   290  	for _, dg := range dirtyGames {
   291  		if game, ok := m.games.Get(dg.FlipGameIDStr()); ok {
   292  			status := game.(chat1.UICoinFlipStatus)
   293  			m.getVisualizer().Visualize(&status)
   294  			presentStatus := status.DeepCopy()
   295  			m.sortParticipants(&presentStatus)
   296  			updates = append(updates, presentStatus)
   297  		}
   298  	}
   299  	ctx, cancel := context.WithTimeout(ctx, 5*time.Second)
   300  	defer cancel()
   301  	if err := ui.ChatCoinFlipStatus(ctx, updates); err != nil {
   302  		m.Debug(ctx, "notifyDirtyGames: failed to notify status: %s", err)
   303  	} else {
   304  		m.Debug(ctx, "notifyDirtyGames: UI notified")
   305  	}
   306  }
   307  
   308  func (m *FlipManager) notificationLoop(shutdownCh chan struct{}) {
   309  	duration := 50 * time.Millisecond
   310  	next := m.clock.Now().Add(duration)
   311  	m.Debug(context.Background(), "notificationLoop: starting")
   312  	for {
   313  		select {
   314  		case <-m.clock.AfterTime(next):
   315  			m.notifyDirtyGames()
   316  			next = m.clock.Now().Add(duration)
   317  		case <-m.forceCh:
   318  			m.notifyDirtyGames()
   319  			next = m.clock.Now().Add(duration)
   320  		case <-shutdownCh:
   321  			m.Debug(context.Background(), "notificationLoop: exiting")
   322  			return
   323  		}
   324  	}
   325  }
   326  
   327  func (m *FlipManager) sortParticipants(status *chat1.UICoinFlipStatus) {
   328  	sort.Slice(status.Participants, func(i, j int) bool {
   329  		return status.Participants[i].Username < status.Participants[j].Username
   330  	})
   331  }
   332  
   333  func (m *FlipManager) addParticipant(ctx context.Context, status *chat1.UICoinFlipStatus,
   334  	update flip.CommitmentUpdate) {
   335  	username, deviceName, _, err := m.G().GetUPAKLoader().LookupUsernameAndDevice(ctx,
   336  		keybase1.UID(update.User.U.String()), keybase1.DeviceID(update.User.D.String()))
   337  	if err != nil {
   338  		m.Debug(ctx, "addParticipant: failed to get username/device (using IDs): %s", err)
   339  		username = libkb.NewNormalizedUsername(update.User.U.String())
   340  		deviceName = update.User.D.String()
   341  	}
   342  	status.Participants = append(status.Participants, chat1.UICoinFlipParticipant{
   343  		Uid:        update.User.U.String(),
   344  		DeviceID:   update.User.D.String(),
   345  		Username:   username.String(),
   346  		DeviceName: deviceName,
   347  		Commitment: update.Commitment.String(),
   348  	})
   349  	endingS := ""
   350  	if len(status.Participants) > 1 {
   351  		endingS = "s"
   352  	}
   353  	status.ProgressText = fmt.Sprintf("Gathered %d commitment%s", len(status.Participants), endingS)
   354  }
   355  
   356  func (m *FlipManager) finalizeParticipants(ctx context.Context, status *chat1.UICoinFlipStatus,
   357  	cc flip.CommitmentComplete) {
   358  	completeMap := make(map[string]bool)
   359  	mapKey := func(u, d string) string {
   360  		return u + "," + d
   361  	}
   362  	for _, p := range cc.Players {
   363  		completeMap[mapKey(p.Ud.U.String(), p.Ud.D.String())] = true
   364  	}
   365  	var filteredParts []chat1.UICoinFlipParticipant
   366  	for _, p := range status.Participants {
   367  		if completeMap[mapKey(p.Uid, p.DeviceID)] {
   368  			filteredParts = append(filteredParts, p)
   369  		}
   370  	}
   371  	filteredMap := make(map[string]bool)
   372  	for _, p := range filteredParts {
   373  		filteredMap[mapKey(p.Uid, p.DeviceID)] = true
   374  	}
   375  	status.Participants = filteredParts
   376  	for _, p := range cc.Players {
   377  		if !filteredMap[mapKey(p.Ud.U.String(), p.Ud.D.String())] {
   378  			m.addParticipant(ctx, status, flip.CommitmentUpdate{
   379  				User:       p.Ud,
   380  				Commitment: p.C,
   381  			})
   382  		}
   383  	}
   384  }
   385  
   386  func (m *FlipManager) addReveal(ctx context.Context, status *chat1.UICoinFlipStatus,
   387  	update flip.RevealUpdate) {
   388  	numReveals := 0
   389  	for index, p := range status.Participants {
   390  		if p.Reveal != nil {
   391  			numReveals++
   392  		}
   393  		if p.Uid == update.User.U.String() && p.DeviceID == update.User.D.String() {
   394  			reveal := update.Reveal.String()
   395  			status.Participants[index].Reveal = &reveal
   396  			numReveals++
   397  		}
   398  	}
   399  	status.ProgressText = fmt.Sprintf("%d participants have revealed secrets", numReveals)
   400  }
   401  
   402  func (m *FlipManager) cardIndex(card string) (int, error) {
   403  	if index, ok := m.cardMap[card]; ok {
   404  		return index, nil
   405  	}
   406  	return 0, fmt.Errorf("unknown card: %s", card)
   407  }
   408  
   409  func (m *FlipManager) addCardHandResult(ctx context.Context, status *chat1.UICoinFlipStatus,
   410  	result flip.Result, hmi hostMessageInfo) {
   411  	deckIndex := 0
   412  	numCards := len(result.Shuffle)
   413  	handSize := int(hmi.HandCardCount)
   414  	var uiHandResult []chat1.UICoinFlipHand
   415  	for _, target := range hmi.HandTargets {
   416  		if numCards-handSize < deckIndex {
   417  			uiHandResult = append(uiHandResult, chat1.UICoinFlipHand{
   418  				Target: target,
   419  			})
   420  			continue
   421  		}
   422  		uiHand := chat1.UICoinFlipHand{
   423  			Target: target,
   424  		}
   425  		for di := deckIndex; di < deckIndex+handSize; di++ {
   426  			card := hmi.ShuffleItems[result.Shuffle[di]]
   427  			cardIndex, err := m.cardIndex(card)
   428  			if err != nil {
   429  				m.Debug(ctx, "addCardHandResult: failed to get card: %s", err)
   430  				m.setGenericError(status, "Failed to describe card hand result")
   431  				return
   432  			}
   433  			uiHand.Hand = append(uiHand.Hand, cardIndex)
   434  		}
   435  		uiHandResult = append(uiHandResult, uiHand)
   436  		deckIndex += handSize
   437  	}
   438  	resultInfo := chat1.NewUICoinFlipResultWithHands(uiHandResult)
   439  	status.ResultInfo = &resultInfo
   440  }
   441  
   442  func (m *FlipManager) setGenericError(status *chat1.UICoinFlipStatus, errMsg string) {
   443  	status.Phase = chat1.UICoinFlipPhase_ERROR
   444  	status.ProgressText = errMsg
   445  	errorInfo := chat1.NewUICoinFlipErrorWithGeneric(status.ProgressText)
   446  	status.ErrorInfo = &errorInfo
   447  }
   448  
   449  func (m *FlipManager) resultToText(result chat1.UICoinFlipResult) string {
   450  	typ, err := result.Typ()
   451  	if err != nil {
   452  		return ""
   453  	}
   454  	switch typ {
   455  	case chat1.UICoinFlipResultTyp_COIN:
   456  		if result.Coin() {
   457  			return "HEADS"
   458  		}
   459  		return "TAILS"
   460  	case chat1.UICoinFlipResultTyp_NUMBER:
   461  		return result.Number()
   462  	case chat1.UICoinFlipResultTyp_DECK:
   463  		var cards []string
   464  		for _, cardIndex := range result.Deck() {
   465  			cards = append(cards, m.cardReverseMap[cardIndex])
   466  		}
   467  		return strings.TrimRight(strings.Join(cards, ", "), " ")
   468  	case chat1.UICoinFlipResultTyp_SHUFFLE:
   469  		return strings.TrimRight(strings.Join(result.Shuffle(), ", "), " ")
   470  	case chat1.UICoinFlipResultTyp_HANDS:
   471  		var rows []string
   472  		for index, hand := range result.Hands() {
   473  			if len(hand.Hand) == 0 {
   474  				rows = append(rows, fmt.Sprintf("%d. %s: 🤨", index+1, hand.Target))
   475  			} else {
   476  				var cards []string
   477  				for _, cardIndex := range hand.Hand {
   478  					cards = append(cards, m.cardReverseMap[cardIndex])
   479  				}
   480  				rows = append(rows, fmt.Sprintf("%d. %s: %s", index+1, hand.Target,
   481  					strings.TrimRight(strings.Join(cards, ", "), " ")))
   482  			}
   483  		}
   484  		return strings.Join(rows, "\n")
   485  	}
   486  	return ""
   487  }
   488  
   489  func (m *FlipManager) addResult(ctx context.Context, status *chat1.UICoinFlipStatus, result flip.Result,
   490  	convID chat1.ConversationID) {
   491  	defer func() {
   492  		if status.ResultInfo != nil {
   493  			status.ResultText = m.resultToText(*status.ResultInfo)
   494  		}
   495  		if len(status.ResultText) > 0 {
   496  			status.ProgressText += " (complete)"
   497  		}
   498  	}()
   499  	hmi, err := m.getHostMessageInfo(ctx, convID)
   500  	switch {
   501  	case err != nil:
   502  		m.Debug(ctx, "addResult: failed to describe result: %s", err)
   503  		m.setGenericError(status, "Failed to describe result")
   504  	case result.Big != nil:
   505  		lb := new(big.Int)
   506  		res := new(big.Int)
   507  		lb.SetString(hmi.LowerBound, 0)
   508  		res.Add(lb, result.Big)
   509  		resultInfo := chat1.NewUICoinFlipResultWithNumber(res.String())
   510  		status.ResultInfo = &resultInfo
   511  	case result.Bool != nil:
   512  		resultInfo := chat1.NewUICoinFlipResultWithCoin(*result.Bool)
   513  		status.ResultInfo = &resultInfo
   514  	case result.Int != nil:
   515  		resultInfo := chat1.NewUICoinFlipResultWithNumber(fmt.Sprintf("%d", *result.Int))
   516  		status.ResultInfo = &resultInfo
   517  	case len(result.Shuffle) > 0:
   518  		if hmi.HandCardCount > 0 {
   519  			m.addCardHandResult(ctx, status, result, hmi)
   520  			return
   521  		}
   522  		if len(hmi.ShuffleItems) != len(result.Shuffle) {
   523  			m.setGenericError(status, "Failed to describe shuffle result")
   524  			return
   525  		}
   526  		items := make([]string, len(hmi.ShuffleItems))
   527  		for index, r := range result.Shuffle {
   528  			items[index] = utils.EscapeForDecorate(ctx, hmi.ShuffleItems[r])
   529  		}
   530  		var resultInfo chat1.UICoinFlipResult
   531  		if hmi.DeckShuffle {
   532  			var cardIndexes []int
   533  			for _, card := range items {
   534  				cardIndex, err := m.cardIndex(card)
   535  				if err != nil {
   536  					m.Debug(ctx, "addResult: failed to get card: %s", err)
   537  					m.setGenericError(status, "Failed to describe deck result")
   538  					return
   539  				}
   540  				cardIndexes = append(cardIndexes, cardIndex)
   541  			}
   542  			resultInfo = chat1.NewUICoinFlipResultWithDeck(cardIndexes)
   543  		} else {
   544  			resultInfo = chat1.NewUICoinFlipResultWithShuffle(items)
   545  		}
   546  		status.ResultInfo = &resultInfo
   547  	}
   548  }
   549  
   550  func (m *FlipManager) queueDirtyGameID(ctx context.Context, gameID chat1.FlipGameID, force bool) {
   551  	m.gamesMu.Lock()
   552  	m.dirtyGames[gameID.FlipGameIDStr()] = gameID
   553  	m.gamesMu.Unlock()
   554  	if force {
   555  		select {
   556  		case m.forceCh <- struct{}{}:
   557  		default:
   558  			m.Debug(ctx, "queueDirtyGameID: failed to write onto forceCh!")
   559  		}
   560  	}
   561  }
   562  
   563  func (m *FlipManager) getErrorParticipant(ctx context.Context, a flip.UserDevice) chat1.UICoinFlipErrorParticipant {
   564  	username, deviceName, _, err := m.G().GetUPAKLoader().LookupUsernameAndDevice(ctx,
   565  		keybase1.UID(a.U.String()), keybase1.DeviceID(a.D.String()))
   566  	if err != nil {
   567  		m.Debug(ctx, "getErrorParticipant: failed to get names: %s", err)
   568  		return chat1.UICoinFlipErrorParticipant{
   569  			User:   a.U.String(),
   570  			Device: a.D.String(),
   571  		}
   572  	}
   573  	return chat1.UICoinFlipErrorParticipant{
   574  		User:   username.String(),
   575  		Device: deviceName,
   576  	}
   577  
   578  }
   579  
   580  func (m *FlipManager) formatError(ctx context.Context, rawErr error) chat1.UICoinFlipError {
   581  	switch terr := rawErr.(type) {
   582  	case flip.AbsenteesError:
   583  		// lookup all the absentees
   584  		var absentees []chat1.UICoinFlipErrorParticipant
   585  		for _, a := range terr.Absentees {
   586  			absentees = append(absentees, m.getErrorParticipant(ctx, a))
   587  		}
   588  		return chat1.NewUICoinFlipErrorWithAbsentee(chat1.UICoinFlipAbsenteeError{
   589  			Absentees: absentees,
   590  		})
   591  	case flip.TimeoutError:
   592  		return chat1.NewUICoinFlipErrorWithTimeout()
   593  	case flip.GameAbortedError:
   594  		return chat1.NewUICoinFlipErrorWithAborted()
   595  	case flip.DuplicateRegistrationError:
   596  		return chat1.NewUICoinFlipErrorWithDupreg(m.getErrorParticipant(ctx, terr.U))
   597  	case flip.DuplicateCommitmentCompleteError:
   598  		return chat1.NewUICoinFlipErrorWithDupcommitcomplete(m.getErrorParticipant(ctx, terr.U))
   599  	case flip.DuplicateRevealError:
   600  		return chat1.NewUICoinFlipErrorWithDupreveal(m.getErrorParticipant(ctx, terr.U))
   601  	case flip.CommitmentMismatchError:
   602  		return chat1.NewUICoinFlipErrorWithCommitmismatch(m.getErrorParticipant(ctx, terr.U))
   603  	}
   604  	return chat1.NewUICoinFlipErrorWithGeneric(rawErr.Error())
   605  }
   606  
   607  func (m *FlipManager) handleSummaryUpdate(ctx context.Context, gameID chat1.FlipGameID,
   608  	update *flip.GameSummary, convID chat1.ConversationID, force bool) (status chat1.UICoinFlipStatus) {
   609  	defer m.queueDirtyGameID(ctx, gameID, force)
   610  	if update.Err != nil {
   611  		var parts []chat1.UICoinFlipParticipant
   612  		oldGame, ok := m.games.Get(gameID.FlipGameIDStr())
   613  		if ok {
   614  			parts = oldGame.(chat1.UICoinFlipStatus).Participants
   615  		}
   616  		formatted := m.formatError(ctx, update.Err)
   617  		status = chat1.UICoinFlipStatus{
   618  			GameID:       gameID.FlipGameIDStr(),
   619  			Phase:        chat1.UICoinFlipPhase_ERROR,
   620  			ProgressText: fmt.Sprintf("Something went wrong: %s", update.Err),
   621  			Participants: parts,
   622  			ErrorInfo:    &formatted,
   623  		}
   624  		m.games.Add(gameID.FlipGameIDStr(), status)
   625  		return status
   626  	}
   627  	status = chat1.UICoinFlipStatus{
   628  		GameID: gameID.FlipGameIDStr(),
   629  		Phase:  chat1.UICoinFlipPhase_COMPLETE,
   630  	}
   631  	m.addResult(ctx, &status, update.Result, convID)
   632  	for _, p := range update.Players {
   633  		m.addParticipant(ctx, &status, flip.CommitmentUpdate{
   634  			User:       p.Device,
   635  			Commitment: p.Commitment,
   636  		})
   637  		if p.Reveal != nil {
   638  			m.addReveal(ctx, &status, flip.RevealUpdate{
   639  				User:   p.Device,
   640  				Reveal: *p.Reveal,
   641  			})
   642  		}
   643  	}
   644  	status.ProgressText = "Complete"
   645  	m.games.Add(gameID.FlipGameIDStr(), status)
   646  	return status
   647  }
   648  
   649  func (m *FlipManager) handleUpdate(ctx context.Context, update flip.GameStateUpdateMessage, force bool) (err error) {
   650  	gameID := update.Metadata.GameID
   651  	defer m.Trace(ctx, &err, "handleUpdate: gameID: %s", gameID)()
   652  	defer func() {
   653  		if err == nil {
   654  			m.queueDirtyGameID(ctx, gameID, force)
   655  		}
   656  	}()
   657  	var status chat1.UICoinFlipStatus
   658  	rawGame, ok := m.games.Get(gameID.FlipGameIDStr())
   659  	if ok {
   660  		status = rawGame.(chat1.UICoinFlipStatus)
   661  	} else {
   662  		status = chat1.UICoinFlipStatus{
   663  			GameID: gameID.FlipGameIDStr(),
   664  		}
   665  	}
   666  
   667  	switch {
   668  	case update.Err != nil:
   669  		m.Debug(ctx, "handleUpdate: error received")
   670  		status.Phase = chat1.UICoinFlipPhase_ERROR
   671  		status.ProgressText = fmt.Sprintf("Something went wrong: %s", update.Err)
   672  		formatted := m.formatError(ctx, update.Err)
   673  		status.ErrorInfo = &formatted
   674  	case update.Commitment != nil:
   675  		m.Debug(ctx, "handleUpdate: commit received")
   676  		// Only care about these while we are in the commitment phase
   677  		if status.Phase == chat1.UICoinFlipPhase_COMMITMENT {
   678  			status.ErrorInfo = nil
   679  			status.Phase = chat1.UICoinFlipPhase_COMMITMENT
   680  			m.addParticipant(ctx, &status, *update.Commitment)
   681  		}
   682  	case update.CommitmentComplete != nil:
   683  		m.Debug(ctx, "handleUpdate: complete received")
   684  		status.ErrorInfo = nil
   685  		status.Phase = chat1.UICoinFlipPhase_REVEALS
   686  		m.finalizeParticipants(ctx, &status, *update.CommitmentComplete)
   687  	case update.Reveal != nil:
   688  		m.Debug(ctx, "handleUpdate: reveal received")
   689  		m.addReveal(ctx, &status, *update.Reveal)
   690  	case update.Result != nil:
   691  		m.Debug(ctx, "handleUpdate: result received")
   692  		status.Phase = chat1.UICoinFlipPhase_COMPLETE
   693  		status.ErrorInfo = nil
   694  		m.addResult(ctx, &status, *update.Result, update.Metadata.ConversationID)
   695  	default:
   696  		return errors.New("unknown update kind")
   697  	}
   698  	m.games.Add(gameID.FlipGameIDStr(), status)
   699  	return nil
   700  }
   701  
   702  func (m *FlipManager) updateLoop(shutdownCh chan struct{}) {
   703  	m.Debug(context.Background(), "updateLoop: starting")
   704  	for {
   705  		select {
   706  		case msg := <-m.dealer.UpdateCh():
   707  			err := m.handleUpdate(m.makeBkgContext(), msg, false)
   708  			if err != nil {
   709  				m.Debug(context.TODO(), "updateLoop: error handling update: %+v", err)
   710  			}
   711  		case <-shutdownCh:
   712  			m.Debug(context.Background(), "updateLoop: exiting")
   713  			return
   714  		}
   715  	}
   716  }
   717  
   718  const gameIDTopicNamePrefix = "__keybase_coinflip_game_"
   719  
   720  func (m *FlipManager) gameTopicNameFromGameID(gameID chat1.FlipGameID) string {
   721  	return fmt.Sprintf("%s%s", gameIDTopicNamePrefix, gameID)
   722  }
   723  
   724  var errFailedToParse = errors.New("failed to parse")
   725  
   726  func (m *FlipManager) parseMultiDie(arg string, nPlayersApprox int) (start flip.Start, err error) {
   727  	lb := new(big.Int)
   728  	val, ok := lb.SetString(arg, 0)
   729  	if !ok {
   730  		return start, errFailedToParse
   731  	}
   732  	// needs to be a positive number > 0
   733  	if val.Sign() <= 0 {
   734  		return start, errFailedToParse
   735  	}
   736  	return flip.NewStartWithBigInt(m.clock.Now(), val, nPlayersApprox), nil
   737  }
   738  
   739  const shuffleSeparaters = ",,"
   740  
   741  func (m *FlipManager) parseShuffle(arg string, nPlayersApprox int) (start flip.Start, metadata flipTextMetadata, err error) {
   742  	if strings.ContainsAny(arg, shuffleSeparaters) {
   743  		var shuffleItems []string
   744  		for _, tok := range strings.FieldsFunc(arg, func(c rune) bool {
   745  			return strings.ContainsRune(shuffleSeparaters, c)
   746  		}) {
   747  			shuffleItems = append(shuffleItems, strings.Trim(tok, " "))
   748  		}
   749  		return flip.NewStartWithShuffle(m.clock.Now(), int64(len(shuffleItems)), nPlayersApprox),
   750  			flipTextMetadata{
   751  				ShuffleItems: shuffleItems,
   752  			}, nil
   753  	}
   754  	return start, metadata, errFailedToParse
   755  }
   756  
   757  func (m *FlipManager) parseRange(arg string, nPlayersApprox int) (start flip.Start, metadata flipTextMetadata, err error) {
   758  	if !strings.Contains(arg, "..") || strings.Contains(arg, ",") {
   759  		return start, metadata, errFailedToParse
   760  	}
   761  	toks := strings.Split(arg, "..")
   762  	if len(toks) != 2 {
   763  		return start, metadata, errFailedToParse
   764  	}
   765  	lb, ok := new(big.Int).SetString(toks[0], 0)
   766  	if !ok {
   767  		return start, metadata, errFailedToParse
   768  	}
   769  	ub, ok := new(big.Int).SetString(toks[1], 0)
   770  	if !ok {
   771  		return start, metadata, errFailedToParse
   772  	}
   773  	one := new(big.Int).SetInt64(1)
   774  	diff := new(big.Int)
   775  	diff.Sub(ub, lb)
   776  	diff = diff.Add(diff, one)
   777  	if diff.Sign() <= 0 {
   778  		return start, metadata, errFailedToParse
   779  	}
   780  	return flip.NewStartWithBigInt(m.clock.Now(), diff, nPlayersApprox), flipTextMetadata{
   781  		LowerBound: lb.String(),
   782  	}, nil
   783  }
   784  
   785  func (m *FlipManager) parseSpecials(arg string, usernames []string,
   786  	nPlayersApprox int) (start flip.Start, metadata flipTextMetadata, err error) {
   787  	switch {
   788  	case strings.HasPrefix(arg, "cards"):
   789  		deckShuffle, deckShuffleMetadata, _ := m.parseShuffle(m.deck, nPlayersApprox)
   790  		deckShuffleMetadata.DeckShuffle = true
   791  		if arg == "cards" {
   792  			return deckShuffle, deckShuffleMetadata, nil
   793  		}
   794  		toks := strings.Split(arg, " ")
   795  		if len(toks) < 3 {
   796  			return deckShuffle, deckShuffleMetadata, nil
   797  		}
   798  		handCount, err := strconv.ParseUint(toks[1], 0, 0)
   799  		if err != nil {
   800  			return deckShuffle, deckShuffleMetadata, nil
   801  		}
   802  		var targets []string
   803  		handParts := strings.Split(strings.Join(toks[2:], " "), ",")
   804  		if len(handParts) == 1 && (handParts[0] == "@here" || handParts[0] == "@channel") {
   805  			targets = usernames
   806  		} else {
   807  			for _, pt := range handParts {
   808  				t := strings.Trim(pt, " ")
   809  				if len(t) > 0 {
   810  					targets = append(targets, t)
   811  				}
   812  			}
   813  		}
   814  		return deckShuffle, flipTextMetadata{
   815  			ShuffleItems:  deckShuffleMetadata.ShuffleItems,
   816  			HandCardCount: uint(handCount),
   817  			HandTargets:   targets,
   818  		}, nil
   819  	case arg == "@here" || arg == "@channel":
   820  		if len(usernames) == 0 {
   821  			return flip.NewStartWithShuffle(m.clock.Now(), 1, nPlayersApprox), flipTextMetadata{
   822  				ShuffleItems:      []string{"@here"},
   823  				ConvMemberShuffle: true,
   824  			}, nil
   825  		}
   826  		return flip.NewStartWithShuffle(m.clock.Now(), int64(len(usernames)), nPlayersApprox),
   827  			flipTextMetadata{
   828  				ShuffleItems:      usernames,
   829  				ConvMemberShuffle: true,
   830  			}, nil
   831  	}
   832  	return start, metadata, errFailedToParse
   833  }
   834  
   835  func (m *FlipManager) startFromText(text string, convMembers []string) (start flip.Start, metadata flipTextMetadata) {
   836  	var err error
   837  	nPlayersApprox := len(convMembers)
   838  	toks := strings.Split(strings.TrimRight(text, " "), " ")
   839  	if len(toks) == 1 {
   840  		return flip.NewStartWithBool(m.clock.Now(), nPlayersApprox), flipTextMetadata{}
   841  	}
   842  	// Combine into one argument if there is more than one
   843  	arg := strings.Join(toks[1:], " ")
   844  	// Check for special flips
   845  	if start, metadata, err = m.parseSpecials(arg, convMembers, nPlayersApprox); err == nil {
   846  		return start, metadata
   847  	}
   848  	// Check for /flip 20
   849  	if start, err = m.parseMultiDie(arg, nPlayersApprox); err == nil {
   850  		return start, flipTextMetadata{
   851  			LowerBound: "1",
   852  		}
   853  	}
   854  	// Check for /flip mikem,karenm,lisam
   855  	if start, metadata, err = m.parseShuffle(arg, nPlayersApprox); err == nil {
   856  		return start, metadata
   857  	}
   858  	// Check for /flip 2..8
   859  	if start, metadata, err = m.parseRange(arg, nPlayersApprox); err == nil {
   860  		return start, metadata
   861  	}
   862  	// Just shuffle the one unknown thing
   863  	return flip.NewStartWithShuffle(m.clock.Now(), 1, nPlayersApprox), flipTextMetadata{
   864  		ShuffleItems: []string{arg},
   865  	}
   866  }
   867  
   868  func (m *FlipManager) getHostMessageInfo(ctx context.Context, convID chat1.ConversationID) (res hostMessageInfo, err error) {
   869  	m.Debug(ctx, "getHostMessageInfo: getting host message info for: %s", convID)
   870  	uid, err := utils.AssertLoggedInUID(ctx, m.G())
   871  	if err != nil {
   872  		return res, err
   873  	}
   874  	reason := chat1.GetThreadReason_COINFLIP
   875  	msg, err := m.G().ChatHelper.GetMessage(ctx, uid, convID, 2, false, &reason)
   876  	if err != nil {
   877  		return res, err
   878  	}
   879  	if !msg.IsValid() {
   880  		return res, errors.New("host message invalid")
   881  	}
   882  	if !msg.Valid().MessageBody.IsType(chat1.MessageType_FLIP) {
   883  		return res, fmt.Errorf("invalid host message type: %v", msg.GetMessageType())
   884  	}
   885  	body := msg.Valid().MessageBody.Flip().Text
   886  	if err := json.Unmarshal([]byte(body), &res); err != nil {
   887  		return res, err
   888  	}
   889  	return res, nil
   890  }
   891  
   892  func (m *FlipManager) DescribeFlipText(ctx context.Context, text string) string {
   893  	defer m.Trace(ctx, nil, "DescribeFlipText")()
   894  	start, metadata := m.startFromText(text, nil)
   895  	typ, err := start.Params.T()
   896  	if err != nil {
   897  		m.Debug(ctx, "DescribeFlipText: failed get start typ: %s", err)
   898  		return ""
   899  	}
   900  	switch typ {
   901  	case flip.FlipType_BIG:
   902  		if metadata.LowerBound == "1" {
   903  			return fmt.Sprintf("*%s-sided die roll*", new(big.Int).SetBytes(start.Params.Big()))
   904  		}
   905  		lb, _ := new(big.Int).SetString(metadata.LowerBound, 0)
   906  		ub := new(big.Int).Sub(new(big.Int).SetBytes(start.Params.Big()), new(big.Int).SetInt64(1))
   907  		return fmt.Sprintf("*Number in range %s..%s*", metadata.LowerBound,
   908  			new(big.Int).Add(lb, ub))
   909  	case flip.FlipType_BOOL:
   910  		return "*HEADS* or *TAILS*"
   911  	case flip.FlipType_SHUFFLE:
   912  		if metadata.DeckShuffle {
   913  			return "*Shuffling a deck of cards*"
   914  		} else if metadata.ConvMemberShuffle {
   915  			return "*Shuffling all members of the conversation*"
   916  		} else if metadata.HandCardCount > 0 {
   917  			return fmt.Sprintf("*Dealing hands of %d cards*", metadata.HandCardCount)
   918  		}
   919  		return fmt.Sprintf("*Shuffling %s*",
   920  			strings.TrimRight(strings.Join(metadata.ShuffleItems, ", "), " "))
   921  	}
   922  	return ""
   923  }
   924  
   925  func (m *FlipManager) setStartFlipSendStatus(ctx context.Context, outboxID chat1.OutboxID,
   926  	status types.FlipSendStatus, flipConvID *chat1.ConversationID) {
   927  	payload := startFlipSendStatus{
   928  		status: status,
   929  	}
   930  	if flipConvID != nil {
   931  		payload.flipConvID = *flipConvID
   932  	}
   933  	m.flipConvs.Add(outboxID.String(), payload)
   934  	m.G().MessageDeliverer.ForceDeliverLoop(ctx)
   935  }
   936  
   937  // StartFlip implements the types.CoinFlipManager interface
   938  func (m *FlipManager) StartFlip(ctx context.Context, uid gregor1.UID, hostConvID chat1.ConversationID,
   939  	tlfName, text string, inOutboxID *chat1.OutboxID) (err error) {
   940  	defer m.Trace(ctx, &err, "StartFlip: convID: %s", hostConvID)()
   941  	gameID := flip.GenerateGameID()
   942  	m.Debug(ctx, "StartFlip: using gameID: %s", gameID)
   943  
   944  	// Get host conv using local storage, just bail out if we don't have it
   945  	hostConv, err := utils.GetVerifiedConv(ctx, m.G(), uid, hostConvID,
   946  		types.InboxSourceDataSourceLocalOnly)
   947  	if err != nil {
   948  		return err
   949  	}
   950  
   951  	// First generate the message representing the flip into the host conversation. We also wait for it
   952  	// to actually get sent before doing anything flip related.
   953  	var outboxID chat1.OutboxID
   954  	if inOutboxID != nil {
   955  		outboxID = *inOutboxID
   956  	} else {
   957  		if outboxID, err = storage.NewOutboxID(); err != nil {
   958  			return err
   959  		}
   960  	}
   961  
   962  	// Generate dev channel for game message
   963  	var conv chat1.ConversationLocal
   964  	var participants []string
   965  	m.setStartFlipSendStatus(ctx, outboxID, types.FlipSendStatusInProgress, nil)
   966  	convCreatedCh := make(chan error)
   967  	go func() {
   968  		var err error
   969  		topicName := m.gameTopicNameFromGameID(gameID)
   970  		membersType := hostConv.GetMembersType()
   971  		switch membersType {
   972  		case chat1.ConversationMembersType_IMPTEAMUPGRADE:
   973  			// just override this to use native
   974  			membersType = chat1.ConversationMembersType_IMPTEAMNATIVE
   975  			fallthrough
   976  		case chat1.ConversationMembersType_IMPTEAMNATIVE:
   977  			tlfName = utils.AddUserToTLFName(m.G(), tlfName, keybase1.TLFVisibility_PRIVATE,
   978  				membersType)
   979  		default:
   980  		}
   981  		// Get conv participants
   982  		if participants, err = utils.GetConvParticipantUsernames(ctx, m.G(), uid, hostConvID); err != nil {
   983  			convCreatedCh <- err
   984  			return
   985  		}
   986  		// Preserve the ephemeral lifetime from the conv/message to the game
   987  		// conversation.
   988  		elf, err := utils.EphemeralLifetimeFromConv(ctx, m.G(), hostConv)
   989  		if err != nil {
   990  			m.Debug(ctx, "StartFlip: failed to get ephemeral lifetime from conv: %s", err)
   991  			convCreatedCh <- err
   992  			return
   993  		}
   994  		var retentionPolicy *chat1.RetentionPolicy
   995  		if elf != nil {
   996  			retentionPolicy = new(chat1.RetentionPolicy)
   997  			*retentionPolicy = chat1.NewRetentionPolicyWithEphemeral(chat1.RpEphemeral{Age: *elf})
   998  		}
   999  		conv, _, err = NewConversationWithMemberSourceConv(ctx, m.G(), uid, tlfName, &topicName,
  1000  			chat1.TopicType_DEV, membersType,
  1001  			keybase1.TLFVisibility_PRIVATE, nil, m.ri, NewConvFindExistingSkip, retentionPolicy, &hostConvID)
  1002  		convCreatedCh <- err
  1003  	}()
  1004  
  1005  	listener := newSentMessageListener(m.G(), outboxID)
  1006  	nid := m.G().NotifyRouter.AddListener(listener)
  1007  	if err := m.sendNonblock(ctx, uid, hostConvID, text, tlfName, outboxID, gameID, chat1.TopicType_CHAT); err != nil {
  1008  		m.Debug(ctx, "StartFlip: failed to send flip message: %s", err)
  1009  		m.setStartFlipSendStatus(ctx, outboxID, types.FlipSendStatusError, nil)
  1010  		m.G().NotifyRouter.RemoveListener(nid)
  1011  		return err
  1012  	}
  1013  	if err := <-convCreatedCh; err != nil {
  1014  		m.setStartFlipSendStatus(ctx, outboxID, types.FlipSendStatusError, nil)
  1015  		m.G().NotifyRouter.RemoveListener(nid)
  1016  		return err
  1017  	}
  1018  	flipConvID := conv.GetConvID()
  1019  	m.Debug(ctx, "StartFlip: flip conv created: %s", flipConvID)
  1020  	m.setStartFlipSendStatus(ctx, outboxID, types.FlipSendStatusSent, &flipConvID)
  1021  	sendRes := <-listener.listenCh
  1022  	m.G().NotifyRouter.RemoveListener(nid)
  1023  	if sendRes.Err != nil {
  1024  		return sendRes.Err
  1025  	}
  1026  
  1027  	// Record metadata of the host message into the game thread as the first message
  1028  	m.Debug(ctx, "StartFlip: generating parameters for %d players", len(participants))
  1029  	start, metadata := m.startFromText(text, participants)
  1030  	infoBody, err := json.Marshal(hostMessageInfo{
  1031  		flipTextMetadata: metadata,
  1032  		ConvID:           hostConvID,
  1033  		MsgID:            sendRes.MsgID,
  1034  	})
  1035  	if err != nil {
  1036  		return err
  1037  	}
  1038  	if err := m.G().ChatHelper.SendMsgByID(ctx, flipConvID, tlfName,
  1039  		chat1.NewMessageBodyWithFlip(chat1.MessageFlip{
  1040  			Text:   string(infoBody),
  1041  			GameID: gameID,
  1042  		}), chat1.MessageType_FLIP, keybase1.TLFVisibility_PRIVATE); err != nil {
  1043  		return err
  1044  	}
  1045  
  1046  	// Start the game
  1047  	return m.dealer.StartFlipWithGameID(ctx, start, flipConvID, gameID)
  1048  }
  1049  
  1050  func (m *FlipManager) shouldIgnoreInject(ctx context.Context, hostConvID, flipConvID chat1.ConversationID,
  1051  	gameID chat1.FlipGameID) bool {
  1052  	if m.dealer.IsGameActive(ctx, flipConvID, gameID) {
  1053  		return false
  1054  	}
  1055  	// Ignore any flip messages for non-active games when not in the foreground
  1056  	appBkg := m.G().IsMobileAppType() &&
  1057  		m.G().MobileAppState.State() != keybase1.MobileAppState_FOREGROUND
  1058  	partViolation := m.isConvParticipationViolation(ctx, hostConvID)
  1059  	return appBkg || partViolation
  1060  }
  1061  
  1062  func (m *FlipManager) isConvParticipationViolation(ctx context.Context, convID chat1.ConversationID) bool {
  1063  	m.partMu.Lock()
  1064  	defer m.partMu.Unlock()
  1065  	if rec, ok := m.convParticipations[convID.ConvIDStr()]; ok {
  1066  		m.Debug(ctx, "isConvParticipationViolation: rec: count: %d remain: %v", rec.count,
  1067  			m.clock.Now().Sub(rec.reset))
  1068  		if rec.reset.Before(m.clock.Now()) {
  1069  			return false
  1070  		}
  1071  		if rec.count >= m.maxConvParticipations {
  1072  			m.Debug(ctx, "isConvParticipationViolation: violation: convID: %s remaining: %v",
  1073  				convID, m.clock.Now().Sub(rec.reset))
  1074  			return true
  1075  		}
  1076  		return false
  1077  	}
  1078  	return false
  1079  }
  1080  
  1081  func (m *FlipManager) recordConvParticipation(ctx context.Context, convID chat1.ConversationID) {
  1082  	m.partMu.Lock()
  1083  	defer m.partMu.Unlock()
  1084  	addNew := func() {
  1085  		m.convParticipations[convID.ConvIDStr()] = convParticipationsRateLimit{
  1086  			count: 1,
  1087  			reset: m.clock.Now().Add(m.maxConvParticipationsReset),
  1088  		}
  1089  	}
  1090  	if rec, ok := m.convParticipations[convID.ConvIDStr()]; ok {
  1091  		if rec.reset.Before(m.clock.Now()) {
  1092  			addNew()
  1093  		} else {
  1094  			rec.count++
  1095  			m.convParticipations[convID.ConvIDStr()] = rec
  1096  		}
  1097  	} else {
  1098  		addNew()
  1099  	}
  1100  }
  1101  
  1102  func (m *FlipManager) injectIncomingChat(ctx context.Context, uid gregor1.UID,
  1103  	convID, hostConvID chat1.ConversationID, gameID chat1.FlipGameID, msg chat1.MessageUnboxed) error {
  1104  	if !msg.IsValid() {
  1105  		m.Debug(ctx, "injectIncomingChat: skipping invalid message: %d", msg.GetMessageID())
  1106  		return errors.New("invalid message")
  1107  	}
  1108  	if msg.Valid().ClientHeader.OutboxID != nil &&
  1109  		m.isSentOutboxID(ctx, gameID, *msg.Valid().ClientHeader.OutboxID) {
  1110  		m.Debug(ctx, "injectIncomingChat: skipping sent outboxID message: %d outboxID: %s ",
  1111  			msg.GetMessageID(), msg.Valid().ClientHeader.OutboxID)
  1112  		return nil
  1113  	}
  1114  	body := msg.Valid().MessageBody
  1115  	if !body.IsType(chat1.MessageType_FLIP) {
  1116  		return errors.New("non-flip message")
  1117  	}
  1118  
  1119  	sender := flip.UserDevice{
  1120  		U: msg.Valid().ClientHeader.Sender,
  1121  		D: msg.Valid().ClientHeader.SenderDevice,
  1122  	}
  1123  	m.recordConvParticipation(ctx, hostConvID) // record the inject for rate limiting purposes
  1124  	m.gameMsgIDs.Add(gameID.FlipGameIDStr(), msg.GetMessageID())
  1125  	m.Debug(ctx, "injectIncomingChat: injecting: gameID: %s msgID: %d", gameID, msg.GetMessageID())
  1126  	return m.dealer.InjectIncomingChat(ctx, sender, convID, gameID,
  1127  		flip.MakeGameMessageEncoded(body.Flip().Text), m.isStartMsgID(msg.GetMessageID()))
  1128  }
  1129  
  1130  func (m *FlipManager) updateActiveGame(ctx context.Context, uid gregor1.UID, convID chat1.ConversationID,
  1131  	hostConvID chat1.ConversationID, nextMsg chat1.MessageUnboxed, gameID chat1.FlipGameID) (err error) {
  1132  	defer func() {
  1133  		if err == nil {
  1134  			if err = m.injectIncomingChat(ctx, uid, convID, hostConvID, gameID, nextMsg); err != nil {
  1135  				m.Debug(ctx, "updateActiveGame: failed to inject next message: %s", err)
  1136  			}
  1137  		}
  1138  	}()
  1139  	m.Debug(ctx, "updateActiveGame: uid: %s convID: %s gameID: %s nextMsgID: %d", uid, convID, gameID,
  1140  		nextMsg.GetMessageID())
  1141  	// Get current msg ID of the game if we know about it
  1142  	var msgIDStart chat1.MessageID
  1143  	if storedMsgIDIface, ok := m.gameMsgIDs.Get(gameID.FlipGameIDStr()); ok {
  1144  		storedMsgID := storedMsgIDIface.(chat1.MessageID)
  1145  		if nextMsg.GetMessageID() == storedMsgID+1 {
  1146  			m.Debug(ctx, "updateActiveGame: truly incremental update, injecting...")
  1147  			return nil
  1148  		} else if nextMsg.GetMessageID() <= storedMsgID {
  1149  			m.Debug(ctx, "updateActiveGame: update from the past, ignoring: stored: %d", storedMsgID)
  1150  			return errors.New("update from the past")
  1151  		}
  1152  		m.Debug(ctx, "updateActiveGame: gapped update: storedMsgID: %d", storedMsgID)
  1153  		msgIDStart = storedMsgID
  1154  	} else {
  1155  		if m.isStartMsgID(nextMsg.GetMessageID()) {
  1156  			// if this is a start msg, then just send it in
  1157  			m.Debug(ctx, "updateActiveGame: starting new game: convID: %s gameID: %s", convID, gameID)
  1158  			return nil
  1159  		}
  1160  		m.Debug(ctx, "updateActiveGame: unknown game, setting start to 0")
  1161  	}
  1162  	// Otherwise, grab the thread and inject everything that has happened so far
  1163  	tv, err := m.G().ConvSource.PullFull(ctx, convID, uid, chat1.GetThreadReason_COINFLIP, nil, nil)
  1164  	if err != nil {
  1165  		return err
  1166  	}
  1167  	m.Debug(ctx, "updateActiveGame: got %d messages, injecting...", len(tv.Messages))
  1168  	for i := len(tv.Messages) - 3; i >= 0; i-- {
  1169  		msg := tv.Messages[i]
  1170  		if msg.GetMessageID() <= msgIDStart {
  1171  			m.Debug(ctx, "updateActiveGame: skipping known msgID: %d", msg.GetMessageID())
  1172  			continue
  1173  		}
  1174  		if msg.GetMessageID() >= nextMsg.GetMessageID() {
  1175  			m.Debug(ctx, "updateActiveGame: reached current msgID, finishing...")
  1176  			return nil
  1177  		}
  1178  		if err := m.injectIncomingChat(ctx, uid, convID, hostConvID, gameID, msg); err != nil {
  1179  			m.Debug(ctx, "updateActiveGame: failed to inject: %s", err)
  1180  		}
  1181  	}
  1182  	return nil
  1183  }
  1184  
  1185  func (m *FlipManager) maybeInjectLoop(shutdownCh chan struct{}) {
  1186  	m.Debug(context.Background(), "maybeInjectLoop: starting")
  1187  	for {
  1188  		select {
  1189  		case closure := <-m.maybeInjectCh:
  1190  			closure()
  1191  		case <-shutdownCh:
  1192  			m.Debug(context.Background(), "maybeInjectLoop: exiting loop")
  1193  			return
  1194  		}
  1195  	}
  1196  }
  1197  
  1198  // MaybeInjectFlipMessage implements the types.CoinFlipManager interface
  1199  func (m *FlipManager) MaybeInjectFlipMessage(ctx context.Context, boxedMsg chat1.MessageBoxed,
  1200  	inboxVers chat1.InboxVers, uid gregor1.UID, convID chat1.ConversationID, topicType chat1.TopicType) bool {
  1201  	// earliest of outs if this isn't a dev convo, an error, or the outbox ID message
  1202  	if topicType != chat1.TopicType_DEV || boxedMsg.GetMessageType() != chat1.MessageType_FLIP ||
  1203  		m.isHostMessageInfoMsgID(boxedMsg.GetMessageID()) {
  1204  		return false
  1205  	}
  1206  	defer m.Trace(ctx, nil, "MaybeInjectFlipMessage: uid: %s convID: %s", uid, convID)()
  1207  
  1208  	// Update inbox for this guy
  1209  	if err := m.G().InboxSource.UpdateInboxVersion(ctx, uid, inboxVers); err != nil {
  1210  		m.Debug(ctx, "MaybeInjectFlipMessage: failed to update inbox version: %s", err)
  1211  		// charge forward here, we will figure it out
  1212  	}
  1213  	if err := storage.New(m.G(), nil).SetMaxMsgID(ctx, convID, uid, boxedMsg.GetMessageID()); err != nil {
  1214  		m.Debug(ctx, "MaybeInjectFlipMessage: failed to write max msgid: %s", err)
  1215  		// charge forward from this error
  1216  	}
  1217  	// Unbox the message
  1218  	conv, err := utils.GetUnverifiedConv(ctx, m.G(), uid, convID, types.InboxSourceDataSourceAll)
  1219  	if err != nil {
  1220  		m.Debug(ctx, "MaybeInjectFlipMessage: failed to get conversation for unbox: %s", err)
  1221  		return true
  1222  	}
  1223  	msg, err := NewBoxer(m.G()).UnboxMessage(ctx, boxedMsg, conv.Conv, nil)
  1224  	if err != nil {
  1225  		m.Debug(ctx, "MaybeInjectFlipMessage: failed to unbox: %s", err)
  1226  		return true
  1227  	}
  1228  	if !msg.IsValid() {
  1229  		m.Debug(ctx, "MaybeInjectFlipMessage: failed to unbox msg")
  1230  		return true
  1231  	}
  1232  	body := msg.Valid().MessageBody
  1233  	if !body.IsType(chat1.MessageType_FLIP) {
  1234  		m.Debug(ctx, "MaybeInjectFlipMessage: bogus flip message with a non-flip body")
  1235  		return true
  1236  	}
  1237  	// Ignore anything from the current device
  1238  	ctx = globals.BackgroundChatCtx(ctx, m.G())
  1239  	select {
  1240  	case m.maybeInjectCh <- func() {
  1241  		defer m.Trace(ctx, nil,
  1242  			"MaybeInjectFlipMessage(goroutine): uid: %s convID: %s id: %d",
  1243  			uid, convID, msg.GetMessageID())()
  1244  		if m.Me().Eq(flip.UserDevice{
  1245  			U: msg.Valid().ClientHeader.Sender,
  1246  			D: msg.Valid().ClientHeader.SenderDevice,
  1247  		}) {
  1248  			m.gameMsgIDs.Add(body.Flip().GameID.FlipGameIDStr(), msg.GetMessageID())
  1249  			return
  1250  		}
  1251  		// Check to see if we are going to participate from this inject
  1252  		hmi, err := m.getHostMessageInfo(ctx, convID)
  1253  		if err != nil {
  1254  			m.Debug(ctx, "MaybeInjectFlipMessage: failed to get host message info: %s", err)
  1255  			return
  1256  		}
  1257  		if m.shouldIgnoreInject(ctx, hmi.ConvID, convID, body.Flip().GameID) {
  1258  			m.Debug(ctx, "MaybeInjectFlipMessage: ignored flip message")
  1259  			return
  1260  		}
  1261  		// Check to see if the game is unknown, and if so, then rebuild and see what we can do
  1262  		if err := m.updateActiveGame(ctx, uid, convID, hmi.ConvID, msg, body.Flip().GameID); err != nil {
  1263  			m.Debug(ctx, "MaybeInjectFlipMessage: failed to rebuild non-active game: %s", err)
  1264  		}
  1265  	}:
  1266  	default:
  1267  		m.Debug(ctx, "MaybeInjectFlipMessage: failed to dispatch job, queue full!")
  1268  	}
  1269  	return true
  1270  }
  1271  
  1272  func (m *FlipManager) HasActiveGames(ctx context.Context) bool {
  1273  	return m.dealer.HasActiveGames(ctx)
  1274  }
  1275  
  1276  func (m *FlipManager) loadGame(ctx context.Context, job loadGameJob) (err error) {
  1277  	defer m.Trace(ctx, &err,
  1278  		"loadGame: hostConvID: %s flipConvID: %s gameID: %s hostMsgID: %d",
  1279  		job.hostConvID, job.flipConvID, job.gameID, job.hostMsgID)()
  1280  	defer func() {
  1281  		if err != nil {
  1282  			job.errCh <- err
  1283  		}
  1284  	}()
  1285  
  1286  	// Check to make sure the flip conversation aligns with the host message
  1287  	flipConvID := job.flipConvID
  1288  	hmi, err := m.getHostMessageInfo(ctx, flipConvID)
  1289  	if err != nil {
  1290  		m.Debug(ctx, "loadGame: failed to get host message info: %s", err)
  1291  		return err
  1292  	}
  1293  	if !(hmi.ConvID.Eq(job.hostConvID) && hmi.MsgID == job.hostMsgID) {
  1294  		m.Debug(ctx, "loadGame: host message info mismatch: job.hostConvID: %s hmi.ConvID: %s job.hostMsgID: %d hmi.msgID: %d", job.hostConvID, hmi.ConvID, job.hostMsgID, hmi.MsgID)
  1295  		return errors.New("flip conversation does not match host message info")
  1296  	}
  1297  
  1298  	tv, err := m.G().ConvSource.PullFull(ctx, flipConvID, job.uid,
  1299  		chat1.GetThreadReason_COINFLIP, nil, nil)
  1300  	if err != nil {
  1301  		m.Debug(ctx, "loadGame: failed to pull thread:  %s", err)
  1302  		return err
  1303  	}
  1304  	if len(tv.Messages) < 3 {
  1305  		m.Debug(ctx, "loadGame: not enough messages to replay")
  1306  		return errors.New("not enough messages")
  1307  	}
  1308  	var history flip.GameHistory
  1309  	for index := len(tv.Messages) - 3; index >= 0; index-- {
  1310  		msg := tv.Messages[index]
  1311  		if !msg.IsValid() {
  1312  			m.Debug(ctx, "loadGame: skipping invalid message: id: %d", msg.GetMessageID())
  1313  			continue
  1314  		}
  1315  		body := msg.Valid().MessageBody
  1316  		if !body.IsType(chat1.MessageType_FLIP) {
  1317  			continue
  1318  		}
  1319  		history = append(history, flip.GameMessageReplayed{
  1320  			GameMessageWrappedEncoded: flip.GameMessageWrappedEncoded{
  1321  				Sender: flip.UserDevice{
  1322  					U: msg.Valid().ClientHeader.Sender,
  1323  					D: msg.Valid().ClientHeader.SenderDevice,
  1324  				},
  1325  				GameID:              job.gameID,
  1326  				Body:                flip.MakeGameMessageEncoded(body.Flip().Text),
  1327  				FirstInConversation: m.isStartMsgID(msg.GetMessageID()),
  1328  			},
  1329  			Time: msg.Valid().ServerHeader.Ctime.Time(),
  1330  		})
  1331  	}
  1332  	m.Debug(ctx, "loadGame: playing back %d messages from history", len(history))
  1333  	summary, err := flip.Replay(ctx, m, history)
  1334  	if err != nil {
  1335  		m.Debug(ctx, "loadGame: failed to replay history: %s", err)
  1336  		// Make sure we aren't current playing this game, and bail out if we are
  1337  		if m.dealer.IsGameActive(ctx, flipConvID, job.gameID) {
  1338  			m.Debug(ctx, "loadGame: game is currently active, bailing out")
  1339  			return errors.New("game is active")
  1340  		}
  1341  		// Spawn off this error notification in a goroutine and only deliver it if the game is not active
  1342  		// after the timer
  1343  		summary = &flip.GameSummary{
  1344  			Err: err,
  1345  		}
  1346  		go func(ctx context.Context, summary *flip.GameSummary) {
  1347  			m.clock.Sleep(5 * time.Second)
  1348  			rawGame, ok := m.games.Get(job.gameID.FlipGameIDStr())
  1349  			if ok {
  1350  				status := rawGame.(chat1.UICoinFlipStatus)
  1351  				switch status.Phase {
  1352  				case chat1.UICoinFlipPhase_ERROR:
  1353  					// we'll send our error if there is an error on the screen
  1354  				default:
  1355  					// any other phase we will send nothing
  1356  					m.Debug(ctx, "loadGame: after pausing, we have a status in phase: %v", status.Phase)
  1357  					return
  1358  				}
  1359  			}
  1360  			m.Debug(ctx, "loadGame: game had no action after pausing, sending error")
  1361  			job.resCh <- m.handleSummaryUpdate(ctx, job.gameID, summary, flipConvID, true)
  1362  		}(globals.BackgroundChatCtx(ctx, m.G()), summary)
  1363  	} else {
  1364  		job.resCh <- m.handleSummaryUpdate(ctx, job.gameID, summary, flipConvID, true)
  1365  	}
  1366  	return nil
  1367  }
  1368  
  1369  func (m *FlipManager) loadGameLoop(shutdownCh chan struct{}) {
  1370  	for {
  1371  		select {
  1372  		case job := <-m.loadGameCh:
  1373  			ctx := m.makeBkgContext()
  1374  			if err := m.loadGame(ctx, job); err != nil {
  1375  				m.Debug(ctx, "loadGameLoop: failed to load game: %s", err)
  1376  			}
  1377  		case <-shutdownCh:
  1378  			return
  1379  		}
  1380  	}
  1381  }
  1382  
  1383  // LoadFlip implements the types.CoinFlipManager interface
  1384  func (m *FlipManager) LoadFlip(ctx context.Context, uid gregor1.UID, hostConvID chat1.ConversationID,
  1385  	hostMsgID chat1.MessageID, flipConvID chat1.ConversationID, gameID chat1.FlipGameID) (res chan chat1.UICoinFlipStatus, err chan error) {
  1386  	defer m.Trace(ctx, nil, "LoadFlip")()
  1387  	stored, ok := m.games.Get(gameID.FlipGameIDStr())
  1388  	if ok {
  1389  		switch stored.(chat1.UICoinFlipStatus).Phase {
  1390  		case chat1.UICoinFlipPhase_ERROR:
  1391  			// do nothing here, just replay if we are storing an error
  1392  		default:
  1393  			m.queueDirtyGameID(ctx, gameID, true)
  1394  			res = make(chan chat1.UICoinFlipStatus, 1)
  1395  			res <- stored.(chat1.UICoinFlipStatus)
  1396  			err = make(chan error, 1)
  1397  			return res, err
  1398  		}
  1399  	}
  1400  	// If we miss the in-memory game storage, attempt to replay the game
  1401  	job := loadGameJob{
  1402  		uid:        uid,
  1403  		hostConvID: hostConvID,
  1404  		hostMsgID:  hostMsgID,
  1405  		flipConvID: flipConvID,
  1406  		gameID:     gameID,
  1407  		resCh:      make(chan chat1.UICoinFlipStatus, 1),
  1408  		errCh:      make(chan error, 1),
  1409  	}
  1410  	select {
  1411  	case m.loadGameCh <- job:
  1412  	default:
  1413  		m.Debug(ctx, "LoadFlip: queue full: gameID: %s hostConvID %s flipConvID: %s", gameID, hostConvID,
  1414  			flipConvID)
  1415  		job.errCh <- errors.New("queue full")
  1416  	}
  1417  	return job.resCh, job.errCh
  1418  }
  1419  
  1420  func (m *FlipManager) IsFlipConversationCreated(ctx context.Context, outboxID chat1.OutboxID) (convID chat1.ConversationID, status types.FlipSendStatus) {
  1421  	defer m.Trace(ctx, nil, "IsFlipConversationCreated")()
  1422  	if rec, ok := m.flipConvs.Get(outboxID.String()); ok {
  1423  		status := rec.(startFlipSendStatus)
  1424  		switch status.status {
  1425  		case types.FlipSendStatusSent:
  1426  			convID = status.flipConvID
  1427  		default:
  1428  			// Nothing to do for other status types.
  1429  		}
  1430  		return convID, status.status
  1431  	}
  1432  	return convID, types.FlipSendStatusError
  1433  }
  1434  
  1435  // CLogf implements the flip.DealersHelper interface
  1436  func (m *FlipManager) CLogf(ctx context.Context, fmt string, args ...interface{}) {
  1437  	m.Debug(ctx, fmt, args...)
  1438  }
  1439  
  1440  // Clock implements the flip.DealersHelper interface
  1441  func (m *FlipManager) Clock() clockwork.Clock {
  1442  	return m.clock
  1443  }
  1444  
  1445  // ServerTime implements the flip.DealersHelper interface
  1446  func (m *FlipManager) ServerTime(ctx context.Context) (res time.Time, err error) {
  1447  	ctx = globals.ChatCtx(ctx, m.G(), keybase1.TLFIdentifyBehavior_CHAT_SKIP, nil, nil)
  1448  	defer m.Trace(ctx, &err, "ServerTime")()
  1449  	if m.testingServerClock != nil {
  1450  		return m.testingServerClock.Now(), nil
  1451  	}
  1452  	sres, err := m.ri().ServerNow(ctx)
  1453  	if err != nil {
  1454  		return res, err
  1455  	}
  1456  	return sres.Now.Time(), nil
  1457  }
  1458  
  1459  func (m *FlipManager) sendNonblock(ctx context.Context, initiatorUID gregor1.UID,
  1460  	convID chat1.ConversationID, text, tlfName string, outboxID chat1.OutboxID,
  1461  	gameID chat1.FlipGameID, topicType chat1.TopicType) error {
  1462  	sender := NewNonblockingSender(m.G(), NewBlockingSender(m.G(), NewBoxer(m.G()), m.ri))
  1463  	_, _, err := sender.Send(ctx, convID, chat1.MessagePlaintext{
  1464  		MessageBody: chat1.NewMessageBodyWithFlip(chat1.MessageFlip{
  1465  			Text:   text,
  1466  			GameID: gameID,
  1467  		}),
  1468  		ClientHeader: chat1.MessageClientHeader{
  1469  			TlfName:     tlfName,
  1470  			MessageType: chat1.MessageType_FLIP,
  1471  			Conv: chat1.ConversationIDTriple{
  1472  				TopicType: topicType,
  1473  			},
  1474  			// Prefill this value in case a restricted bot is running the flip
  1475  			// so bot keys are used instead of regular team keys.
  1476  			BotUID: &initiatorUID,
  1477  		},
  1478  	}, 0, &outboxID, nil, nil)
  1479  	return err
  1480  }
  1481  
  1482  func (m *FlipManager) isSentOutboxID(ctx context.Context, gameID chat1.FlipGameID, outboxID chat1.OutboxID) bool {
  1483  	m.gameOutboxIDMu.Lock()
  1484  	defer m.gameOutboxIDMu.Unlock()
  1485  	if omIface, ok := m.gameOutboxIDs.Get(gameID.FlipGameIDStr()); ok {
  1486  		om := omIface.(map[string]bool)
  1487  		return om[outboxID.String()]
  1488  	}
  1489  	return false
  1490  }
  1491  
  1492  func (m *FlipManager) registerSentOutboxID(ctx context.Context, gameID chat1.FlipGameID,
  1493  	outboxID chat1.OutboxID) {
  1494  	m.gameOutboxIDMu.Lock()
  1495  	defer m.gameOutboxIDMu.Unlock()
  1496  	var om map[string]bool
  1497  	if omIface, ok := m.gameOutboxIDs.Get(gameID.FlipGameIDStr()); ok {
  1498  		om = omIface.(map[string]bool)
  1499  	} else {
  1500  		om = make(map[string]bool)
  1501  	}
  1502  	om[outboxID.String()] = true
  1503  	m.gameOutboxIDs.Add(gameID.FlipGameIDStr(), om)
  1504  }
  1505  
  1506  // SendChat implements the flip.DealersHelper interface
  1507  func (m *FlipManager) SendChat(ctx context.Context, initatorUID gregor1.UID, convID chat1.ConversationID, gameID chat1.FlipGameID,
  1508  	msg flip.GameMessageEncoded) (err error) {
  1509  	ctx = globals.ChatCtx(ctx, m.G(), keybase1.TLFIdentifyBehavior_CHAT_SKIP, nil, nil)
  1510  	defer m.Trace(ctx, &err, "SendChat: convID: %s", convID)()
  1511  	uid, err := utils.AssertLoggedInUID(ctx, m.G())
  1512  	if err != nil {
  1513  		return err
  1514  	}
  1515  	conv, err := utils.GetVerifiedConv(ctx, m.G(), uid, convID, types.InboxSourceDataSourceAll)
  1516  	if err != nil {
  1517  		return err
  1518  	}
  1519  	outboxID, err := storage.NewOutboxID()
  1520  	if err != nil {
  1521  		return err
  1522  	}
  1523  	m.registerSentOutboxID(ctx, gameID, outboxID)
  1524  	return m.sendNonblock(ctx, initatorUID, convID, msg.String(), conv.Info.TlfName, outboxID, gameID,
  1525  		chat1.TopicType_DEV)
  1526  }
  1527  
  1528  // Me implements the flip.DealersHelper interface
  1529  func (m *FlipManager) Me() flip.UserDevice {
  1530  	ad := m.G().ActiveDevice
  1531  	did := ad.DeviceID()
  1532  	hdid := make([]byte, libkb.DeviceIDLen)
  1533  	if err := did.ToBytes(hdid); err != nil {
  1534  		return flip.UserDevice{}
  1535  	}
  1536  	return flip.UserDevice{
  1537  		U: gregor1.UID(ad.UID().ToBytes()),
  1538  		D: gregor1.DeviceID(hdid),
  1539  	}
  1540  }
  1541  
  1542  func (m *FlipManager) ShouldCommit(ctx context.Context) bool {
  1543  	if !m.G().IsMobileAppType() {
  1544  		should := m.G().DesktopAppState.AwakeAndUnlocked(m.G().MetaContext(ctx))
  1545  		if !should {
  1546  			m.Debug(ctx, "ShouldCommit -> false")
  1547  		}
  1548  		return should
  1549  	}
  1550  	return true
  1551  }
  1552  
  1553  // clearGameCache should only be used by tests
  1554  func (m *FlipManager) clearGameCache() {
  1555  	m.games.Purge()
  1556  }
  1557  
  1558  type FlipVisualizer struct {
  1559  	width, height         int
  1560  	commitmentColors      [256]color.RGBA
  1561  	secretColors          [256]color.RGBA
  1562  	commitmentMatchColors [256]color.RGBA
  1563  }
  1564  
  1565  func NewFlipVisualizer(width, height int) *FlipVisualizer {
  1566  	v := &FlipVisualizer{
  1567  		height: height, // 40
  1568  		width:  width,  // 64
  1569  	}
  1570  	for i := 0; i < 256; i++ {
  1571  		v.commitmentColors[i] = color.RGBA{
  1572  			R: uint8(i),
  1573  			G: uint8((128 + i*5) % 128),
  1574  			B: 255,
  1575  			A: 128,
  1576  		}
  1577  		v.secretColors[i] = color.RGBA{
  1578  			R: 255,
  1579  			G: uint8(64 + i/2),
  1580  			B: 0,
  1581  			A: 255,
  1582  		}
  1583  		v.commitmentMatchColors[i] = color.RGBA{
  1584  			R: uint8(i * 3 / 4),
  1585  			G: uint8((192 + i*4) % 64),
  1586  			B: 255,
  1587  			A: 255,
  1588  		}
  1589  	}
  1590  	return v
  1591  }
  1592  
  1593  func (v *FlipVisualizer) fillCell(img *image.NRGBA, x, y, cellHeight, cellWidth int, b byte,
  1594  	palette [256]color.RGBA) {
  1595  	for i := x; i < x+cellWidth; i++ {
  1596  		for j := y; j < y+cellHeight; j++ {
  1597  			img.Set(i, j, palette[b])
  1598  		}
  1599  	}
  1600  }
  1601  
  1602  func (v *FlipVisualizer) fillRow(img *image.NRGBA, startY, cellHeight, cellWidth int,
  1603  	source string, palette [256]color.RGBA) {
  1604  	b, _ := hex.DecodeString(source)
  1605  	x := 0
  1606  	for i := 0; i < len(b); i++ {
  1607  		v.fillCell(img, x, startY, cellHeight, cellWidth, b[i], palette)
  1608  		x += cellWidth
  1609  	}
  1610  }
  1611  
  1612  func (v *FlipVisualizer) Visualize(status *chat1.UICoinFlipStatus) {
  1613  	cellWidth := int(math.Round(float64(v.width) / 32.0))
  1614  	v.width = 32 * cellWidth
  1615  	commitmentImg := image.NewNRGBA(image.Rect(0, 0, v.width, v.height))
  1616  	secretImg := image.NewNRGBA(image.Rect(0, 0, v.width, v.height))
  1617  	numParts := len(status.Participants)
  1618  	if numParts > 0 {
  1619  		startY := 0
  1620  		// just add these next 2 things
  1621  		heightAccum := float64(0) // how far into the image we should be
  1622  		rawRowHeight := float64(v.height) / float64(numParts)
  1623  		for _, p := range status.Participants {
  1624  			heightAccum += rawRowHeight
  1625  			rowHeight := int(math.Round(heightAccum - float64(startY)))
  1626  			if rowHeight > 0 {
  1627  				if p.Reveal != nil {
  1628  					v.fillRow(commitmentImg, startY, rowHeight, cellWidth, p.Commitment,
  1629  						v.commitmentMatchColors)
  1630  					v.fillRow(secretImg, startY, rowHeight, cellWidth, *p.Reveal, v.secretColors)
  1631  				} else {
  1632  					v.fillRow(commitmentImg, startY, rowHeight, cellWidth, p.Commitment, v.commitmentColors)
  1633  				}
  1634  				startY += rowHeight
  1635  			}
  1636  		}
  1637  	}
  1638  	var commitmentBuf, secretBuf bytes.Buffer
  1639  	_ = png.Encode(&commitmentBuf, commitmentImg)
  1640  	_ = png.Encode(&secretBuf, secretImg)
  1641  	status.CommitmentVisualization = base64.StdEncoding.EncodeToString(commitmentBuf.Bytes())
  1642  	status.RevealVisualization = base64.StdEncoding.EncodeToString(secretBuf.Bytes())
  1643  }