removed all "paid" features - no more stars, gifts, or grams

This commit is contained in:
onysd 2026-08-07 01:50:10 +03:00
parent d4451d753c
commit 21d8e91756
165 changed files with 318 additions and 40948 deletions

View file

@ -17,8 +17,6 @@ import (
"telesrv/internal/admin"
"telesrv/internal/domain"
"telesrv/internal/officialgifts"
"telesrv/internal/seed/giftdemo"
)
type Config struct {
@ -44,7 +42,6 @@ type Service interface {
AccountAvatar(ctx context.Context, userID int64) ([]byte, string, bool, error)
SetAccountFrozen(ctx context.Context, req admin.SetAccountFrozenRequest) (admin.CommandResult, error)
GrantPremium(ctx context.Context, req admin.GrantPremiumRequest) (admin.CommandResult, error)
GrantStars(ctx context.Context, req admin.GrantStarsRequest) (admin.CommandResult, error)
SetVerified(ctx context.Context, req admin.SetVerifiedRequest) (admin.CommandResult, error)
SetUserFlags(ctx context.Context, req admin.SetUserFlagsRequest) (admin.CommandResult, error)
SetChannelVerified(ctx context.Context, req admin.SetChannelVerifiedRequest) (admin.CommandResult, error)
@ -70,18 +67,6 @@ type Service interface {
RevokeSessions(ctx context.Context, req admin.RevokeSessionsRequest) (admin.CommandResult, error)
DeletePrivateMessages(ctx context.Context, req admin.DeletePrivateMessagesRequest) (admin.CommandResult, error)
DeletePrivateHistory(ctx context.Context, req admin.DeletePrivateHistoryRequest) (admin.CommandResult, error)
ImportStarGift(ctx context.Context, req admin.ImportStarGiftRequest) (admin.CommandResult, error)
ImportDefaultStarGift(ctx context.Context, req admin.ImportDefaultStarGiftRequest) (admin.CommandResult, error)
ImportAllDefaultStarGifts(ctx context.Context, req admin.ImportAllDefaultStarGiftsRequest) (admin.CommandResult, error)
DefaultStarGifts() []giftdemo.GiftInfo
DefaultStarGiftAnimation(ctx context.Context, id int) ([]byte, bool, error)
ImportOfficialStarGift(ctx context.Context, req admin.ImportOfficialStarGiftRequest) (admin.CommandResult, error)
ImportAllOfficialStarGifts(ctx context.Context, req admin.ImportAllOfficialStarGiftsRequest) (admin.CommandResult, error)
OfficialStarGifts(ctx context.Context) ([]officialgifts.GiftSummary, error)
OfficialStarGiftAnimation(ctx context.Context, sourceGiftID string) ([]byte, bool, error)
PublishStarGiftCollectibles(ctx context.Context, req admin.PublishStarGiftCollectiblesRequest) (admin.CommandResult, error)
SetStarGiftEnabled(ctx context.Context, req admin.SetStarGiftEnabledRequest) (admin.CommandResult, error)
SetStarGiftSortOrder(ctx context.Context, req admin.SetStarGiftSortOrderRequest) (admin.CommandResult, error)
SetStickerSetArchived(ctx context.Context, req admin.SetStickerSetArchivedRequest) (admin.CommandResult, error)
SetStickerSetSortOrder(ctx context.Context, req admin.SetStickerSetSortOrderRequest) (admin.CommandResult, error)
RenameStickerSet(ctx context.Context, req admin.RenameStickerSetRequest) (admin.CommandResult, error)
@ -90,11 +75,7 @@ type Service interface {
AddStickerToSet(ctx context.Context, req admin.AddStickerToSetRequest) (admin.CommandResult, error)
RemoveStickerFromSet(ctx context.Context, req admin.RemoveStickerFromSetRequest) (admin.CommandResult, error)
StickerDocumentAnimation(ctx context.Context, documentID int64) ([]byte, string, bool, error)
GiveGift(ctx context.Context, req admin.GiveGiftRequest) (admin.CommandResult, error)
StarGiftAnimation(ctx context.Context, giftID int64) ([]byte, bool, error)
EmojiAnimation(ctx context.Context, documentID int64) ([]byte, bool, error)
StarGiftCollectibles(ctx context.Context, giftID int64) (domain.StarGiftUpgradePreview, bool, error)
StarGiftCollectibleAnimation(ctx context.Context, giftID int64, kind domain.StarGiftCollectibleAttributeKind, attributeID int64) ([]byte, bool, error)
ModerationCases(ctx context.Context, filter domain.ModerationCaseFilter) ([]domain.ModerationCase, error)
ModerationCase(ctx context.Context, caseID int64) (domain.ModerationCaseDetail, bool, error)
ModerationReport(ctx context.Context, reportID int64) (domain.ModerationReport, bool, error)
@ -109,11 +90,6 @@ type Service interface {
CollectibleUsernames(ctx context.Context, filter domain.CollectibleUsernameFilter) ([]domain.CollectibleUsername, error)
CollectibleUsernameByID(ctx context.Context, id int64) (domain.CollectibleUsername, error)
CollectibleUsernameTransfers(ctx context.Context, collectibleID int64, limit int) ([]domain.CollectibleUsernameTransfer, error)
RecomputeAccountRating(ctx context.Context, req admin.RecomputeAccountRatingRequest) (admin.CommandResult, error)
AdjustAccountRating(ctx context.Context, req admin.AdjustAccountRatingRequest) (admin.CommandResult, error)
AccountRating(ctx context.Context, userID int64) (domain.AccountRating, error)
AccountRatings(ctx context.Context, filter domain.AccountRatingFilter) ([]domain.AccountRating, error)
AccountRatingEvents(ctx context.Context, userID int64, limit int) ([]domain.AccountRatingEvent, error)
ClaimVerification(ctx context.Context, req admin.ClaimVerificationRequest) (admin.CommandResult, error)
ApproveVerification(ctx context.Context, req admin.ApproveVerificationRequest) (admin.CommandResult, error)
RejectVerification(ctx context.Context, req admin.RejectVerificationRequest) (admin.CommandResult, error)
@ -195,7 +171,6 @@ func (s *Server) routes() http.Handler {
mux.HandleFunc("POST /v1/accounts/set-frozen", s.authenticated(s.handleSetAccountFrozen))
mux.HandleFunc("GET /v1/accounts/{id}/avatar", s.authenticated(s.handleAccountAvatar))
mux.HandleFunc("POST /v1/accounts/grant-premium", s.authenticated(s.handleGrantPremium))
mux.HandleFunc("POST /v1/accounts/grant-stars", s.authenticated(s.handleGrantStars))
mux.HandleFunc("POST /v1/accounts/set-verified", s.authenticated(s.handleSetVerified))
mux.HandleFunc("POST /v1/accounts/set-flags", s.authenticated(s.handleSetUserFlags))
mux.HandleFunc("POST /v1/accounts/set-support", s.authenticated(s.handleSetSupport))
@ -221,18 +196,6 @@ func (s *Server) routes() http.Handler {
mux.HandleFunc("POST /v1/bots/export-token", s.authenticated(s.handleExportBotToken))
mux.HandleFunc("POST /v1/messages/delete", s.authenticated(s.handleDeleteMessages))
mux.HandleFunc("POST /v1/messages/delete-history", s.authenticated(s.handleDeleteHistory))
mux.HandleFunc("POST /v1/gifts/import", s.authenticated(s.handleImportStarGift))
mux.HandleFunc("GET /v1/default-gifts", s.authenticated(s.handleDefaultStarGifts))
mux.HandleFunc("GET /v1/default-gifts/{id}/animation", s.authenticated(s.handleDefaultStarGiftAnimation))
mux.HandleFunc("POST /v1/default-gifts/import", s.authenticated(s.handleImportDefaultStarGift))
mux.HandleFunc("POST /v1/default-gifts/import-all", s.authenticated(s.handleImportAllDefaultStarGifts))
mux.HandleFunc("GET /v1/official-gifts", s.authenticated(s.handleOfficialStarGifts))
mux.HandleFunc("GET /v1/official-gifts/{id}/animation", s.authenticated(s.handleOfficialStarGiftAnimation))
mux.HandleFunc("POST /v1/official-gifts/import", s.authenticated(s.handleImportOfficialStarGift))
mux.HandleFunc("POST /v1/official-gifts/import-all", s.authenticated(s.handleImportAllOfficialStarGifts))
mux.HandleFunc("POST /v1/gifts/{id}/collectibles/publish", s.authenticated(s.handlePublishStarGiftCollectibles))
mux.HandleFunc("POST /v1/gifts/set-enabled", s.authenticated(s.handleSetStarGiftEnabled))
mux.HandleFunc("POST /v1/gifts/set-sort-order", s.authenticated(s.handleSetStarGiftSortOrder))
mux.HandleFunc("POST /v1/stickers/set-archived", s.authenticated(s.handleSetStickerSetArchived))
mux.HandleFunc("POST /v1/stickers/set-sort-order", s.authenticated(s.handleSetStickerSetSortOrder))
mux.HandleFunc("POST /v1/stickers/rename", s.authenticated(s.handleRenameStickerSet))
@ -241,11 +204,7 @@ func (s *Server) routes() http.Handler {
mux.HandleFunc("POST /v1/stickers/add", s.authenticated(s.handleAddStickerToSet))
mux.HandleFunc("POST /v1/stickers/remove", s.authenticated(s.handleRemoveStickerFromSet))
mux.HandleFunc("GET /v1/stickers/documents/{id}/animation", s.authenticated(s.handleStickerDocumentAnimation))
mux.HandleFunc("POST /v1/gifts/give", s.authenticated(s.handleGiveGift))
mux.HandleFunc("GET /v1/gifts/{id}/animation", s.authenticated(s.handleStarGiftAnimation))
mux.HandleFunc("GET /v1/emoji/{id}/animation", s.authenticated(s.handleEmojiAnimation))
mux.HandleFunc("GET /v1/gifts/{id}/collectibles", s.authenticated(s.handleStarGiftCollectibles))
mux.HandleFunc("GET /v1/gifts/{id}/collectibles/{kind}/{attribute_id}/animation", s.authenticated(s.handleStarGiftCollectibleAnimation))
mux.HandleFunc("GET /v1/moderation/cases", s.authenticated(s.handleModerationCases))
mux.HandleFunc("GET /v1/moderation/cases/{id}", s.authenticated(s.handleModerationCase))
mux.HandleFunc("GET /v1/moderation/reports/{id}", s.authenticated(s.handleModerationReport))
@ -259,10 +218,6 @@ func (s *Server) routes() http.Handler {
mux.HandleFunc("POST /v1/collectible-usernames/delete", s.authenticated(s.handleDeleteCollectibleUsername))
mux.HandleFunc("GET /v1/collectible-usernames", s.authenticated(s.handleCollectibleUsernames))
mux.HandleFunc("GET /v1/collectible-usernames/{id}", s.authenticated(s.handleCollectibleUsername))
mux.HandleFunc("POST /v1/account-ratings/recompute", s.authenticated(s.handleRecomputeAccountRating))
mux.HandleFunc("POST /v1/account-ratings/adjust", s.authenticated(s.handleAdjustAccountRating))
mux.HandleFunc("GET /v1/account-ratings", s.authenticated(s.handleAccountRatings))
mux.HandleFunc("GET /v1/account-ratings/{id}", s.authenticated(s.handleAccountRating))
// Official platform verification. Unlike every route above, these carry a
// named permission, so a scoped token can be given the review surface and
// nothing else. Revocation additionally requires verification.revoke.
@ -337,15 +292,6 @@ func (s *Server) handleGrantPremium(w http.ResponseWriter, r *http.Request) {
writeCommandResult(w, result, err)
}
func (s *Server) handleGrantStars(w http.ResponseWriter, r *http.Request) {
var req admin.GrantStarsRequest
if !decodeJSON(w, r, &req) {
return
}
result, err := s.svc.GrantStars(r.Context(), req)
writeCommandResult(w, result, err)
}
func (s *Server) handleSetVerified(w http.ResponseWriter, r *http.Request) {
var req admin.SetVerifiedRequest
if !decodeJSON(w, r, &req) {
@ -634,231 +580,6 @@ func (s *Server) handleDeleteHistory(w http.ResponseWriter, r *http.Request) {
writeCommandResult(w, result, err)
}
func (s *Server) handleImportStarGift(w http.ResponseWriter, r *http.Request) {
defer r.Body.Close()
r.Body = http.MaxBytesReader(w, r.Body, 5<<20)
if err := r.ParseMultipartForm(1 << 20); err != nil {
writeError(w, http.StatusBadRequest, "invalid multipart form: "+err.Error())
return
}
if r.MultipartForm != nil {
defer r.MultipartForm.RemoveAll()
}
var req admin.ImportStarGiftRequest
dec := json.NewDecoder(strings.NewReader(r.FormValue("metadata")))
dec.DisallowUnknownFields()
if err := dec.Decode(&req); err != nil {
writeError(w, http.StatusBadRequest, "invalid metadata: "+err.Error())
return
}
file, header, err := r.FormFile("file")
if err != nil {
writeError(w, http.StatusBadRequest, "animation file is required")
return
}
defer file.Close()
data, err := io.ReadAll(io.LimitReader(file, (4<<20)+1))
if err != nil || len(data) == 0 || len(data) > 4<<20 {
writeError(w, http.StatusBadRequest, "animation file is empty or too large")
return
}
req.FileName = header.Filename
req.Data = data
result, err := s.svc.ImportStarGift(r.Context(), req)
writeCommandResult(w, result, err)
}
func (s *Server) handleDefaultStarGifts(w http.ResponseWriter, r *http.Request) {
items := s.svc.DefaultStarGifts()
writeJSON(w, http.StatusOK, map[string]any{"gifts": items})
}
func (s *Server) handleDefaultStarGiftAnimation(w http.ResponseWriter, r *http.Request) {
id, err := strconv.Atoi(r.PathValue("id"))
if err != nil || id <= 0 {
writeError(w, http.StatusBadRequest, "invalid gift id")
return
}
raw, found, err := s.svc.DefaultStarGiftAnimation(r.Context(), id)
if err != nil {
writeError(w, http.StatusInternalServerError, err.Error())
return
}
if !found {
writeError(w, http.StatusNotFound, "default gift animation not found")
return
}
w.Header().Set("Content-Type", "application/json; charset=utf-8")
w.Header().Set("Cache-Control", "private, max-age=60")
w.WriteHeader(http.StatusOK)
_, _ = w.Write(raw)
}
func (s *Server) handleImportDefaultStarGift(w http.ResponseWriter, r *http.Request) {
var req admin.ImportDefaultStarGiftRequest
if !decodeJSON(w, r, &req) {
return
}
result, err := s.svc.ImportDefaultStarGift(r.Context(), req)
writeCommandResult(w, result, err)
}
func (s *Server) handleImportAllDefaultStarGifts(w http.ResponseWriter, r *http.Request) {
var req admin.ImportAllDefaultStarGiftsRequest
if !decodeJSON(w, r, &req) {
return
}
result, err := s.svc.ImportAllDefaultStarGifts(r.Context(), req)
writeCommandResult(w, result, err)
}
func (s *Server) handleOfficialStarGifts(w http.ResponseWriter, r *http.Request) {
items, err := s.svc.OfficialStarGifts(r.Context())
if err != nil {
status := http.StatusInternalServerError
if errors.Is(err, officialgifts.ErrUnavailable) {
status = http.StatusServiceUnavailable
}
writeError(w, status, err.Error())
return
}
result := make([]map[string]any, 0, len(items))
for _, item := range items {
result = append(result, officialStarGiftListItem(item))
}
writeJSON(w, http.StatusOK, map[string]any{"gifts": result})
}
func officialStarGiftListItem(item officialgifts.GiftSummary) map[string]any {
return map[string]any{
"source_gift_id": strconv.FormatInt(item.ID, 10), "title": item.Title,
"stars": strconv.FormatInt(item.Stars, 10), "convert_stars": strconv.FormatInt(item.ConvertStars, 10),
"upgrade_stars": strconv.FormatInt(item.UpgradeStars, 10),
"availability_total": item.AvailabilityTotal, "limited": item.Limited, "sold_out": item.SoldOut,
"model_count": item.ModelCount, "pattern_count": item.PatternCount, "backdrop_count": item.BackdropCount,
"crafted_model_count": item.CraftedModelCount, "can_upgrade": item.CanUpgrade(), "can_craft": item.CanCraft(),
"document_id": strconv.FormatInt(item.DocumentID, 10), "animation_validated": item.AnimationValidated,
}
}
func (s *Server) handleOfficialStarGiftAnimation(w http.ResponseWriter, r *http.Request) {
raw, found, err := s.svc.OfficialStarGiftAnimation(r.Context(), r.PathValue("id"))
if err != nil {
writeError(w, http.StatusInternalServerError, err.Error())
return
}
if !found {
writeError(w, http.StatusNotFound, "official gift animation not found")
return
}
w.Header().Set("Content-Type", "application/json; charset=utf-8")
w.Header().Set("Cache-Control", "private, max-age=60")
w.WriteHeader(http.StatusOK)
_, _ = w.Write(raw)
}
func (s *Server) handleImportOfficialStarGift(w http.ResponseWriter, r *http.Request) {
var req admin.ImportOfficialStarGiftRequest
if !decodeJSON(w, r, &req) {
return
}
result, err := s.svc.ImportOfficialStarGift(r.Context(), req)
writeCommandResult(w, result, err)
}
func (s *Server) handleImportAllOfficialStarGifts(w http.ResponseWriter, r *http.Request) {
var req admin.ImportAllOfficialStarGiftsRequest
if !decodeJSON(w, r, &req) {
return
}
result, err := s.svc.ImportAllOfficialStarGifts(r.Context(), req)
writeCommandResult(w, result, err)
}
func (s *Server) handlePublishStarGiftCollectibles(w http.ResponseWriter, r *http.Request) {
defer r.Body.Close()
giftID, err := strconv.ParseInt(r.PathValue("id"), 10, 64)
if err != nil || giftID <= 0 {
writeError(w, http.StatusBadRequest, "invalid gift id")
return
}
r.Body = http.MaxBytesReader(w, r.Body, 64<<20)
if err := r.ParseMultipartForm(8 << 20); err != nil {
writeError(w, http.StatusBadRequest, "invalid collectible multipart form: "+err.Error())
return
}
if r.MultipartForm != nil {
defer r.MultipartForm.RemoveAll()
}
var req admin.PublishStarGiftCollectiblesRequest
dec := json.NewDecoder(strings.NewReader(r.FormValue("metadata")))
dec.DisallowUnknownFields()
if err := dec.Decode(&req); err != nil {
writeError(w, http.StatusBadRequest, "invalid metadata: "+err.Error())
return
}
req.GiftID = giftID
seen := make(map[string]struct{}, len(req.Models)+len(req.Patterns))
if len(req.Models)+len(req.Patterns) > 128 {
writeError(w, http.StatusBadRequest, "too many collectible animation files")
return
}
load := func(upload *admin.StarGiftCollectibleAnimationUpload) error {
upload.FileKey = strings.TrimSpace(upload.FileKey)
if upload.FileKey == "" {
return fmt.Errorf("animation file key is required")
}
if _, ok := seen[upload.FileKey]; ok {
return fmt.Errorf("duplicate animation file key %q", upload.FileKey)
}
seen[upload.FileKey] = struct{}{}
file, header, err := r.FormFile(upload.FileKey)
if err != nil {
return fmt.Errorf("animation file %q is required", upload.FileKey)
}
defer file.Close()
data, err := io.ReadAll(io.LimitReader(file, (4<<20)+1))
if err != nil || len(data) == 0 || len(data) > 4<<20 {
return fmt.Errorf("animation file %q is empty or too large", upload.FileKey)
}
upload.FileName = header.Filename
upload.Data = data
return nil
}
for i := range req.Models {
if err := load(&req.Models[i]); err != nil {
writeError(w, http.StatusBadRequest, err.Error())
return
}
}
for i := range req.Patterns {
if err := load(&req.Patterns[i]); err != nil {
writeError(w, http.StatusBadRequest, err.Error())
return
}
}
result, err := s.svc.PublishStarGiftCollectibles(r.Context(), req)
writeCommandResult(w, result, err)
}
func (s *Server) handleSetStarGiftEnabled(w http.ResponseWriter, r *http.Request) {
var req admin.SetStarGiftEnabledRequest
if !decodeJSON(w, r, &req) {
return
}
result, err := s.svc.SetStarGiftEnabled(r.Context(), req)
writeCommandResult(w, result, err)
}
func (s *Server) handleSetStarGiftSortOrder(w http.ResponseWriter, r *http.Request) {
var req admin.SetStarGiftSortOrderRequest
if !decodeJSON(w, r, &req) {
return
}
result, err := s.svc.SetStarGiftSortOrder(r.Context(), req)
writeCommandResult(w, result, err)
}
func (s *Server) handleSetStickerSetArchived(w http.ResponseWriter, r *http.Request) {
var req admin.SetStickerSetArchivedRequest
if !decodeJSON(w, r, &req) {
@ -993,36 +714,6 @@ func (s *Server) handleStickerDocumentAnimation(w http.ResponseWriter, r *http.R
_, _ = w.Write(raw)
}
func (s *Server) handleGiveGift(w http.ResponseWriter, r *http.Request) {
var req admin.GiveGiftRequest
if !decodeJSON(w, r, &req) {
return
}
result, err := s.svc.GiveGift(r.Context(), req)
writeCommandResult(w, result, err)
}
func (s *Server) handleStarGiftAnimation(w http.ResponseWriter, r *http.Request) {
giftID, err := strconv.ParseInt(r.PathValue("id"), 10, 64)
if err != nil || giftID <= 0 {
writeError(w, http.StatusBadRequest, "invalid gift id")
return
}
raw, found, err := s.svc.StarGiftAnimation(r.Context(), giftID)
if err != nil {
writeError(w, http.StatusInternalServerError, err.Error())
return
}
if !found {
writeError(w, http.StatusNotFound, "gift animation not found")
return
}
w.Header().Set("Content-Type", "application/json; charset=utf-8")
w.Header().Set("Cache-Control", "private, max-age=60")
w.WriteHeader(http.StatusOK)
_, _ = w.Write(raw)
}
func (s *Server) handleEmojiAnimation(w http.ResponseWriter, r *http.Request) {
documentID, err := strconv.ParseInt(r.PathValue("id"), 10, 64)
if err != nil || documentID <= 0 {
@ -1044,86 +735,6 @@ func (s *Server) handleEmojiAnimation(w http.ResponseWriter, r *http.Request) {
_, _ = w.Write(raw)
}
func (s *Server) handleStarGiftCollectibles(w http.ResponseWriter, r *http.Request) {
giftID, err := strconv.ParseInt(r.PathValue("id"), 10, 64)
if err != nil || giftID <= 0 {
writeError(w, http.StatusBadRequest, "invalid gift id")
return
}
preview, found, err := s.svc.StarGiftCollectibles(r.Context(), giftID)
if err != nil {
writeError(w, http.StatusInternalServerError, err.Error())
return
}
if !found {
writeJSON(w, http.StatusOK, map[string]any{"found": false, "gift_id": strconv.FormatInt(giftID, 10)})
return
}
writeJSON(w, http.StatusOK, collectiblePreviewResponse(preview))
}
func collectiblePreviewResponse(preview domain.StarGiftUpgradePreview) map[string]any {
attribute := func(value domain.StarGiftCollectibleAttribute) map[string]any {
result := map[string]any{
"id": strconv.FormatInt(value.ID, 10), "name": value.Name, "rarity_kind": value.RarityKind,
"rarity_permille": value.RarityPermille, "crafted": value.Crafted,
"official_document_id": strconv.FormatInt(value.OfficialDocumentID, 10),
"sort_order": value.SortOrder, "kind": value.Kind,
}
if value.Animation != nil {
result["source_name"] = value.Animation.SourceName
result["source_format"] = value.Animation.SourceFormat
}
if value.Kind == domain.StarGiftCollectibleBackdrop {
result["backdrop_id"] = value.BackdropID
result["center_color"] = value.CenterColor
result["edge_color"] = value.EdgeColor
result["pattern_color"] = value.PatternColor
result["text_color"] = value.TextColor
}
return result
}
mapAttributes := func(values []domain.StarGiftCollectibleAttribute) []map[string]any {
result := make([]map[string]any, 0, len(values))
for _, value := range values {
result = append(result, attribute(value))
}
return result
}
return map[string]any{
"found": true, "gift_id": strconv.FormatInt(preview.GiftID, 10), "revision": preview.Revision,
"upgrade_stars": strconv.FormatInt(preview.UpgradeStars, 10),
"supply_total": preview.SupplyTotal, "issued": preview.Issued,
"slug_prefix": preview.SlugPrefix,
"models": mapAttributes(preview.Models), "patterns": mapAttributes(preview.Patterns),
"backdrops": mapAttributes(preview.Backdrops),
}
}
func (s *Server) handleStarGiftCollectibleAnimation(w http.ResponseWriter, r *http.Request) {
giftID, err := strconv.ParseInt(r.PathValue("id"), 10, 64)
attributeID, attrErr := strconv.ParseInt(r.PathValue("attribute_id"), 10, 64)
kind := domain.StarGiftCollectibleAttributeKind(r.PathValue("kind"))
if err != nil || giftID <= 0 || attrErr != nil || attributeID <= 0 ||
(kind != domain.StarGiftCollectibleModel && kind != domain.StarGiftCollectiblePattern) {
writeError(w, http.StatusBadRequest, "invalid collectible animation")
return
}
raw, found, err := s.svc.StarGiftCollectibleAnimation(r.Context(), giftID, kind, attributeID)
if err != nil {
writeError(w, http.StatusInternalServerError, err.Error())
return
}
if !found {
writeError(w, http.StatusNotFound, "collectible animation not found")
return
}
w.Header().Set("Content-Type", "application/json; charset=utf-8")
w.Header().Set("Cache-Control", "private, max-age=60")
w.WriteHeader(http.StatusOK)
_, _ = w.Write(raw)
}
type moderationClaimRequest struct {
ExpectedVersion int64 `json:"expected_version"`
Actor string `json:"actor"`
@ -1450,24 +1061,6 @@ func (s *Server) handleDeleteCollectibleUsername(w http.ResponseWriter, r *http.
writeCommandResult(w, result, err)
}
func (s *Server) handleRecomputeAccountRating(w http.ResponseWriter, r *http.Request) {
var req admin.RecomputeAccountRatingRequest
if !decodeJSON(w, r, &req) {
return
}
result, err := s.svc.RecomputeAccountRating(r.Context(), req)
writeCommandResult(w, result, err)
}
func (s *Server) handleAdjustAccountRating(w http.ResponseWriter, r *http.Request) {
var req admin.AdjustAccountRatingRequest
if !decodeJSON(w, r, &req) {
return
}
result, err := s.svc.AdjustAccountRating(r.Context(), req)
writeCommandResult(w, result, err)
}
func (s *Server) handleCollectibleUsernames(w http.ResponseWriter, r *http.Request) {
query := r.URL.Query()
filter := domain.CollectibleUsernameFilter{
@ -1533,66 +1126,6 @@ func (s *Server) handleCollectibleUsername(w http.ResponseWriter, r *http.Reques
})
}
func (s *Server) handleAccountRatings(w http.ResponseWriter, r *http.Request) {
query := r.URL.Query()
minLevel, ok := optionalQueryInt(w, query, "min_level")
if !ok {
return
}
userID, ok := optionalQueryInt64(w, query, "user_id")
if !ok {
return
}
beforeID, ok := optionalQueryInt64(w, query, "before_id")
if !ok {
return
}
limit, ok := optionalQueryInt(w, query, "limit")
if !ok {
return
}
items, err := s.svc.AccountRatings(r.Context(), domain.AccountRatingFilter{
MinLevel: minLevel, UserID: userID, BeforeID: beforeID, Limit: limit,
})
if err != nil {
writeAccountRatingError(w, err)
return
}
ratings := make([]map[string]any, 0, len(items))
for _, item := range items {
ratings = append(ratings, accountRatingResponse(item))
}
writeJSON(w, http.StatusOK, map[string]any{"ratings": ratings})
}
func (s *Server) handleAccountRating(w http.ResponseWriter, r *http.Request) {
userID, ok := moderationPathID(w, r, "id")
if !ok {
return
}
rating, err := s.svc.AccountRating(r.Context(), userID)
if err != nil {
writeAccountRatingError(w, err)
return
}
limit, ok := optionalQueryInt(w, r.URL.Query(), "limit")
if !ok {
return
}
events, err := s.svc.AccountRatingEvents(r.Context(), userID, limit)
if err != nil {
writeAccountRatingError(w, err)
return
}
ledger := make([]map[string]any, 0, len(events))
for _, item := range events {
ledger = append(ledger, accountRatingEventResponse(item))
}
writeJSON(w, http.StatusOK, map[string]any{
"rating": accountRatingResponse(rating), "events": ledger,
})
}
// collectibleOwnerFilter reads the optional owner filter. At most one of the two
// identifiers may be present, mirroring the mint/transfer request shape.
func collectibleOwnerFilter(w http.ResponseWriter, query url.Values) (domain.Peer, bool) {
@ -1695,50 +1228,6 @@ func collectibleUsernameTransferResponse(item domain.CollectibleUsernameTransfer
// accountRatingResponse renders one composite rating. The score and every
// component stay decimal strings for the same exactness reason as the asset ids.
func accountRatingResponse(rating domain.AccountRating) map[string]any {
out := map[string]any{
"user_id": strconv.FormatInt(rating.UserID, 10),
"level": rating.Level,
"stars": strconv.FormatInt(rating.Stars, 10),
"current_level_stars": strconv.FormatInt(rating.CurrentLevelStars, 10),
"has_next_level": rating.HasNextLevel,
"stars_component": strconv.FormatInt(rating.StarsComponent, 10),
"activity_component": strconv.FormatInt(rating.ActivityComponent, 10),
"penalty_component": strconv.FormatInt(rating.PenaltyComponent, 10),
"manual_component": strconv.FormatInt(rating.ManualComponent, 10),
"pending_stars": strconv.FormatInt(rating.PendingStars, 10),
"version": strconv.FormatInt(rating.Version, 10),
}
if rating.HasNextLevel {
out["next_level_stars"] = strconv.FormatInt(rating.NextLevelStars, 10)
}
if !rating.PendingDate.IsZero() {
out["pending_date"] = rating.PendingDate.UTC().Format(time.RFC3339)
}
if !rating.ComputedAt.IsZero() {
out["computed_at"] = rating.ComputedAt.UTC().Format(time.RFC3339)
}
if !rating.UpdatedAt.IsZero() {
out["updated_at"] = rating.UpdatedAt.UTC().Format(time.RFC3339)
}
return out
}
func accountRatingEventResponse(event domain.AccountRatingEvent) map[string]any {
out := map[string]any{
"id": strconv.FormatInt(event.ID, 10),
"user_id": strconv.FormatInt(event.UserID, 10),
"kind": string(event.Kind),
"amount": strconv.FormatInt(event.Amount, 10),
"reason": event.Reason,
"actor": event.Actor,
"command_key": event.CommandKey,
}
if !event.CreatedAt.IsZero() {
out["created_at"] = event.CreatedAt.UTC().Format(time.RFC3339)
}
return out
}
// writeCollectibleUsernameError maps a collectible-username failure onto its
// stable admin code and the matching HTTP status, the way writeModerationError
@ -1760,18 +1249,6 @@ func writeCollectibleUsernameError(w http.ResponseWriter, err error) {
writeCodedError(w, status, code, err.Error())
}
func writeAccountRatingError(w http.ResponseWriter, err error) {
code := admin.AccountRatingErrorCode(err)
status := http.StatusInternalServerError
switch code {
case admin.CodeRatingNotFound:
status = http.StatusNotFound
case admin.CodeRatingAdjustmentInvalid, admin.CodeRatingWeightsInvalid:
status = http.StatusBadRequest
}
writeCodedError(w, status, code, err.Error())
}
func writeCodedError(w http.ResponseWriter, status int, code, msg string) {
body := map[string]string{"error": msg}
if code != "" {

View file

@ -1,10 +1,8 @@
package adminapi
import (
"bytes"
"context"
"encoding/json"
"mime/multipart"
"net/http"
"net/http/httptest"
"strings"
@ -12,8 +10,6 @@ import (
"telesrv/internal/admin"
"telesrv/internal/domain"
"telesrv/internal/officialgifts"
"telesrv/internal/seed/giftdemo"
)
func TestAdminAPIRequiresBearerToken(t *testing.T) {
@ -278,20 +274,6 @@ func TestAdminAPISetVerified(t *testing.T) {
}
}
func TestAdminAPIGrantStars(t *testing.T) {
srv := &Server{token: "secret", svc: fakeService{}}
req := httptest.NewRequest(http.MethodPost, "/v1/accounts/grant-stars", strings.NewReader(`{"command_id":"c-stars","actor":"ops","reason":"manual grant","dry_run":true,"user_id":1001,"amount":500}`))
req.Header.Set("Authorization", "Bearer secret")
rec := httptest.NewRecorder()
srv.routes().ServeHTTP(rec, req)
if rec.Code != http.StatusOK {
t.Fatalf("status=%d body=%s", rec.Code, rec.Body.String())
}
if !strings.Contains(rec.Body.String(), `"command_id":"c-stars"`) {
t.Fatalf("body=%s", rec.Body.String())
}
}
func TestAdminAPISetChannelVerified(t *testing.T) {
srv := &Server{token: "secret", svc: fakeService{}}
req := httptest.NewRequest(http.MethodPost, "/v1/channels/set-verified", strings.NewReader(`{"command_id":"c3","actor":"ops","reason":"official","dry_run":true,"channel_id":2001,"verified":true}`))
@ -306,103 +288,6 @@ func TestAdminAPISetChannelVerified(t *testing.T) {
}
}
func TestAdminAPIImportStarGiftMultipart(t *testing.T) {
var body bytes.Buffer
writer := multipart.NewWriter(&body)
if err := writer.WriteField("metadata", `{"command_id":"gift-1","actor":"ops","reason":"catalog","dry_run":true,"title":"Gift","stars":50,"convert_stars":25,"enabled":true,"sort_order":3}`); err != nil {
t.Fatal(err)
}
part, err := writer.CreateFormFile("file", "gift.lottie")
if err != nil {
t.Fatal(err)
}
animation := []byte(`{"v":"5.7","w":512,"h":512,"fr":30,"ip":0,"op":30,"layers":[{}]}`)
if _, err := part.Write(animation); err != nil {
t.Fatal(err)
}
if err := writer.Close(); err != nil {
t.Fatal(err)
}
svc := &captureGiftService{}
srv := &Server{token: "secret", svc: svc}
req := httptest.NewRequest(http.MethodPost, "/v1/gifts/import", &body)
req.Header.Set("Authorization", "Bearer secret")
req.Header.Set("Content-Type", writer.FormDataContentType())
rec := httptest.NewRecorder()
srv.routes().ServeHTTP(rec, req)
if rec.Code != http.StatusOK {
t.Fatalf("status=%d body=%s", rec.Code, rec.Body.String())
}
if svc.req.CommandID != "gift-1" || svc.req.FileName != "gift.lottie" || !bytes.Equal(svc.req.Data, animation) || svc.req.Stars != 50 || svc.req.ConvertStars != 25 {
t.Fatalf("decoded gift request = %+v", svc.req)
}
}
func TestAdminAPIPublishStarGiftCollectiblesMultipart(t *testing.T) {
var body bytes.Buffer
writer := multipart.NewWriter(&body)
metadata := `{"command_id":"pool-1","actor":"ops","reason":"pool","dry_run":true,"upgrade_stars":125,"supply_total":100,"slug_prefix":"cake","models":[{"name":"Ruby","rarity_permille":500,"sort_order":0,"file_key":"model-0"},{"name":"Sapphire","rarity_permille":500,"sort_order":1,"file_key":"model-1"}],"patterns":[{"name":"Stars","rarity_permille":500,"sort_order":0,"file_key":"pattern-0"},{"name":"Moons","rarity_permille":500,"sort_order":1,"file_key":"pattern-1"}],"backdrops":[{"name":"Night","backdrop_id":1,"center_color":1122867,"edge_color":2241348,"pattern_color":3359829,"text_color":16777215,"rarity_permille":500,"sort_order":0},{"name":"Day","backdrop_id":2,"center_color":11189196,"edge_color":7833753,"pattern_color":14544639,"text_color":1118481,"rarity_permille":500,"sort_order":1}]}`
if err := writer.WriteField("metadata", metadata); err != nil {
t.Fatal(err)
}
for key, name := range map[string]string{
"model-0": "ruby.lottie", "model-1": "sapphire.lottie",
"pattern-0": "stars.tgs", "pattern-1": "moons.tgs",
} {
part, err := writer.CreateFormFile(key, name)
if err != nil {
t.Fatal(err)
}
if _, err := part.Write([]byte(key)); err != nil {
t.Fatal(err)
}
}
if err := writer.Close(); err != nil {
t.Fatal(err)
}
svc := &captureCollectibleService{}
srv := &Server{token: "secret", svc: svc}
req := httptest.NewRequest(http.MethodPost, "/v1/gifts/11/collectibles/publish", &body)
req.Header.Set("Authorization", "Bearer secret")
req.Header.Set("Content-Type", writer.FormDataContentType())
rec := httptest.NewRecorder()
srv.routes().ServeHTTP(rec, req)
if rec.Code != http.StatusOK {
t.Fatalf("status=%d body=%s", rec.Code, rec.Body.String())
}
if svc.req.GiftID != 11 || len(svc.req.Models) != 2 || svc.req.Models[0].FileName != "ruby.lottie" ||
string(svc.req.Patterns[0].Data) != "pattern-0" || len(svc.req.Backdrops) != 2 || svc.req.Backdrops[1].BackdropID != 2 {
t.Fatalf("decoded collectible request = %+v", svc.req)
}
}
func TestCollectiblePreviewResponsePreservesInt64AsDecimalStrings(t *testing.T) {
const maxInt64 = int64(9223372036854775807)
got := collectiblePreviewResponse(domain.StarGiftUpgradePreview{
GiftID: maxInt64,
UpgradeStars: maxInt64,
Models: []domain.StarGiftCollectibleAttribute{{
ID: maxInt64,
Kind: domain.StarGiftCollectibleModel,
Name: "Exact",
RarityKind: domain.StarGiftRarityPermille,
RarityPermille: 1000,
OfficialDocumentID: maxInt64,
}},
})
if got["gift_id"] != "9223372036854775807" || got["upgrade_stars"] != "9223372036854775807" {
t.Fatalf("preview ids = %#v", got)
}
models, ok := got["models"].([]map[string]any)
if !ok || len(models) != 1 {
t.Fatalf("preview models = %#v", got["models"])
}
if models[0]["id"] != "9223372036854775807" || models[0]["official_document_id"] != "9223372036854775807" {
t.Fatalf("preview model ids = %#v", models[0])
}
}
type fakeService struct{}
type captureFreezeService struct {
@ -410,31 +295,11 @@ type captureFreezeService struct {
req admin.SetAccountFrozenRequest
}
type captureGiftService struct {
fakeService
req admin.ImportStarGiftRequest
}
type captureCollectibleService struct {
fakeService
req admin.PublishStarGiftCollectiblesRequest
}
func (s *captureFreezeService) SetAccountFrozen(_ context.Context, req admin.SetAccountFrozenRequest) (admin.CommandResult, error) {
s.req = req
return admin.CommandResult{CommandID: req.CommandID, Status: "completed", DryRun: req.DryRun}, nil
}
func (s *captureGiftService) ImportStarGift(_ context.Context, req admin.ImportStarGiftRequest) (admin.CommandResult, error) {
s.req = req
return admin.CommandResult{CommandID: req.CommandID, Status: "completed", DryRun: req.DryRun}, nil
}
func (s *captureCollectibleService) PublishStarGiftCollectibles(_ context.Context, req admin.PublishStarGiftCollectiblesRequest) (admin.CommandResult, error) {
s.req = req
return admin.CommandResult{CommandID: req.CommandID, Status: "completed", DryRun: req.DryRun}, nil
}
func (fakeService) SetAccountFrozen(_ context.Context, req admin.SetAccountFrozenRequest) (admin.CommandResult, error) {
return admin.CommandResult{CommandID: req.CommandID, Status: "completed", DryRun: req.DryRun}, nil
}
@ -443,10 +308,6 @@ func (fakeService) GrantPremium(_ context.Context, req admin.GrantPremiumRequest
return admin.CommandResult{CommandID: req.CommandID, Status: "completed", DryRun: req.DryRun}, nil
}
func (fakeService) GrantStars(_ context.Context, req admin.GrantStarsRequest) (admin.CommandResult, error) {
return admin.CommandResult{CommandID: req.CommandID, Status: "completed", DryRun: req.DryRun}, nil
}
func (fakeService) SetVerified(_ context.Context, req admin.SetVerifiedRequest) (admin.CommandResult, error) {
return admin.CommandResult{CommandID: req.CommandID, Status: "completed", DryRun: req.DryRun}, nil
}
@ -483,10 +344,6 @@ func (fakeService) SetSupport(_ context.Context, req admin.SetSupportRequest) (a
return admin.CommandResult{CommandID: req.CommandID, Status: "completed", DryRun: req.DryRun}, nil
}
func (fakeService) GiveGift(_ context.Context, req admin.GiveGiftRequest) (admin.CommandResult, error) {
return admin.CommandResult{CommandID: req.CommandID, Status: "completed", DryRun: req.DryRun}, nil
}
func (fakeService) SetUsername(_ context.Context, req admin.SetUsernameRequest) (admin.CommandResult, error) {
return admin.CommandResult{CommandID: req.CommandID, Status: "completed", DryRun: req.DryRun}, nil
}
@ -551,18 +408,6 @@ func (fakeService) DeletePrivateHistory(context.Context, admin.DeletePrivateHist
return admin.CommandResult{}, nil
}
func (fakeService) ImportStarGift(_ context.Context, req admin.ImportStarGiftRequest) (admin.CommandResult, error) {
return admin.CommandResult{CommandID: req.CommandID, Status: "completed", DryRun: req.DryRun}, nil
}
func (fakeService) ImportDefaultStarGift(_ context.Context, req admin.ImportDefaultStarGiftRequest) (admin.CommandResult, error) {
return admin.CommandResult{CommandID: req.CommandID, Status: "completed", DryRun: req.DryRun}, nil
}
func (fakeService) ImportAllDefaultStarGifts(_ context.Context, req admin.ImportAllDefaultStarGiftsRequest) (admin.CommandResult, error) {
return admin.CommandResult{CommandID: req.CommandID, Status: "completed", DryRun: req.DryRun}, nil
}
func (fakeService) AccountAvatar(context.Context, int64) ([]byte, string, bool, error) {
return nil, "", false, nil
}
@ -599,58 +444,10 @@ func (fakeService) StickerDocumentAnimation(context.Context, int64) ([]byte, str
return nil, "", false, nil
}
func (fakeService) DefaultStarGifts() []giftdemo.GiftInfo {
return giftdemo.List()
}
func (fakeService) DefaultStarGiftAnimation(context.Context, int) ([]byte, bool, error) {
return []byte(`{"v":"5.7","w":512,"h":512}`), true, nil
}
func (fakeService) ImportOfficialStarGift(_ context.Context, req admin.ImportOfficialStarGiftRequest) (admin.CommandResult, error) {
return admin.CommandResult{CommandID: req.CommandID, Status: "completed", DryRun: req.DryRun}, nil
}
func (fakeService) ImportAllOfficialStarGifts(_ context.Context, req admin.ImportAllOfficialStarGiftsRequest) (admin.CommandResult, error) {
return admin.CommandResult{CommandID: req.CommandID, Status: "completed", DryRun: req.DryRun}, nil
}
func (fakeService) OfficialStarGifts(context.Context) ([]officialgifts.GiftSummary, error) {
return nil, nil
}
func (fakeService) OfficialStarGiftAnimation(context.Context, string) ([]byte, bool, error) {
return []byte(`{"v":"5.7","w":512,"h":512}`), true, nil
}
func (fakeService) PublishStarGiftCollectibles(_ context.Context, req admin.PublishStarGiftCollectiblesRequest) (admin.CommandResult, error) {
return admin.CommandResult{CommandID: req.CommandID, Status: "completed", DryRun: req.DryRun}, nil
}
func (fakeService) SetStarGiftEnabled(_ context.Context, req admin.SetStarGiftEnabledRequest) (admin.CommandResult, error) {
return admin.CommandResult{CommandID: req.CommandID, Status: "completed", DryRun: req.DryRun}, nil
}
func (fakeService) SetStarGiftSortOrder(_ context.Context, req admin.SetStarGiftSortOrderRequest) (admin.CommandResult, error) {
return admin.CommandResult{CommandID: req.CommandID, Status: "completed", DryRun: req.DryRun}, nil
}
func (fakeService) StarGiftAnimation(context.Context, int64) ([]byte, bool, error) {
return []byte(`{"v":"5.7","w":512,"h":512}`), true, nil
}
func (fakeService) EmojiAnimation(context.Context, int64) ([]byte, bool, error) {
return []byte(`{"v":"5.7","w":100,"h":100}`), true, nil
}
func (fakeService) StarGiftCollectibles(context.Context, int64) (domain.StarGiftUpgradePreview, bool, error) {
return domain.StarGiftUpgradePreview{}, false, nil
}
func (fakeService) StarGiftCollectibleAnimation(context.Context, int64, domain.StarGiftCollectibleAttributeKind, int64) ([]byte, bool, error) {
return []byte(`{"v":"5.7","w":512,"h":512}`), true, nil
}
func (fakeService) ModerationCases(context.Context, domain.ModerationCaseFilter) ([]domain.ModerationCase, error) {
return nil, nil
}
@ -747,64 +544,12 @@ func maxInt64Collectible() domain.CollectibleUsername {
}
}
type captureAccountRatingService struct {
fakeService
recompute admin.RecomputeAccountRatingRequest
adjust admin.AdjustAccountRatingRequest
filter domain.AccountRatingFilter
}
func (s *captureAccountRatingService) RecomputeAccountRating(_ context.Context, req admin.RecomputeAccountRatingRequest) (admin.CommandResult, error) {
s.recompute = req
return admin.CommandResult{CommandID: req.CommandID, Status: "completed", DryRun: req.DryRun}, nil
}
func (s *captureAccountRatingService) AdjustAccountRating(_ context.Context, req admin.AdjustAccountRatingRequest) (admin.CommandResult, error) {
s.adjust = req
return admin.CommandResult{CommandID: req.CommandID, Status: "completed", DryRun: req.DryRun}, nil
}
func (s *captureAccountRatingService) AccountRatings(_ context.Context, filter domain.AccountRatingFilter) ([]domain.AccountRating, error) {
s.filter = filter
return []domain.AccountRating{maxInt64Rating()}, nil
}
func (s *captureAccountRatingService) AccountRating(_ context.Context, userID int64) (domain.AccountRating, error) {
rating := maxInt64Rating()
rating.UserID = userID
return rating, nil
}
func (s *captureAccountRatingService) AccountRatingEvents(_ context.Context, userID int64, _ int) ([]domain.AccountRatingEvent, error) {
return []domain.AccountRatingEvent{{
ID: 9223372036854775807, UserID: userID,
Kind: domain.AccountRatingEventManual, Amount: -9223372036854775807,
Actor: "ops", Reason: "abuse",
}}, nil
}
func maxInt64Rating() domain.AccountRating {
return domain.AccountRating{
UserID: 1001,
Level: 7,
Stars: 9223372036854775807,
CurrentLevelStars: 4900,
NextLevelStars: 6400,
HasNextLevel: true,
StarsComponent: 9223372036854775807,
ManualComponent: -1500,
Version: 9223372036854775807,
}
}
func TestAdminAPICollectibleUsernameCommandsRequireToken(t *testing.T) {
srv := &Server{token: "secret", svc: fakeService{}}
for _, path := range []string{
"/v1/collectible-usernames/mint",
"/v1/collectible-usernames/transfer",
"/v1/collectible-usernames/revoke",
"/v1/account-ratings/recompute",
"/v1/account-ratings/adjust",
} {
req := httptest.NewRequest(http.MethodPost, path, strings.NewReader(`{}`))
rec := httptest.NewRecorder()
@ -816,8 +561,6 @@ func TestAdminAPICollectibleUsernameCommandsRequireToken(t *testing.T) {
for _, path := range []string{
"/v1/collectible-usernames",
"/v1/collectible-usernames/7",
"/v1/account-ratings",
"/v1/account-ratings/7",
} {
req := httptest.NewRequest(http.MethodGet, path, nil)
rec := httptest.NewRecorder()
@ -875,28 +618,6 @@ func TestAdminAPITransferAndRevokeCollectibleUsername(t *testing.T) {
}
}
func TestAdminAPIAccountRatingCommands(t *testing.T) {
svc := &captureAccountRatingService{}
srv := &Server{token: "secret", svc: svc}
recompute := httptest.NewRequest(http.MethodPost, "/v1/account-ratings/recompute", strings.NewReader(
`{"command_id":"rc-1","actor":"ops","reason":"support ticket","dry_run":true,"user_id":"1001"}`))
recompute.Header.Set("Authorization", "Bearer secret")
recomputeRec := httptest.NewRecorder()
srv.routes().ServeHTTP(recomputeRec, recompute)
if recomputeRec.Code != http.StatusOK || svc.recompute.UserID != 1001 || !svc.recompute.DryRun {
t.Fatalf("recompute status=%d request=%+v body=%s", recomputeRec.Code, svc.recompute, recomputeRec.Body.String())
}
adjust := httptest.NewRequest(http.MethodPost, "/v1/account-ratings/adjust", strings.NewReader(
`{"command_id":"adj-1","actor":"ops","reason":"manual penalty","user_id":"1001","amount":"-2500"}`))
adjust.Header.Set("Authorization", "Bearer secret")
adjustRec := httptest.NewRecorder()
srv.routes().ServeHTTP(adjustRec, adjust)
if adjustRec.Code != http.StatusOK || svc.adjust.Amount != -2500 || svc.adjust.DryRun {
t.Fatalf("adjust status=%d request=%+v body=%s", adjustRec.Code, svc.adjust, adjustRec.Body.String())
}
}
func TestAdminAPICollectibleUsernameReadsUseDecimalStrings(t *testing.T) {
svc := &captureCollectibleUsernameService{}
srv := &Server{token: "secret", svc: svc}
@ -939,44 +660,7 @@ func TestAdminAPICollectibleUsernameReadsUseDecimalStrings(t *testing.T) {
}
}
func TestAdminAPIAccountRatingReadsUseDecimalStrings(t *testing.T) {
svc := &captureAccountRatingService{}
srv := &Server{token: "secret", svc: svc}
list := httptest.NewRequest(http.MethodGet, "/v1/account-ratings?min_level=3&user_id=1001&limit=10&before_id=99", nil)
list.Header.Set("Authorization", "Bearer secret")
listRec := httptest.NewRecorder()
srv.routes().ServeHTTP(listRec, list)
if listRec.Code != http.StatusOK {
t.Fatalf("list status=%d body=%s", listRec.Code, listRec.Body.String())
}
if svc.filter.MinLevel != 3 || svc.filter.UserID != 1001 || svc.filter.Limit != 10 || svc.filter.BeforeID != 99 {
t.Fatalf("rating filter = %+v", svc.filter)
}
if !strings.Contains(listRec.Body.String(), `"stars":"9223372036854775807"`) {
t.Fatalf("rating list lost int64 precision: %s", listRec.Body.String())
}
detail := httptest.NewRequest(http.MethodGet, "/v1/account-ratings/1001", nil)
detail.Header.Set("Authorization", "Bearer secret")
detailRec := httptest.NewRecorder()
srv.routes().ServeHTTP(detailRec, detail)
if detailRec.Code != http.StatusOK {
t.Fatalf("detail status=%d body=%s", detailRec.Code, detailRec.Body.String())
}
var payload struct {
Rating map[string]any `json:"rating"`
Events []map[string]any `json:"events"`
}
if err := json.Unmarshal(detailRec.Body.Bytes(), &payload); err != nil {
t.Fatalf("decode detail: %v", err)
}
if payload.Rating["user_id"] != "1001" || payload.Rating["stars"] != "9223372036854775807" ||
len(payload.Events) != 1 || payload.Events[0]["amount"] != "-9223372036854775807" {
t.Fatalf("rating detail payload = %+v", payload)
}
}
func TestAdminAPIMissingCollectibleAndRatingReportCodedErrors(t *testing.T) {
func TestAdminAPIMissingCollectibleReportsCodedError(t *testing.T) {
srv := &Server{token: "secret", svc: fakeService{}}
asset := httptest.NewRequest(http.MethodGet, "/v1/collectible-usernames/5", nil)
asset.Header.Set("Authorization", "Bearer secret")
@ -986,15 +670,6 @@ func TestAdminAPIMissingCollectibleAndRatingReportCodedErrors(t *testing.T) {
!strings.Contains(assetRec.Body.String(), `"code":"`+admin.CodeCollectibleNotFound+`"`) {
t.Fatalf("missing asset status=%d body=%s", assetRec.Code, assetRec.Body.String())
}
rating := httptest.NewRequest(http.MethodGet, "/v1/account-ratings/5", nil)
rating.Header.Set("Authorization", "Bearer secret")
ratingRec := httptest.NewRecorder()
srv.routes().ServeHTTP(ratingRec, rating)
if ratingRec.Code != http.StatusNotFound ||
!strings.Contains(ratingRec.Body.String(), `"code":"`+admin.CodeRatingNotFound+`"`) {
t.Fatalf("missing rating status=%d body=%s", ratingRec.Code, ratingRec.Body.String())
}
}
func (fakeService) MintCollectibleUsername(_ context.Context, req admin.MintCollectibleUsernameRequest) (admin.CommandResult, error) {
@ -1024,23 +699,3 @@ func (fakeService) CollectibleUsernameByID(context.Context, int64) (domain.Colle
func (fakeService) CollectibleUsernameTransfers(context.Context, int64, int) ([]domain.CollectibleUsernameTransfer, error) {
return nil, nil
}
func (fakeService) RecomputeAccountRating(_ context.Context, req admin.RecomputeAccountRatingRequest) (admin.CommandResult, error) {
return admin.CommandResult{CommandID: req.CommandID, Status: "completed", DryRun: req.DryRun}, nil
}
func (fakeService) AdjustAccountRating(_ context.Context, req admin.AdjustAccountRatingRequest) (admin.CommandResult, error) {
return admin.CommandResult{CommandID: req.CommandID, Status: "completed", DryRun: req.DryRun}, nil
}
func (fakeService) AccountRating(context.Context, int64) (domain.AccountRating, error) {
return domain.AccountRating{}, domain.ErrAccountRatingNotFound
}
func (fakeService) AccountRatings(context.Context, domain.AccountRatingFilter) ([]domain.AccountRating, error) {
return nil, nil
}
func (fakeService) AccountRatingEvents(context.Context, int64, int) ([]domain.AccountRatingEvent, error) {
return nil, nil
}