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
+37
View File
@@ -38,6 +38,8 @@ func writeChatError(w http.ResponseWriter, err error) {
response.Error(w, http.StatusNotFound, err.Error())
case errors.Is(err, service.ErrChatBadEntity):
response.Error(w, http.StatusBadRequest, err.Error())
case errors.Is(err, repository.ErrStickerNotOwned):
response.Error(w, http.StatusForbidden, "that sticker isn't in your collection")
case errors.Is(err, service.ErrChatBadEmoji):
response.Error(w, http.StatusBadRequest, err.Error())
case errors.Is(err, service.ErrChatBadQuote):
@@ -206,6 +208,8 @@ type sendReq struct {
// quote is a passage shared from the library. `path` must be app-relative;
// the service refuses anything that could navigate off-site.
Quote *quoteReq `json:"quote"`
// sticker_key sends reward artwork. Ownership is checked server-side.
StickerKey *string `json:"sticker_key"`
// entity shares something else in the app — a post — as a card. Only the
// reference; the BFF resolves the contents and re-checks visibility.
Entity *entityReq `json:"entity"`
@@ -249,6 +253,7 @@ func (h *ChatHandler) Send(w http.ResponseWriter, r *http.Request) {
ReplyToID: req.ReplyToID,
PreviewURL: req.PreviewURL,
Quote: quote,
StickerKey: req.StickerKey,
Entity: entity,
Attachments: media,
})
@@ -675,6 +680,38 @@ func (h *ChatHandler) MessageReactions(w http.ResponseWriter, r *http.Request) {
response.JSON(w, http.StatusOK, list)
}
// Stickers — GET /chat/stickers. What the caller may send.
func (h *ChatHandler) Stickers(w http.ResponseWriter, r *http.Request) {
list, err := h.svc.Stickers(r.Context(), middleware.UserID(r.Context()))
if err != nil {
writeChatError(w, err)
return
}
response.JSON(w, http.StatusOK, list)
}
type saveStickerReq struct {
Key string `json:"key"`
}
// SaveSticker — POST /chat/conversations/{id}/messages/{messageID}/sticker
//
// Keyed to the message it was sent on, not just the sticker key: that is what
// proves the caller was actually sent it rather than guessing at the catalog.
func (h *ChatHandler) SaveSticker(w http.ResponseWriter, r *http.Request) {
var req saveStickerReq
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
response.Error(w, http.StatusBadRequest, "invalid JSON body")
return
}
if err := h.svc.SaveSticker(r.Context(), middleware.UserID(r.Context()),
chi.URLParam(r, "messageID"), req.Key); err != nil {
writeChatError(w, err)
return
}
response.NoContent(w)
}
// Typing — POST /chat/conversations/{id}/typing
//
// Fire-and-forget: nothing is stored and the response carries nothing, because
+52
View File
@@ -2,11 +2,14 @@ package handler
import (
"encoding/json"
"errors"
"net/http"
"time"
"github.com/go-chi/chi/v5"
"github.com/prabhupadasays/spsays-bff/internal/middleware"
"github.com/prabhupadasays/spsays-bff/internal/model"
"github.com/prabhupadasays/spsays-bff/internal/repository"
"github.com/prabhupadasays/spsays-bff/internal/service"
"github.com/prabhupadasays/spsays-bff/pkg/response"
)
@@ -114,6 +117,55 @@ func (h *SadhnaHandler) Rewards(w http.ResponseWriter, r *http.Request) {
// AckRewards — POST /sadhna/rewards/ack. Clears pending celebrations once the
// client has shown the confetti, so a gift surfaces exactly once.
// OpenGift — POST /sadhna/rewards/gifts/{key}/open
//
// Called after the celebration is dismissed. Idempotent: opening an already
// opened gift changes nothing.
func (h *SadhnaHandler) OpenGift(w http.ResponseWriter, r *http.Request) {
if err := h.svc.OpenGift(r.Context(),
middleware.UserID(r.Context()), chi.URLParam(r, "key")); err != nil {
response.Error(w, http.StatusInternalServerError, "could not open that gift")
return
}
response.NoContent(w)
}
// GiftableRewards — GET /admin/reward-gifts/catalog
func (h *SadhnaHandler) GiftableRewards(w http.ResponseWriter, r *http.Request) {
list, err := h.svc.GiftableRewards(r.Context())
if err != nil {
response.Error(w, http.StatusInternalServerError, "could not load the catalog")
return
}
response.JSON(w, http.StatusOK, list)
}
type giftReq struct {
RewardKey string `json:"reward_key"`
}
// GiftToEveryone — POST /admin/reward-gifts
//
// Hands one reward to every account, unopened. Idempotent, so a repeated
// click tops up only the people who don't have it rather than duplicating.
func (h *SadhnaHandler) GiftToEveryone(w http.ResponseWriter, r *http.Request) {
var req giftReq
if err := json.NewDecoder(r.Body).Decode(&req); err != nil || req.RewardKey == "" {
response.Error(w, http.StatusBadRequest, "reward_key is required")
return
}
n, err := h.svc.GiftToEveryone(r.Context(), req.RewardKey)
if err != nil {
if errors.Is(err, repository.ErrRewardNotFound) {
response.Error(w, http.StatusNotFound, "no such reward")
return
}
response.Error(w, http.StatusInternalServerError, "could not send the gift")
return
}
response.JSON(w, http.StatusOK, map[string]int64{"granted": n})
}
func (h *SadhnaHandler) AckRewards(w http.ResponseWriter, r *http.Request) {
uid := middleware.UserID(r.Context())
if err := h.svc.AckRewards(r.Context(), uid); err != nil {