@openclaw/nextcloud-talk 2026.3.13 → 2026.5.1-beta.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/api.ts +1 -0
- package/channel-plugin-api.ts +1 -0
- package/contract-api.ts +4 -0
- package/doctor-contract-api.ts +1 -0
- package/index.ts +15 -12
- package/openclaw.plugin.json +804 -1
- package/package.json +31 -4
- package/runtime-api.ts +33 -0
- package/secret-contract-api.ts +5 -0
- package/setup-entry.ts +13 -0
- package/src/accounts.ts +25 -42
- package/src/approval-auth.test.ts +17 -0
- package/src/approval-auth.ts +27 -0
- package/src/channel-api.ts +5 -0
- package/src/channel.adapters.ts +52 -0
- package/src/channel.core.test.ts +75 -0
- package/src/{channel.startup.test.ts → channel.lifecycle.test.ts} +29 -27
- package/src/channel.ts +157 -385
- package/src/config-schema.ts +24 -18
- package/src/core.test.ts +397 -0
- package/src/doctor-contract.ts +9 -0
- package/src/doctor.test.ts +40 -0
- package/src/doctor.ts +10 -0
- package/src/gateway.ts +109 -0
- package/src/inbound.authz.test.ts +87 -22
- package/src/inbound.behavior.test.ts +202 -0
- package/src/inbound.ts +28 -26
- package/src/monitor-runtime.ts +138 -0
- package/src/monitor.replay.test.ts +238 -0
- package/src/monitor.test-harness.ts +2 -2
- package/src/monitor.ts +125 -153
- package/src/policy.ts +23 -31
- package/src/replay-guard.ts +74 -11
- package/src/room-info.test.ts +116 -0
- package/src/room-info.ts +11 -3
- package/src/runtime.ts +6 -3
- package/src/secret-contract.ts +103 -0
- package/src/secret-input.ts +1 -10
- package/src/send.cfg-threading.test.ts +153 -0
- package/src/send.runtime.ts +8 -0
- package/src/send.ts +109 -77
- package/src/session-route.ts +40 -0
- package/src/setup-core.ts +248 -0
- package/src/setup-surface.ts +190 -0
- package/src/setup.test.ts +422 -0
- package/src/signature.ts +20 -10
- package/src/types.ts +19 -16
- package/tsconfig.json +16 -0
- package/src/accounts.test.ts +0 -30
- package/src/config-schema.test.ts +0 -36
- package/src/format.ts +0 -79
- package/src/monitor.auth-order.test.ts +0 -28
- package/src/monitor.backend.test.ts +0 -27
- package/src/monitor.read-body.test.ts +0 -16
- package/src/normalize.test.ts +0 -28
- package/src/onboarding.ts +0 -302
- package/src/policy.test.ts +0 -138
- package/src/replay-guard.test.ts +0 -70
- package/src/send.test.ts +0 -98
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
2
|
+
import type { PluginRuntime, RuntimeEnv } from "../runtime-api.js";
|
|
3
|
+
import type { ResolvedNextcloudTalkAccount } from "./accounts.js";
|
|
4
|
+
import { handleNextcloudTalkInbound } from "./inbound.js";
|
|
5
|
+
import { setNextcloudTalkRuntime } from "./runtime.js";
|
|
6
|
+
import type { CoreConfig, NextcloudTalkInboundMessage } from "./types.js";
|
|
7
|
+
|
|
8
|
+
const {
|
|
9
|
+
createChannelPairingControllerMock,
|
|
10
|
+
dispatchInboundReplyWithBaseMock,
|
|
11
|
+
readStoreAllowFromForDmPolicyMock,
|
|
12
|
+
resolveDmGroupAccessWithCommandGateMock,
|
|
13
|
+
resolveAllowlistProviderRuntimeGroupPolicyMock,
|
|
14
|
+
resolveDefaultGroupPolicyMock,
|
|
15
|
+
warnMissingProviderGroupPolicyFallbackOnceMock,
|
|
16
|
+
} = vi.hoisted(() => {
|
|
17
|
+
return {
|
|
18
|
+
createChannelPairingControllerMock: vi.fn(),
|
|
19
|
+
dispatchInboundReplyWithBaseMock: vi.fn(),
|
|
20
|
+
readStoreAllowFromForDmPolicyMock: vi.fn(),
|
|
21
|
+
resolveDmGroupAccessWithCommandGateMock: vi.fn(),
|
|
22
|
+
resolveAllowlistProviderRuntimeGroupPolicyMock: vi.fn(),
|
|
23
|
+
resolveDefaultGroupPolicyMock: vi.fn(),
|
|
24
|
+
warnMissingProviderGroupPolicyFallbackOnceMock: vi.fn(),
|
|
25
|
+
};
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
const sendMessageNextcloudTalkMock = vi.hoisted(() => vi.fn());
|
|
29
|
+
const resolveNextcloudTalkRoomKindMock = vi.hoisted(() => vi.fn());
|
|
30
|
+
|
|
31
|
+
vi.mock("../runtime-api.js", async () => {
|
|
32
|
+
const actual = await vi.importActual<typeof import("../runtime-api.js")>("../runtime-api.js");
|
|
33
|
+
return {
|
|
34
|
+
...actual,
|
|
35
|
+
createChannelPairingController: createChannelPairingControllerMock,
|
|
36
|
+
dispatchInboundReplyWithBase: dispatchInboundReplyWithBaseMock,
|
|
37
|
+
readStoreAllowFromForDmPolicy: readStoreAllowFromForDmPolicyMock,
|
|
38
|
+
resolveDmGroupAccessWithCommandGate: resolveDmGroupAccessWithCommandGateMock,
|
|
39
|
+
resolveAllowlistProviderRuntimeGroupPolicy: resolveAllowlistProviderRuntimeGroupPolicyMock,
|
|
40
|
+
resolveDefaultGroupPolicy: resolveDefaultGroupPolicyMock,
|
|
41
|
+
warnMissingProviderGroupPolicyFallbackOnce: warnMissingProviderGroupPolicyFallbackOnceMock,
|
|
42
|
+
};
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
vi.mock("./send.js", () => ({
|
|
46
|
+
sendMessageNextcloudTalk: sendMessageNextcloudTalkMock,
|
|
47
|
+
}));
|
|
48
|
+
|
|
49
|
+
vi.mock("./room-info.js", async () => {
|
|
50
|
+
const actual = await vi.importActual<typeof import("./room-info.js")>("./room-info.js");
|
|
51
|
+
return {
|
|
52
|
+
...actual,
|
|
53
|
+
resolveNextcloudTalkRoomKind: resolveNextcloudTalkRoomKindMock,
|
|
54
|
+
};
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
function installRuntime(params?: {
|
|
58
|
+
buildMentionRegexes?: () => RegExp[];
|
|
59
|
+
matchesMentionPatterns?: (body: string, regexes: RegExp[]) => boolean;
|
|
60
|
+
}) {
|
|
61
|
+
setNextcloudTalkRuntime({
|
|
62
|
+
channel: {
|
|
63
|
+
pairing: {
|
|
64
|
+
readAllowFromStore: vi.fn(async () => []),
|
|
65
|
+
upsertPairingRequest: vi.fn(async () => ({ code: "123456", created: true })),
|
|
66
|
+
},
|
|
67
|
+
commands: {
|
|
68
|
+
shouldHandleTextCommands: vi.fn(() => false),
|
|
69
|
+
},
|
|
70
|
+
text: {
|
|
71
|
+
hasControlCommand: vi.fn(() => false),
|
|
72
|
+
},
|
|
73
|
+
mentions: {
|
|
74
|
+
buildMentionRegexes: params?.buildMentionRegexes ?? vi.fn(() => []),
|
|
75
|
+
matchesMentionPatterns: params?.matchesMentionPatterns ?? vi.fn(() => false),
|
|
76
|
+
},
|
|
77
|
+
},
|
|
78
|
+
} as unknown as PluginRuntime);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function createRuntimeEnv() {
|
|
82
|
+
return {
|
|
83
|
+
log: vi.fn(),
|
|
84
|
+
error: vi.fn(),
|
|
85
|
+
} as unknown as RuntimeEnv;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function createAccount(
|
|
89
|
+
overrides?: Partial<ResolvedNextcloudTalkAccount>,
|
|
90
|
+
): ResolvedNextcloudTalkAccount {
|
|
91
|
+
return {
|
|
92
|
+
accountId: "default",
|
|
93
|
+
enabled: true,
|
|
94
|
+
baseUrl: "https://cloud.example.com",
|
|
95
|
+
secret: "secret",
|
|
96
|
+
secretSource: "config",
|
|
97
|
+
config: {
|
|
98
|
+
dmPolicy: "pairing",
|
|
99
|
+
allowFrom: [],
|
|
100
|
+
groupPolicy: "allowlist",
|
|
101
|
+
groupAllowFrom: [],
|
|
102
|
+
},
|
|
103
|
+
...overrides,
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function createMessage(
|
|
108
|
+
overrides?: Partial<NextcloudTalkInboundMessage>,
|
|
109
|
+
): NextcloudTalkInboundMessage {
|
|
110
|
+
return {
|
|
111
|
+
messageId: "msg-1",
|
|
112
|
+
roomToken: "room-1",
|
|
113
|
+
roomName: "Room 1",
|
|
114
|
+
senderId: "user-1",
|
|
115
|
+
senderName: "Alice",
|
|
116
|
+
text: "hello",
|
|
117
|
+
mediaType: "text/plain",
|
|
118
|
+
timestamp: Date.now(),
|
|
119
|
+
isGroupChat: false,
|
|
120
|
+
...overrides,
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
describe("nextcloud-talk inbound behavior", () => {
|
|
125
|
+
beforeEach(() => {
|
|
126
|
+
vi.clearAllMocks();
|
|
127
|
+
installRuntime();
|
|
128
|
+
resolveNextcloudTalkRoomKindMock.mockResolvedValue("direct");
|
|
129
|
+
resolveDefaultGroupPolicyMock.mockReturnValue("allowlist");
|
|
130
|
+
resolveAllowlistProviderRuntimeGroupPolicyMock.mockReturnValue({
|
|
131
|
+
groupPolicy: "allowlist",
|
|
132
|
+
providerMissingFallbackApplied: false,
|
|
133
|
+
});
|
|
134
|
+
warnMissingProviderGroupPolicyFallbackOnceMock.mockReturnValue(undefined);
|
|
135
|
+
readStoreAllowFromForDmPolicyMock.mockResolvedValue([]);
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
// The DM pairing assertion currently depends on a mocked runtime barrel that Vitest
|
|
139
|
+
// does not bind reliably for this extension package.
|
|
140
|
+
it.skip("issues a DM pairing challenge and sends the challenge text", async () => {
|
|
141
|
+
createChannelPairingControllerMock.mockReturnValue({
|
|
142
|
+
readStoreForDmPolicy: vi.fn(),
|
|
143
|
+
issueChallenge: vi.fn(),
|
|
144
|
+
});
|
|
145
|
+
resolveDmGroupAccessWithCommandGateMock.mockReturnValue({
|
|
146
|
+
decision: "pairing",
|
|
147
|
+
reason: "pairing_required",
|
|
148
|
+
commandAuthorized: false,
|
|
149
|
+
effectiveGroupAllowFrom: [],
|
|
150
|
+
});
|
|
151
|
+
sendMessageNextcloudTalkMock.mockResolvedValue(undefined);
|
|
152
|
+
|
|
153
|
+
const statusSink = vi.fn();
|
|
154
|
+
await handleNextcloudTalkInbound({
|
|
155
|
+
message: createMessage(),
|
|
156
|
+
account: createAccount(),
|
|
157
|
+
config: { channels: { "nextcloud-talk": {} } } as CoreConfig,
|
|
158
|
+
runtime: createRuntimeEnv(),
|
|
159
|
+
statusSink,
|
|
160
|
+
});
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
it("drops unmentioned group traffic before dispatch", async () => {
|
|
164
|
+
installRuntime({
|
|
165
|
+
buildMentionRegexes: vi.fn(() => [/@openclaw/i]),
|
|
166
|
+
matchesMentionPatterns: vi.fn(() => false),
|
|
167
|
+
});
|
|
168
|
+
createChannelPairingControllerMock.mockReturnValue({
|
|
169
|
+
readStoreForDmPolicy: vi.fn(),
|
|
170
|
+
issueChallenge: vi.fn(),
|
|
171
|
+
});
|
|
172
|
+
resolveNextcloudTalkRoomKindMock.mockResolvedValue("group");
|
|
173
|
+
resolveDmGroupAccessWithCommandGateMock.mockReturnValue({
|
|
174
|
+
decision: "allow",
|
|
175
|
+
reason: "allow",
|
|
176
|
+
commandAuthorized: false,
|
|
177
|
+
effectiveGroupAllowFrom: ["user-1"],
|
|
178
|
+
});
|
|
179
|
+
const runtime = createRuntimeEnv();
|
|
180
|
+
|
|
181
|
+
await handleNextcloudTalkInbound({
|
|
182
|
+
message: createMessage({
|
|
183
|
+
roomToken: "room-group",
|
|
184
|
+
roomName: "Ops",
|
|
185
|
+
isGroupChat: true,
|
|
186
|
+
}),
|
|
187
|
+
account: createAccount({
|
|
188
|
+
config: {
|
|
189
|
+
dmPolicy: "pairing",
|
|
190
|
+
allowFrom: [],
|
|
191
|
+
groupPolicy: "allowlist",
|
|
192
|
+
groupAllowFrom: ["user-1"],
|
|
193
|
+
},
|
|
194
|
+
}),
|
|
195
|
+
config: { channels: { "nextcloud-talk": {} } } as CoreConfig,
|
|
196
|
+
runtime,
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
expect(dispatchInboundReplyWithBaseMock).not.toHaveBeenCalled();
|
|
200
|
+
expect(runtime.log).toHaveBeenCalledWith("nextcloud-talk: drop room room-group (no mention)");
|
|
201
|
+
});
|
|
202
|
+
});
|
package/src/inbound.ts
CHANGED
|
@@ -1,20 +1,19 @@
|
|
|
1
|
+
import { normalizeOptionalString } from "openclaw/plugin-sdk/text-runtime";
|
|
1
2
|
import {
|
|
2
3
|
GROUP_POLICY_BLOCKED_LABEL,
|
|
3
|
-
|
|
4
|
+
createChannelPairingController,
|
|
5
|
+
deliverFormattedTextWithAttachments,
|
|
4
6
|
dispatchInboundReplyWithBase,
|
|
5
|
-
formatTextWithAttachmentLinks,
|
|
6
|
-
issuePairingChallenge,
|
|
7
7
|
logInboundDrop,
|
|
8
8
|
readStoreAllowFromForDmPolicy,
|
|
9
|
-
resolveDmGroupAccessWithCommandGate,
|
|
10
|
-
resolveOutboundMediaUrls,
|
|
11
9
|
resolveAllowlistProviderRuntimeGroupPolicy,
|
|
12
10
|
resolveDefaultGroupPolicy,
|
|
11
|
+
resolveDmGroupAccessWithCommandGate,
|
|
13
12
|
warnMissingProviderGroupPolicyFallbackOnce,
|
|
14
|
-
type OutboundReplyPayload,
|
|
15
13
|
type OpenClawConfig,
|
|
14
|
+
type OutboundReplyPayload,
|
|
16
15
|
type RuntimeEnv,
|
|
17
|
-
} from "
|
|
16
|
+
} from "../runtime-api.js";
|
|
18
17
|
import type { ResolvedNextcloudTalkAccount } from "./accounts.js";
|
|
19
18
|
import {
|
|
20
19
|
normalizeNextcloudTalkAllowlist,
|
|
@@ -27,27 +26,29 @@ import {
|
|
|
27
26
|
import { resolveNextcloudTalkRoomKind } from "./room-info.js";
|
|
28
27
|
import { getNextcloudTalkRuntime } from "./runtime.js";
|
|
29
28
|
import { sendMessageNextcloudTalk } from "./send.js";
|
|
30
|
-
import type { CoreConfig,
|
|
29
|
+
import type { CoreConfig, NextcloudTalkInboundMessage } from "./types.js";
|
|
31
30
|
|
|
32
31
|
const CHANNEL_ID = "nextcloud-talk" as const;
|
|
33
32
|
|
|
34
33
|
async function deliverNextcloudTalkReply(params: {
|
|
34
|
+
cfg: CoreConfig;
|
|
35
35
|
payload: OutboundReplyPayload;
|
|
36
36
|
roomToken: string;
|
|
37
37
|
accountId: string;
|
|
38
38
|
statusSink?: (patch: { lastOutboundAt?: number }) => void;
|
|
39
39
|
}): Promise<void> {
|
|
40
|
-
const { payload, roomToken, accountId, statusSink } = params;
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
40
|
+
const { cfg, payload, roomToken, accountId, statusSink } = params;
|
|
41
|
+
await deliverFormattedTextWithAttachments({
|
|
42
|
+
payload,
|
|
43
|
+
send: async ({ text, replyToId }) => {
|
|
44
|
+
await sendMessageNextcloudTalk(roomToken, text, {
|
|
45
|
+
cfg,
|
|
46
|
+
accountId,
|
|
47
|
+
replyTo: replyToId,
|
|
48
|
+
});
|
|
49
|
+
statusSink?.({ lastOutboundAt: Date.now() });
|
|
50
|
+
},
|
|
49
51
|
});
|
|
50
|
-
statusSink?.({ lastOutboundAt: Date.now() });
|
|
51
52
|
}
|
|
52
53
|
|
|
53
54
|
export async function handleNextcloudTalkInbound(params: {
|
|
@@ -59,7 +60,7 @@ export async function handleNextcloudTalkInbound(params: {
|
|
|
59
60
|
}): Promise<void> {
|
|
60
61
|
const { message, account, config, runtime, statusSink } = params;
|
|
61
62
|
const core = getNextcloudTalkRuntime();
|
|
62
|
-
const pairing =
|
|
63
|
+
const pairing = createChannelPairingController({
|
|
63
64
|
core,
|
|
64
65
|
channel: CHANNEL_ID,
|
|
65
66
|
accountId: account.accountId,
|
|
@@ -90,7 +91,7 @@ export async function handleNextcloudTalkInbound(params: {
|
|
|
90
91
|
providerConfigPresent:
|
|
91
92
|
((config.channels as Record<string, unknown> | undefined)?.["nextcloud-talk"] ??
|
|
92
93
|
undefined) !== undefined,
|
|
93
|
-
groupPolicy: account.config.groupPolicy
|
|
94
|
+
groupPolicy: account.config.groupPolicy,
|
|
94
95
|
defaultGroupPolicy,
|
|
95
96
|
});
|
|
96
97
|
warnMissingProviderGroupPolicyFallbackOnce({
|
|
@@ -114,7 +115,6 @@ export async function handleNextcloudTalkInbound(params: {
|
|
|
114
115
|
const roomMatch = resolveNextcloudTalkRoomMatch({
|
|
115
116
|
rooms: account.config.rooms,
|
|
116
117
|
roomToken,
|
|
117
|
-
roomName,
|
|
118
118
|
});
|
|
119
119
|
const roomConfig = roomMatch.roomConfig;
|
|
120
120
|
if (isGroup && !roomMatch.allowed) {
|
|
@@ -174,14 +174,15 @@ export async function handleNextcloudTalkInbound(params: {
|
|
|
174
174
|
} else {
|
|
175
175
|
if (access.decision !== "allow") {
|
|
176
176
|
if (access.decision === "pairing") {
|
|
177
|
-
await
|
|
178
|
-
channel: CHANNEL_ID,
|
|
177
|
+
await pairing.issueChallenge({
|
|
179
178
|
senderId,
|
|
180
179
|
senderIdLine: `Your Nextcloud user id: ${senderId}`,
|
|
181
180
|
meta: { name: senderName || undefined },
|
|
182
|
-
upsertPairingRequest: pairing.upsertPairingRequest,
|
|
183
181
|
sendPairingReply: async (text) => {
|
|
184
|
-
await sendMessageNextcloudTalk(roomToken, text, {
|
|
182
|
+
await sendMessageNextcloudTalk(roomToken, text, {
|
|
183
|
+
cfg: config,
|
|
184
|
+
accountId: account.accountId,
|
|
185
|
+
});
|
|
185
186
|
statusSink?.({ lastOutboundAt: Date.now() });
|
|
186
187
|
},
|
|
187
188
|
onReplyError: (err) => {
|
|
@@ -258,7 +259,7 @@ export async function handleNextcloudTalkInbound(params: {
|
|
|
258
259
|
body: rawBody,
|
|
259
260
|
});
|
|
260
261
|
|
|
261
|
-
const groupSystemPrompt = roomConfig?.systemPrompt
|
|
262
|
+
const groupSystemPrompt = normalizeOptionalString(roomConfig?.systemPrompt);
|
|
262
263
|
|
|
263
264
|
const ctxPayload = core.channel.reply.finalizeInboundContext({
|
|
264
265
|
Body: body,
|
|
@@ -295,6 +296,7 @@ export async function handleNextcloudTalkInbound(params: {
|
|
|
295
296
|
core,
|
|
296
297
|
deliver: async (payload) => {
|
|
297
298
|
await deliverNextcloudTalkReply({
|
|
299
|
+
cfg: config,
|
|
298
300
|
payload,
|
|
299
301
|
roomToken,
|
|
300
302
|
accountId: account.accountId,
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
import os from "node:os";
|
|
2
|
+
import { resolveLoggerBackedRuntime } from "openclaw/plugin-sdk/extension-shared";
|
|
3
|
+
import type { RuntimeEnv } from "openclaw/plugin-sdk/runtime";
|
|
4
|
+
import { normalizeLowercaseStringOrEmpty } from "openclaw/plugin-sdk/text-runtime";
|
|
5
|
+
import { resolveNextcloudTalkAccount } from "./accounts.js";
|
|
6
|
+
import { handleNextcloudTalkInbound } from "./inbound.js";
|
|
7
|
+
import {
|
|
8
|
+
createNextcloudTalkWebhookServer,
|
|
9
|
+
processNextcloudTalkReplayGuardedMessage,
|
|
10
|
+
} from "./monitor.js";
|
|
11
|
+
import { createNextcloudTalkReplayGuard } from "./replay-guard.js";
|
|
12
|
+
import { getNextcloudTalkRuntime } from "./runtime.js";
|
|
13
|
+
import type { CoreConfig, NextcloudTalkInboundMessage } from "./types.js";
|
|
14
|
+
|
|
15
|
+
const DEFAULT_WEBHOOK_PORT = 8788;
|
|
16
|
+
const DEFAULT_WEBHOOK_HOST = "0.0.0.0";
|
|
17
|
+
const DEFAULT_WEBHOOK_PATH = "/nextcloud-talk-webhook";
|
|
18
|
+
|
|
19
|
+
function normalizeOrigin(value: string): string | null {
|
|
20
|
+
try {
|
|
21
|
+
return normalizeLowercaseStringOrEmpty(new URL(value).origin);
|
|
22
|
+
} catch {
|
|
23
|
+
return null;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
type NextcloudTalkMonitorOptions = {
|
|
28
|
+
accountId?: string;
|
|
29
|
+
config?: CoreConfig;
|
|
30
|
+
runtime?: RuntimeEnv;
|
|
31
|
+
abortSignal?: AbortSignal;
|
|
32
|
+
onMessage?: (message: NextcloudTalkInboundMessage) => void | Promise<void>;
|
|
33
|
+
statusSink?: (patch: { lastInboundAt?: number; lastOutboundAt?: number }) => void;
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
export async function monitorNextcloudTalkProvider(
|
|
37
|
+
opts: NextcloudTalkMonitorOptions,
|
|
38
|
+
): Promise<{ stop: () => void }> {
|
|
39
|
+
const core = getNextcloudTalkRuntime();
|
|
40
|
+
const cfg = opts.config ?? (core.config.current() as CoreConfig);
|
|
41
|
+
const account = resolveNextcloudTalkAccount({
|
|
42
|
+
cfg,
|
|
43
|
+
accountId: opts.accountId,
|
|
44
|
+
});
|
|
45
|
+
const runtime: RuntimeEnv = resolveLoggerBackedRuntime(
|
|
46
|
+
opts.runtime,
|
|
47
|
+
core.logging.getChildLogger(),
|
|
48
|
+
);
|
|
49
|
+
|
|
50
|
+
if (!account.secret) {
|
|
51
|
+
throw new Error(`Nextcloud Talk bot secret not configured for account "${account.accountId}"`);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const port = account.config.webhookPort ?? DEFAULT_WEBHOOK_PORT;
|
|
55
|
+
const host = account.config.webhookHost ?? DEFAULT_WEBHOOK_HOST;
|
|
56
|
+
const path = account.config.webhookPath ?? DEFAULT_WEBHOOK_PATH;
|
|
57
|
+
|
|
58
|
+
const logger = core.logging.getChildLogger({
|
|
59
|
+
channel: "nextcloud-talk",
|
|
60
|
+
accountId: account.accountId,
|
|
61
|
+
});
|
|
62
|
+
const expectedBackendOrigin = normalizeOrigin(account.baseUrl);
|
|
63
|
+
const replayGuard = createNextcloudTalkReplayGuard({
|
|
64
|
+
stateDir: core.state.resolveStateDir(process.env, os.homedir),
|
|
65
|
+
onDiskError: (error) => {
|
|
66
|
+
logger.warn(
|
|
67
|
+
`[nextcloud-talk:${account.accountId}] replay guard disk error: ${String(error)}`,
|
|
68
|
+
);
|
|
69
|
+
},
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
const { start, stop } = createNextcloudTalkWebhookServer({
|
|
73
|
+
port,
|
|
74
|
+
host,
|
|
75
|
+
path,
|
|
76
|
+
secret: account.secret,
|
|
77
|
+
isBackendAllowed: (backend) => {
|
|
78
|
+
if (!expectedBackendOrigin) {
|
|
79
|
+
return true;
|
|
80
|
+
}
|
|
81
|
+
const backendOrigin = normalizeOrigin(backend);
|
|
82
|
+
return backendOrigin === expectedBackendOrigin;
|
|
83
|
+
},
|
|
84
|
+
processMessage: async (message) => {
|
|
85
|
+
const result = await processNextcloudTalkReplayGuardedMessage({
|
|
86
|
+
replayGuard,
|
|
87
|
+
accountId: account.accountId,
|
|
88
|
+
message,
|
|
89
|
+
handleMessage: async () => {
|
|
90
|
+
core.channel.activity.record({
|
|
91
|
+
channel: "nextcloud-talk",
|
|
92
|
+
accountId: account.accountId,
|
|
93
|
+
direction: "inbound",
|
|
94
|
+
at: message.timestamp,
|
|
95
|
+
});
|
|
96
|
+
if (opts.onMessage) {
|
|
97
|
+
await opts.onMessage(message);
|
|
98
|
+
} else {
|
|
99
|
+
await handleNextcloudTalkInbound({
|
|
100
|
+
message,
|
|
101
|
+
account,
|
|
102
|
+
config: cfg,
|
|
103
|
+
runtime,
|
|
104
|
+
statusSink: opts.statusSink,
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
},
|
|
108
|
+
});
|
|
109
|
+
if (result === "duplicate") {
|
|
110
|
+
logger.warn(
|
|
111
|
+
`[nextcloud-talk:${account.accountId}] replayed webhook ignored room=${message.roomToken} messageId=${message.messageId}`,
|
|
112
|
+
);
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
},
|
|
116
|
+
onMessage: async () => {},
|
|
117
|
+
onError: (error) => {
|
|
118
|
+
logger.error(`[nextcloud-talk:${account.accountId}] webhook error: ${error.message}`);
|
|
119
|
+
},
|
|
120
|
+
abortSignal: opts.abortSignal,
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
if (opts.abortSignal?.aborted) {
|
|
124
|
+
return { stop };
|
|
125
|
+
}
|
|
126
|
+
await start();
|
|
127
|
+
if (opts.abortSignal?.aborted) {
|
|
128
|
+
stop();
|
|
129
|
+
return { stop };
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
const publicUrl =
|
|
133
|
+
account.config.webhookPublicUrl ??
|
|
134
|
+
`http://${host === "0.0.0.0" ? "localhost" : host}:${port}${path}`;
|
|
135
|
+
logger.info(`[nextcloud-talk:${account.accountId}] webhook listening on ${publicUrl}`);
|
|
136
|
+
|
|
137
|
+
return { stop };
|
|
138
|
+
}
|