github.com/keybase/client/go@v0.0.0-20240309051027-028f7c731f8b/chat/flip/flip.go (about) 1 package flip 2 3 import ( 4 "context" 5 "io" 6 "math/big" 7 "sync" 8 "time" 9 10 chat1 "github.com/keybase/client/go/protocol/chat1" 11 gregor1 "github.com/keybase/client/go/protocol/gregor1" 12 clockwork "github.com/keybase/clockwork" 13 ) 14 15 // GameMessageEncoded is a game message that is shipped over the chat channel. Inside, it's a base64-encoded 16 // msgpack object (generated via AVDL->go compiler), but it's safe to think of it just as an opaque string. 17 type GameMessageEncoded string 18 19 // GameMessageWrappedEncoded contains a sender, a gameID and a Body. The GameID should never be reused. 20 type GameMessageWrappedEncoded struct { 21 Sender UserDevice 22 GameID chat1.FlipGameID // the game ID of this game, also specified (encoded) in GameMessageEncoded 23 Body GameMessageEncoded // base64-encoded GameMessaageBody that comes in over chat 24 FirstInConversation bool // on if this is the first message in the conversation 25 } 26 27 type CommitmentUpdate struct { 28 User UserDevice 29 Commitment Commitment 30 } 31 32 type RevealUpdate struct { 33 User UserDevice 34 Reveal Secret 35 } 36 37 // GameStateUpdateMessage is sent from the game dealer out to the calling chat client, to update him 38 // on changes to game state that happened. All update messages are relative to the given GameMetadata. 39 // For each update, only one of Err, Commitment, Reveal, CommitmentComplete or Result will be non-nil. 40 type GameStateUpdateMessage struct { 41 Metadata GameMetadata 42 // only one of the following will be non-nil 43 Err error 44 Commitment *CommitmentUpdate 45 Reveal *RevealUpdate 46 CommitmentComplete *CommitmentComplete 47 Result *Result 48 } 49 50 // Dealer is a peristent process that runs in the chat client that deals out a game. It can have multiple 51 // games running at once. 52 type Dealer struct { 53 sync.Mutex 54 dh DealersHelper 55 games map[GameKey](chan<- *GameMessageWrapped) 56 gameIDs map[GameIDKey]GameMetadata 57 shutdownMu sync.Mutex 58 shutdownCh chan struct{} 59 chatInputCh chan *GameMessageWrapped 60 gameUpdateCh chan GameStateUpdateMessage 61 previousGames map[GameIDKey]bool 62 } 63 64 // ReplayHelper contains hooks needed to replay a flip. 65 type ReplayHelper interface { 66 CLogf(ctx context.Context, fmt string, args ...interface{}) 67 } 68 69 // DealersHelper is an interface that calling chat clients need to implement. 70 type DealersHelper interface { 71 ReplayHelper 72 Clock() clockwork.Clock 73 ServerTime(context.Context) (time.Time, error) 74 SendChat(ctx context.Context, initiatorUID gregor1.UID, ch chat1.ConversationID, gameID chat1.FlipGameID, msg GameMessageEncoded) error 75 Me() UserDevice 76 ShouldCommit(ctx context.Context) bool // Whether to send new commitments for games. 77 } 78 79 // NewDealer makes a new Dealer with a given DealersHelper 80 func NewDealer(dh DealersHelper) *Dealer { 81 return &Dealer{ 82 dh: dh, 83 games: make(map[GameKey](chan<- *GameMessageWrapped)), 84 gameIDs: make(map[GameIDKey]GameMetadata), 85 chatInputCh: make(chan *GameMessageWrapped), 86 gameUpdateCh: make(chan GameStateUpdateMessage, 500), 87 previousGames: make(map[GameIDKey]bool), 88 } 89 } 90 91 // UpdateCh returns a channel that sends a sequence of GameStateUpdateMessages, each notifying the 92 // UI about changes to ongoing games. 93 func (d *Dealer) UpdateCh() <-chan GameStateUpdateMessage { 94 return d.gameUpdateCh 95 } 96 97 // Run a dealer in a given context. It will run as long as it isn't shutdown. 98 func (d *Dealer) Run(ctx context.Context) error { 99 d.shutdownMu.Lock() 100 shutdownCh := make(chan struct{}) 101 d.shutdownCh = shutdownCh 102 d.shutdownMu.Unlock() 103 for { 104 select { 105 106 case <-ctx.Done(): 107 return ctx.Err() 108 109 // This channel never closes 110 case msg := <-d.chatInputCh: 111 err := d.handleMessage(ctx, msg) 112 if err != nil { 113 d.dh.CLogf(ctx, "Error reading message: %s", err.Error()) 114 } 115 116 // exit the loop if we've shutdown 117 case <-shutdownCh: 118 return io.EOF 119 120 } 121 } 122 } 123 124 // Stop a dealer on process shutdown. 125 func (d *Dealer) Stop() { 126 d.shutdownMu.Lock() 127 if d.shutdownCh != nil { 128 close(d.shutdownCh) 129 d.shutdownCh = nil 130 } 131 d.shutdownMu.Unlock() 132 d.stopGames() 133 } 134 135 // StartFlip starts a new flip. Pass it some start parameters as well as a chat conversationID that it 136 // will take place in. 137 func (d *Dealer) StartFlip(ctx context.Context, start Start, conversationID chat1.ConversationID) (err error) { 138 _, err = d.startFlip(ctx, start, conversationID) 139 return err 140 } 141 142 // StartFlipWithGameID starts a new flip. Pass it some start parameters as well as a chat conversationID 143 // that it will take place in. Also takes a GameID 144 func (d *Dealer) StartFlipWithGameID(ctx context.Context, start Start, conversationID chat1.ConversationID, 145 gameID chat1.FlipGameID) (err error) { 146 _, err = d.startFlipWithGameID(ctx, start, conversationID, gameID) 147 return err 148 } 149 150 // InjectIncomingChat should be called whenever a new flip game comes in that's relevant for flips. 151 // Call this with the sender's information, the channel information, and the body data that came in. 152 // The last bool is true only if this is the first message in the channel. The current model is that only 153 // one "game" is allowed for each chat channel. So any prior messages in the channel mean it might be replay. 154 // This is significantly less general than an earlier model, which is why we introduced the concept of 155 // a gameID, so it might be changed in the future. 156 func (d *Dealer) InjectIncomingChat(ctx context.Context, sender UserDevice, 157 conversationID chat1.ConversationID, gameID chat1.FlipGameID, body GameMessageEncoded, 158 firstInConversation bool) error { 159 gmwe := GameMessageWrappedEncoded{ 160 Sender: sender, 161 GameID: gameID, 162 Body: body, 163 FirstInConversation: firstInConversation, 164 } 165 msg, err := gmwe.Decode() 166 if err != nil { 167 return err 168 } 169 if !msg.Msg.Md.ConversationID.Eq(conversationID) { 170 return BadChannelError{G: msg.Msg.Md, C: conversationID} 171 } 172 if !msg.isForwardable() { 173 return UnforwardableMessageError{G: msg.Msg.Md} 174 } 175 if !msg.Msg.Md.GameID.Eq(gameID) { 176 return BadGameIDError{G: msg.Msg.Md, I: gameID} 177 } 178 d.chatInputCh <- msg 179 return nil 180 } 181 182 // NewStartWithBool makes new start parameters that yield a coinflip game. 183 func NewStartWithBool(now time.Time, nPlayers int) Start { 184 ret := newStart(now, nPlayers) 185 ret.Params = NewFlipParametersWithBool() 186 return ret 187 } 188 189 // NewStartWithInt makes new start parameters that yield a coinflip game that picks an int between 190 // 0 and mod. 191 func NewStartWithInt(now time.Time, mod int64, nPlayers int) Start { 192 ret := newStart(now, nPlayers) 193 ret.Params = NewFlipParametersWithInt(mod) 194 return ret 195 } 196 197 // NewStartWithBigInt makes new start parameters that yield a coinflip game that picks big int between 198 // 0 and mod. 199 func NewStartWithBigInt(now time.Time, mod *big.Int, nPlayers int) Start { 200 ret := newStart(now, nPlayers) 201 ret.Params = NewFlipParametersWithBig(mod.Bytes()) 202 return ret 203 } 204 205 // NewStartWithShuffle makes new start parameters for a coinflip that randomly permutes the numbers 206 // between 0 and n, exclusive. This can be used to shuffle an array of names. 207 func NewStartWithShuffle(now time.Time, n int64, nPlayers int) Start { 208 ret := newStart(now, nPlayers) 209 ret.Params = NewFlipParametersWithShuffle(n) 210 return ret 211 } 212 213 func (d *Dealer) IsGameActive(ctx context.Context, conversationID chat1.ConversationID, gameID chat1.FlipGameID) bool { 214 d.Lock() 215 defer d.Unlock() 216 md, found := d.gameIDs[GameIDToKey(gameID)] 217 return found && md.ConversationID.Eq(conversationID) 218 } 219 220 func (d *Dealer) HasActiveGames(ctx context.Context) bool { 221 d.Lock() 222 defer d.Unlock() 223 return len(d.gameIDs) > 0 224 }