Enhance bot and WebUI functionality with new features and environment updates
- Added support for new environment variables in `.env.example` for public site and legal operator information. - Integrated core commands into the bot's command structure for improved functionality. - Implemented a new `publishBotStatus` function to update bot status in Redis, enhancing monitoring capabilities. - Updated the landing page to include features, invite links, and support server access, improving user experience. - Enhanced localization with new keys for core commands and landing page elements in both English and German. - Improved error handling and logging for bot presence updates and status publishing.
This commit is contained in:
@@ -3,28 +3,20 @@ import { z } from 'zod';
|
||||
|
||||
config();
|
||||
|
||||
const emptyToUndefined = (value: unknown) =>
|
||||
value === '' || value === undefined || value === null ? undefined : value;
|
||||
|
||||
const EnvSchema = z.object({
|
||||
NODE_ENV: z.enum(['development', 'test', 'production']).default('development'),
|
||||
DATABASE_URL: z.string().url(),
|
||||
REDIS_URL: z.string().url(),
|
||||
BOT_CLIENT_ID: z.string().min(1),
|
||||
BOT_CLIENT_SECRET: z.string().min(1),
|
||||
/**
|
||||
* Discord bot token. The WebUI does not run a discord.js Client/gateway
|
||||
* connection, but uses this token for narrow, on-demand Discord REST calls
|
||||
* (e.g. building a guild backup snapshot) and to enqueue BullMQ jobs that
|
||||
* the bot process picks up for actions that require a live Client
|
||||
* (posting messages, restoring a backup).
|
||||
*/
|
||||
BOT_TOKEN: z.string().min(1),
|
||||
WEBUI_URL: z.string().url().default('http://localhost:3000'),
|
||||
SESSION_SECRET: z.string().min(32, 'SESSION_SECRET must be at least 32 characters long'),
|
||||
SENTRY_DSN: z.string().optional(),
|
||||
SENTRY_DSN: z.preprocess(emptyToUndefined, z.string().optional()),
|
||||
DEFAULT_LOCALE: z.enum(['de', 'en']).default('de'),
|
||||
/**
|
||||
* Comma-separated Discord user IDs that are treated as global bot owners
|
||||
* for the (future) owner panel. Optional for now.
|
||||
*/
|
||||
OWNER_USER_IDS: z.preprocess(
|
||||
(value) => (typeof value === 'string' && value.trim() === '' ? undefined : value),
|
||||
z
|
||||
@@ -38,8 +30,21 @@ const EnvSchema = z.object({
|
||||
.filter((id) => id.length > 0)
|
||||
: []
|
||||
)
|
||||
)
|
||||
),
|
||||
SUPPORT_SERVER_URL: z.preprocess(emptyToUndefined, z.string().url().optional()),
|
||||
LEGAL_OPERATOR_NAME: z.preprocess(emptyToUndefined, z.string().min(1).optional()),
|
||||
LEGAL_OPERATOR_ADDRESS: z.preprocess(emptyToUndefined, z.string().min(1).optional()),
|
||||
LEGAL_OPERATOR_EMAIL: z.preprocess(emptyToUndefined, z.string().email().optional()),
|
||||
LEGAL_OPERATOR_PHONE: z.preprocess(emptyToUndefined, z.string().min(1).optional())
|
||||
});
|
||||
|
||||
export const env = EnvSchema.parse(process.env);
|
||||
export type Env = typeof env;
|
||||
|
||||
export function buildBotInviteUrl(clientId = env.BOT_CLIENT_ID): string {
|
||||
const url = new URL('https://discord.com/api/oauth2/authorize');
|
||||
url.searchParams.set('client_id', clientId);
|
||||
url.searchParams.set('permissions', '8');
|
||||
url.searchParams.set('scope', 'bot applications.commands');
|
||||
return url.toString();
|
||||
}
|
||||
|
||||
24
apps/webui/src/lib/public-stats.ts
Normal file
24
apps/webui/src/lib/public-stats.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
import { prisma } from './prisma';
|
||||
|
||||
export async function getPublicStats() {
|
||||
const [guildCount, memberLevels, memberEconomies, users] = await Promise.all([
|
||||
prisma.guild.count(),
|
||||
prisma.memberLevel.findMany({ select: { userId: true }, distinct: ['userId'] }),
|
||||
prisma.memberEconomy.findMany({ select: { userId: true }, distinct: ['userId'] }),
|
||||
prisma.user.count()
|
||||
]);
|
||||
|
||||
const unique = new Set<string>();
|
||||
for (const row of memberLevels) {
|
||||
unique.add(row.userId);
|
||||
}
|
||||
for (const row of memberEconomies) {
|
||||
unique.add(row.userId);
|
||||
}
|
||||
|
||||
return {
|
||||
guildCount,
|
||||
approximateUserCount: Math.max(unique.size, users),
|
||||
updatedAt: new Date().toISOString()
|
||||
};
|
||||
}
|
||||
56
apps/webui/src/lib/public-status.ts
Normal file
56
apps/webui/src/lib/public-status.ts
Normal file
@@ -0,0 +1,56 @@
|
||||
import { BOT_STATUS_REDIS_KEY, BotStatusPayloadSchema, PRESENCE_REDIS_KEY } from '@nexumi/shared';
|
||||
import { prisma } from './prisma';
|
||||
import { redis } from './redis';
|
||||
|
||||
export async function getPublicStatus() {
|
||||
const [rawStatus, rawPresence, updates] = await Promise.all([
|
||||
redis.get(BOT_STATUS_REDIS_KEY),
|
||||
redis.get(PRESENCE_REDIS_KEY),
|
||||
prisma.changelogEntry.findMany({
|
||||
orderBy: { publishedAt: 'desc' },
|
||||
take: 10
|
||||
})
|
||||
]);
|
||||
|
||||
let botStatus = null;
|
||||
if (rawStatus) {
|
||||
const parsed = BotStatusPayloadSchema.safeParse(JSON.parse(rawStatus));
|
||||
botStatus = parsed.success ? parsed.data : null;
|
||||
}
|
||||
|
||||
let maintenanceMode = false;
|
||||
if (rawPresence) {
|
||||
try {
|
||||
const presence = JSON.parse(rawPresence) as { maintenanceMode?: boolean };
|
||||
maintenanceMode = Boolean(presence.maintenanceMode);
|
||||
} catch {
|
||||
maintenanceMode = false;
|
||||
}
|
||||
} else {
|
||||
const row = await prisma.botPresenceConfig.findUnique({ where: { id: 'singleton' } });
|
||||
maintenanceMode = row?.maintenanceMode ?? false;
|
||||
}
|
||||
|
||||
const degraded = !botStatus;
|
||||
const overall = maintenanceMode ? 'maintenance' : degraded ? 'degraded' : 'operational';
|
||||
|
||||
return {
|
||||
ok: !maintenanceMode,
|
||||
overall,
|
||||
maintenanceMode,
|
||||
degraded,
|
||||
shards: botStatus?.shards ?? [],
|
||||
apiLatencyMs: botStatus?.apiLatencyMs ?? null,
|
||||
guildCount: botStatus?.guildCount ?? (await prisma.guild.count()),
|
||||
uptimeSeconds: botStatus?.uptimeSeconds ?? null,
|
||||
updates: updates.map((entry) => ({
|
||||
id: entry.id,
|
||||
version: entry.version,
|
||||
title: entry.title,
|
||||
body: entry.body,
|
||||
publishedAt: entry.publishedAt.toISOString(),
|
||||
isIncident: entry.version.toLowerCase().startsWith('incident')
|
||||
})),
|
||||
updatedAt: botStatus?.updatedAt ?? new Date().toISOString()
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user