[www] Fix room badge grants and profile badge lists (#148)

This commit is contained in:
dukedotdev
2026-10-27 23:14:10 -04:00
committed by GitHub
parent 1d55ae8b80
commit fd30182f3d
7 changed files with 353 additions and 11 deletions
+10 -7
View File
@@ -1819,7 +1819,7 @@ function StaffPlayerActions({ account, navigate }: { account: PublicAccount; nav
e.preventDefault()
void itemGift.run(async () => {
const res = await call<{
kind: 'custom_item' | 'skin' | 'consumable'
kind: 'custom_item' | 'badge' | 'skin' | 'consumable'
name: string
prefabName?: string
owned?: number
@@ -1829,6 +1829,8 @@ function StaffPlayerActions({ account, navigate }: { account: PublicAccount; nav
})
setItemId('')
switch (res.kind) {
case 'badge':
return `Granted the badge “${res.name}” to @${account.username}.`
case 'skin':
return `Sent the skin “${res.name}” (${res.prefabName}) to @${account.username}.`
case 'consumable':
@@ -1840,12 +1842,12 @@ function StaffPlayerActions({ account, navigate }: { account: PublicAccount; nav
}}
>
<label>
Gift item
Gift item or badge
<span className="staff-gift">
<input
type="text"
value={itemId}
placeholder="Custom item, skin or consumable id"
placeholder="Badge, custom item, skin or consumable id"
spellCheck={false}
required
onChange={(e) => setItemId(e.target.value)}
@@ -1855,10 +1857,11 @@ function StaffPlayerActions({ account, navigate }: { account: PublicAccount; nav
</button>
</span>
<span className="hint">
A custom avatar item’s guid, a skin’s modification guid, a consumable’s item desc,
or a store id — the kind is worked out from the id. Arrives as a gift box, as if
bought from the store; free to them, and a creator isn’t paid. Consumables stack:
send again for another.
A badge or custom avatar item’s guid, a skin’s modification guid, a consumable’s
item desc, or a store id — the kind is worked out from the id. Arrives as a gift
box, as if bought from the store; free to them, and a creator isn’t paid.
Consumables stack: send again for another. Badges must be published and can be
granted while the player is offline.
</span>
</label>
{itemGift.error && <p className="error">{itemGift.error}</p>}
+7 -2
View File
@@ -1413,7 +1413,12 @@ async function giftCustomItem(
return c.json({ error: 'This player already owns that item' }, 409)
}
await grantCustomAvatarItem(c.env.DB, playerId, item.CustomAvatarItemId)
if (item.OutfitType === 1000 && item.CurrentSaves.length === 0) {
return c.json({ error: 'That badge has no published image' }, 400)
}
if (!(await grantCustomAvatarItem(c.env.DB, playerId, item.CustomAvatarItemId))) {
return c.json({ error: 'This player already owns that item' }, 409)
}
const content = staffGiftContent({
GiftContext: GIFT_CONTEXT_PURCHASED_GIFT_A,
CustomAvatarItemId: item.CustomAvatarItemId,
@@ -1439,7 +1444,7 @@ async function giftCustomItem(
await announceGift(c, playerId, gift.id, content)
return c.json({
kind: 'custom_item',
kind: item.OutfitType === 1000 ? 'badge' : 'custom_item',
playerId,
customAvatarItemId: item.CustomAvatarItemId,
name: item.Name,
+72
View File
@@ -37,6 +37,7 @@ import { generateToken } from '@repo/jwt'
import {
createCustomAvatarItem,
SCHEMA_DDL as CUSTOM_AVATAR_ITEM_SCHEMA_DDL,
importCustomAvatarItem,
} from '../../../../api/src/custom-avatar-items-db'
import {
createReport,
@@ -2637,6 +2638,77 @@ it('gifts a player a custom item in a gift box', async () => {
])
})
it('grants a published room badge through profile gifting without room presence, once', async () => {
await clearHub()
await updateAccount(env.DB, 8530, { username: 'BadgeRecipient' })
const seed = await seedCustomItem('0a1b2c3d-0000-4000-8000-000000000030', 8531)
const badge = await importCustomAvatarItem(env.DB, {
...seed,
Name: 'Room Champion',
OutfitType: 1000,
CustomBadgeMetadata: { RoomId: 12345 },
CurrentSaves: [
{
CustomAvatarItemSaveId: 1,
CustomAvatarItemId: seed.CustomAvatarItemId,
UnityAssetId: crypto.randomUUID(),
BodyType: 0,
OutfitType: 1000,
QAState: 0,
CreatedAt: seed.CreatedAt,
ModifiedAt: seed.ModifiedAt,
Description: null,
ThumbnailFileName: 'badge.png',
AdditionalConfiguration: '{}',
UnityAsset: 'badge.png',
UnityAssetHash: '',
UnityAsset2: null,
UnityAsset2Hash: null,
},
],
})
const responses = await Promise.all([
giftItem(8530, badge.CustomAvatarItemId.toUpperCase(), 'Congratulations!'),
giftItem(8530, badge.CustomAvatarItemId, 'Congratulations!'),
])
expect(responses.map((r) => r.status).sort((a, b) => a - b)).toEqual([200, 409])
const body = await responses.find((r) => r.status === 200)!.json()
expect(body).toMatchObject({ kind: 'badge', name: 'Room Champion' })
await expect(ownedCustomAvatarItemIds(env.DB, 8530, [badge.CustomAvatarItemId])).resolves.toEqual(
new Set([badge.CustomAvatarItemId])
)
const gifts = await getPendingGifts(env.DB, 8530)
expect(gifts).toHaveLength(1)
expect(gifts[0]).toMatchObject({
CustomAvatarItemId: badge.CustomAvatarItemId,
AvatarItemType: 0,
GiftContext: 500,
Message: 'Congratulations!',
})
const frames = await hubFrames()
expect(frames).toHaveLength(1)
expect(frames[0]).toMatchObject({
playerId: 8530,
notificationType: 31,
data: { CustomAvatarItemId: badge.CustomAvatarItemId, Message: 'Congratulations!' },
})
expect(await auditRows('gift_custom_item', 8530)).toHaveLength(1)
expect((await giftItem(8530, badge.CustomAvatarItemId)).status).toBe(409)
const draft = await importCustomAvatarItem(env.DB, {
...badge,
CustomAvatarItemId: crypto.randomUUID(),
Accessibility: 0,
})
expect((await giftItem(8530, draft.CustomAvatarItemId)).status).toBe(404)
const unbuilt = await importCustomAvatarItem(env.DB, {
...badge,
CustomAvatarItemId: crypto.randomUUID(),
CurrentSaves: [],
})
expect((await giftItem(8530, unbuilt.CustomAvatarItemId)).status).toBe(400)
expect(await getPendingGifts(env.DB, 8530)).toHaveLength(1)
})
it('refuses a custom item gift the store would refuse', async () => {
await updateAccount(env.DB, 8340, { username: 'Picky' })
const published = await seedCustomItem('0a1b2c3d-0000-4000-8000-000000000002', 8341)