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