@openclaw/nextcloud-talk 2026.5.2 → 2026.5.3-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/dist/api.js +2 -0
- package/dist/channel-BVVRsVr5.js +1788 -0
- package/dist/channel-plugin-api.js +2 -0
- package/dist/contract-api.js +2 -0
- package/dist/doctor-contract-CYlB-4Bf.js +7 -0
- package/dist/doctor-contract-api.js +2 -0
- package/dist/index.js +22 -0
- package/dist/runtime-api-BcCzeRN9.js +15 -0
- package/dist/runtime-api.js +2 -0
- package/dist/secret-contract-api.js +2 -0
- package/dist/secret-contract-bczDw2-2.js +86 -0
- package/dist/setup-entry.js +15 -0
- package/package.json +14 -6
- package/api.ts +0 -1
- package/channel-plugin-api.ts +0 -1
- package/contract-api.ts +0 -4
- package/doctor-contract-api.ts +0 -1
- package/index.ts +0 -20
- package/runtime-api.ts +0 -33
- package/secret-contract-api.ts +0 -5
- package/setup-entry.ts +0 -13
- package/src/accounts.ts +0 -139
- package/src/approval-auth.test.ts +0 -17
- package/src/approval-auth.ts +0 -27
- package/src/channel-api.ts +0 -5
- package/src/channel.adapters.ts +0 -52
- package/src/channel.core.test.ts +0 -75
- package/src/channel.lifecycle.test.ts +0 -81
- package/src/channel.ts +0 -195
- package/src/config-schema.ts +0 -79
- package/src/core.test.ts +0 -397
- package/src/doctor-contract.ts +0 -9
- package/src/doctor.test.ts +0 -40
- package/src/doctor.ts +0 -10
- package/src/gateway.ts +0 -109
- package/src/inbound.authz.test.ts +0 -149
- package/src/inbound.behavior.test.ts +0 -202
- package/src/inbound.ts +0 -320
- package/src/monitor-runtime.ts +0 -138
- package/src/monitor.replay.test.ts +0 -279
- package/src/monitor.test-fixtures.ts +0 -30
- package/src/monitor.test-harness.ts +0 -59
- package/src/monitor.ts +0 -385
- package/src/normalize.ts +0 -44
- package/src/policy.ts +0 -180
- package/src/replay-guard.ts +0 -128
- package/src/room-info.test.ts +0 -116
- package/src/room-info.ts +0 -148
- package/src/runtime.ts +0 -9
- package/src/secret-contract.ts +0 -103
- package/src/secret-input.ts +0 -4
- package/src/send.cfg-threading.test.ts +0 -153
- package/src/send.runtime.ts +0 -8
- package/src/send.ts +0 -236
- package/src/session-route.ts +0 -40
- package/src/setup-core.ts +0 -248
- package/src/setup-surface.ts +0 -190
- package/src/setup.test.ts +0 -422
- package/src/signature.ts +0 -82
- package/src/types.ts +0 -193
- package/tsconfig.json +0 -16
|
@@ -1,149 +0,0 @@
|
|
|
1
|
-
import { 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
|
-
function installInboundAuthzRuntime(params: {
|
|
9
|
-
readAllowFromStore: () => Promise<string[]>;
|
|
10
|
-
buildMentionRegexes: () => RegExp[];
|
|
11
|
-
}) {
|
|
12
|
-
setNextcloudTalkRuntime({
|
|
13
|
-
channel: {
|
|
14
|
-
pairing: {
|
|
15
|
-
readAllowFromStore: params.readAllowFromStore,
|
|
16
|
-
},
|
|
17
|
-
commands: {
|
|
18
|
-
shouldHandleTextCommands: () => false,
|
|
19
|
-
},
|
|
20
|
-
text: {
|
|
21
|
-
hasControlCommand: () => false,
|
|
22
|
-
},
|
|
23
|
-
mentions: {
|
|
24
|
-
buildMentionRegexes: params.buildMentionRegexes,
|
|
25
|
-
matchesMentionPatterns: () => false,
|
|
26
|
-
},
|
|
27
|
-
},
|
|
28
|
-
} as unknown as PluginRuntime);
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
function createTestRuntimeEnv(): RuntimeEnv {
|
|
32
|
-
return {
|
|
33
|
-
log: vi.fn(),
|
|
34
|
-
error: vi.fn(),
|
|
35
|
-
} as unknown as RuntimeEnv;
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
describe("nextcloud-talk inbound authz", () => {
|
|
39
|
-
it("does not treat DM pairing-store entries as group allowlist entries", async () => {
|
|
40
|
-
const readAllowFromStore = vi.fn(async () => ["attacker"]);
|
|
41
|
-
const buildMentionRegexes = vi.fn(() => [/@openclaw/i]);
|
|
42
|
-
|
|
43
|
-
installInboundAuthzRuntime({ readAllowFromStore, buildMentionRegexes });
|
|
44
|
-
|
|
45
|
-
const message: NextcloudTalkInboundMessage = {
|
|
46
|
-
messageId: "m-1",
|
|
47
|
-
roomToken: "room-1",
|
|
48
|
-
roomName: "Room 1",
|
|
49
|
-
senderId: "attacker",
|
|
50
|
-
senderName: "Attacker",
|
|
51
|
-
text: "hello",
|
|
52
|
-
mediaType: "text/plain",
|
|
53
|
-
timestamp: Date.now(),
|
|
54
|
-
isGroupChat: true,
|
|
55
|
-
};
|
|
56
|
-
|
|
57
|
-
const account: ResolvedNextcloudTalkAccount = {
|
|
58
|
-
accountId: "default",
|
|
59
|
-
enabled: true,
|
|
60
|
-
baseUrl: "",
|
|
61
|
-
secret: "",
|
|
62
|
-
secretSource: "none", // pragma: allowlist secret
|
|
63
|
-
config: {
|
|
64
|
-
dmPolicy: "pairing",
|
|
65
|
-
allowFrom: [],
|
|
66
|
-
groupPolicy: "allowlist",
|
|
67
|
-
groupAllowFrom: [],
|
|
68
|
-
},
|
|
69
|
-
};
|
|
70
|
-
|
|
71
|
-
const config: CoreConfig = {
|
|
72
|
-
channels: {
|
|
73
|
-
"nextcloud-talk": {
|
|
74
|
-
dmPolicy: "pairing",
|
|
75
|
-
allowFrom: [],
|
|
76
|
-
groupPolicy: "allowlist",
|
|
77
|
-
groupAllowFrom: [],
|
|
78
|
-
},
|
|
79
|
-
},
|
|
80
|
-
};
|
|
81
|
-
|
|
82
|
-
await handleNextcloudTalkInbound({
|
|
83
|
-
message,
|
|
84
|
-
account,
|
|
85
|
-
config,
|
|
86
|
-
runtime: createTestRuntimeEnv(),
|
|
87
|
-
});
|
|
88
|
-
|
|
89
|
-
expect(readAllowFromStore).toHaveBeenCalledWith({
|
|
90
|
-
channel: "nextcloud-talk",
|
|
91
|
-
accountId: "default",
|
|
92
|
-
});
|
|
93
|
-
expect(buildMentionRegexes).not.toHaveBeenCalled();
|
|
94
|
-
});
|
|
95
|
-
|
|
96
|
-
it("matches group rooms by token instead of colliding room names", async () => {
|
|
97
|
-
const readAllowFromStore = vi.fn(async () => []);
|
|
98
|
-
const buildMentionRegexes = vi.fn(() => [/@openclaw/i]);
|
|
99
|
-
|
|
100
|
-
installInboundAuthzRuntime({ readAllowFromStore, buildMentionRegexes });
|
|
101
|
-
|
|
102
|
-
const message: NextcloudTalkInboundMessage = {
|
|
103
|
-
messageId: "m-2",
|
|
104
|
-
roomToken: "room-attacker",
|
|
105
|
-
roomName: "Room Trusted",
|
|
106
|
-
senderId: "trusted-user",
|
|
107
|
-
senderName: "Trusted User",
|
|
108
|
-
text: "hello",
|
|
109
|
-
mediaType: "text/plain",
|
|
110
|
-
timestamp: Date.now(),
|
|
111
|
-
isGroupChat: true,
|
|
112
|
-
};
|
|
113
|
-
|
|
114
|
-
const account: ResolvedNextcloudTalkAccount = {
|
|
115
|
-
accountId: "default",
|
|
116
|
-
enabled: true,
|
|
117
|
-
baseUrl: "",
|
|
118
|
-
secret: "",
|
|
119
|
-
secretSource: "none",
|
|
120
|
-
config: {
|
|
121
|
-
dmPolicy: "pairing",
|
|
122
|
-
allowFrom: [],
|
|
123
|
-
groupPolicy: "allowlist",
|
|
124
|
-
groupAllowFrom: ["trusted-user"],
|
|
125
|
-
rooms: {
|
|
126
|
-
"room-trusted": {
|
|
127
|
-
enabled: true,
|
|
128
|
-
},
|
|
129
|
-
},
|
|
130
|
-
},
|
|
131
|
-
};
|
|
132
|
-
|
|
133
|
-
await handleNextcloudTalkInbound({
|
|
134
|
-
message,
|
|
135
|
-
account,
|
|
136
|
-
config: {
|
|
137
|
-
channels: {
|
|
138
|
-
"nextcloud-talk": {
|
|
139
|
-
groupPolicy: "allowlist",
|
|
140
|
-
groupAllowFrom: ["trusted-user"],
|
|
141
|
-
},
|
|
142
|
-
},
|
|
143
|
-
},
|
|
144
|
-
runtime: createTestRuntimeEnv(),
|
|
145
|
-
});
|
|
146
|
-
|
|
147
|
-
expect(buildMentionRegexes).not.toHaveBeenCalled();
|
|
148
|
-
});
|
|
149
|
-
});
|
|
@@ -1,202 +0,0 @@
|
|
|
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
DELETED
|
@@ -1,320 +0,0 @@
|
|
|
1
|
-
import { normalizeOptionalString } from "openclaw/plugin-sdk/text-runtime";
|
|
2
|
-
import {
|
|
3
|
-
GROUP_POLICY_BLOCKED_LABEL,
|
|
4
|
-
createChannelPairingController,
|
|
5
|
-
deliverFormattedTextWithAttachments,
|
|
6
|
-
dispatchInboundReplyWithBase,
|
|
7
|
-
logInboundDrop,
|
|
8
|
-
readStoreAllowFromForDmPolicy,
|
|
9
|
-
resolveAllowlistProviderRuntimeGroupPolicy,
|
|
10
|
-
resolveDefaultGroupPolicy,
|
|
11
|
-
resolveDmGroupAccessWithCommandGate,
|
|
12
|
-
warnMissingProviderGroupPolicyFallbackOnce,
|
|
13
|
-
type OpenClawConfig,
|
|
14
|
-
type OutboundReplyPayload,
|
|
15
|
-
type RuntimeEnv,
|
|
16
|
-
} from "../runtime-api.js";
|
|
17
|
-
import type { ResolvedNextcloudTalkAccount } from "./accounts.js";
|
|
18
|
-
import {
|
|
19
|
-
normalizeNextcloudTalkAllowlist,
|
|
20
|
-
resolveNextcloudTalkAllowlistMatch,
|
|
21
|
-
resolveNextcloudTalkGroupAllow,
|
|
22
|
-
resolveNextcloudTalkMentionGate,
|
|
23
|
-
resolveNextcloudTalkRequireMention,
|
|
24
|
-
resolveNextcloudTalkRoomMatch,
|
|
25
|
-
} from "./policy.js";
|
|
26
|
-
import { resolveNextcloudTalkRoomKind } from "./room-info.js";
|
|
27
|
-
import { getNextcloudTalkRuntime } from "./runtime.js";
|
|
28
|
-
import { sendMessageNextcloudTalk } from "./send.js";
|
|
29
|
-
import type { CoreConfig, NextcloudTalkInboundMessage } from "./types.js";
|
|
30
|
-
|
|
31
|
-
const CHANNEL_ID = "nextcloud-talk" as const;
|
|
32
|
-
|
|
33
|
-
async function deliverNextcloudTalkReply(params: {
|
|
34
|
-
cfg: CoreConfig;
|
|
35
|
-
payload: OutboundReplyPayload;
|
|
36
|
-
roomToken: string;
|
|
37
|
-
accountId: string;
|
|
38
|
-
statusSink?: (patch: { lastOutboundAt?: number }) => void;
|
|
39
|
-
}): Promise<void> {
|
|
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
|
-
},
|
|
51
|
-
});
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
export async function handleNextcloudTalkInbound(params: {
|
|
55
|
-
message: NextcloudTalkInboundMessage;
|
|
56
|
-
account: ResolvedNextcloudTalkAccount;
|
|
57
|
-
config: CoreConfig;
|
|
58
|
-
runtime: RuntimeEnv;
|
|
59
|
-
statusSink?: (patch: { lastInboundAt?: number; lastOutboundAt?: number }) => void;
|
|
60
|
-
}): Promise<void> {
|
|
61
|
-
const { message, account, config, runtime, statusSink } = params;
|
|
62
|
-
const core = getNextcloudTalkRuntime();
|
|
63
|
-
const pairing = createChannelPairingController({
|
|
64
|
-
core,
|
|
65
|
-
channel: CHANNEL_ID,
|
|
66
|
-
accountId: account.accountId,
|
|
67
|
-
});
|
|
68
|
-
|
|
69
|
-
const rawBody = message.text?.trim() ?? "";
|
|
70
|
-
if (!rawBody) {
|
|
71
|
-
return;
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
const roomKind = await resolveNextcloudTalkRoomKind({
|
|
75
|
-
account,
|
|
76
|
-
roomToken: message.roomToken,
|
|
77
|
-
runtime,
|
|
78
|
-
});
|
|
79
|
-
const isGroup = roomKind === "direct" ? false : roomKind === "group" ? true : message.isGroupChat;
|
|
80
|
-
const senderId = message.senderId;
|
|
81
|
-
const senderName = message.senderName;
|
|
82
|
-
const roomToken = message.roomToken;
|
|
83
|
-
const roomName = message.roomName;
|
|
84
|
-
|
|
85
|
-
statusSink?.({ lastInboundAt: message.timestamp });
|
|
86
|
-
|
|
87
|
-
const dmPolicy = account.config.dmPolicy ?? "pairing";
|
|
88
|
-
const defaultGroupPolicy = resolveDefaultGroupPolicy(config as OpenClawConfig);
|
|
89
|
-
const { groupPolicy, providerMissingFallbackApplied } =
|
|
90
|
-
resolveAllowlistProviderRuntimeGroupPolicy({
|
|
91
|
-
providerConfigPresent:
|
|
92
|
-
((config.channels as Record<string, unknown> | undefined)?.["nextcloud-talk"] ??
|
|
93
|
-
undefined) !== undefined,
|
|
94
|
-
groupPolicy: account.config.groupPolicy,
|
|
95
|
-
defaultGroupPolicy,
|
|
96
|
-
});
|
|
97
|
-
warnMissingProviderGroupPolicyFallbackOnce({
|
|
98
|
-
providerMissingFallbackApplied,
|
|
99
|
-
providerKey: "nextcloud-talk",
|
|
100
|
-
accountId: account.accountId,
|
|
101
|
-
blockedLabel: GROUP_POLICY_BLOCKED_LABEL.room,
|
|
102
|
-
log: (message) => runtime.log?.(message),
|
|
103
|
-
});
|
|
104
|
-
|
|
105
|
-
const configAllowFrom = normalizeNextcloudTalkAllowlist(account.config.allowFrom);
|
|
106
|
-
const configGroupAllowFrom = normalizeNextcloudTalkAllowlist(account.config.groupAllowFrom);
|
|
107
|
-
const storeAllowFrom = await readStoreAllowFromForDmPolicy({
|
|
108
|
-
provider: CHANNEL_ID,
|
|
109
|
-
accountId: account.accountId,
|
|
110
|
-
dmPolicy,
|
|
111
|
-
readStore: pairing.readStoreForDmPolicy,
|
|
112
|
-
});
|
|
113
|
-
const storeAllowList = normalizeNextcloudTalkAllowlist(storeAllowFrom);
|
|
114
|
-
|
|
115
|
-
const roomMatch = resolveNextcloudTalkRoomMatch({
|
|
116
|
-
rooms: account.config.rooms,
|
|
117
|
-
roomToken,
|
|
118
|
-
});
|
|
119
|
-
const roomConfig = roomMatch.roomConfig;
|
|
120
|
-
if (isGroup && !roomMatch.allowed) {
|
|
121
|
-
runtime.log?.(`nextcloud-talk: drop room ${roomToken} (not allowlisted)`);
|
|
122
|
-
return;
|
|
123
|
-
}
|
|
124
|
-
if (roomConfig?.enabled === false) {
|
|
125
|
-
runtime.log?.(`nextcloud-talk: drop room ${roomToken} (disabled)`);
|
|
126
|
-
return;
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
const roomAllowFrom = normalizeNextcloudTalkAllowlist(roomConfig?.allowFrom);
|
|
130
|
-
|
|
131
|
-
const allowTextCommands = core.channel.commands.shouldHandleTextCommands({
|
|
132
|
-
cfg: config as OpenClawConfig,
|
|
133
|
-
surface: CHANNEL_ID,
|
|
134
|
-
});
|
|
135
|
-
const useAccessGroups =
|
|
136
|
-
(config.commands as Record<string, unknown> | undefined)?.useAccessGroups !== false;
|
|
137
|
-
const hasControlCommand = core.channel.text.hasControlCommand(rawBody, config as OpenClawConfig);
|
|
138
|
-
const access = resolveDmGroupAccessWithCommandGate({
|
|
139
|
-
isGroup,
|
|
140
|
-
dmPolicy,
|
|
141
|
-
groupPolicy,
|
|
142
|
-
allowFrom: configAllowFrom,
|
|
143
|
-
groupAllowFrom: configGroupAllowFrom,
|
|
144
|
-
storeAllowFrom: storeAllowList,
|
|
145
|
-
isSenderAllowed: (allowFrom) =>
|
|
146
|
-
resolveNextcloudTalkAllowlistMatch({
|
|
147
|
-
allowFrom,
|
|
148
|
-
senderId,
|
|
149
|
-
}).allowed,
|
|
150
|
-
command: {
|
|
151
|
-
useAccessGroups,
|
|
152
|
-
allowTextCommands,
|
|
153
|
-
hasControlCommand,
|
|
154
|
-
},
|
|
155
|
-
});
|
|
156
|
-
const commandAuthorized = access.commandAuthorized;
|
|
157
|
-
const effectiveGroupAllowFrom = access.effectiveGroupAllowFrom;
|
|
158
|
-
|
|
159
|
-
if (isGroup) {
|
|
160
|
-
if (access.decision !== "allow") {
|
|
161
|
-
runtime.log?.(`nextcloud-talk: drop group sender ${senderId} (reason=${access.reason})`);
|
|
162
|
-
return;
|
|
163
|
-
}
|
|
164
|
-
const groupAllow = resolveNextcloudTalkGroupAllow({
|
|
165
|
-
groupPolicy,
|
|
166
|
-
outerAllowFrom: effectiveGroupAllowFrom,
|
|
167
|
-
innerAllowFrom: roomAllowFrom,
|
|
168
|
-
senderId,
|
|
169
|
-
});
|
|
170
|
-
if (!groupAllow.allowed) {
|
|
171
|
-
runtime.log?.(`nextcloud-talk: drop group sender ${senderId} (policy=${groupPolicy})`);
|
|
172
|
-
return;
|
|
173
|
-
}
|
|
174
|
-
} else {
|
|
175
|
-
if (access.decision !== "allow") {
|
|
176
|
-
if (access.decision === "pairing") {
|
|
177
|
-
await pairing.issueChallenge({
|
|
178
|
-
senderId,
|
|
179
|
-
senderIdLine: `Your Nextcloud user id: ${senderId}`,
|
|
180
|
-
meta: { name: senderName || undefined },
|
|
181
|
-
sendPairingReply: async (text) => {
|
|
182
|
-
await sendMessageNextcloudTalk(roomToken, text, {
|
|
183
|
-
cfg: config,
|
|
184
|
-
accountId: account.accountId,
|
|
185
|
-
});
|
|
186
|
-
statusSink?.({ lastOutboundAt: Date.now() });
|
|
187
|
-
},
|
|
188
|
-
onReplyError: (err) => {
|
|
189
|
-
runtime.error?.(`nextcloud-talk: pairing reply failed for ${senderId}: ${String(err)}`);
|
|
190
|
-
},
|
|
191
|
-
});
|
|
192
|
-
}
|
|
193
|
-
runtime.log?.(`nextcloud-talk: drop DM sender ${senderId} (reason=${access.reason})`);
|
|
194
|
-
return;
|
|
195
|
-
}
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
if (access.shouldBlockControlCommand) {
|
|
199
|
-
logInboundDrop({
|
|
200
|
-
log: (message) => runtime.log?.(message),
|
|
201
|
-
channel: CHANNEL_ID,
|
|
202
|
-
reason: "control command (unauthorized)",
|
|
203
|
-
target: senderId,
|
|
204
|
-
});
|
|
205
|
-
return;
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
const mentionRegexes = core.channel.mentions.buildMentionRegexes(config as OpenClawConfig);
|
|
209
|
-
const wasMentioned = mentionRegexes.length
|
|
210
|
-
? core.channel.mentions.matchesMentionPatterns(rawBody, mentionRegexes)
|
|
211
|
-
: false;
|
|
212
|
-
const shouldRequireMention = isGroup
|
|
213
|
-
? resolveNextcloudTalkRequireMention({
|
|
214
|
-
roomConfig,
|
|
215
|
-
wildcardConfig: roomMatch.wildcardConfig,
|
|
216
|
-
})
|
|
217
|
-
: false;
|
|
218
|
-
const mentionGate = resolveNextcloudTalkMentionGate({
|
|
219
|
-
isGroup,
|
|
220
|
-
requireMention: shouldRequireMention,
|
|
221
|
-
wasMentioned,
|
|
222
|
-
allowTextCommands,
|
|
223
|
-
hasControlCommand,
|
|
224
|
-
commandAuthorized,
|
|
225
|
-
});
|
|
226
|
-
if (isGroup && mentionGate.shouldSkip) {
|
|
227
|
-
runtime.log?.(`nextcloud-talk: drop room ${roomToken} (no mention)`);
|
|
228
|
-
return;
|
|
229
|
-
}
|
|
230
|
-
|
|
231
|
-
const route = core.channel.routing.resolveAgentRoute({
|
|
232
|
-
cfg: config as OpenClawConfig,
|
|
233
|
-
channel: CHANNEL_ID,
|
|
234
|
-
accountId: account.accountId,
|
|
235
|
-
peer: {
|
|
236
|
-
kind: isGroup ? "group" : "direct",
|
|
237
|
-
id: isGroup ? roomToken : senderId,
|
|
238
|
-
},
|
|
239
|
-
});
|
|
240
|
-
|
|
241
|
-
const fromLabel = isGroup ? `room:${roomName || roomToken}` : senderName || `user:${senderId}`;
|
|
242
|
-
const storePath = core.channel.session.resolveStorePath(
|
|
243
|
-
(config.session as Record<string, unknown> | undefined)?.store as string | undefined,
|
|
244
|
-
{
|
|
245
|
-
agentId: route.agentId,
|
|
246
|
-
},
|
|
247
|
-
);
|
|
248
|
-
const envelopeOptions = core.channel.reply.resolveEnvelopeFormatOptions(config as OpenClawConfig);
|
|
249
|
-
const previousTimestamp = core.channel.session.readSessionUpdatedAt({
|
|
250
|
-
storePath,
|
|
251
|
-
sessionKey: route.sessionKey,
|
|
252
|
-
});
|
|
253
|
-
const body = core.channel.reply.formatAgentEnvelope({
|
|
254
|
-
channel: "Nextcloud Talk",
|
|
255
|
-
from: fromLabel,
|
|
256
|
-
timestamp: message.timestamp,
|
|
257
|
-
previousTimestamp,
|
|
258
|
-
envelope: envelopeOptions,
|
|
259
|
-
body: rawBody,
|
|
260
|
-
});
|
|
261
|
-
|
|
262
|
-
const groupSystemPrompt = normalizeOptionalString(roomConfig?.systemPrompt);
|
|
263
|
-
|
|
264
|
-
const ctxPayload = core.channel.reply.finalizeInboundContext({
|
|
265
|
-
Body: body,
|
|
266
|
-
BodyForAgent: rawBody,
|
|
267
|
-
RawBody: rawBody,
|
|
268
|
-
CommandBody: rawBody,
|
|
269
|
-
From: isGroup ? `nextcloud-talk:room:${roomToken}` : `nextcloud-talk:${senderId}`,
|
|
270
|
-
To: `nextcloud-talk:${roomToken}`,
|
|
271
|
-
SessionKey: route.sessionKey,
|
|
272
|
-
AccountId: route.accountId,
|
|
273
|
-
ChatType: isGroup ? "group" : "direct",
|
|
274
|
-
ConversationLabel: fromLabel,
|
|
275
|
-
SenderName: senderName || undefined,
|
|
276
|
-
SenderId: senderId,
|
|
277
|
-
GroupSubject: isGroup ? roomName || roomToken : undefined,
|
|
278
|
-
GroupSystemPrompt: isGroup ? groupSystemPrompt : undefined,
|
|
279
|
-
Provider: CHANNEL_ID,
|
|
280
|
-
Surface: CHANNEL_ID,
|
|
281
|
-
WasMentioned: isGroup ? wasMentioned : undefined,
|
|
282
|
-
MessageSid: message.messageId,
|
|
283
|
-
Timestamp: message.timestamp,
|
|
284
|
-
OriginatingChannel: CHANNEL_ID,
|
|
285
|
-
OriginatingTo: `nextcloud-talk:${roomToken}`,
|
|
286
|
-
CommandAuthorized: commandAuthorized,
|
|
287
|
-
});
|
|
288
|
-
|
|
289
|
-
await dispatchInboundReplyWithBase({
|
|
290
|
-
cfg: config as OpenClawConfig,
|
|
291
|
-
channel: CHANNEL_ID,
|
|
292
|
-
accountId: account.accountId,
|
|
293
|
-
route,
|
|
294
|
-
storePath,
|
|
295
|
-
ctxPayload,
|
|
296
|
-
core,
|
|
297
|
-
deliver: async (payload) => {
|
|
298
|
-
await deliverNextcloudTalkReply({
|
|
299
|
-
cfg: config,
|
|
300
|
-
payload,
|
|
301
|
-
roomToken,
|
|
302
|
-
accountId: account.accountId,
|
|
303
|
-
statusSink,
|
|
304
|
-
});
|
|
305
|
-
},
|
|
306
|
-
onRecordError: (err) => {
|
|
307
|
-
runtime.error?.(`nextcloud-talk: failed updating session meta: ${String(err)}`);
|
|
308
|
-
},
|
|
309
|
-
onDispatchError: (err, info) => {
|
|
310
|
-
runtime.error?.(`nextcloud-talk ${info.kind} reply failed: ${String(err)}`);
|
|
311
|
-
},
|
|
312
|
-
replyOptions: {
|
|
313
|
-
skillFilter: roomConfig?.skills,
|
|
314
|
-
disableBlockStreaming:
|
|
315
|
-
typeof account.config.blockStreaming === "boolean"
|
|
316
|
-
? !account.config.blockStreaming
|
|
317
|
-
: undefined,
|
|
318
|
-
},
|
|
319
|
-
});
|
|
320
|
-
}
|