* feat(channels): add Synology Chat native channel Webhook-based integration with Synology NAS Chat (DSM 7+). Supports outgoing webhooks, incoming messages, multi-account, DM policies, rate limiting, and input sanitization. - HMAC-based constant-time token validation - Configurable SSL verification (allowInsecureSsl) for self-signed NAS certs - 54 unit tests across 5 test suites - Follows the same ChannelPlugin pattern as LINE/Discord/Telegram Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * feat(synology-chat): add pairing, warnings, messaging, agent hints - Enable media capability (file_url already supported by client) - Add pairing.notifyApproval to message approved users - Add security.collectWarnings for missing token/URL, insecure SSL, open DM policy - Add messaging.normalizeTarget and targetResolver for user ID resolution - Add directory stubs (self, listPeers, listGroups) - Add agentPrompt.messageToolHints with Synology Chat formatting guide - 63 tests (up from 54), all passing Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
88 lines
3.2 KiB
TypeScript
88 lines
3.2 KiB
TypeScript
/**
|
|
* Account resolution: reads config from channels.synology-chat,
|
|
* merges per-account overrides, falls back to environment variables.
|
|
*/
|
|
|
|
import type { SynologyChatChannelConfig, ResolvedSynologyChatAccount } from "./types.js";
|
|
|
|
/** Extract the channel config from the full OpenClaw config object. */
|
|
function getChannelConfig(cfg: any): SynologyChatChannelConfig | undefined {
|
|
return cfg?.channels?.["synology-chat"];
|
|
}
|
|
|
|
/** Parse allowedUserIds from string or array to string[]. */
|
|
function parseAllowedUserIds(raw: string | string[] | undefined): string[] {
|
|
if (!raw) return [];
|
|
if (Array.isArray(raw)) return raw.filter(Boolean);
|
|
return raw
|
|
.split(",")
|
|
.map((s) => s.trim())
|
|
.filter(Boolean);
|
|
}
|
|
|
|
/**
|
|
* List all configured account IDs for this channel.
|
|
* Returns ["default"] if there's a base config, plus any named accounts.
|
|
*/
|
|
export function listAccountIds(cfg: any): string[] {
|
|
const channelCfg = getChannelConfig(cfg);
|
|
if (!channelCfg) return [];
|
|
|
|
const ids = new Set<string>();
|
|
|
|
// If base config has a token, there's a "default" account
|
|
const hasBaseToken = channelCfg.token || process.env.SYNOLOGY_CHAT_TOKEN;
|
|
if (hasBaseToken) {
|
|
ids.add("default");
|
|
}
|
|
|
|
// Named accounts
|
|
if (channelCfg.accounts) {
|
|
for (const id of Object.keys(channelCfg.accounts)) {
|
|
ids.add(id);
|
|
}
|
|
}
|
|
|
|
return Array.from(ids);
|
|
}
|
|
|
|
/**
|
|
* Resolve a specific account by ID with full defaults applied.
|
|
* Falls back to env vars for the "default" account.
|
|
*/
|
|
export function resolveAccount(cfg: any, accountId?: string | null): ResolvedSynologyChatAccount {
|
|
const channelCfg = getChannelConfig(cfg) ?? {};
|
|
const id = accountId || "default";
|
|
|
|
// Account-specific overrides (if named account exists)
|
|
const accountOverride = channelCfg.accounts?.[id] ?? {};
|
|
|
|
// Env var fallbacks (primarily for the "default" account)
|
|
const envToken = process.env.SYNOLOGY_CHAT_TOKEN ?? "";
|
|
const envIncomingUrl = process.env.SYNOLOGY_CHAT_INCOMING_URL ?? "";
|
|
const envNasHost = process.env.SYNOLOGY_NAS_HOST ?? "localhost";
|
|
const envAllowedUserIds = process.env.SYNOLOGY_ALLOWED_USER_IDS ?? "";
|
|
const envRateLimit = process.env.SYNOLOGY_RATE_LIMIT;
|
|
const envBotName = process.env.OPENCLAW_BOT_NAME ?? "OpenClaw";
|
|
|
|
// Merge: account override > base channel config > env var
|
|
return {
|
|
accountId: id,
|
|
enabled: accountOverride.enabled ?? channelCfg.enabled ?? true,
|
|
token: accountOverride.token ?? channelCfg.token ?? envToken,
|
|
incomingUrl: accountOverride.incomingUrl ?? channelCfg.incomingUrl ?? envIncomingUrl,
|
|
nasHost: accountOverride.nasHost ?? channelCfg.nasHost ?? envNasHost,
|
|
webhookPath: accountOverride.webhookPath ?? channelCfg.webhookPath ?? "/webhook/synology",
|
|
dmPolicy: accountOverride.dmPolicy ?? channelCfg.dmPolicy ?? "allowlist",
|
|
allowedUserIds: parseAllowedUserIds(
|
|
accountOverride.allowedUserIds ?? channelCfg.allowedUserIds ?? envAllowedUserIds,
|
|
),
|
|
rateLimitPerMinute:
|
|
accountOverride.rateLimitPerMinute ??
|
|
channelCfg.rateLimitPerMinute ??
|
|
(envRateLimit ? parseInt(envRateLimit, 10) || 30 : 30),
|
|
botName: accountOverride.botName ?? channelCfg.botName ?? envBotName,
|
|
allowInsecureSsl: accountOverride.allowInsecureSsl ?? channelCfg.allowInsecureSsl ?? false,
|
|
};
|
|
}
|