github.com/keybase/client/go@v0.0.0-20241007131713-f10651d043c8/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 }