feat: add stickers

This commit is contained in:
Krispire Admin
2026-08-30 02:18:53 +05:30
parent cee2d3c874
commit fecb9bc045
15 changed files with 752 additions and 6 deletions
+150
View File
@@ -2459,3 +2459,153 @@ func TestSend_ReplyCarriesItsQuoteImmediately(t *testing.T) {
t.Error("a retried reply lost its quote")
}
}
// grantSticker gives a user a reward the way sādhana does, returning its key.
func grantSticker(t *testing.T, pg *repository.Postgres, userID, key string) string {
t.Helper()
ctx := context.Background()
var rewardID string
if err := pg.Pool.QueryRow(ctx, `
INSERT INTO sadhna_rewards (key, name, image_url)
VALUES ($1, $2, 'https://example.invalid/art.png')
ON CONFLICT (key) DO UPDATE SET name = EXCLUDED.name
RETURNING id`, key, "Sticker "+key).Scan(&rewardID); err != nil {
t.Fatalf("create reward: %v", err)
}
t.Cleanup(func() {
_, _ = pg.Pool.Exec(ctx, `DELETE FROM sadhna_rewards WHERE id = $1`, rewardID)
})
if userID != "" {
// milestone is unique per user; a large random-ish one avoids clashing
// with any real streak rows.
if _, err := pg.Pool.Exec(ctx, `
INSERT INTO sadhna_reward_grants (user_id, reward_id, milestone)
VALUES ($1, $2, $3) ON CONFLICT DO NOTHING`,
userID, rewardID, 900000+len(key)); err != nil {
t.Fatalf("grant reward: %v", err)
}
}
return key
}
func stickerAttachment(t *testing.T, msgs []model.Message) *model.Attachment {
t.Helper()
for i := range msgs {
for j := range msgs[i].Attachments {
if msgs[i].Attachments[j].Kind == model.AttachmentSticker {
return &msgs[i].Attachments[j]
}
}
}
return nil
}
// A sticker key is not a capability: sending one you never earned must fail,
// or the whole catalog is available to anyone who can guess a key.
func TestStickers_OnlySendWhatYouOwn(t *testing.T) {
svc, _, pg := testChatService(t)
ctx := context.Background()
a, b := chatUser(t, pg, "A"), chatUser(t, pg, "B")
conv, err := svc.OpenDM(ctx, a, b)
if err != nil {
t.Fatal(err)
}
cleanupConv(t, pg, conv.ID)
mine := grantSticker(t, pg, a, "sticker_owned_"+a[:8])
// Exists in the catalog, granted to nobody.
unearned := grantSticker(t, pg, "", "sticker_unearned_"+a[:8])
if _, err := svc.Send(ctx, a, conv.ID, SendRequest{StickerKey: &mine}); err != nil {
t.Fatalf("sending an earned sticker: %v", err)
}
msgs, _ := svc.Messages(ctx, a, conv.ID, 0, 10)
att := stickerAttachment(t, msgs)
if att == nil {
t.Fatal("an earned sticker did not attach")
}
if att.ImageURL == nil || *att.ImageURL == "" || att.EntityID == nil || *att.EntityID != mine {
t.Errorf("attachment = %+v, want the artwork and its key", att)
}
// Not earned, not saved: the message sends, but carries no sticker.
if _, err := svc.Send(ctx, a, conv.ID, SendRequest{
Body: "trying", StickerKey: &unearned,
}); err != nil {
t.Fatal(err)
}
msgs, _ = svc.Messages(ctx, a, conv.ID, 0, 10)
for i := range msgs {
for j := range msgs[i].Attachments {
if att := msgs[i].Attachments[j]; att.Kind == model.AttachmentSticker &&
att.EntityID != nil && *att.EntityID == unearned {
t.Error("a sticker the sender never earned was attached")
}
}
}
}
// Saving is how a sticker spreads — but only from a message actually sent to
// you, in a thread you are actually in.
func TestStickers_SaveFromChat(t *testing.T) {
svc, _, pg := testChatService(t)
ctx := context.Background()
a, b, outsider := chatUser(t, pg, "A"), chatUser(t, pg, "B"), chatUser(t, pg, "C")
conv, err := svc.OpenDM(ctx, a, b)
if err != nil {
t.Fatal(err)
}
cleanupConv(t, pg, conv.ID)
key := grantSticker(t, pg, a, "sticker_shared_"+a[:8])
sent, err := svc.Send(ctx, a, conv.ID, SendRequest{StickerKey: &key})
if err != nil {
t.Fatal(err)
}
// B has not earned it, so it isn't in their drawer yet.
before, _ := svc.Stickers(ctx, b)
for _, s := range before {
if s.Key == key {
t.Fatal("B has the sticker before saving it")
}
}
if err := svc.SaveSticker(ctx, b, sent.ID, key); err != nil {
t.Fatalf("saving a sticker sent to you: %v", err)
}
after, _ := svc.Stickers(ctx, b)
var found *model.Sticker
for i := range after {
if after[i].Key == key {
found = &after[i]
}
}
if found == nil {
t.Fatal("saved sticker is not in B's drawer")
}
if !found.Saved {
t.Error("a sticker from a chat should be marked as saved, not earned")
}
// B can now send it themselves.
if _, err := svc.Send(ctx, b, conv.ID, SendRequest{StickerKey: &key}); err != nil {
t.Errorf("B should be able to send a saved sticker: %v", err)
}
// Saving twice is harmless.
if err := svc.SaveSticker(ctx, b, sent.ID, key); err != nil {
t.Errorf("second save should be idempotent: %v", err)
}
// Someone outside the thread cannot save from it, even knowing both ids.
if err := svc.SaveSticker(ctx, outsider, sent.ID, key); !errors.Is(err, repository.ErrStickerNotOwned) {
t.Errorf("outsider save: err = %v, want ErrStickerNotOwned", err)
}
// The sender's own drawer still reports it as earned, not saved.
mine, _ := svc.Stickers(ctx, a)
for _, s := range mine {
if s.Key == key && s.Saved {
t.Error("an earned sticker is being reported as saved")
}
}
}