feat: custom stickers apis

This commit is contained in:
Krispire Admin
2026-08-30 03:02:49 +05:30
parent fecb9bc045
commit 4427849647
6 changed files with 156 additions and 4 deletions
+3 -1
View File
@@ -223,7 +223,9 @@ func (s *BroadcastService) PresignBanner(ctx context.Context, contentType string
if !ok || kind != model.AttachmentImage {
return "", "", ErrChatBadMedia
}
return presignImage(ctx, s.r2, strings.TrimSuffix(bannerPrefix, "/"), "banner", contentType)
// Returns the KEY: broadcasts.banner_key stores a key, and withBanner
// builds the URL from it at read time.
return presignImageKey(ctx, s.r2, strings.TrimSuffix(bannerPrefix, "/"), "banner", contentType)
}
func (s *BroadcastService) withBanner(b *model.Broadcast, key *string) {
+65 -2
View File
@@ -16,7 +16,12 @@ package service
import (
"context"
crand "crypto/rand"
"encoding/hex"
"errors"
"math/rand"
"regexp"
"strings"
"time"
"github.com/prabhupadasays/spsays-bff/internal/model"
@@ -64,7 +69,10 @@ type SadhnaService struct {
posts *repository.PostRepo
users *repository.UserRepo
rewards *repository.RewardRepo
logger *zap.Logger
// r2 stores custom gift artwork. Optional: nil disables custom gifts
// rather than failing anything that already works.
r2 *repository.R2
logger *zap.Logger
}
func NewSadhnaService(
@@ -72,9 +80,13 @@ func NewSadhnaService(
posts *repository.PostRepo,
users *repository.UserRepo,
rewards *repository.RewardRepo,
r2 *repository.R2,
logger *zap.Logger,
) *SadhnaService {
return &SadhnaService{logs: logs, posts: posts, users: users, rewards: rewards, logger: logger}
return &SadhnaService{
logs: logs, posts: posts, users: users, rewards: rewards,
r2: r2, logger: logger,
}
}
const dayFmt = "2006-01-02"
@@ -415,6 +427,57 @@ func (s *SadhnaService) GiftToEveryone(ctx context.Context, rewardKey string) (i
return s.rewards.GrantGiftToEveryone(ctx, reward.ID)
}
// customKeyPrefix namespaces admin-made rewards away from the seeded catalog,
// so Seed can never collide with one.
const customKeyPrefix = "custom_"
var nonSlug = regexp.MustCompile(`[^a-z0-9]+`)
// customRewardKey derives a stable, unique key from a name.
//
// The random suffix is what makes it unique: two gifts may reasonably share a
// name, and a collision would overwrite the first one's artwork.
func customRewardKey(name string) (string, error) {
slug := strings.Trim(nonSlug.ReplaceAllString(strings.ToLower(name), "_"), "_")
if slug == "" {
slug = "gift"
}
if len(slug) > 32 {
slug = slug[:32]
}
suffix := make([]byte, 4)
if _, err := crand.Read(suffix); err != nil {
return "", err
}
return customKeyPrefix + slug + "_" + hex.EncodeToString(suffix), nil
}
// CreateCustomGift adds a new sticker to the catalog. It is not granted to
// anyone until it is sent.
func (s *SadhnaService) CreateCustomGift(ctx context.Context, name, meaning, imageURL string) (*model.SadhnaReward, error) {
name = strings.TrimSpace(name)
if name == "" {
return nil, errors.New("a gift needs a name")
}
if strings.TrimSpace(imageURL) == "" {
return nil, errors.New("a gift needs an image")
}
key, err := customRewardKey(name)
if err != nil {
return nil, err
}
return s.rewards.CreateCustom(ctx, key, name, strings.TrimSpace(meaning), imageURL)
}
// PresignGiftImage issues an upload URL for custom sticker artwork.
//
// Returns the PUBLIC URL, not a key: sadhna_rewards.image_url stores an
// absolute address, and stickers render for every participant in a chat, so
// the artwork belongs in the public bucket alongside avatars and post images.
func (s *SadhnaService) PresignGiftImage(ctx context.Context, contentType string) (uploadURL, publicURL string, err error) {
return presignImage(ctx, s.r2, "reward-gifts", "custom", contentType)
}
// GiftableRewards is the catalog an admin picks from.
func (s *SadhnaService) GiftableRewards(ctx context.Context) ([]model.SadhnaReward, error) {
return s.rewards.ListGiftable(ctx)
+23
View File
@@ -775,6 +775,29 @@ func (s *CollectionService) CoverPresign(ctx context.Context, userID, contentTyp
// presignImage builds an R2 presign for a user-scoped media key under the
// given prefix. The random suffix invalidates browser caches when the image
// is replaced.
// presignImageKey is presignImage's sibling for callers that store the storage
// KEY rather than a URL — so the CDN domain can change without rewriting rows.
// Handing those callers a public URL silently doubles it at read time.
func presignImageKey(ctx context.Context, r2 *repository.R2, prefix, name, contentType string) (uploadURL, key string, err error) {
if r2 == nil {
return "", "", errors.New("media uploads are not configured")
}
ext, ok := imageExtensionFor(contentType)
if !ok {
return "", "", fmt.Errorf("unsupported content type: %s", contentType)
}
suffix := make([]byte, 8)
if _, err := rand.Read(suffix); err != nil {
return "", "", err
}
key = path.Join(prefix, name, hex.EncodeToString(suffix)+ext)
uploadURL, _, err = r2.PresignPut(ctx, key, contentType, 5*time.Minute)
if err != nil {
return "", "", err
}
return uploadURL, key, nil
}
func presignImage(ctx context.Context, r2 *repository.R2, prefix, userID, contentType string) (uploadURL, publicURL string, err error) {
if r2 == nil {
return "", "", errors.New("media uploads are not configured")