Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
115 changes: 98 additions & 17 deletions ServerBadges/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ import { GuildStore, React, UserStore } from "@webpack/common";
const UserProfileStore = findStoreLazy("UserProfileStore");
const GuildProfileStore = findStoreLazy("GuildProfileStore");
const InviteStore = findStoreLazy("InviteStore");
const LocaleStore = findStoreLazy("LocaleStore");

type Override = "none" | "verified" | "partner" | "both";

Expand All @@ -59,6 +60,54 @@ const PARTNER_BADGE = {

const OUR_MARK = "__vcServerBadges";

// Discord localizes "Partnered Server Owner" server-side for real badges, so the
// client doesn't ship the string — we supply our own per-locale labels and fall
// back to English. Keys are Discord locale codes (LocaleStore.locale).
//
// CONFIRMED as Discord's exact official wording: en-US, en-GB, fr.
// All others are best-effort translations and MAY differ from Discord's official
// phrasing — verify against a real badge in that language and correct as needed.
const PARTNER_LABELS: Record<string, string> = {
"en-US": "Partnered Server Owner",
"en-GB": "Partnered Server Owner",
"fr": "Propriétaire d'un serveur partenaire",
// — best-effort below —
"da": "Ejer af en partnerserver",
"de": "Inhaber eines Partnerservers",
"es-ES": "Propietario de un servidor asociado",
"es-419": "Propietario de un servidor asociado",
"hr": "Vlasnik partnerskog poslužitelja",
"it": "Proprietario di un server partner",
"lt": "Partnerio serverio savininkas",
"hu": "Partnerszerver tulajdonosa",
"nl": "Eigenaar van een partnerserver",
"no": "Eier av en partnerserver",
"pl": "Właściciel serwera partnerskiego",
"pt-BR": "Proprietário de servidor parceiro",
"ro": "Proprietar al unui server partener",
"fi": "Kumppanipalvelimen omistaja",
"sv-SE": "Ägare av en partnerserver",
"vi": "Chủ sở hữu máy chủ đối tác",
"tr": "Partner Sunucu Sahibi",
"cs": "Vlastník partnerského serveru",
"el": "Ιδιοκτήτης συνεργαζόμενου διακομιστή",
"bg": "Собственик на партньорски сървър",
"ru": "Владелец партнёрского сервера",
"uk": "Власник партнерського сервера",
"hi": "पार्टनर सर्वर के मालिक",
"th": "เจ้าของเซิร์ฟเวอร์พาร์ทเนอร์",
"zh-CN": "合作服务器所有者",
"ja": "パートナーサーバーのオーナー",
"zh-TW": "合作夥伴伺服器擁有者",
"ko": "파트너 서버 소유자"
};

function partnerLabel(): string {
const loc: string = LocaleStore?.locale ?? LocaleStore?.getLocale?.() ?? "en-US";
// exact locale ("es-ES") -> language prefix ("es") -> English
return PARTNER_LABELS[loc] ?? PARTNER_LABELS[loc.split("-")[0]] ?? PARTNER_LABELS["en-US"];
}

// ─── Owned-servers settings panel ────────────────────────────────────────────

const OVERRIDE_OPTIONS: { value: Override; label: string; }[] = [
Expand Down Expand Up @@ -161,6 +210,11 @@ const settings = definePluginSettings({
// remove a server's genuine features.
const injectedFeatures = new Map<string, Set<string>>();

// Whether the plugin is currently enabled. Only start()/stop() flip this, and
// all injection is gated on it — so interacting with the settings panel while
// the plugin is toggled off can't sneak injections back in.
let enabled = false;

function ownedOverriddenGuilds() {
const me = UserStore.getCurrentUser();
const guilds = GuildStore.getGuilds() as Record<string, any>;
Expand Down Expand Up @@ -222,6 +276,14 @@ function addArrayFeatures(obj: any, features: string[]) {
}
}

// Inverse of addArrayFeatures — used on cleanup.
function removeArrayFeatures(obj: any, features: string[]) {
if (!obj || !Array.isArray(obj.features)) return;
if (features.some(f => obj.features.includes(f))) {
try { obj.features = obj.features.filter((f: string) => !features.includes(f)); } catch { /* not writable */ }
}
}

// The "server profile" card (server-settings preview) reads GuildProfileStore,
// which has its OWN plain-array `features` — separate from the GuildStore record.
function syncGuildProfiles() {
Expand Down Expand Up @@ -269,12 +331,13 @@ function syncProfileBadge() {
if (!profile || !Array.isArray(profile.badges)) return;

const badges: any[] = profile.badges;
const label = partnerLabel();
const hasReal = badges.some(b => b?.id === PARTNER_BADGE.id && !b?.[OUR_MARK]);
const hasOurs = badges.some(b => b?.[OUR_MARK]);
const ours = badges.find(b => b?.[OUR_MARK]);
const want = settings.store.profileBadge && ownsPartneredServer() && !hasReal;

if (want && hasOurs) return; // already added
if (!want && !hasOurs) return; // nothing to remove
if (want && ours?.description === label) return; // already added with the right label
if (!want && !ours) return; // nothing to remove

const cleaned = badges.filter(b => !b?.[OUR_MARK]);

Expand All @@ -283,19 +346,44 @@ function syncProfileBadge() {
// Insert after any leading Nitro/premium badges.
let idx = 0;
while (idx < cleaned.length && String(cleaned[idx]?.id).startsWith("premium")) idx++;
next = [...cleaned.slice(0, idx), { ...PARTNER_BADGE, [OUR_MARK]: true }, ...cleaned.slice(idx)];
next = [...cleaned.slice(0, idx), { ...PARTNER_BADGE, description: label, [OUR_MARK]: true }, ...cleaned.slice(idx)];
}

try { profile.badges = next; } catch { /* badges not writable in this build */ }
}

function sync() {
if (!enabled) return; // never inject while the plugin is off
syncGuildFeatures();
syncGuildProfiles();
syncInvites();
syncProfileBadge();
}

// Undo every injection across all surfaces, using injectedFeatures as the record
// of exactly what we added (so genuine features are never touched).
function removeAllInjections() {
for (const [id, mine] of injectedFeatures) {
const feats = [...mine];
const guild = GuildStore.getGuild(id);
for (const f of feats) guild?.features?.delete?.(f);

removeArrayFeatures(GuildProfileStore?.getProfile?.(id), feats);

const code = InviteStore?.getInviteKeyForGuildId?.(id);
const inv = code && InviteStore?.getInvite?.(code);
removeArrayFeatures(inv?.guild, feats);
removeArrayFeatures(inv?.profile, feats);
}
injectedFeatures.clear();

const me = UserStore.getCurrentUser();
const profile = me && UserProfileStore?.getUserProfile?.(me.id);
if (profile && Array.isArray(profile.badges) && profile.badges.some((b: any) => b?.[OUR_MARK])) {
try { profile.badges = profile.badges.filter((b: any) => !b?.[OUR_MARK]); } catch { /* ignore */ }
}
}

// ─── Lifecycle ────────────────────────────────────────────────────────────────

export default definePlugin({
Expand All @@ -305,6 +393,7 @@ export default definePlugin({
settings,

start() {
enabled = true;
sync();
// Re-apply whenever any of the relevant data is (re)loaded or updated, so
// late-loading guilds/profiles still get patched. We mutate/replace objects
Expand All @@ -314,27 +403,19 @@ export default definePlugin({
UserProfileStore?.addChangeListener?.(sync);
GuildProfileStore?.addChangeListener?.(sync);
InviteStore?.addChangeListener?.(sync);
LocaleStore?.addChangeListener?.(sync);
},

stop() {
enabled = false;

GuildStore.removeChangeListener(sync);
UserStore.removeChangeListener(sync);
UserProfileStore?.removeChangeListener?.(sync);
GuildProfileStore?.removeChangeListener?.(sync);
InviteStore?.removeChangeListener?.(sync);
LocaleStore?.removeChangeListener?.(sync);

// Remove the guild features we injected.
for (const [id, mine] of injectedFeatures) {
const guild = GuildStore.getGuild(id);
for (const f of mine) guild?.features?.delete?.(f);
}
injectedFeatures.clear();

// Remove the profile badge we injected.
const me = UserStore.getCurrentUser();
const profile = me && UserProfileStore?.getUserProfile?.(me.id);
if (profile && Array.isArray(profile.badges) && profile.badges.some((b: any) => b?.[OUR_MARK])) {
try { profile.badges = profile.badges.filter((b: any) => !b?.[OUR_MARK]); } catch { /* ignore */ }
}
removeAllInjections();
}
});