feat(server): wire Discord DM pairing notifications
This commit is contained in:
@@ -1,11 +1,11 @@
|
||||
/**
|
||||
* Yonexus Server - Discord Notification Service
|
||||
*
|
||||
*
|
||||
* Sends pairing notifications to the configured admin user via Discord DM.
|
||||
*/
|
||||
|
||||
import type { PairingRequest } from "../services/pairing.js";
|
||||
import { redactPairingCode } from "../core/logging.js";
|
||||
import { redactPairingCode, safeErrorMessage } from "../core/logging.js";
|
||||
|
||||
export interface DiscordNotificationService {
|
||||
/**
|
||||
@@ -20,55 +20,143 @@ export interface DiscordNotificationConfig {
|
||||
adminUserId: string;
|
||||
}
|
||||
|
||||
export interface DiscordApiResponse {
|
||||
ok: boolean;
|
||||
status: number;
|
||||
json(): Promise<unknown>;
|
||||
}
|
||||
|
||||
export type DiscordFetch = (
|
||||
input: string,
|
||||
init?: {
|
||||
method?: string;
|
||||
headers?: Record<string, string>;
|
||||
body?: string;
|
||||
}
|
||||
) => Promise<DiscordApiResponse>;
|
||||
|
||||
interface CreateDmChannelResponse {
|
||||
id?: string;
|
||||
}
|
||||
|
||||
interface SendDiscordDirectMessageOptions {
|
||||
config: DiscordNotificationConfig;
|
||||
message: string;
|
||||
fetcher?: DiscordFetch;
|
||||
}
|
||||
|
||||
const DISCORD_API_BASE_URL = "https://discord.com/api/v10";
|
||||
|
||||
/**
|
||||
* Create a Discord notification service.
|
||||
*
|
||||
* Note: This is a framework stub. Full implementation requires:
|
||||
* - Discord bot client integration (e.g., using discord.js)
|
||||
* - DM channel creation/fetching
|
||||
* - Error handling for blocked DMs, invalid tokens, etc.
|
||||
*
|
||||
* For v1, this provides the interface and a mock implementation
|
||||
* that logs to console. Production deployments should replace
|
||||
* this with actual Discord bot integration.
|
||||
* Create a Discord notification service backed by Discord's REST API.
|
||||
*
|
||||
* Flow:
|
||||
* 1. Create or fetch a DM channel for the configured admin user
|
||||
* 2. Post the formatted pairing message into that DM channel
|
||||
*/
|
||||
export function createDiscordNotificationService(
|
||||
config: DiscordNotificationConfig
|
||||
config: DiscordNotificationConfig,
|
||||
options: { fetcher?: DiscordFetch } = {}
|
||||
): DiscordNotificationService {
|
||||
const fetcher = options.fetcher ?? getDefaultFetch();
|
||||
|
||||
return {
|
||||
async sendPairingNotification(request: PairingRequest): Promise<boolean> {
|
||||
const redactedCode = redactPairingCode(request.pairingCode);
|
||||
|
||||
// Log to console (visible in OpenClaw logs)
|
||||
console.log("[Yonexus.Server] Pairing notification (Discord DM stub):", {
|
||||
identifier: request.identifier,
|
||||
pairingCode: redactedCode,
|
||||
expiresAt: request.expiresAt,
|
||||
ttlSeconds: request.ttlSeconds,
|
||||
adminUserId: config.adminUserId
|
||||
});
|
||||
|
||||
// TODO: Replace with actual Discord bot integration
|
||||
// Example with discord.js:
|
||||
// const client = new Client({ intents: [GatewayIntentBits.DirectMessages] });
|
||||
// await client.login(config.botToken);
|
||||
// const user = await client.users.fetch(config.adminUserId);
|
||||
// await user.send(message);
|
||||
|
||||
// For now, return true to allow pairing flow to continue
|
||||
// In production, this should return false if DM fails
|
||||
return true;
|
||||
if (!config.botToken.trim() || !config.adminUserId.trim()) {
|
||||
console.error("[Yonexus.Server] Discord DM notification misconfigured", {
|
||||
hasBotToken: Boolean(config.botToken.trim()),
|
||||
hasAdminUserId: Boolean(config.adminUserId.trim())
|
||||
});
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
await sendDiscordDirectMessage({
|
||||
config,
|
||||
message: formatPairingMessage(request),
|
||||
fetcher
|
||||
});
|
||||
|
||||
console.log("[Yonexus.Server] Pairing notification sent via Discord DM", {
|
||||
identifier: request.identifier,
|
||||
pairingCode: redactPairingCode(request.pairingCode),
|
||||
expiresAt: request.expiresAt,
|
||||
ttlSeconds: request.ttlSeconds,
|
||||
adminUserId: config.adminUserId
|
||||
});
|
||||
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error("[Yonexus.Server] Failed to send Discord DM pairing notification", {
|
||||
identifier: request.identifier,
|
||||
pairingCode: redactPairingCode(request.pairingCode),
|
||||
adminUserId: config.adminUserId,
|
||||
error: safeErrorMessage(error)
|
||||
});
|
||||
return false;
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
async function sendDiscordDirectMessage(
|
||||
options: SendDiscordDirectMessageOptions
|
||||
): Promise<void> {
|
||||
const { config, message, fetcher } = options;
|
||||
const headers = {
|
||||
Authorization: `Bot ${config.botToken}`,
|
||||
"Content-Type": "application/json"
|
||||
};
|
||||
|
||||
const dmResponse = await fetcher(`${DISCORD_API_BASE_URL}/users/@me/channels`, {
|
||||
method: "POST",
|
||||
headers,
|
||||
body: JSON.stringify({ recipient_id: config.adminUserId })
|
||||
});
|
||||
|
||||
if (!dmResponse.ok) {
|
||||
throw new Error(`Discord DM channel creation failed with status ${dmResponse.status}`);
|
||||
}
|
||||
|
||||
const dmPayload = (await dmResponse.json()) as CreateDmChannelResponse;
|
||||
const channelId = dmPayload.id?.trim();
|
||||
if (!channelId) {
|
||||
throw new Error("Discord DM channel creation did not return a channel id");
|
||||
}
|
||||
|
||||
const messageResponse = await fetcher(`${DISCORD_API_BASE_URL}/channels/${channelId}/messages`, {
|
||||
method: "POST",
|
||||
headers,
|
||||
body: JSON.stringify({ content: message })
|
||||
});
|
||||
|
||||
if (!messageResponse.ok) {
|
||||
throw new Error(`Discord DM send failed with status ${messageResponse.status}`);
|
||||
}
|
||||
|
||||
await messageResponse.json();
|
||||
}
|
||||
|
||||
function getDefaultFetch(): DiscordFetch {
|
||||
if (typeof fetch !== "function") {
|
||||
throw new Error("Global fetch is not available in this runtime");
|
||||
}
|
||||
|
||||
return (input, init) =>
|
||||
fetch(input, {
|
||||
method: init?.method,
|
||||
headers: init?.headers,
|
||||
body: init?.body
|
||||
}) as Promise<DiscordApiResponse>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Format a pairing request as a Discord DM message.
|
||||
*/
|
||||
export function formatPairingMessage(request: PairingRequest): string {
|
||||
const expiresDate = new Date(request.expiresAt * 1000);
|
||||
const expiresStr = expiresDate.toISOString();
|
||||
|
||||
|
||||
return [
|
||||
"🔐 **Yonexus Pairing Request**",
|
||||
"",
|
||||
@@ -90,7 +178,7 @@ export function createMockNotificationService(
|
||||
options: { shouldSucceed?: boolean } = {}
|
||||
): DiscordNotificationService {
|
||||
const shouldSucceed = options.shouldSucceed ?? true;
|
||||
|
||||
|
||||
return {
|
||||
async sendPairingNotification(request: PairingRequest): Promise<boolean> {
|
||||
console.log("[Yonexus.Server] Mock pairing notification:");
|
||||
|
||||
Reference in New Issue
Block a user