feat: add stickers
This commit is contained in:
@@ -23,6 +23,9 @@ var (
|
||||
ErrAttachmentGone = errors.New("attachment not found")
|
||||
ErrMessageNotHere = errors.New("message is not in this conversation")
|
||||
ErrUserGone = errors.New("user not found")
|
||||
// ErrStickerNotOwned covers both "no such sticker" and "not yours" on
|
||||
// purpose: the catalog shouldn't be enumerable by probing keys.
|
||||
ErrStickerNotOwned = errors.New("sticker not available")
|
||||
)
|
||||
|
||||
type ChatRepo struct{ pg *Postgres }
|
||||
@@ -1227,6 +1230,101 @@ func (r *ChatRepo) ClearReaction(ctx context.Context, messageID, userID string)
|
||||
return conversationID, nil
|
||||
}
|
||||
|
||||
// Stickers lists everything a user may send: rewards they earned through
|
||||
// sādhana, plus stickers they saved from a chat.
|
||||
//
|
||||
// One UNION rather than two calls and a merge in Go — "may I send this?" and
|
||||
// "what can I send?" then have exactly one answer, and it stays correct if a
|
||||
// user both earns and is sent the same sticker.
|
||||
func (r *ChatRepo) Stickers(ctx context.Context, userID string) ([]model.Sticker, error) {
|
||||
rows, err := r.pg.Pool.Query(ctx, `
|
||||
SELECT sr.key, sr.name, sr.image_url, bool_and(src.saved) AS saved
|
||||
FROM (
|
||||
SELECT reward_id, FALSE AS saved FROM sadhna_reward_grants WHERE user_id = $1
|
||||
UNION ALL
|
||||
SELECT reward_id, TRUE AS saved FROM saved_stickers WHERE user_id = $1
|
||||
) src
|
||||
JOIN sadhna_rewards sr ON sr.id = src.reward_id
|
||||
WHERE sr.active
|
||||
GROUP BY sr.key, sr.name, sr.image_url, sr.sort
|
||||
ORDER BY sr.sort, sr.name`, userID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("stickers: %w", err)
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
out := []model.Sticker{}
|
||||
for rows.Next() {
|
||||
var s model.Sticker
|
||||
if err := rows.Scan(&s.Key, &s.Name, &s.ImageURL, &s.Saved); err != nil {
|
||||
return nil, fmt.Errorf("scan sticker: %w", err)
|
||||
}
|
||||
out = append(out, s)
|
||||
}
|
||||
return out, rows.Err()
|
||||
}
|
||||
|
||||
// Sticker looks up one piece of artwork a user is entitled to send.
|
||||
// ErrStickerNotOwned when they have neither earned nor saved it.
|
||||
func (r *ChatRepo) Sticker(ctx context.Context, userID, key string) (*model.Sticker, error) {
|
||||
var s model.Sticker
|
||||
err := r.pg.Pool.QueryRow(ctx, `
|
||||
SELECT sr.key, sr.name, sr.image_url
|
||||
FROM sadhna_rewards sr
|
||||
WHERE sr.key = $2 AND sr.active
|
||||
AND (EXISTS (SELECT 1 FROM sadhna_reward_grants g
|
||||
WHERE g.user_id = $1 AND g.reward_id = sr.id)
|
||||
OR EXISTS (SELECT 1 FROM saved_stickers ss
|
||||
WHERE ss.user_id = $1 AND ss.reward_id = sr.id))`,
|
||||
userID, key).Scan(&s.Key, &s.Name, &s.ImageURL)
|
||||
if errors.Is(err, pgx.ErrNoRows) {
|
||||
return nil, ErrStickerNotOwned
|
||||
}
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("sticker: %w", err)
|
||||
}
|
||||
return &s, nil
|
||||
}
|
||||
|
||||
// SaveSticker keeps a sticker someone sent. Idempotent.
|
||||
//
|
||||
// Only stickers that actually appear on a message in a thread the caller is
|
||||
// in can be saved — otherwise knowing a key would be enough to collect the
|
||||
// whole catalog without earning any of it.
|
||||
func (r *ChatRepo) SaveSticker(ctx context.Context, userID, key, fromMessageID string) error {
|
||||
tag, err := r.pg.Pool.Exec(ctx, `
|
||||
INSERT INTO saved_stickers (user_id, reward_id, saved_from)
|
||||
SELECT $1, sr.id, m.sender_id
|
||||
FROM message_attachments a
|
||||
JOIN messages m ON m.id = a.message_id
|
||||
JOIN conversation_participants p
|
||||
ON p.conversation_id = m.conversation_id AND p.user_id = $1 AND p.left_at IS NULL
|
||||
JOIN sadhna_rewards sr ON sr.key = a.entity_id AND sr.active
|
||||
WHERE a.message_id = $3 AND a.kind = 'sticker' AND a.entity_id = $2
|
||||
ON CONFLICT (user_id, reward_id) DO NOTHING`,
|
||||
userID, key, fromMessageID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("save sticker: %w", err)
|
||||
}
|
||||
// Zero rows means the sticker isn't on that message, or the caller isn't
|
||||
// in the thread. Both are "you can't have this", not "already saved" —
|
||||
// a repeat save conflicts instead, which also reports zero.
|
||||
if tag.RowsAffected() == 0 {
|
||||
var exists bool
|
||||
if err := r.pg.Pool.QueryRow(ctx, `
|
||||
SELECT EXISTS (SELECT 1 FROM saved_stickers ss
|
||||
JOIN sadhna_rewards sr ON sr.id = ss.reward_id
|
||||
WHERE ss.user_id = $1 AND sr.key = $2)`,
|
||||
userID, key).Scan(&exists); err != nil {
|
||||
return fmt.Errorf("save sticker check: %w", err)
|
||||
}
|
||||
if !exists {
|
||||
return ErrStickerNotOwned
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// ReactionDetails lists everyone who reacted to one message, newest first.
|
||||
//
|
||||
// Separate from ReactionsFor: the grouped counts are what a thread renders,
|
||||
|
||||
Reference in New Issue
Block a user