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