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