From 7feec54cafc3c67a1b0ed4157e67bcde44c35849 Mon Sep 17 00:00:00 2001 From: Eli Mallon Date: Thu, 30 Apr 2026 12:23:13 -0700 Subject: [PATCH] Badge-gate cross-user commands MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Streamer (goose.art): "you should have mod permissions to affect any user besides yourself i think". !shrink @target, !grow @target, and `reload @target` all reach into another chatter's pet state. Behind a chatterIsBadged() check now, same gate that already protects !barrelroll / !refresh / !clearall — anyone without a place.stream.badge.issuance record on the stream.place account gets a console.log denial. Self-affecting commands (bulge, blur, up/down, reload self, !reset, etc.) remain ungated. Co-Authored-By: Claude Opus 4.7 (1M context) --- pets.html | 59 +++++++++++++++++++++++++++++++------------------------ 1 file changed, 33 insertions(+), 26 deletions(-) diff --git a/pets.html b/pets.html index 816a380..3b30c90 100644 --- a/pets.html +++ b/pets.html @@ -1613,21 +1613,25 @@ async function handleChatMessage(authorDid, record) { } // `reload @other-handle` — drop another chatter's profile + pet so their newly - // uploaded sprite resolves on their next message. Bounded by 'pets currently - // on stage'; griefable but harmless (target reappears with fresh data). + // uploaded sprite resolves on their next message. Mod-only (badge-gated) per + // streamer feedback: actions that affect a different user need permission. const reloadOtherMatch = (record.text || '').match(/^\s*(?:reload|reloadsprite|reload\s*sprite|refresh|respawn)\s+(?:rpg\.actor\s+sprite\s+for\s+)?@?([A-Za-z0-9\.\-_]+)\s*$/i); if (reloadOtherMatch && reloadOtherMatch[1].toLowerCase() !== 'sprite') { - const target = reloadOtherMatch[1].toLowerCase(); - for (const [did, p] of pets) { - const h = (p.handle || '').toLowerCase(); - if (h === target || h.split('.')[0] === target || h.startsWith(target + '.')) { - profileCache.delete(did); - profileFetchedAt.delete(did); - p.dispose(); - pets.delete(did); - console.log('[pets] reloaded target', did, '@' + h); - break; + if (await chatterIsBadged(authorDid)) { + const target = reloadOtherMatch[1].toLowerCase(); + for (const [did, p] of pets) { + const h = (p.handle || '').toLowerCase(); + if (h === target || h.split('.')[0] === target || h.startsWith(target + '.')) { + profileCache.delete(did); + profileFetchedAt.delete(did); + p.dispose(); + pets.delete(did); + console.log('[pets] reloaded target', did, '@' + h); + break; + } } + } else { + console.log('[pets] reload @target denied — chatter has no badge', authorDid); } } @@ -1749,24 +1753,27 @@ async function handleChatMessage(authorDid, record) { } // !shrink @target / !grow @target — affect another chatter's pet by handle. - // Looks up the named pet by handle (case-insensitive). Silently no-op if - // the target isn't currently on stage. + // Mod-only (badge-gated) per streamer: cross-user commands need mod perms. const shrinkRayMatch = text.match(/!(shrink|shrinkray|grow|growray)\s+@?([\w\.\-]+)/i); if (shrinkRayMatch) { - const isShrink = /^shrink/i.test(shrinkRayMatch[1]); - const target = shrinkRayMatch[2].toLowerCase(); - let foundPet = null; - for (const p of pets.values()) { - const h = (p.handle || '').toLowerCase(); - if (h === target || h.split('.')[0] === target || h.startsWith(target + '.')) { - foundPet = p; break; + if (await chatterIsBadged(authorDid)) { + const isShrink = /^shrink/i.test(shrinkRayMatch[1]); + const target = shrinkRayMatch[2].toLowerCase(); + let foundPet = null; + for (const p of pets.values()) { + const h = (p.handle || '').toLowerCase(); + if (h === target || h.split('.')[0] === target || h.startsWith(target + '.')) { + foundPet = p; break; + } + } + if (foundPet) { + foundPet.setDepthScale(foundPet.depthScale * (isShrink ? 1 / 1.5 : 1.5)); + foundPet.pulseHeart && foundPet.pulseHeart(); + } else { + console.log('[pets] !shrink/!grow target not on stage:', target); } - } - if (foundPet) { - foundPet.setDepthScale(foundPet.depthScale * (isShrink ? 1 / 1.5 : 1.5)); - foundPet.pulseHeart && foundPet.pulseHeart(); } else { - console.log('[pets] !shrink/!grow target not on stage:', target); + console.log('[pets] !shrink/!grow denied — chatter has no badge', authorDid); } } -- 2.51.2