| import { |
| GROUP_POLICY_BLOCKED_LABEL, |
| createScopedPairingAccess, |
| dispatchInboundReplyWithBase, |
| formatTextWithAttachmentLinks, |
| issuePairingChallenge, |
| logInboundDrop, |
| readStoreAllowFromForDmPolicy, |
| resolveDmGroupAccessWithCommandGate, |
| resolveOutboundMediaUrls, |
| resolveAllowlistProviderRuntimeGroupPolicy, |
| resolveDefaultGroupPolicy, |
| warnMissingProviderGroupPolicyFallbackOnce, |
| type OutboundReplyPayload, |
| type OpenClawConfig, |
| type RuntimeEnv, |
| } from "openclaw/plugin-sdk/nextcloud-talk"; |
| import type { ResolvedNextcloudTalkAccount } from "./accounts.js"; |
| import { |
| normalizeNextcloudTalkAllowlist, |
| resolveNextcloudTalkAllowlistMatch, |
| resolveNextcloudTalkGroupAllow, |
| resolveNextcloudTalkMentionGate, |
| resolveNextcloudTalkRequireMention, |
| resolveNextcloudTalkRoomMatch, |
| } from "./policy.js"; |
| import { resolveNextcloudTalkRoomKind } from "./room-info.js"; |
| import { getNextcloudTalkRuntime } from "./runtime.js"; |
| import { sendMessageNextcloudTalk } from "./send.js"; |
| import type { CoreConfig, GroupPolicy, NextcloudTalkInboundMessage } from "./types.js"; |
|
|
| const CHANNEL_ID = "nextcloud-talk" as const; |
|
|
| async function deliverNextcloudTalkReply(params: { |
| payload: OutboundReplyPayload; |
| roomToken: string; |
| accountId: string; |
| statusSink?: (patch: { lastOutboundAt?: number }) => void; |
| }): Promise<void> { |
| const { payload, roomToken, accountId, statusSink } = params; |
| const combined = formatTextWithAttachmentLinks(payload.text, resolveOutboundMediaUrls(payload)); |
| if (!combined) { |
| return; |
| } |
|
|
| await sendMessageNextcloudTalk(roomToken, combined, { |
| accountId, |
| replyTo: payload.replyToId, |
| }); |
| statusSink?.({ lastOutboundAt: Date.now() }); |
| } |
|
|
| export async function handleNextcloudTalkInbound(params: { |
| message: NextcloudTalkInboundMessage; |
| account: ResolvedNextcloudTalkAccount; |
| config: CoreConfig; |
| runtime: RuntimeEnv; |
| statusSink?: (patch: { lastInboundAt?: number; lastOutboundAt?: number }) => void; |
| }): Promise<void> { |
| const { message, account, config, runtime, statusSink } = params; |
| const core = getNextcloudTalkRuntime(); |
| const pairing = createScopedPairingAccess({ |
| core, |
| channel: CHANNEL_ID, |
| accountId: account.accountId, |
| }); |
|
|
| const rawBody = message.text?.trim() ?? ""; |
| if (!rawBody) { |
| return; |
| } |
|
|
| const roomKind = await resolveNextcloudTalkRoomKind({ |
| account, |
| roomToken: message.roomToken, |
| runtime, |
| }); |
| const isGroup = roomKind === "direct" ? false : roomKind === "group" ? true : message.isGroupChat; |
| const senderId = message.senderId; |
| const senderName = message.senderName; |
| const roomToken = message.roomToken; |
| const roomName = message.roomName; |
|
|
| statusSink?.({ lastInboundAt: message.timestamp }); |
|
|
| const dmPolicy = account.config.dmPolicy ?? "pairing"; |
| const defaultGroupPolicy = resolveDefaultGroupPolicy(config as OpenClawConfig); |
| const { groupPolicy, providerMissingFallbackApplied } = |
| resolveAllowlistProviderRuntimeGroupPolicy({ |
| providerConfigPresent: |
| ((config.channels as Record<string, unknown> | undefined)?.["nextcloud-talk"] ?? |
| undefined) !== undefined, |
| groupPolicy: account.config.groupPolicy as GroupPolicy | undefined, |
| defaultGroupPolicy, |
| }); |
| warnMissingProviderGroupPolicyFallbackOnce({ |
| providerMissingFallbackApplied, |
| providerKey: "nextcloud-talk", |
| accountId: account.accountId, |
| blockedLabel: GROUP_POLICY_BLOCKED_LABEL.room, |
| log: (message) => runtime.log?.(message), |
| }); |
|
|
| const configAllowFrom = normalizeNextcloudTalkAllowlist(account.config.allowFrom); |
| const configGroupAllowFrom = normalizeNextcloudTalkAllowlist(account.config.groupAllowFrom); |
| const storeAllowFrom = await readStoreAllowFromForDmPolicy({ |
| provider: CHANNEL_ID, |
| accountId: account.accountId, |
| dmPolicy, |
| readStore: pairing.readStoreForDmPolicy, |
| }); |
| const storeAllowList = normalizeNextcloudTalkAllowlist(storeAllowFrom); |
|
|
| const roomMatch = resolveNextcloudTalkRoomMatch({ |
| rooms: account.config.rooms, |
| roomToken, |
| roomName, |
| }); |
| const roomConfig = roomMatch.roomConfig; |
| if (isGroup && !roomMatch.allowed) { |
| runtime.log?.(`nextcloud-talk: drop room ${roomToken} (not allowlisted)`); |
| return; |
| } |
| if (roomConfig?.enabled === false) { |
| runtime.log?.(`nextcloud-talk: drop room ${roomToken} (disabled)`); |
| return; |
| } |
|
|
| const roomAllowFrom = normalizeNextcloudTalkAllowlist(roomConfig?.allowFrom); |
|
|
| const allowTextCommands = core.channel.commands.shouldHandleTextCommands({ |
| cfg: config as OpenClawConfig, |
| surface: CHANNEL_ID, |
| }); |
| const useAccessGroups = |
| (config.commands as Record<string, unknown> | undefined)?.useAccessGroups !== false; |
| const hasControlCommand = core.channel.text.hasControlCommand(rawBody, config as OpenClawConfig); |
| const access = resolveDmGroupAccessWithCommandGate({ |
| isGroup, |
| dmPolicy, |
| groupPolicy, |
| allowFrom: configAllowFrom, |
| groupAllowFrom: configGroupAllowFrom, |
| storeAllowFrom: storeAllowList, |
| isSenderAllowed: (allowFrom) => |
| resolveNextcloudTalkAllowlistMatch({ |
| allowFrom, |
| senderId, |
| }).allowed, |
| command: { |
| useAccessGroups, |
| allowTextCommands, |
| hasControlCommand, |
| }, |
| }); |
| const commandAuthorized = access.commandAuthorized; |
| const effectiveGroupAllowFrom = access.effectiveGroupAllowFrom; |
|
|
| if (isGroup) { |
| if (access.decision !== "allow") { |
| runtime.log?.(`nextcloud-talk: drop group sender ${senderId} (reason=${access.reason})`); |
| return; |
| } |
| const groupAllow = resolveNextcloudTalkGroupAllow({ |
| groupPolicy, |
| outerAllowFrom: effectiveGroupAllowFrom, |
| innerAllowFrom: roomAllowFrom, |
| senderId, |
| }); |
| if (!groupAllow.allowed) { |
| runtime.log?.(`nextcloud-talk: drop group sender ${senderId} (policy=${groupPolicy})`); |
| return; |
| } |
| } else { |
| if (access.decision !== "allow") { |
| if (access.decision === "pairing") { |
| await issuePairingChallenge({ |
| channel: CHANNEL_ID, |
| senderId, |
| senderIdLine: `Your Nextcloud user id: ${senderId}`, |
| meta: { name: senderName || undefined }, |
| upsertPairingRequest: pairing.upsertPairingRequest, |
| sendPairingReply: async (text) => { |
| await sendMessageNextcloudTalk(roomToken, text, { accountId: account.accountId }); |
| statusSink?.({ lastOutboundAt: Date.now() }); |
| }, |
| onReplyError: (err) => { |
| runtime.error?.(`nextcloud-talk: pairing reply failed for ${senderId}: ${String(err)}`); |
| }, |
| }); |
| } |
| runtime.log?.(`nextcloud-talk: drop DM sender ${senderId} (reason=${access.reason})`); |
| return; |
| } |
| } |
|
|
| if (access.shouldBlockControlCommand) { |
| logInboundDrop({ |
| log: (message) => runtime.log?.(message), |
| channel: CHANNEL_ID, |
| reason: "control command (unauthorized)", |
| target: senderId, |
| }); |
| return; |
| } |
|
|
| const mentionRegexes = core.channel.mentions.buildMentionRegexes(config as OpenClawConfig); |
| const wasMentioned = mentionRegexes.length |
| ? core.channel.mentions.matchesMentionPatterns(rawBody, mentionRegexes) |
| : false; |
| const shouldRequireMention = isGroup |
| ? resolveNextcloudTalkRequireMention({ |
| roomConfig, |
| wildcardConfig: roomMatch.wildcardConfig, |
| }) |
| : false; |
| const mentionGate = resolveNextcloudTalkMentionGate({ |
| isGroup, |
| requireMention: shouldRequireMention, |
| wasMentioned, |
| allowTextCommands, |
| hasControlCommand, |
| commandAuthorized, |
| }); |
| if (isGroup && mentionGate.shouldSkip) { |
| runtime.log?.(`nextcloud-talk: drop room ${roomToken} (no mention)`); |
| return; |
| } |
|
|
| const route = core.channel.routing.resolveAgentRoute({ |
| cfg: config as OpenClawConfig, |
| channel: CHANNEL_ID, |
| accountId: account.accountId, |
| peer: { |
| kind: isGroup ? "group" : "direct", |
| id: isGroup ? roomToken : senderId, |
| }, |
| }); |
|
|
| const fromLabel = isGroup ? `room:${roomName || roomToken}` : senderName || `user:${senderId}`; |
| const storePath = core.channel.session.resolveStorePath( |
| (config.session as Record<string, unknown> | undefined)?.store as string | undefined, |
| { |
| agentId: route.agentId, |
| }, |
| ); |
| const envelopeOptions = core.channel.reply.resolveEnvelopeFormatOptions(config as OpenClawConfig); |
| const previousTimestamp = core.channel.session.readSessionUpdatedAt({ |
| storePath, |
| sessionKey: route.sessionKey, |
| }); |
| const body = core.channel.reply.formatAgentEnvelope({ |
| channel: "Nextcloud Talk", |
| from: fromLabel, |
| timestamp: message.timestamp, |
| previousTimestamp, |
| envelope: envelopeOptions, |
| body: rawBody, |
| }); |
|
|
| const groupSystemPrompt = roomConfig?.systemPrompt?.trim() || undefined; |
|
|
| const ctxPayload = core.channel.reply.finalizeInboundContext({ |
| Body: body, |
| BodyForAgent: rawBody, |
| RawBody: rawBody, |
| CommandBody: rawBody, |
| From: isGroup ? `nextcloud-talk:room:${roomToken}` : `nextcloud-talk:${senderId}`, |
| To: `nextcloud-talk:${roomToken}`, |
| SessionKey: route.sessionKey, |
| AccountId: route.accountId, |
| ChatType: isGroup ? "group" : "direct", |
| ConversationLabel: fromLabel, |
| SenderName: senderName || undefined, |
| SenderId: senderId, |
| GroupSubject: isGroup ? roomName || roomToken : undefined, |
| GroupSystemPrompt: isGroup ? groupSystemPrompt : undefined, |
| Provider: CHANNEL_ID, |
| Surface: CHANNEL_ID, |
| WasMentioned: isGroup ? wasMentioned : undefined, |
| MessageSid: message.messageId, |
| Timestamp: message.timestamp, |
| OriginatingChannel: CHANNEL_ID, |
| OriginatingTo: `nextcloud-talk:${roomToken}`, |
| CommandAuthorized: commandAuthorized, |
| }); |
|
|
| await dispatchInboundReplyWithBase({ |
| cfg: config as OpenClawConfig, |
| channel: CHANNEL_ID, |
| accountId: account.accountId, |
| route, |
| storePath, |
| ctxPayload, |
| core, |
| deliver: async (payload) => { |
| await deliverNextcloudTalkReply({ |
| payload, |
| roomToken, |
| accountId: account.accountId, |
| statusSink, |
| }); |
| }, |
| onRecordError: (err) => { |
| runtime.error?.(`nextcloud-talk: failed updating session meta: ${String(err)}`); |
| }, |
| onDispatchError: (err, info) => { |
| runtime.error?.(`nextcloud-talk ${info.kind} reply failed: ${String(err)}`); |
| }, |
| replyOptions: { |
| skillFilter: roomConfig?.skills, |
| disableBlockStreaming: |
| typeof account.config.blockStreaming === "boolean" |
| ? !account.config.blockStreaming |
| : undefined, |
| }, |
| }); |
| } |
|
|