github.com/keybase/client/go@v0.0.0-20241007131713-f10651d043c8/chat/emojisource_test.go (about) 1 package chat 2 3 import ( 4 "context" 5 "encoding/base64" 6 "encoding/json" 7 "net/http" 8 "os" 9 "regexp" 10 "strings" 11 "testing" 12 "time" 13 14 "github.com/keybase/client/go/kbtest" 15 16 "github.com/keybase/client/go/chat/attachments" 17 "github.com/keybase/client/go/chat/types" 18 "github.com/keybase/client/go/chat/utils" 19 "github.com/keybase/client/go/kbhttp/manager" 20 "github.com/keybase/client/go/protocol/chat1" 21 "github.com/keybase/client/go/protocol/gregor1" 22 "github.com/stretchr/testify/require" 23 ) 24 25 var decorateBegin = "$>kb$" 26 var decorateEnd = "$<kb$" 27 28 func checkEmoji(ctx context.Context, t *testing.T, tc *kbtest.ChatTestContext, 29 uid gregor1.UID, conv chat1.ConversationInfoLocal, msgID chat1.MessageID, emoji string) { 30 msg, err := tc.Context().ConvSource.GetMessage(ctx, conv.Id, uid, msgID, nil, nil, true) 31 require.NoError(t, err) 32 require.True(t, msg.IsValid()) 33 require.Equal(t, 1, len(msg.Valid().Emojis)) 34 require.Equal(t, emoji, msg.Valid().Emojis[0].Alias) 35 uimsg := utils.PresentMessageUnboxed(ctx, tc.Context(), msg, uid, conv.Id) 36 require.True(t, uimsg.IsValid()) 37 require.NotNil(t, uimsg.Valid().DecoratedTextBody) 38 checker := regexp.MustCompile(utils.ServiceDecorationPrefix) 39 require.True(t, checker.Match([]byte(*uimsg.Valid().DecoratedTextBody))) 40 payload := strings.ReplaceAll(*uimsg.Valid().DecoratedTextBody, decorateBegin, "") 41 payload = strings.ReplaceAll(payload, decorateEnd, "") 42 t.Logf("payload: %s", payload) 43 dat, err := base64.StdEncoding.DecodeString(payload) 44 require.NoError(t, err) 45 var dec chat1.UITextDecoration 46 require.NoError(t, json.Unmarshal(dat, &dec)) 47 typ, err := dec.Typ() 48 require.NoError(t, err) 49 require.Equal(t, chat1.UITextDecorationTyp_EMOJI, typ) 50 require.True(t, dec.Emoji().Source.IsHTTPSrv()) 51 resp, err := http.Get(dec.Emoji().Source.Httpsrv()) 52 require.NoError(t, err) 53 defer resp.Body.Close() 54 require.Equal(t, http.StatusOK, resp.StatusCode) 55 } 56 57 func TestEmojiSourceBasic(t *testing.T) { 58 useRemoteMock = false 59 defer func() { useRemoteMock = true }() 60 ctc := makeChatTestContext(t, "TestEmojiSourceBasic", 1) 61 defer ctc.cleanup() 62 63 users := ctc.users() 64 uid := users[0].User.GetUID().ToBytes() 65 tc := ctc.world.Tcs[users[0].Username] 66 ctx := ctc.as(t, users[0]).startCtx 67 ri := ctc.as(t, users[0]).ri 68 tc.ChatG.AttachmentURLSrv = NewAttachmentHTTPSrv(tc.Context(), 69 manager.NewSrv(tc.Context().ExternalG()), 70 types.DummyAttachmentFetcher{}, 71 func() chat1.RemoteInterface { return ri }) 72 store := attachments.NewStoreTesting(tc.Context(), nil) 73 uploader := attachments.NewUploader(tc.Context(), store, mockSigningRemote{}, 74 func() chat1.RemoteInterface { return ri }, 1) 75 tc.ChatG.AttachmentUploader = uploader 76 filename := "./testdata/party_parrot.gif" 77 tc.ChatG.EmojiSource.(*DevConvEmojiSource).tempDir = os.TempDir() 78 79 conv := mustCreateConversationForTest(t, ctc, users[0], chat1.TopicType_CHAT, 80 chat1.ConversationMembersType_IMPTEAMNATIVE) 81 82 t.Logf("admin") 83 source, err := tc.Context().EmojiSource.Add(ctx, uid, conv.Id, "party_parrot", filename, false) 84 require.NoError(t, err) 85 _, err = tc.Context().EmojiSource.Add(ctx, uid, conv.Id, "+1", filename, false) 86 require.NoError(t, err) 87 88 teamConv := mustCreateConversationForTest(t, ctc, users[0], chat1.TopicType_CHAT, 89 chat1.ConversationMembersType_TEAM) 90 91 _, err = tc.Context().EmojiSource.Add(ctx, uid, teamConv.Id, "mike2", filename, false) 92 require.NoError(t, err) 93 _, err = tc.Context().EmojiSource.Add(ctx, uid, teamConv.Id, "party_parrot2", filename, false) 94 require.NoError(t, err) 95 96 res, err := tc.Context().EmojiSource.Get(ctx, uid, nil, chat1.EmojiFetchOpts{ 97 GetCreationInfo: true, 98 GetAliases: true, 99 }) 100 require.NoError(t, err) 101 require.Equal(t, 2, len(res.Emojis)) 102 for _, group := range res.Emojis { 103 require.True(t, group.Name == conv.TlfName || group.Name == teamConv.TlfName) 104 require.Equal(t, 2, len(group.Emojis)) 105 for _, emoji := range group.Emojis { 106 require.True(t, emoji.Alias == "+1#2" || emoji.Alias == "party_parrot" || 107 emoji.Alias == "mike2" || emoji.Alias == "party_parrot2", emoji.Alias) 108 styp, err := emoji.Source.Typ() 109 require.NoError(t, err) 110 require.Equal(t, chat1.EmojiLoadSourceTyp_HTTPSRV, styp) 111 require.NotZero(t, len(emoji.Source.Httpsrv())) 112 } 113 } 114 115 t.Logf("decorate") 116 msgID := mustPostLocalForTest(t, ctc, users[0], conv, chat1.NewMessageBodyWithText(chat1.MessageText{ 117 Body: ":party_parrot:", 118 })) 119 checkEmoji(ctx, t, tc, uid, conv, msgID, "party_parrot") 120 121 t.Logf("remove") 122 _, err = tc.Context().ConvSource.GetMessage(ctx, source.Message().ConvID, uid, source.Message().MsgID, 123 nil, nil, true) 124 require.NoError(t, err) 125 require.NoError(t, tc.Context().EmojiSource.Remove(ctx, uid, conv.Id, "party_parrot")) 126 require.True(t, source.IsMessage()) 127 _, err = tc.Context().ConvSource.GetMessage(ctx, source.Message().ConvID, uid, source.Message().MsgID, 128 nil, nil, true) 129 require.Error(t, err) 130 res, err = tc.Context().EmojiSource.Get(ctx, uid, &conv.Id, chat1.EmojiFetchOpts{ 131 GetCreationInfo: true, 132 GetAliases: true, 133 }) 134 require.NoError(t, err) 135 checked := false 136 for _, group := range res.Emojis { 137 if group.Name == conv.TlfName { 138 require.Equal(t, 1, len(group.Emojis)) 139 checked = true 140 } 141 } 142 require.True(t, checked) 143 144 t.Logf("alias") 145 _, err = tc.Context().EmojiSource.AddAlias(ctx, uid, conv.Id, "mike2", "+1") 146 require.NoError(t, err) 147 res, err = tc.Context().EmojiSource.Get(ctx, uid, &conv.Id, chat1.EmojiFetchOpts{ 148 GetCreationInfo: true, 149 GetAliases: true, 150 }) 151 require.NoError(t, err) 152 checked = false 153 for _, group := range res.Emojis { 154 if group.Name == conv.TlfName { 155 require.Equal(t, 2, len(group.Emojis)) 156 checked = true 157 } 158 } 159 require.True(t, checked) 160 msgID = mustPostLocalForTest(t, ctc, users[0], conv, chat1.NewMessageBodyWithText(chat1.MessageText{ 161 Body: ":mike2:", 162 })) 163 checkEmoji(ctx, t, tc, uid, conv, msgID, "mike2") 164 require.NoError(t, tc.Context().EmojiSource.Remove(ctx, uid, conv.Id, "mike2")) 165 res, err = tc.Context().EmojiSource.Get(ctx, uid, &conv.Id, chat1.EmojiFetchOpts{ 166 GetCreationInfo: true, 167 GetAliases: true, 168 }) 169 require.NoError(t, err) 170 checked = false 171 for _, group := range res.Emojis { 172 if group.Name == conv.TlfName { 173 t.Logf("emojis: %+v", group.Emojis) 174 require.Equal(t, 1, len(group.Emojis)) 175 checked = true 176 } 177 } 178 require.True(t, checked) 179 msgID = mustPostLocalForTest(t, ctc, users[0], conv, chat1.NewMessageBodyWithText(chat1.MessageText{ 180 Body: ":+1#2:", 181 })) 182 checkEmoji(ctx, t, tc, uid, conv, msgID, "+1#2") 183 _, err = tc.Context().EmojiSource.AddAlias(ctx, uid, conv.Id, "mike2", "+1") 184 require.NoError(t, err) 185 require.NoError(t, tc.Context().EmojiSource.Remove(ctx, uid, conv.Id, "+1")) 186 res, err = tc.Context().EmojiSource.Get(ctx, uid, &conv.Id, chat1.EmojiFetchOpts{ 187 GetCreationInfo: true, 188 GetAliases: true, 189 }) 190 require.NoError(t, err) 191 checked = false 192 for _, group := range res.Emojis { 193 if group.Name == conv.TlfName { 194 require.Zero(t, len(group.Emojis)) 195 checked = true 196 } 197 } 198 require.True(t, checked) 199 200 t.Logf("stock alias") 201 _, err = tc.Context().EmojiSource.AddAlias(ctx, uid, conv.Id, ":my+1:", ":+1::skin-tone-0:") 202 require.NoError(t, err) 203 res, err = tc.Context().EmojiSource.Get(ctx, uid, &conv.Id, chat1.EmojiFetchOpts{ 204 GetCreationInfo: true, 205 GetAliases: true, 206 }) 207 require.NoError(t, err) 208 checked = false 209 for _, group := range res.Emojis { 210 if group.Name == conv.TlfName { 211 require.Len(t, group.Emojis, 1) 212 emoji := group.Emojis[0] 213 require.Equal(t, chat1.Emoji{ 214 Alias: ":my+1:", 215 IsBig: false, 216 IsReacji: false, 217 IsCrossTeam: false, 218 IsAlias: true, 219 Teamname: &conv.TlfName, 220 Source: chat1.NewEmojiLoadSourceWithStr(":+1::skin-tone-0:"), 221 NoAnimSource: chat1.NewEmojiLoadSourceWithStr(":+1::skin-tone-0:"), 222 RemoteSource: chat1.NewEmojiRemoteSourceWithStockalias(chat1.EmojiStockAlias{ 223 Text: ":+1::skin-tone-0:", 224 Username: users[0].Username, 225 Time: gregor1.ToTime(ctc.world.Fc.Now()), 226 }), 227 CreationInfo: &chat1.EmojiCreationInfo{ 228 Username: users[0].Username, 229 Time: gregor1.ToTime(ctc.world.Fc.Now()), 230 }, 231 }, emoji) 232 checked = true 233 } 234 } 235 require.True(t, checked) 236 } 237 238 type emojiAliasTestCase struct { 239 input, output string 240 emojis []chat1.HarvestedEmoji 241 } 242 243 func TestEmojiSourceAliasDecorate(t *testing.T) { 244 useRemoteMock = false 245 defer func() { useRemoteMock = true }() 246 ctc := makeChatTestContext(t, "TestEmojiSourceAliasDecorate", 1) 247 defer ctc.cleanup() 248 249 users := ctc.users() 250 uid := users[0].User.GetUID().ToBytes() 251 tc := ctc.world.Tcs[users[0].Username] 252 ctx := ctc.as(t, users[0]).startCtx 253 254 source := tc.Context().EmojiSource.(*DevConvEmojiSource) 255 testCases := []emojiAliasTestCase{ 256 { 257 input: "this is a test! :my+1: <- thumbs up", 258 output: "this is a test! :+1::skin-tone-0: <- thumbs up", 259 emojis: []chat1.HarvestedEmoji{ 260 { 261 Alias: "my+1", 262 Source: chat1.NewEmojiRemoteSourceWithStockalias(chat1.EmojiStockAlias{ 263 Text: ":+1::skin-tone-0:", 264 }), 265 }}, 266 }, 267 { 268 input: ":my+1: <- :nothing: dksjdksdj :: :alias:", 269 output: ":+1::skin-tone-0: <- :nothing: dksjdksdj :: :karen:", 270 emojis: []chat1.HarvestedEmoji{ 271 { 272 Alias: "my+1", 273 Source: chat1.NewEmojiRemoteSourceWithStockalias(chat1.EmojiStockAlias{ 274 Text: ":+1::skin-tone-0:", 275 }), 276 }, 277 { 278 Alias: "alias", 279 Source: chat1.NewEmojiRemoteSourceWithStockalias(chat1.EmojiStockAlias{ 280 Text: ":karen:", 281 }), 282 }, 283 }, 284 }, 285 { 286 input: ":nothing: dskjdksdjs ::: :my+1: <- :nothing: dksjdksdj :: :alias: !!", 287 output: ":nothing: dskjdksdjs ::: :+1::skin-tone-0: <- :nothing: dksjdksdj :: :karen: !!", 288 emojis: []chat1.HarvestedEmoji{ 289 { 290 Alias: "my+1", 291 Source: chat1.NewEmojiRemoteSourceWithStockalias(chat1.EmojiStockAlias{ 292 Text: ":+1::skin-tone-0:", 293 }), 294 }, 295 { 296 Alias: "alias", 297 Source: chat1.NewEmojiRemoteSourceWithStockalias(chat1.EmojiStockAlias{ 298 Text: ":karen:", 299 }), 300 }, 301 }, 302 }, 303 } 304 for _, testCase := range testCases { 305 output := source.Decorate(ctx, testCase.input, uid, chat1.MessageType_TEXT, testCase.emojis) 306 require.Equal(t, testCase.output, output) 307 } 308 } 309 310 func TestEmojiSourceCrossTeam(t *testing.T) { 311 useRemoteMock = false 312 defer func() { useRemoteMock = true }() 313 ctc := makeChatTestContext(t, "TestEmojiSourceCrossTeam", 4) 314 defer ctc.cleanup() 315 316 users := ctc.users() 317 uid := users[0].User.GetUID().ToBytes() 318 uid1 := gregor1.UID(users[1].User.GetUID().ToBytes()) 319 tc := ctc.world.Tcs[users[0].Username] 320 tc1 := ctc.world.Tcs[users[1].Username] 321 ctx := ctc.as(t, users[0]).startCtx 322 ctx1 := ctc.as(t, users[1]).startCtx 323 ri := ctc.as(t, users[0]).ri 324 ri1 := ctc.as(t, users[1]).ri 325 store := attachments.NewStoreTesting(tc.Context(), nil) 326 fetcher := NewRemoteAttachmentFetcher(tc.Context(), store) 327 source := tc.Context().EmojiSource.(*DevConvEmojiSource) 328 source1 := tc1.Context().EmojiSource.(*DevConvEmojiSource) 329 source.tempDir = os.TempDir() 330 source1.tempDir = os.TempDir() 331 syncCreated := make(chan struct{}, 10) 332 syncRefresh := make(chan struct{}, 10) 333 source.testingCreatedSyncConv = syncCreated 334 source.testingRefreshedSyncConv = syncRefresh 335 timeout := 2 * time.Second 336 expectCreated := func(expect bool) { 337 if expect { 338 select { 339 case <-syncCreated: 340 case <-time.After(timeout): 341 require.Fail(t, "no sync created") 342 } 343 } else { 344 time.Sleep(100 * time.Millisecond) 345 select { 346 case <-syncCreated: 347 require.Fail(t, "no sync created expected") 348 default: 349 } 350 } 351 } 352 expectRefresh := func(expect bool) { 353 if expect { 354 select { 355 case <-syncRefresh: 356 case <-time.After(timeout): 357 require.Fail(t, "no sync refresh") 358 } 359 } else { 360 time.Sleep(100 * time.Millisecond) 361 select { 362 case <-syncRefresh: 363 require.Fail(t, "no sync refresh expected") 364 default: 365 } 366 } 367 } 368 369 tc.ChatG.AttachmentURLSrv = NewAttachmentHTTPSrv(tc.Context(), 370 manager.NewSrv(tc.Context().ExternalG()), 371 fetcher, func() chat1.RemoteInterface { return ri }) 372 tc1.ChatG.AttachmentURLSrv = NewAttachmentHTTPSrv(tc1.Context(), 373 manager.NewSrv(tc1.Context().ExternalG()), 374 fetcher, func() chat1.RemoteInterface { return ri1 }) 375 uploader := attachments.NewUploader(tc.Context(), store, mockSigningRemote{}, 376 func() chat1.RemoteInterface { return ri }, 1) 377 tc.ChatG.AttachmentUploader = uploader 378 filename := "./testdata/party_parrot.gif" 379 t.Logf("uid1: %s", uid1) 380 381 aloneConv := mustCreateConversationForTest(t, ctc, users[0], chat1.TopicType_CHAT, 382 chat1.ConversationMembersType_TEAM) 383 sharedConv := mustCreateConversationForTest(t, ctc, users[0], chat1.TopicType_CHAT, 384 chat1.ConversationMembersType_TEAM, users[1], users[3]) 385 sharedConv2 := mustCreateConversationForTest(t, ctc, users[0], chat1.TopicType_CHAT, 386 chat1.ConversationMembersType_TEAM, users[2], users[3]) 387 388 t.Logf("basic") 389 _, err := tc.Context().EmojiSource.Add(ctx, uid, aloneConv.Id, "party_parrot", filename, false) 390 require.NoError(t, err) 391 _, err = tc.Context().EmojiSource.Add(ctx, uid, aloneConv.Id, "success-kid", filename, false) 392 require.NoError(t, err) 393 _, err = tc.Context().EmojiSource.Add(ctx, uid, sharedConv2.Id, "mike", filename, false) 394 require.NoError(t, err) 395 _, err = tc.Context().EmojiSource.Add(ctx, uid, sharedConv2.Id, "rock", filename, false) 396 require.NoError(t, err) 397 398 msgID := mustPostLocalForTest(t, ctc, users[0], sharedConv, chat1.NewMessageBodyWithText(chat1.MessageText{ 399 Body: ":party_parrot:", 400 })) 401 checkEmoji(ctx1, t, tc1, uid1, sharedConv, msgID, "party_parrot") 402 expectCreated(true) 403 expectRefresh(true) 404 msgID = mustPostLocalForTest(t, ctc, users[0], sharedConv, chat1.NewMessageBodyWithText(chat1.MessageText{ 405 Body: ":success-kid:", 406 })) 407 checkEmoji(ctx1, t, tc1, uid1, sharedConv, msgID, "success-kid") 408 expectCreated(false) 409 expectRefresh(true) 410 msgID = mustPostLocalForTest(t, ctc, users[0], sharedConv, chat1.NewMessageBodyWithText(chat1.MessageText{ 411 Body: ":party_parrot:", 412 })) 413 checkEmoji(ctx1, t, tc1, uid1, sharedConv, msgID, "party_parrot") 414 expectCreated(false) 415 expectRefresh(false) 416 t.Logf("post from different source") 417 msgID = mustPostLocalForTest(t, ctc, users[0], sharedConv, chat1.NewMessageBodyWithText(chat1.MessageText{ 418 Body: ":mike:", 419 })) 420 checkEmoji(ctx1, t, tc1, uid1, sharedConv, msgID, "mike") 421 expectCreated(true) 422 expectRefresh(true) 423 msgID = mustPostLocalForTest(t, ctc, users[0], sharedConv, chat1.NewMessageBodyWithText(chat1.MessageText{ 424 Body: ":mike:", 425 })) 426 checkEmoji(ctx1, t, tc1, uid1, sharedConv, msgID, "mike") 427 expectCreated(false) 428 expectRefresh(false) 429 t.Logf("different user tries posting after convs are created") 430 msgID = mustPostLocalForTest(t, ctc, users[3], sharedConv, chat1.NewMessageBodyWithText(chat1.MessageText{ 431 Body: ":mike:", 432 })) 433 checkEmoji(ctx1, t, tc1, uid1, sharedConv, msgID, "mike") 434 expectCreated(false) 435 expectRefresh(false) 436 437 t.Logf("collision") 438 _, err = tc.Context().EmojiSource.Add(ctx, uid, sharedConv2.Id, "party_parrot", filename, false) 439 require.NoError(t, err) 440 msgID = mustPostLocalForTest(t, ctc, users[0], sharedConv, chat1.NewMessageBodyWithText(chat1.MessageText{ 441 Body: ":party_parrot#2:", 442 })) 443 checkEmoji(ctx1, t, tc1, uid1, sharedConv, msgID, "party_parrot#2") 444 445 // error on edit 446 _, err = tc.Context().EmojiSource.Add(ctx, uid, aloneConv.Id, "party_parrot", filename, false) 447 require.Error(t, err) 448 _, err = tc.Context().EmojiSource.Add(ctx, uid, aloneConv.Id, "party_parrot", filename, true) 449 require.NoError(t, err) 450 451 t.Logf("stock collision") 452 msgID = mustPostLocalForTest(t, ctc, users[0], sharedConv, chat1.NewMessageBodyWithText(chat1.MessageText{ 453 Body: ":rock#2:", 454 })) 455 checkEmoji(ctx1, t, tc1, uid1, sharedConv, msgID, "rock#2") 456 expectCreated(false) 457 expectRefresh(true) 458 } 459 460 type emojiTestCase struct { 461 body string 462 expected []emojiMatch 463 } 464 465 func TestEmojiSourceParse(t *testing.T) { 466 es := &DevConvEmojiSource{} 467 ctx := context.TODO() 468 469 testCases := []emojiTestCase{ 470 { 471 body: "x :miked:", 472 expected: []emojiMatch{ 473 { 474 name: "miked", 475 position: []int{2, 9}, 476 }, 477 }, 478 }, 479 { 480 body: ":333mm__--M:", 481 expected: []emojiMatch{ 482 { 483 name: "333mm__--M", 484 position: []int{0, 12}, 485 }, 486 }, 487 }, 488 { 489 body: ":mike: :lisa:", 490 expected: []emojiMatch{ 491 { 492 name: "mike", 493 position: []int{0, 6}, 494 }, 495 { 496 name: "lisa", 497 position: []int{7, 13}, 498 }, 499 }, 500 }, 501 { 502 body: ":mike::lisa:", 503 expected: []emojiMatch{ 504 { 505 name: "mike", 506 position: []int{0, 6}, 507 }, 508 { 509 name: "lisa", 510 position: []int{6, 12}, 511 }, 512 }, 513 }, 514 { 515 body: "::", 516 }, 517 } 518 for _, tc := range testCases { 519 res := es.parse(ctx, tc.body) 520 require.Equal(t, tc.expected, res) 521 } 522 } 523 524 func TestEmojiSourceIsStock(t *testing.T) { 525 es := &DevConvEmojiSource{} 526 require.True(t, es.IsStockEmoji("+1")) 527 require.True(t, es.IsStockEmoji(":+1:")) 528 require.True(t, es.IsStockEmoji(":+1::skin-tone-5:")) 529 require.False(t, es.IsStockEmoji("foo")) 530 }