diff --git a/apps/bot/src/presence.ts b/apps/bot/src/presence.ts index c94bdff..2be5608 100644 --- a/apps/bot/src/presence.ts +++ b/apps/bot/src/presence.ts @@ -120,6 +120,23 @@ export async function readPresenceConfig(context: BotContext): Promise { const config = await readPresenceConfig(context); + + if (config.maintenanceMode) { + const text = + config.maintenanceMessage?.trim() || + 'Maintenance mode — please try again later.'; + await context.client.user?.setPresence({ + status: 'dnd', + activities: [ + { + name: text.slice(0, 128), + type: ActivityType.Watching + } + ] + }); + return; + } + const messages = config.rotatingMessages.length > 0 ? config.rotatingMessages : [config.activityText]; const index = Math.floor(Date.now() / 60_000) % messages.length; diff --git a/apps/webui/src/app/dashboard/[guildId]/welcome/page.tsx b/apps/webui/src/app/dashboard/[guildId]/welcome/page.tsx index fe20cf1..4c7adc7 100644 --- a/apps/webui/src/app/dashboard/[guildId]/welcome/page.tsx +++ b/apps/webui/src/app/dashboard/[guildId]/welcome/page.tsx @@ -1,9 +1,10 @@ import { notFound } from 'next/navigation'; -import { buildWelcomePreviewVars, WelcomeForm } from '@/components/modules/welcome-form'; +import { WelcomeForm } from '@/components/modules/welcome-form'; import { requireGuildAccessOrRedirect } from '@/lib/auth'; import { getManageableGuild } from '@/lib/guilds'; import { getLocale, t } from '@/lib/i18n'; import { getWelcomeDashboard } from '@/lib/module-configs/welcome'; +import { buildWelcomePreviewVars } from '@/lib/welcome-preview-vars'; interface WelcomePageProps { params: Promise<{ guildId: string }>; diff --git a/apps/webui/src/app/owner/presence/page.tsx b/apps/webui/src/app/owner/presence/page.tsx index b2a8246..71ea8b3 100644 --- a/apps/webui/src/app/owner/presence/page.tsx +++ b/apps/webui/src/app/owner/presence/page.tsx @@ -1,16 +1,21 @@ -import { PresenceForm } from '@/components/owner/owner-forms'; +import { ownerRoleAtLeast } from '@nexumi/shared'; +import { PresenceForm } from '@/components/owner/presence-form'; import { getLocale, t } from '@/lib/i18n'; +import { requireOwnerOrRedirect } from '@/lib/owner-auth'; import { getPresenceConfig } from '@/lib/owner-presence'; export default async function OwnerPresencePage() { + const session = await requireOwnerOrRedirect('VIEWER'); const [locale, config] = await Promise.all([getLocale(), getPresenceConfig()]); + const canEdit = ownerRoleAtLeast(session.ownerRole, 'ADMIN'); + return (

{t(locale, 'owner.presence.title')}

{t(locale, 'owner.presence.subtitle')}

- +
); } diff --git a/apps/webui/src/components/modules/welcome-form.tsx b/apps/webui/src/components/modules/welcome-form.tsx index 4367552..4218d5c 100644 --- a/apps/webui/src/components/modules/welcome-form.tsx +++ b/apps/webui/src/components/modules/welcome-form.tsx @@ -1,12 +1,6 @@ 'use client'; -import type { - DashboardGuild, - MessageComponentsV2, - SessionUser, - WelcomeConfigDashboard, - WelcomeEmbed -} from '@nexumi/shared'; +import type { MessageComponentsV2, WelcomeConfigDashboard, WelcomeEmbed } from '@nexumi/shared'; import { FieldAnchor } from '@/components/layout/field-anchor'; import { useTranslations } from '@/components/locale-provider'; import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'; @@ -31,45 +25,6 @@ import { Textarea } from '@/components/ui/textarea'; import type { SettingsSaveResult } from '@/components/settings/settings-form'; import { SettingsForm } from '@/components/settings/settings-form'; -function discordDefaultAvatarUrl(userId: string): string { - try { - const index = Number((BigInt(userId) >> 22n) % 6n); - return `https://cdn.discordapp.com/embed/avatars/${index}.png`; - } catch { - return 'https://cdn.discordapp.com/embed/avatars/0.png'; - } -} - -function userAvatarUrl(user: SessionUser): string { - return user.avatar - ? `https://cdn.discordapp.com/avatars/${user.id}/${user.avatar}.png?size=256` - : discordDefaultAvatarUrl(user.id); -} - -function guildIconUrl(guild: DashboardGuild): string { - return guild.icon - ? `https://cdn.discordapp.com/icons/${guild.id}/${guild.icon}.png?size=128` - : discordDefaultAvatarUrl(guild.id); -} - -/** Preview placeholders from the logged-in dashboard user and current guild. */ -export function buildWelcomePreviewVars( - user: SessionUser, - guild: DashboardGuild -): Record { - const displayName = user.globalName?.trim() || user.username; - return { - user: `@${displayName}`, - 'user.name': displayName, - 'user.tag': user.username, - 'user.id': user.id, - 'user.avatar': userAvatarUrl(user), - server: guild.name, - 'server.icon': guildIconUrl(guild), - memberCount: '—' - }; -} - interface LocalWelcomeValue extends Omit< WelcomeConfigDashboard, 'welcomeEmbed' | 'leaveEmbed' | 'welcomeComponents' | 'leaveComponents' diff --git a/apps/webui/src/components/owner/owner-forms.tsx b/apps/webui/src/components/owner/owner-forms.tsx index 89517be..97bb51d 100644 --- a/apps/webui/src/components/owner/owner-forms.tsx +++ b/apps/webui/src/components/owner/owner-forms.tsx @@ -3,115 +3,18 @@ import { useRouter } from 'next/navigation'; import { useMemo, useState, useTransition } from 'react'; import { toast } from 'sonner'; -import type { BotPresenceConfig, OwnerGuildListItem, OwnerRole } from '@nexumi/shared'; +import type { OwnerGuildListItem, OwnerRole } from '@nexumi/shared'; import { useTranslations } from '@/components/locale-provider'; -import { SettingsForm } from '@/components/settings/settings-form'; import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar'; import { Button } from '@/components/ui/button'; import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; import { Input } from '@/components/ui/input'; -import { Label } from '@/components/ui/label'; async function readError(response: Response): Promise { const data = (await response.json().catch(() => null)) as { error?: string } | null; return data?.error ?? 'Request failed'; } -export function PresenceForm({ initialValue }: { initialValue: BotPresenceConfig }) { - const t = useTranslations(); - return ( - { - const response = await fetch('/api/owner/presence', { - method: 'PATCH', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify(value) - }); - if (!response.ok) { - return { ok: false, error: await readError(response) }; - } - return { ok: true }; - }} - > - {({ value, setValue, error }) => ( - - - {error &&

{error}

} -
-
- - -
-
- - -
-
-
- - setValue({ ...value, activityText: e.target.value })} /> -
-
- -