1package flip 2 3import ( 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. 17type GameMessageEncoded string 18 19// GameMessageWrappedEncoded contains a sender, a gameID and a Body. The GameID should never be reused. 20type 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 27type CommitmentUpdate struct { 28 User UserDevice 29 Commitment Commitment 30} 31 32type 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. 40type 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. 52type 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. 65type 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. 70type 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 80func 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. 93func (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. 98func (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. 125func (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. 137func (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 144func (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. 156func (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. 183func 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. 191func 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. 199func 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. 207func NewStartWithShuffle(now time.Time, n int64, nPlayers int) Start { 208 ret := newStart(now, nPlayers) 209 ret.Params = NewFlipParametersWithShuffle(n) 210 return ret 211} 212 213func (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 220func (d *Dealer) HasActiveGames(ctx context.Context) bool { 221 d.Lock() 222 defer d.Unlock() 223 return len(d.gameIDs) > 0 224} 225