github.com/keybase/client/go@v0.0.0-20240309051027-028f7c731f8b/chat/botcommands_test.go (about) 1 package chat 2 3 import ( 4 "errors" 5 "testing" 6 "time" 7 8 "github.com/keybase/client/go/chat/types" 9 "github.com/keybase/client/go/chat/utils" 10 "github.com/keybase/client/go/libkb" 11 "github.com/keybase/client/go/protocol/chat1" 12 "github.com/keybase/client/go/protocol/gregor1" 13 "github.com/keybase/client/go/protocol/keybase1" 14 "github.com/stretchr/testify/require" 15 ) 16 17 type dummyUIRouter struct { 18 libkb.UIRouter 19 } 20 21 func (d dummyUIRouter) GetChatUI() (libkb.ChatUI, error) { 22 return nil, nil 23 } 24 25 func (d dummyUIRouter) Shutdown() {} 26 27 func TestBotCommandManager(t *testing.T) { 28 useRemoteMock = false 29 defer func() { useRemoteMock = true }() 30 ctc := makeChatTestContext(t, "TestBotCommandManager", 3) 31 defer ctc.cleanup() 32 33 timeout := 20 * time.Second 34 users := ctc.users() 35 listener := newServerChatListener() 36 ctc.as(t, users[0]).h.G().NotifyRouter.AddListener(listener) 37 tc := ctc.world.Tcs[users[0].Username] 38 tc.G.UIRouter = dummyUIRouter{} 39 tc1 := ctc.world.Tcs[users[1].Username] 40 tc1.G.UIRouter = dummyUIRouter{} 41 tc2 := ctc.world.Tcs[users[2].Username] 42 tc2.G.UIRouter = dummyUIRouter{} 43 ctx := ctc.as(t, users[0]).startCtx 44 ctx1 := ctc.as(t, users[1]).startCtx 45 ctx2 := ctc.as(t, users[2]).startCtx 46 uid := gregor1.UID(users[0].GetUID().ToBytes()) 47 botua := users[2] 48 t.Logf("uid: %s", uid) 49 listener0 := newServerChatListener() 50 ctc.as(t, users[0]).h.G().NotifyRouter.AddListener(listener0) 51 52 impConv := mustCreateConversationForTest(t, ctc, users[0], chat1.TopicType_CHAT, 53 chat1.ConversationMembersType_IMPTEAMNATIVE) 54 impConv1 := mustCreateConversationForTest(t, ctc, users[1], chat1.TopicType_CHAT, 55 chat1.ConversationMembersType_IMPTEAMNATIVE, users[0]) 56 t.Logf("impconv: %s", impConv.Id) 57 t.Logf("impconv1: %s", impConv1.Id) 58 teamConv := mustCreateConversationForTest(t, ctc, users[0], chat1.TopicType_CHAT, 59 chat1.ConversationMembersType_TEAM) 60 t.Logf("teamconv: %x", teamConv.Id.DbShortForm()) 61 62 // test public 63 alias := "MIKE BOT" 64 commands := []chat1.AdvertiseCommandsParam{ 65 { 66 Typ: chat1.BotCommandsAdvertisementTyp_PUBLIC, 67 Commands: []chat1.UserBotCommandInput{ 68 { 69 Name: "status", 70 Description: "get status", 71 Usage: "just type it", 72 }, 73 }, 74 }, 75 } 76 require.NoError(t, tc.Context().BotCommandManager.Advertise(ctx, &alias, commands)) 77 cmds, _, err := tc.Context().BotCommandManager.ListCommands(ctx, impConv.Id) 78 require.NoError(t, err) 79 require.Zero(t, len(cmds)) 80 81 errCh, err := tc.Context().BotCommandManager.UpdateCommands(ctx, impConv.Id, nil) 82 require.NoError(t, err) 83 84 errCh1, err := tc1.Context().BotCommandManager.UpdateCommands(ctx1, impConv1.Id, nil) 85 require.NoError(t, err) 86 87 readErrCh := func(errCh chan error) error { 88 select { 89 case err := <-errCh: 90 return err 91 case <-time.After(timeout): 92 return errors.New("timeout") 93 } 94 } 95 select { 96 case convID := <-listener0.convUpdate: 97 require.Equal(t, impConv.Id, convID) 98 case <-time.After(timeout): 99 require.Fail(t, "no stale") 100 } 101 impConvLocal, err := utils.GetVerifiedConv(ctx, tc.Context(), uid, impConv.Id, 102 types.InboxSourceDataSourceAll) 103 require.NoError(t, err) 104 typ, err := impConvLocal.BotCommands.Typ() 105 require.NoError(t, err) 106 require.Equal(t, chat1.ConversationCommandGroupsTyp_CUSTOM, typ) 107 require.Equal(t, 1, len(impConvLocal.BotCommands.Custom().Commands)) 108 require.Equal(t, "status", impConvLocal.BotCommands.Custom().Commands[0].Name) 109 require.NoError(t, readErrCh(errCh)) 110 cmds, _, err = tc.Context().BotCommandManager.ListCommands(ctx, impConv.Id) 111 require.NoError(t, err) 112 require.Equal(t, 1, len(cmds)) 113 require.Equal(t, "status", cmds[0].Name) 114 115 // make sure the cmds are cached 116 for i := 0; i < 5; i++ { 117 errCh, err = tc.Context().BotCommandManager.UpdateCommands(ctx, impConv.Id, nil) 118 require.NoError(t, err) 119 require.NoError(t, readErrCh(errCh)) 120 } 121 select { 122 case <-listener0.convUpdate: 123 require.Fail(t, "commands not cached") 124 default: 125 } 126 127 require.NoError(t, readErrCh(errCh1)) 128 cmds, _, err = tc1.Context().BotCommandManager.ListCommands(ctx1, impConv1.Id) 129 require.NoError(t, err) 130 require.Equal(t, 1, len(cmds)) 131 require.Equal(t, "status", cmds[0].Name) 132 133 // test team 134 teamID, err := keybase1.TeamIDFromString(teamConv.Triple.Tlfid.String()) 135 require.NoError(t, err) 136 pollForSeqno := func(expectedSeqno keybase1.Seqno) { 137 found := false 138 for !found { 139 select { 140 case teamChange := <-listener.teamChangedByID: 141 found = teamChange.TeamID == teamID && 142 teamChange.LatestSeqno == expectedSeqno 143 case <-time.After(20 * time.Second): 144 require.Fail(t, "no event received") 145 } 146 } 147 } 148 149 commands = append(commands, chat1.AdvertiseCommandsParam{ 150 Typ: chat1.BotCommandsAdvertisementTyp_TLFID_CONVS, 151 Commands: []chat1.UserBotCommandInput{{ 152 Name: "teamconvonly", 153 }}, 154 TeamName: &teamConv.TlfName, 155 }, chat1.AdvertiseCommandsParam{ 156 Typ: chat1.BotCommandsAdvertisementTyp_TLFID_MEMBERS, 157 Commands: []chat1.UserBotCommandInput{{ 158 Name: "teammembsonly", 159 }}, 160 TeamName: &teamConv.TlfName, 161 }, chat1.AdvertiseCommandsParam{ 162 Typ: chat1.BotCommandsAdvertisementTyp_CONV, 163 Commands: []chat1.UserBotCommandInput{{ 164 Name: "conv", 165 }}, 166 ConvID: &teamConv.Id, 167 }) 168 require.NoError(t, tc.Context().BotCommandManager.Advertise(ctx, &alias, commands)) 169 170 errCh, err = tc.Context().BotCommandManager.UpdateCommands(ctx, impConv.Id, nil) 171 require.NoError(t, err) 172 require.NoError(t, readErrCh(errCh)) 173 174 errChT, err := tc.Context().BotCommandManager.UpdateCommands(ctx, teamConv.Id, nil) 175 require.NoError(t, err) 176 require.NoError(t, readErrCh(errChT)) 177 178 errCh1, err = tc1.Context().BotCommandManager.UpdateCommands(ctx, impConv1.Id, nil) 179 require.NoError(t, err) 180 require.NoError(t, readErrCh(errCh1)) 181 182 cmds, _, err = tc.Context().BotCommandManager.ListCommands(ctx, impConv.Id) 183 require.NoError(t, err) 184 require.Equal(t, 2, len(cmds)) 185 cmds, _, err = tc.Context().BotCommandManager.ListCommands(ctx, teamConv.Id) 186 require.NoError(t, err) 187 require.Equal(t, 4, len(cmds)) 188 cmds, _, err = tc1.Context().BotCommandManager.ListCommands(ctx1, impConv1.Id) 189 require.NoError(t, err) 190 require.Equal(t, 1, len(cmds)) 191 192 err = ctc.as(t, users[0]).chatLocalHandler().AddBotMember(ctx, chat1.AddBotMemberArg{ 193 ConvID: teamConv.Id, 194 Username: botua.Username, 195 Role: keybase1.TeamRole_RESTRICTEDBOT, 196 BotSettings: &keybase1.TeamBotSettings{Cmds: true}, 197 }) 198 require.NoError(t, err) 199 require.NoError(t, tc2.Context().BotCommandManager.Advertise(ctx2, &alias, commands)) 200 pollForSeqno(3) 201 202 errChT, err = tc.Context().BotCommandManager.UpdateCommands(ctx, teamConv.Id, nil) 203 require.NoError(t, err) 204 require.NoError(t, readErrCh(errChT)) 205 206 cmds, _, err = tc.Context().BotCommandManager.ListCommands(ctx, teamConv.Id) 207 require.NoError(t, err) 208 require.Equal(t, 8, len(cmds)) 209 210 // impteams can't advertise on team types 211 commands = append(commands, chat1.AdvertiseCommandsParam{ 212 Typ: chat1.BotCommandsAdvertisementTyp_TLFID_CONVS, 213 Commands: []chat1.UserBotCommandInput{{ 214 Name: "teamconvonly_implicit", 215 }}, 216 TeamName: &impConv.TlfName, 217 }) 218 require.Error(t, tc.Context().BotCommandManager.Advertise(ctx, &alias, commands)) 219 }