Discord community: role sync, announcements webhook, launcher invite
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DjMbLQujBHunCCu5GpsHaT
This commit is contained in:
11 files changed
+513
-2
No files matched your search
@@ -71,7 +71,7 @@ export interface User {
|
||||
last_seen_ingame: string | null;
|
||||
}
|
||||
|
||||
export interface Group { id: number; name: string; color: string; members: number; luckperms_group?: string }
|
||||
export interface Group { id: number; name: string; color: string; members: number; luckperms_group?: string; discord_role?: string }
|
||||
|
||||
export interface Settings {
|
||||
auth: {
|
||||
|
||||
@@ -16,6 +16,32 @@
|
||||
let testAddress = $state('');
|
||||
let testingEmail = $state(false);
|
||||
let savingConnections = $state(false);
|
||||
let disc = $state<any>(null);
|
||||
let discWebhook = $state('');
|
||||
let discRoles = $state<{ id: string; name: string }[]>([]);
|
||||
let savingDisc = $state(false);
|
||||
let syncingDisc = $state(false);
|
||||
const loadDisc = () => get('/api/admin/discord').then((x) => (disc = x)).catch(toastError);
|
||||
async function saveDisc() {
|
||||
savingDisc = true;
|
||||
try {
|
||||
disc = await put('/api/admin/discord', { role_sync: disc.role_sync, invite_url: disc.invite_url, webhook_url: discWebhook, notify_achievements: disc.notify_achievements, notify_guilds: disc.notify_guilds, notify_members: disc.notify_members });
|
||||
discWebhook = '';
|
||||
toast('Discord community settings saved');
|
||||
loadDisc();
|
||||
} catch (e) { toastError(e); } finally { savingDisc = false; }
|
||||
}
|
||||
async function syncDisc() {
|
||||
syncingDisc = true;
|
||||
try {
|
||||
const r = await post<any>('/api/admin/discord/sync', {});
|
||||
toast(`Checked ${r.checked} · ${r.groups_added} groups added · ${r.roles_added} roles added${r.not_in_server ? ` · ${r.not_in_server} not in server` : ''}`);
|
||||
} catch (e) { toastError(e); } finally { syncingDisc = false; }
|
||||
}
|
||||
async function testDisc() {
|
||||
try { await post('/api/admin/discord/test', {}); toast('Test message sent'); } catch (e) { toastError(e); }
|
||||
}
|
||||
|
||||
|
||||
let info = $state<AuthServerInfo | null>(null);
|
||||
const loadInfo = () => get<AuthServerInfo>('/api/admin/auth-server').then((x) => (info = x)).catch(toastError);
|
||||
@@ -23,6 +49,7 @@
|
||||
$effect(() => {
|
||||
get<Settings>('/api/admin/settings').then((x) => { s = x; usernameRules = x.username_blocklist.join('\n'); }).catch(toastError);
|
||||
get('/api/admin/connections').then((x) => (connections = x)).catch(toastError);
|
||||
loadDisc();
|
||||
loadInfo();
|
||||
});
|
||||
|
||||
@@ -173,6 +200,31 @@
|
||||
<label class="field">Discord server ID<input bind:value={connections.discord_guild_id} placeholder="Optional" autocomplete="off" /></label>
|
||||
<span class="help">Secrets are never returned to your browser after saving. Type <code>-</code> in a secret field to remove it.</span>
|
||||
</section>
|
||||
{#if disc}
|
||||
<section class="card col">
|
||||
<div class="section-title"><KeyRound size={18} /><h2>Discord community</h2></div>
|
||||
<p class="help">{disc.linked_accounts} linked account{disc.linked_accounts === 1 ? '' : 's'}. Role sync needs the bot token and server ID above{disc.bot_ready ? '' : ' (not saved yet)'}. Sync only ever <em>adds</em>: it never removes a role or group. Map roles to groups under Players → Groups.</p>
|
||||
<label class="field">Role sync
|
||||
<select bind:value={disc.role_sync}>
|
||||
<option value="off">Off</option>
|
||||
<option value="discord_to_panel">Discord roles → panel groups</option>
|
||||
<option value="panel_to_discord">Panel groups → Discord roles</option>
|
||||
<option value="both">Both directions</option>
|
||||
</select>
|
||||
</label>
|
||||
<label class="field">Invite link shown in the launcher<input bind:value={disc.invite_url} placeholder="https://discord.gg/yourcode" autocomplete="off" /></label>
|
||||
<label class="field">Announcements webhook<input type="password" bind:value={discWebhook} placeholder={disc.webhook_set ? 'Saved — type to replace, or - to remove' : 'https://discord.com/api/webhooks/…'} autocomplete="off" /></label>
|
||||
<Toggle bind:checked={disc.notify_achievements} label="Announce achievements" />
|
||||
<Toggle bind:checked={disc.notify_guilds} label="Announce new guilds" />
|
||||
<Toggle bind:checked={disc.notify_members} label="Announce new players" />
|
||||
<div class="row wrap">
|
||||
<button class="primary" disabled={savingDisc} onclick={saveDisc}>{savingDisc ? 'Saving…' : 'Save community settings'}</button>
|
||||
<button disabled={syncingDisc || disc.role_sync === 'off' || !disc.bot_ready} onclick={syncDisc}>{syncingDisc ? 'Syncing…' : 'Sync roles now'}</button>
|
||||
<button disabled={!disc.webhook_set} onclick={testDisc}>Send test announcement</button>
|
||||
</div>
|
||||
<span class="help">Roles also sync automatically every 15 minutes.</span>
|
||||
</section>
|
||||
{/if}
|
||||
<section class="card col">
|
||||
<div class="section-title"><Download size={18} /><h2>Resend SMTP</h2></div>
|
||||
<p class="help">Verify the sender domain in Resend, then enter an API key. SCOPENET connects to <code>smtp.resend.com</code> over TLS.</p>
|
||||
|
||||
@@ -111,6 +111,17 @@
|
||||
}
|
||||
}
|
||||
|
||||
async function mapDiscord(g: Group, value: string) {
|
||||
try {
|
||||
await put(`/api/admin/groups/${g.id}/discord-role`, { discord_role: value.trim() });
|
||||
toast(value.trim() ? `${g.name} is mapped to a Discord role` : `${g.name} is no longer mapped to Discord`);
|
||||
refresh();
|
||||
} catch (e) {
|
||||
toastError(e);
|
||||
refresh();
|
||||
}
|
||||
}
|
||||
|
||||
async function removeGroup(g: Group) {
|
||||
try {
|
||||
await del(`/api/admin/groups/${g.id}`);
|
||||
@@ -194,6 +205,8 @@
|
||||
<strong>{g.name}</strong>
|
||||
<span class="muted tiny">{g.members} member{g.members === 1 ? '' : 's'}</span>
|
||||
<span class="spacer"></span>
|
||||
<input class="lp" value={g.discord_role ?? ''} placeholder="Discord role ID" maxlength="24" inputmode="numeric" aria-label="Discord role ID for {g.name}" title="Map to a Discord role (right-click role → Copy role ID)"
|
||||
onchange={(e) => mapDiscord(g, e.currentTarget.value)} />
|
||||
<input class="lp" value={g.luckperms_group ?? ''} placeholder="LuckPerms group" maxlength="64" aria-label="LuckPerms group for {g.name}" title="Map to a LuckPerms group"
|
||||
onchange={(e) => mapLp(g, e.currentTarget.value)} />
|
||||
<button class="ghost icon" aria-label="Delete group {g.name}" onclick={() => removeGroup(g)}><X size={14} /></button>
|
||||
|
||||
Reference in new issue
Block a user