import { resolveNextcloudTalkAccount } from "./accounts.js"; import { getNextcloudTalkRuntime } from "./runtime.js"; import { generateNextcloudTalkSignature } from "./signature.js"; import type { CoreConfig, NextcloudTalkSendResult } from "./types.js"; type NextcloudTalkSendOpts = { baseUrl?: string; secret?: string; accountId?: string; replyTo?: string; verbose?: boolean; }; function resolveCredentials( explicit: { baseUrl?: string; secret?: string }, account: { baseUrl: string; secret: string; accountId: string }, ): { baseUrl: string; secret: string } { const baseUrl = explicit.baseUrl?.trim() ?? account.baseUrl; const secret = explicit.secret?.trim() ?? account.secret; if (!baseUrl) { throw new Error( `Nextcloud Talk baseUrl missing for account "${account.accountId}" (set channels.nextcloud-talk.baseUrl).`, ); } if (!secret) { throw new Error( `Nextcloud Talk bot secret missing for account "${account.accountId}" (set channels.nextcloud-talk.botSecret/botSecretFile or NEXTCLOUD_TALK_BOT_SECRET for default).`, ); } return { baseUrl, secret }; } function normalizeRoomToken(to: string): string { const trimmed = to.trim(); if (!trimmed) throw new Error("Room token is required for Nextcloud Talk sends"); let normalized = trimmed; if (normalized.startsWith("nextcloud-talk:")) { normalized = normalized.slice("nextcloud-talk:".length).trim(); } else if (normalized.startsWith("nc:")) { normalized = normalized.slice("nc:".length).trim(); } if (normalized.startsWith("room:")) { normalized = normalized.slice("room:".length).trim(); } if (!normalized) throw new Error("Room token is required for Nextcloud Talk sends"); return normalized; } export async function sendMessageNextcloudTalk( to: string, text: string, opts: NextcloudTalkSendOpts = {}, ): Promise { const cfg = getNextcloudTalkRuntime().config.loadConfig() as CoreConfig; const account = resolveNextcloudTalkAccount({ cfg, accountId: opts.accountId, }); const { baseUrl, secret } = resolveCredentials( { baseUrl: opts.baseUrl, secret: opts.secret }, account, ); const roomToken = normalizeRoomToken(to); if (!text?.trim()) { throw new Error("Message must be non-empty for Nextcloud Talk sends"); } const body: Record = { message: text.trim(), }; if (opts.replyTo) { body.replyTo = opts.replyTo; } const bodyStr = JSON.stringify(body); const { random, signature } = generateNextcloudTalkSignature({ body: bodyStr, secret, }); const url = `${baseUrl}/ocs/v2.php/apps/spreed/api/v1/bot/${roomToken}/message`; const response = await fetch(url, { method: "POST", headers: { "Content-Type": "application/json", "OCS-APIRequest": "true", "X-Nextcloud-Talk-Bot-Random": random, "X-Nextcloud-Talk-Bot-Signature": signature, }, body: bodyStr, }); if (!response.ok) { const errorBody = await response.text().catch(() => ""); const status = response.status; let errorMsg = `Nextcloud Talk send failed (${status})`; if (status === 400) { errorMsg = `Nextcloud Talk: bad request - ${errorBody || "invalid message format"}`; } else if (status === 401) { errorMsg = "Nextcloud Talk: authentication failed - check bot secret"; } else if (status === 403) { errorMsg = "Nextcloud Talk: forbidden - bot may not have permission in this room"; } else if (status === 404) { errorMsg = `Nextcloud Talk: room not found (token=${roomToken})`; } else if (errorBody) { errorMsg = `Nextcloud Talk send failed: ${errorBody}`; } throw new Error(errorMsg); } let messageId = "unknown"; let timestamp: number | undefined; try { const data = (await response.json()) as { ocs?: { data?: { id?: number | string; timestamp?: number; }; }; }; if (data.ocs?.data?.id != null) { messageId = String(data.ocs.data.id); } if (typeof data.ocs?.data?.timestamp === "number") { timestamp = data.ocs.data.timestamp; } } catch { // Response parsing failed, but message was sent. } if (opts.verbose) { console.log(`[nextcloud-talk] Sent message ${messageId} to room ${roomToken}`); } getNextcloudTalkRuntime().channel.activity.record({ channel: "nextcloud-talk", accountId: account.accountId, direction: "outbound", }); return { messageId, roomToken, timestamp }; } export async function sendReactionNextcloudTalk( roomToken: string, messageId: string, reaction: string, opts: Omit = {}, ): Promise<{ ok: true }> { const cfg = getNextcloudTalkRuntime().config.loadConfig() as CoreConfig; const account = resolveNextcloudTalkAccount({ cfg, accountId: opts.accountId, }); const { baseUrl, secret } = resolveCredentials( { baseUrl: opts.baseUrl, secret: opts.secret }, account, ); const normalizedToken = normalizeRoomToken(roomToken); const body = JSON.stringify({ reaction }); const { random, signature } = generateNextcloudTalkSignature({ body, secret, }); const url = `${baseUrl}/ocs/v2.php/apps/spreed/api/v1/bot/${normalizedToken}/reaction/${messageId}`; const response = await fetch(url, { method: "POST", headers: { "Content-Type": "application/json", "OCS-APIRequest": "true", "X-Nextcloud-Talk-Bot-Random": random, "X-Nextcloud-Talk-Bot-Signature": signature, }, body, }); if (!response.ok) { const errorBody = await response.text().catch(() => ""); throw new Error(`Nextcloud Talk reaction failed: ${response.status} ${errorBody}`.trim()); } return { ok: true }; }