197 lines
5.7 KiB
TypeScript
197 lines
5.7 KiB
TypeScript
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<NextcloudTalkSendResult> {
|
|
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<string, unknown> = {
|
|
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<NextcloudTalkSendOpts, "replyTo"> = {},
|
|
): 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 };
|
|
}
|