github.com/keybase/client/go@v0.0.0-20240309051027-028f7c731f8b/chat/commands/source.go (about)

     1  package commands
     2  
     3  import (
     4  	"context"
     5  	"errors"
     6  	"sort"
     7  	"strings"
     8  
     9  	"github.com/keybase/client/go/chat/globals"
    10  	"github.com/keybase/client/go/chat/types"
    11  	"github.com/keybase/client/go/chat/utils"
    12  	"github.com/keybase/client/go/libkb"
    13  	"github.com/keybase/client/go/protocol/chat1"
    14  	"github.com/keybase/client/go/protocol/gregor1"
    15  	"github.com/keybase/clockwork"
    16  )
    17  
    18  var ErrInvalidCommand = errors.New("invalid command")
    19  var ErrInvalidArguments = errors.New("invalid arguments")
    20  
    21  type Source struct {
    22  	globals.Contextified
    23  	utils.DebugLabeler
    24  
    25  	allCmds  map[int]types.ConversationCommand
    26  	builtins map[chat1.ConversationBuiltinCommandTyp][]types.ConversationCommand
    27  	botCmd   *Bot
    28  	clock    clockwork.Clock
    29  }
    30  
    31  func NewSource(g *globals.Context) *Source {
    32  	s := &Source{
    33  		Contextified: globals.NewContextified(g),
    34  		DebugLabeler: utils.NewDebugLabeler(g.ExternalG(), "Commands.Source", false),
    35  		clock:        clockwork.NewRealClock(),
    36  		botCmd:       NewBot(g),
    37  	}
    38  	s.makeBuiltins()
    39  	return s
    40  }
    41  
    42  const (
    43  	cmdAddEmoji int = iota
    44  	cmdCollapse
    45  	cmdExpand
    46  	cmdFlip
    47  	cmdGiphy
    48  	cmdHeadline
    49  	cmdHide
    50  	cmdJoin
    51  	cmdLeave
    52  	cmdLocation
    53  	cmdMe
    54  	cmdMsg
    55  	cmdMute
    56  	cmdShrug
    57  	cmdUnhide
    58  )
    59  
    60  func (s *Source) allCommands() (res map[int]types.ConversationCommand) {
    61  	res = make(map[int]types.ConversationCommand)
    62  	res[cmdAddEmoji] = NewAddEmoji(s.G())
    63  	res[cmdCollapse] = NewCollapse(s.G())
    64  	res[cmdExpand] = NewExpand(s.G())
    65  	res[cmdFlip] = NewFlip(s.G())
    66  	res[cmdGiphy] = NewGiphy(s.G())
    67  	res[cmdHeadline] = NewHeadline(s.G())
    68  	res[cmdHide] = NewHide(s.G())
    69  	res[cmdJoin] = NewJoin(s.G())
    70  	res[cmdLeave] = NewLeave(s.G())
    71  	res[cmdLocation] = NewLocation(s.G())
    72  	res[cmdMe] = NewMe(s.G())
    73  	res[cmdMsg] = NewMsg(s.G())
    74  	res[cmdMute] = NewMute(s.G())
    75  	res[cmdShrug] = NewShrug(s.G())
    76  	res[cmdUnhide] = NewUnhide(s.G())
    77  	return res
    78  }
    79  
    80  func (s *Source) makeBuiltins() {
    81  	s.allCmds = s.allCommands()
    82  	cmds := s.allCmds
    83  	common := []types.ConversationCommand{
    84  		cmds[cmdCollapse],
    85  		cmds[cmdExpand],
    86  		cmds[cmdFlip],
    87  		cmds[cmdGiphy],
    88  		cmds[cmdHeadline],
    89  		cmds[cmdHide],
    90  		cmds[cmdMe],
    91  		cmds[cmdMsg],
    92  		cmds[cmdMute],
    93  		cmds[cmdShrug],
    94  		cmds[cmdUnhide],
    95  		cmds[cmdAddEmoji],
    96  	}
    97  	if s.G().IsMobileAppType() || s.G().GetRunMode() == libkb.DevelRunMode {
    98  		common = append(common, cmds[cmdLocation])
    99  	}
   100  
   101  	s.builtins = make(map[chat1.ConversationBuiltinCommandTyp][]types.ConversationCommand)
   102  	s.builtins[chat1.ConversationBuiltinCommandTyp_ADHOC] = common
   103  	s.builtins[chat1.ConversationBuiltinCommandTyp_BIGTEAM] = append([]types.ConversationCommand{
   104  		cmds[cmdJoin],
   105  		cmds[cmdLeave],
   106  	}, common...)
   107  	s.builtins[chat1.ConversationBuiltinCommandTyp_BIGTEAMGENERAL] = append([]types.ConversationCommand{
   108  		cmds[cmdJoin],
   109  	}, common...)
   110  	s.builtins[chat1.ConversationBuiltinCommandTyp_SMALLTEAM] = append([]types.ConversationCommand{
   111  		cmds[cmdJoin],
   112  	}, common...)
   113  	for _, cmds := range s.builtins {
   114  		sort.Slice(cmds, func(i, j int) bool {
   115  			return cmds[i].Name() < cmds[j].Name()
   116  		})
   117  	}
   118  }
   119  
   120  func (s *Source) SetClock(clock clockwork.Clock) {
   121  	s.clock = clock
   122  	s.allCmds[cmdLocation].(*Location).SetClock(clock)
   123  }
   124  
   125  func (s *Source) GetBuiltins(ctx context.Context) (res []chat1.BuiltinCommandGroup) {
   126  	for typ, cmds := range s.builtins {
   127  		var exportCmds []chat1.ConversationCommand
   128  		for _, cmd := range cmds {
   129  			exportCmds = append(exportCmds, cmd.Export())
   130  		}
   131  		res = append(res, chat1.BuiltinCommandGroup{
   132  			Typ:      typ,
   133  			Commands: exportCmds,
   134  		})
   135  	}
   136  	sort.Slice(res, func(i, j int) bool {
   137  		return res[i].Typ < res[j].Typ
   138  	})
   139  	return res
   140  }
   141  
   142  func (s *Source) GetBuiltinCommandType(ctx context.Context, c types.ConversationCommandsSpec) chat1.ConversationBuiltinCommandTyp {
   143  	switch c.GetMembersType() {
   144  	case chat1.ConversationMembersType_TEAM:
   145  		switch c.GetTeamType() {
   146  		case chat1.TeamType_COMPLEX:
   147  			if c.GetTopicName() == globals.DefaultTeamTopic {
   148  				return chat1.ConversationBuiltinCommandTyp_BIGTEAMGENERAL
   149  			}
   150  			return chat1.ConversationBuiltinCommandTyp_BIGTEAM
   151  		default:
   152  			return chat1.ConversationBuiltinCommandTyp_SMALLTEAM
   153  		}
   154  	default:
   155  		return chat1.ConversationBuiltinCommandTyp_ADHOC
   156  	}
   157  }
   158  
   159  func (s *Source) ListCommands(ctx context.Context, uid gregor1.UID, conv types.ConversationCommandsSpec) (res chat1.ConversationCommandGroups, err error) {
   160  	defer s.Trace(ctx, &err, "ListCommands")()
   161  	return chat1.NewConversationCommandGroupsWithBuiltin(s.GetBuiltinCommandType(ctx, conv)), nil
   162  }
   163  
   164  func (s *Source) AttemptBuiltinCommand(ctx context.Context, uid gregor1.UID, convID chat1.ConversationID,
   165  	tlfName string, body chat1.MessageBody, replyTo *chat1.MessageID) (handled bool, err error) {
   166  	defer s.Trace(ctx, &err, "AttemptBuiltinCommand")()
   167  	if !body.IsType(chat1.MessageType_TEXT) {
   168  		return false, nil
   169  	}
   170  	text := body.Text().Body
   171  	if !strings.HasPrefix(text, "/") {
   172  		return false, nil
   173  	}
   174  	conv, err := getConvByID(ctx, s.G(), uid, convID)
   175  	if err != nil {
   176  		return false, err
   177  	}
   178  	typ := s.GetBuiltinCommandType(ctx, conv)
   179  	for _, cmd := range s.builtins[typ] {
   180  		if cmd.Match(ctx, text) {
   181  			s.Debug(ctx, "AttemptBuiltinCommand: matched command: %s, executing...", cmd.Name())
   182  			return true, cmd.Execute(ctx, uid, convID, tlfName, text, replyTo)
   183  		}
   184  	}
   185  	return false, nil
   186  }
   187  
   188  func (s *Source) PreviewBuiltinCommand(ctx context.Context, uid gregor1.UID, convID chat1.ConversationID,
   189  	tlfName, text string) {
   190  	defer s.Trace(ctx, nil, "PreviewBuiltinCommand")()
   191  
   192  	// always try bot command, it might do something and is mutually exclusive with the rest of this
   193  	// function
   194  	s.botCmd.Preview(ctx, uid, convID, tlfName, text)
   195  
   196  	// we let all strings through at this point, since we might need to clear a preview in a command
   197  	conv, err := getConvByID(ctx, s.G(), uid, convID)
   198  	if err != nil {
   199  		return
   200  	}
   201  	typ := s.GetBuiltinCommandType(ctx, conv)
   202  	for _, cmd := range s.builtins[typ] {
   203  		// Run preview on everything as long as it is a slash command
   204  		cmd.Preview(ctx, uid, convID, tlfName, text)
   205  	}
   206  }
   207  
   208  func (s *Source) isAdmin() bool { //nolint
   209  	username := s.G().GetEnv().GetUsername().String()
   210  	return admins[username]
   211  }
   212  
   213  var admins = map[string]bool{ //nolint
   214  	"mikem":         true,
   215  	"max":           true,
   216  	"candrencil64":  true,
   217  	"chris":         true,
   218  	"chrisnojima":   true,
   219  	"mlsteele":      true,
   220  	"xgess":         true,
   221  	"karenm":        true,
   222  	"kb_monbot":     true,
   223  	"joshblum":      true,
   224  	"cjb":           true,
   225  	"jzila":         true,
   226  	"patrick":       true,
   227  	"modalduality":  true,
   228  	"strib":         true,
   229  	"songgao":       true,
   230  	"ayoubd":        true,
   231  	"cecileb":       true,
   232  	"adamjspooner":  true,
   233  	"akalin":        true,
   234  	"marcopolo":     true,
   235  	"aimeedavid":    true,
   236  	"jinyang":       true,
   237  	"zapu":          true,
   238  	"jakob223":      true,
   239  	"taruti":        true,
   240  	"pzduniak":      true,
   241  	"zanderz":       true,
   242  	"giphy_tester":  true,
   243  	"candrencil983": true,
   244  	"candrencil889": true,
   245  	"candrencil911": true,
   246  }