@openclaw/msteams 2026.1.29
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/CHANGELOG.md +56 -0
- package/index.ts +18 -0
- package/openclaw.plugin.json +11 -0
- package/package.json +36 -0
- package/src/attachments/download.ts +206 -0
- package/src/attachments/graph.ts +319 -0
- package/src/attachments/html.ts +76 -0
- package/src/attachments/payload.ts +22 -0
- package/src/attachments/shared.ts +235 -0
- package/src/attachments/types.ts +37 -0
- package/src/attachments.test.ts +424 -0
- package/src/attachments.ts +18 -0
- package/src/channel.directory.test.ts +46 -0
- package/src/channel.ts +436 -0
- package/src/conversation-store-fs.test.ts +89 -0
- package/src/conversation-store-fs.ts +155 -0
- package/src/conversation-store-memory.ts +45 -0
- package/src/conversation-store.ts +41 -0
- package/src/directory-live.ts +179 -0
- package/src/errors.test.ts +46 -0
- package/src/errors.ts +158 -0
- package/src/file-consent-helpers.test.ts +234 -0
- package/src/file-consent-helpers.ts +73 -0
- package/src/file-consent.ts +122 -0
- package/src/graph-chat.ts +52 -0
- package/src/graph-upload.ts +445 -0
- package/src/inbound.test.ts +67 -0
- package/src/inbound.ts +38 -0
- package/src/index.ts +4 -0
- package/src/media-helpers.test.ts +186 -0
- package/src/media-helpers.ts +77 -0
- package/src/messenger.test.ts +245 -0
- package/src/messenger.ts +460 -0
- package/src/monitor-handler/inbound-media.ts +123 -0
- package/src/monitor-handler/message-handler.ts +629 -0
- package/src/monitor-handler.ts +166 -0
- package/src/monitor-types.ts +5 -0
- package/src/monitor.ts +290 -0
- package/src/onboarding.ts +432 -0
- package/src/outbound.ts +47 -0
- package/src/pending-uploads.ts +87 -0
- package/src/policy.test.ts +210 -0
- package/src/policy.ts +247 -0
- package/src/polls-store-memory.ts +30 -0
- package/src/polls-store.test.ts +40 -0
- package/src/polls.test.ts +73 -0
- package/src/polls.ts +300 -0
- package/src/probe.test.ts +57 -0
- package/src/probe.ts +99 -0
- package/src/reply-dispatcher.ts +128 -0
- package/src/resolve-allowlist.ts +277 -0
- package/src/runtime.ts +14 -0
- package/src/sdk-types.ts +19 -0
- package/src/sdk.ts +33 -0
- package/src/send-context.ts +156 -0
- package/src/send.ts +489 -0
- package/src/sent-message-cache.test.ts +16 -0
- package/src/sent-message-cache.ts +41 -0
- package/src/storage.ts +22 -0
- package/src/store-fs.ts +80 -0
- package/src/token.ts +19 -0
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
|
|
3
|
+
import type { MSTeamsConfig } from "openclaw/plugin-sdk";
|
|
4
|
+
import {
|
|
5
|
+
isMSTeamsGroupAllowed,
|
|
6
|
+
resolveMSTeamsReplyPolicy,
|
|
7
|
+
resolveMSTeamsRouteConfig,
|
|
8
|
+
} from "./policy.js";
|
|
9
|
+
|
|
10
|
+
describe("msteams policy", () => {
|
|
11
|
+
describe("resolveMSTeamsRouteConfig", () => {
|
|
12
|
+
it("returns team and channel config when present", () => {
|
|
13
|
+
const cfg: MSTeamsConfig = {
|
|
14
|
+
teams: {
|
|
15
|
+
team123: {
|
|
16
|
+
requireMention: false,
|
|
17
|
+
channels: {
|
|
18
|
+
chan456: { requireMention: true },
|
|
19
|
+
},
|
|
20
|
+
},
|
|
21
|
+
},
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
const res = resolveMSTeamsRouteConfig({
|
|
25
|
+
cfg,
|
|
26
|
+
teamId: "team123",
|
|
27
|
+
conversationId: "chan456",
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
expect(res.teamConfig?.requireMention).toBe(false);
|
|
31
|
+
expect(res.channelConfig?.requireMention).toBe(true);
|
|
32
|
+
expect(res.allowlistConfigured).toBe(true);
|
|
33
|
+
expect(res.allowed).toBe(true);
|
|
34
|
+
expect(res.channelMatchKey).toBe("chan456");
|
|
35
|
+
expect(res.channelMatchSource).toBe("direct");
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it("returns undefined configs when teamId is missing", () => {
|
|
39
|
+
const cfg: MSTeamsConfig = {
|
|
40
|
+
teams: { team123: { requireMention: false } },
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
const res = resolveMSTeamsRouteConfig({
|
|
44
|
+
cfg,
|
|
45
|
+
teamId: undefined,
|
|
46
|
+
conversationId: "chan",
|
|
47
|
+
});
|
|
48
|
+
expect(res.teamConfig).toBeUndefined();
|
|
49
|
+
expect(res.channelConfig).toBeUndefined();
|
|
50
|
+
expect(res.allowlistConfigured).toBe(true);
|
|
51
|
+
expect(res.allowed).toBe(false);
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it("matches team and channel by name", () => {
|
|
55
|
+
const cfg: MSTeamsConfig = {
|
|
56
|
+
teams: {
|
|
57
|
+
"My Team": {
|
|
58
|
+
requireMention: true,
|
|
59
|
+
channels: {
|
|
60
|
+
"General Chat": { requireMention: false },
|
|
61
|
+
},
|
|
62
|
+
},
|
|
63
|
+
},
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
const res = resolveMSTeamsRouteConfig({
|
|
67
|
+
cfg,
|
|
68
|
+
teamName: "My Team",
|
|
69
|
+
channelName: "General Chat",
|
|
70
|
+
conversationId: "ignored",
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
expect(res.teamConfig?.requireMention).toBe(true);
|
|
74
|
+
expect(res.channelConfig?.requireMention).toBe(false);
|
|
75
|
+
expect(res.allowed).toBe(true);
|
|
76
|
+
});
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
describe("resolveMSTeamsReplyPolicy", () => {
|
|
80
|
+
it("forces thread replies for direct messages", () => {
|
|
81
|
+
const policy = resolveMSTeamsReplyPolicy({
|
|
82
|
+
isDirectMessage: true,
|
|
83
|
+
globalConfig: { replyStyle: "top-level", requireMention: false },
|
|
84
|
+
});
|
|
85
|
+
expect(policy).toEqual({ requireMention: false, replyStyle: "thread" });
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
it("defaults to requireMention=true and replyStyle=thread", () => {
|
|
89
|
+
const policy = resolveMSTeamsReplyPolicy({
|
|
90
|
+
isDirectMessage: false,
|
|
91
|
+
globalConfig: {},
|
|
92
|
+
});
|
|
93
|
+
expect(policy).toEqual({ requireMention: true, replyStyle: "thread" });
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
it("defaults replyStyle to top-level when requireMention=false", () => {
|
|
97
|
+
const policy = resolveMSTeamsReplyPolicy({
|
|
98
|
+
isDirectMessage: false,
|
|
99
|
+
globalConfig: { requireMention: false },
|
|
100
|
+
});
|
|
101
|
+
expect(policy).toEqual({
|
|
102
|
+
requireMention: false,
|
|
103
|
+
replyStyle: "top-level",
|
|
104
|
+
});
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
it("prefers channel overrides over team and global defaults", () => {
|
|
108
|
+
const policy = resolveMSTeamsReplyPolicy({
|
|
109
|
+
isDirectMessage: false,
|
|
110
|
+
globalConfig: { requireMention: true },
|
|
111
|
+
teamConfig: { requireMention: true },
|
|
112
|
+
channelConfig: { requireMention: false },
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
// requireMention from channel -> false, and replyStyle defaults from requireMention -> top-level
|
|
116
|
+
expect(policy).toEqual({
|
|
117
|
+
requireMention: false,
|
|
118
|
+
replyStyle: "top-level",
|
|
119
|
+
});
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
it("inherits team mention settings when channel config is missing", () => {
|
|
123
|
+
const policy = resolveMSTeamsReplyPolicy({
|
|
124
|
+
isDirectMessage: false,
|
|
125
|
+
globalConfig: { requireMention: true },
|
|
126
|
+
teamConfig: { requireMention: false },
|
|
127
|
+
});
|
|
128
|
+
expect(policy).toEqual({
|
|
129
|
+
requireMention: false,
|
|
130
|
+
replyStyle: "top-level",
|
|
131
|
+
});
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
it("uses explicit replyStyle even when requireMention defaults would differ", () => {
|
|
135
|
+
const policy = resolveMSTeamsReplyPolicy({
|
|
136
|
+
isDirectMessage: false,
|
|
137
|
+
globalConfig: { requireMention: false, replyStyle: "thread" },
|
|
138
|
+
});
|
|
139
|
+
expect(policy).toEqual({ requireMention: false, replyStyle: "thread" });
|
|
140
|
+
});
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
describe("isMSTeamsGroupAllowed", () => {
|
|
144
|
+
it("allows when policy is open", () => {
|
|
145
|
+
expect(
|
|
146
|
+
isMSTeamsGroupAllowed({
|
|
147
|
+
groupPolicy: "open",
|
|
148
|
+
allowFrom: [],
|
|
149
|
+
senderId: "user-id",
|
|
150
|
+
senderName: "User",
|
|
151
|
+
}),
|
|
152
|
+
).toBe(true);
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
it("blocks when policy is disabled", () => {
|
|
156
|
+
expect(
|
|
157
|
+
isMSTeamsGroupAllowed({
|
|
158
|
+
groupPolicy: "disabled",
|
|
159
|
+
allowFrom: ["user-id"],
|
|
160
|
+
senderId: "user-id",
|
|
161
|
+
senderName: "User",
|
|
162
|
+
}),
|
|
163
|
+
).toBe(false);
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
it("blocks allowlist when empty", () => {
|
|
167
|
+
expect(
|
|
168
|
+
isMSTeamsGroupAllowed({
|
|
169
|
+
groupPolicy: "allowlist",
|
|
170
|
+
allowFrom: [],
|
|
171
|
+
senderId: "user-id",
|
|
172
|
+
senderName: "User",
|
|
173
|
+
}),
|
|
174
|
+
).toBe(false);
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
it("allows allowlist when sender matches", () => {
|
|
178
|
+
expect(
|
|
179
|
+
isMSTeamsGroupAllowed({
|
|
180
|
+
groupPolicy: "allowlist",
|
|
181
|
+
allowFrom: ["User-Id"],
|
|
182
|
+
senderId: "user-id",
|
|
183
|
+
senderName: "User",
|
|
184
|
+
}),
|
|
185
|
+
).toBe(true);
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
it("allows allowlist when sender name matches", () => {
|
|
189
|
+
expect(
|
|
190
|
+
isMSTeamsGroupAllowed({
|
|
191
|
+
groupPolicy: "allowlist",
|
|
192
|
+
allowFrom: ["user"],
|
|
193
|
+
senderId: "other",
|
|
194
|
+
senderName: "User",
|
|
195
|
+
}),
|
|
196
|
+
).toBe(true);
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
it("allows allowlist wildcard", () => {
|
|
200
|
+
expect(
|
|
201
|
+
isMSTeamsGroupAllowed({
|
|
202
|
+
groupPolicy: "allowlist",
|
|
203
|
+
allowFrom: ["*"],
|
|
204
|
+
senderId: "other",
|
|
205
|
+
senderName: "User",
|
|
206
|
+
}),
|
|
207
|
+
).toBe(true);
|
|
208
|
+
});
|
|
209
|
+
});
|
|
210
|
+
});
|
package/src/policy.ts
ADDED
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
AllowlistMatch,
|
|
3
|
+
ChannelGroupContext,
|
|
4
|
+
GroupPolicy,
|
|
5
|
+
GroupToolPolicyConfig,
|
|
6
|
+
MSTeamsChannelConfig,
|
|
7
|
+
MSTeamsConfig,
|
|
8
|
+
MSTeamsReplyStyle,
|
|
9
|
+
MSTeamsTeamConfig,
|
|
10
|
+
} from "openclaw/plugin-sdk";
|
|
11
|
+
import {
|
|
12
|
+
buildChannelKeyCandidates,
|
|
13
|
+
normalizeChannelSlug,
|
|
14
|
+
resolveToolsBySender,
|
|
15
|
+
resolveChannelEntryMatchWithFallback,
|
|
16
|
+
resolveNestedAllowlistDecision,
|
|
17
|
+
} from "openclaw/plugin-sdk";
|
|
18
|
+
|
|
19
|
+
export type MSTeamsResolvedRouteConfig = {
|
|
20
|
+
teamConfig?: MSTeamsTeamConfig;
|
|
21
|
+
channelConfig?: MSTeamsChannelConfig;
|
|
22
|
+
allowlistConfigured: boolean;
|
|
23
|
+
allowed: boolean;
|
|
24
|
+
teamKey?: string;
|
|
25
|
+
channelKey?: string;
|
|
26
|
+
channelMatchKey?: string;
|
|
27
|
+
channelMatchSource?: "direct" | "wildcard";
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
export function resolveMSTeamsRouteConfig(params: {
|
|
31
|
+
cfg?: MSTeamsConfig;
|
|
32
|
+
teamId?: string | null | undefined;
|
|
33
|
+
teamName?: string | null | undefined;
|
|
34
|
+
conversationId?: string | null | undefined;
|
|
35
|
+
channelName?: string | null | undefined;
|
|
36
|
+
}): MSTeamsResolvedRouteConfig {
|
|
37
|
+
const teamId = params.teamId?.trim();
|
|
38
|
+
const teamName = params.teamName?.trim();
|
|
39
|
+
const conversationId = params.conversationId?.trim();
|
|
40
|
+
const channelName = params.channelName?.trim();
|
|
41
|
+
const teams = params.cfg?.teams ?? {};
|
|
42
|
+
const allowlistConfigured = Object.keys(teams).length > 0;
|
|
43
|
+
const teamCandidates = buildChannelKeyCandidates(
|
|
44
|
+
teamId,
|
|
45
|
+
teamName,
|
|
46
|
+
teamName ? normalizeChannelSlug(teamName) : undefined,
|
|
47
|
+
);
|
|
48
|
+
const teamMatch = resolveChannelEntryMatchWithFallback({
|
|
49
|
+
entries: teams,
|
|
50
|
+
keys: teamCandidates,
|
|
51
|
+
wildcardKey: "*",
|
|
52
|
+
normalizeKey: normalizeChannelSlug,
|
|
53
|
+
});
|
|
54
|
+
const teamConfig = teamMatch.entry;
|
|
55
|
+
const channels = teamConfig?.channels ?? {};
|
|
56
|
+
const channelAllowlistConfigured = Object.keys(channels).length > 0;
|
|
57
|
+
const channelCandidates = buildChannelKeyCandidates(
|
|
58
|
+
conversationId,
|
|
59
|
+
channelName,
|
|
60
|
+
channelName ? normalizeChannelSlug(channelName) : undefined,
|
|
61
|
+
);
|
|
62
|
+
const channelMatch = resolveChannelEntryMatchWithFallback({
|
|
63
|
+
entries: channels,
|
|
64
|
+
keys: channelCandidates,
|
|
65
|
+
wildcardKey: "*",
|
|
66
|
+
normalizeKey: normalizeChannelSlug,
|
|
67
|
+
});
|
|
68
|
+
const channelConfig = channelMatch.entry;
|
|
69
|
+
|
|
70
|
+
const allowed = resolveNestedAllowlistDecision({
|
|
71
|
+
outerConfigured: allowlistConfigured,
|
|
72
|
+
outerMatched: Boolean(teamConfig),
|
|
73
|
+
innerConfigured: channelAllowlistConfigured,
|
|
74
|
+
innerMatched: Boolean(channelConfig),
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
return {
|
|
78
|
+
teamConfig,
|
|
79
|
+
channelConfig,
|
|
80
|
+
allowlistConfigured,
|
|
81
|
+
allowed,
|
|
82
|
+
teamKey: teamMatch.matchKey ?? teamMatch.key,
|
|
83
|
+
channelKey: channelMatch.matchKey ?? channelMatch.key,
|
|
84
|
+
channelMatchKey: channelMatch.matchKey,
|
|
85
|
+
channelMatchSource:
|
|
86
|
+
channelMatch.matchSource === "direct" || channelMatch.matchSource === "wildcard"
|
|
87
|
+
? channelMatch.matchSource
|
|
88
|
+
: undefined,
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export function resolveMSTeamsGroupToolPolicy(
|
|
93
|
+
params: ChannelGroupContext,
|
|
94
|
+
): GroupToolPolicyConfig | undefined {
|
|
95
|
+
const cfg = params.cfg.channels?.msteams;
|
|
96
|
+
if (!cfg) return undefined;
|
|
97
|
+
const groupId = params.groupId?.trim();
|
|
98
|
+
const groupChannel = params.groupChannel?.trim();
|
|
99
|
+
const groupSpace = params.groupSpace?.trim();
|
|
100
|
+
|
|
101
|
+
const resolved = resolveMSTeamsRouteConfig({
|
|
102
|
+
cfg,
|
|
103
|
+
teamId: groupSpace,
|
|
104
|
+
teamName: groupSpace,
|
|
105
|
+
conversationId: groupId,
|
|
106
|
+
channelName: groupChannel,
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
if (resolved.channelConfig) {
|
|
110
|
+
const senderPolicy = resolveToolsBySender({
|
|
111
|
+
toolsBySender: resolved.channelConfig.toolsBySender,
|
|
112
|
+
senderId: params.senderId,
|
|
113
|
+
senderName: params.senderName,
|
|
114
|
+
senderUsername: params.senderUsername,
|
|
115
|
+
senderE164: params.senderE164,
|
|
116
|
+
});
|
|
117
|
+
if (senderPolicy) return senderPolicy;
|
|
118
|
+
if (resolved.channelConfig.tools) return resolved.channelConfig.tools;
|
|
119
|
+
const teamSenderPolicy = resolveToolsBySender({
|
|
120
|
+
toolsBySender: resolved.teamConfig?.toolsBySender,
|
|
121
|
+
senderId: params.senderId,
|
|
122
|
+
senderName: params.senderName,
|
|
123
|
+
senderUsername: params.senderUsername,
|
|
124
|
+
senderE164: params.senderE164,
|
|
125
|
+
});
|
|
126
|
+
if (teamSenderPolicy) return teamSenderPolicy;
|
|
127
|
+
return resolved.teamConfig?.tools;
|
|
128
|
+
}
|
|
129
|
+
if (resolved.teamConfig) {
|
|
130
|
+
const teamSenderPolicy = resolveToolsBySender({
|
|
131
|
+
toolsBySender: resolved.teamConfig.toolsBySender,
|
|
132
|
+
senderId: params.senderId,
|
|
133
|
+
senderName: params.senderName,
|
|
134
|
+
senderUsername: params.senderUsername,
|
|
135
|
+
senderE164: params.senderE164,
|
|
136
|
+
});
|
|
137
|
+
if (teamSenderPolicy) return teamSenderPolicy;
|
|
138
|
+
if (resolved.teamConfig.tools) return resolved.teamConfig.tools;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
if (!groupId) return undefined;
|
|
142
|
+
|
|
143
|
+
const channelCandidates = buildChannelKeyCandidates(
|
|
144
|
+
groupId,
|
|
145
|
+
groupChannel,
|
|
146
|
+
groupChannel ? normalizeChannelSlug(groupChannel) : undefined,
|
|
147
|
+
);
|
|
148
|
+
for (const teamConfig of Object.values(cfg.teams ?? {})) {
|
|
149
|
+
const match = resolveChannelEntryMatchWithFallback({
|
|
150
|
+
entries: teamConfig?.channels ?? {},
|
|
151
|
+
keys: channelCandidates,
|
|
152
|
+
wildcardKey: "*",
|
|
153
|
+
normalizeKey: normalizeChannelSlug,
|
|
154
|
+
});
|
|
155
|
+
if (match.entry) {
|
|
156
|
+
const senderPolicy = resolveToolsBySender({
|
|
157
|
+
toolsBySender: match.entry.toolsBySender,
|
|
158
|
+
senderId: params.senderId,
|
|
159
|
+
senderName: params.senderName,
|
|
160
|
+
senderUsername: params.senderUsername,
|
|
161
|
+
senderE164: params.senderE164,
|
|
162
|
+
});
|
|
163
|
+
if (senderPolicy) return senderPolicy;
|
|
164
|
+
if (match.entry.tools) return match.entry.tools;
|
|
165
|
+
const teamSenderPolicy = resolveToolsBySender({
|
|
166
|
+
toolsBySender: teamConfig?.toolsBySender,
|
|
167
|
+
senderId: params.senderId,
|
|
168
|
+
senderName: params.senderName,
|
|
169
|
+
senderUsername: params.senderUsername,
|
|
170
|
+
senderE164: params.senderE164,
|
|
171
|
+
});
|
|
172
|
+
if (teamSenderPolicy) return teamSenderPolicy;
|
|
173
|
+
return teamConfig?.tools;
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
return undefined;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
export type MSTeamsReplyPolicy = {
|
|
181
|
+
requireMention: boolean;
|
|
182
|
+
replyStyle: MSTeamsReplyStyle;
|
|
183
|
+
};
|
|
184
|
+
|
|
185
|
+
export type MSTeamsAllowlistMatch = AllowlistMatch<"wildcard" | "id" | "name">;
|
|
186
|
+
|
|
187
|
+
export function resolveMSTeamsAllowlistMatch(params: {
|
|
188
|
+
allowFrom: Array<string | number>;
|
|
189
|
+
senderId: string;
|
|
190
|
+
senderName?: string | null;
|
|
191
|
+
}): MSTeamsAllowlistMatch {
|
|
192
|
+
const allowFrom = params.allowFrom
|
|
193
|
+
.map((entry) => String(entry).trim().toLowerCase())
|
|
194
|
+
.filter(Boolean);
|
|
195
|
+
if (allowFrom.length === 0) return { allowed: false };
|
|
196
|
+
if (allowFrom.includes("*")) {
|
|
197
|
+
return { allowed: true, matchKey: "*", matchSource: "wildcard" };
|
|
198
|
+
}
|
|
199
|
+
const senderId = params.senderId.toLowerCase();
|
|
200
|
+
if (allowFrom.includes(senderId)) {
|
|
201
|
+
return { allowed: true, matchKey: senderId, matchSource: "id" };
|
|
202
|
+
}
|
|
203
|
+
const senderName = params.senderName?.toLowerCase();
|
|
204
|
+
if (senderName && allowFrom.includes(senderName)) {
|
|
205
|
+
return { allowed: true, matchKey: senderName, matchSource: "name" };
|
|
206
|
+
}
|
|
207
|
+
return { allowed: false };
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
export function resolveMSTeamsReplyPolicy(params: {
|
|
211
|
+
isDirectMessage: boolean;
|
|
212
|
+
globalConfig?: MSTeamsConfig;
|
|
213
|
+
teamConfig?: MSTeamsTeamConfig;
|
|
214
|
+
channelConfig?: MSTeamsChannelConfig;
|
|
215
|
+
}): MSTeamsReplyPolicy {
|
|
216
|
+
if (params.isDirectMessage) {
|
|
217
|
+
return { requireMention: false, replyStyle: "thread" };
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
const requireMention =
|
|
221
|
+
params.channelConfig?.requireMention ??
|
|
222
|
+
params.teamConfig?.requireMention ??
|
|
223
|
+
params.globalConfig?.requireMention ??
|
|
224
|
+
true;
|
|
225
|
+
|
|
226
|
+
const explicitReplyStyle =
|
|
227
|
+
params.channelConfig?.replyStyle ??
|
|
228
|
+
params.teamConfig?.replyStyle ??
|
|
229
|
+
params.globalConfig?.replyStyle;
|
|
230
|
+
|
|
231
|
+
const replyStyle: MSTeamsReplyStyle =
|
|
232
|
+
explicitReplyStyle ?? (requireMention ? "thread" : "top-level");
|
|
233
|
+
|
|
234
|
+
return { requireMention, replyStyle };
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
export function isMSTeamsGroupAllowed(params: {
|
|
238
|
+
groupPolicy: GroupPolicy;
|
|
239
|
+
allowFrom: Array<string | number>;
|
|
240
|
+
senderId: string;
|
|
241
|
+
senderName?: string | null;
|
|
242
|
+
}): boolean {
|
|
243
|
+
const { groupPolicy } = params;
|
|
244
|
+
if (groupPolicy === "disabled") return false;
|
|
245
|
+
if (groupPolicy === "open") return true;
|
|
246
|
+
return resolveMSTeamsAllowlistMatch(params).allowed;
|
|
247
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import {
|
|
2
|
+
type MSTeamsPoll,
|
|
3
|
+
type MSTeamsPollStore,
|
|
4
|
+
normalizeMSTeamsPollSelections,
|
|
5
|
+
} from "./polls.js";
|
|
6
|
+
|
|
7
|
+
export function createMSTeamsPollStoreMemory(initial: MSTeamsPoll[] = []): MSTeamsPollStore {
|
|
8
|
+
const polls = new Map<string, MSTeamsPoll>();
|
|
9
|
+
for (const poll of initial) {
|
|
10
|
+
polls.set(poll.id, { ...poll });
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const createPoll = async (poll: MSTeamsPoll) => {
|
|
14
|
+
polls.set(poll.id, { ...poll });
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
const getPoll = async (pollId: string) => polls.get(pollId) ?? null;
|
|
18
|
+
|
|
19
|
+
const recordVote = async (params: { pollId: string; voterId: string; selections: string[] }) => {
|
|
20
|
+
const poll = polls.get(params.pollId);
|
|
21
|
+
if (!poll) return null;
|
|
22
|
+
const normalized = normalizeMSTeamsPollSelections(poll, params.selections);
|
|
23
|
+
poll.votes[params.voterId] = normalized;
|
|
24
|
+
poll.updatedAt = new Date().toISOString();
|
|
25
|
+
polls.set(poll.id, poll);
|
|
26
|
+
return poll;
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
return { createPoll, getPoll, recordVote };
|
|
30
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import os from "node:os";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
|
|
5
|
+
import { describe, expect, it } from "vitest";
|
|
6
|
+
|
|
7
|
+
import { createMSTeamsPollStoreFs } from "./polls.js";
|
|
8
|
+
import { createMSTeamsPollStoreMemory } from "./polls-store-memory.js";
|
|
9
|
+
|
|
10
|
+
const createFsStore = async () => {
|
|
11
|
+
const stateDir = await fs.promises.mkdtemp(path.join(os.tmpdir(), "openclaw-msteams-polls-"));
|
|
12
|
+
return createMSTeamsPollStoreFs({ stateDir });
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
const createMemoryStore = () => createMSTeamsPollStoreMemory();
|
|
16
|
+
|
|
17
|
+
describe.each([
|
|
18
|
+
{ name: "memory", createStore: createMemoryStore },
|
|
19
|
+
{ name: "fs", createStore: createFsStore },
|
|
20
|
+
])("$name poll store", ({ createStore }) => {
|
|
21
|
+
it("stores polls and records normalized votes", async () => {
|
|
22
|
+
const store = await createStore();
|
|
23
|
+
await store.createPoll({
|
|
24
|
+
id: "poll-1",
|
|
25
|
+
question: "Lunch?",
|
|
26
|
+
options: ["Pizza", "Sushi"],
|
|
27
|
+
maxSelections: 1,
|
|
28
|
+
createdAt: new Date().toISOString(),
|
|
29
|
+
votes: {},
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
const poll = await store.recordVote({
|
|
33
|
+
pollId: "poll-1",
|
|
34
|
+
voterId: "user-1",
|
|
35
|
+
selections: ["0", "1"],
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
expect(poll?.votes["user-1"]).toEqual(["0"]);
|
|
39
|
+
});
|
|
40
|
+
});
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import os from "node:os";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
|
|
5
|
+
import { beforeEach, describe, expect, it } from "vitest";
|
|
6
|
+
|
|
7
|
+
import type { PluginRuntime } from "openclaw/plugin-sdk";
|
|
8
|
+
import { buildMSTeamsPollCard, createMSTeamsPollStoreFs, extractMSTeamsPollVote } from "./polls.js";
|
|
9
|
+
import { setMSTeamsRuntime } from "./runtime.js";
|
|
10
|
+
|
|
11
|
+
const runtimeStub = {
|
|
12
|
+
state: {
|
|
13
|
+
resolveStateDir: (env: NodeJS.ProcessEnv = process.env, homedir?: () => string) => {
|
|
14
|
+
const override =
|
|
15
|
+
env.OPENCLAW_STATE_DIR?.trim() || env.OPENCLAW_STATE_DIR?.trim();
|
|
16
|
+
if (override) return override;
|
|
17
|
+
const resolvedHome = homedir ? homedir() : os.homedir();
|
|
18
|
+
return path.join(resolvedHome, ".openclaw");
|
|
19
|
+
},
|
|
20
|
+
},
|
|
21
|
+
} as unknown as PluginRuntime;
|
|
22
|
+
|
|
23
|
+
describe("msteams polls", () => {
|
|
24
|
+
beforeEach(() => {
|
|
25
|
+
setMSTeamsRuntime(runtimeStub);
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it("builds poll cards with fallback text", () => {
|
|
29
|
+
const card = buildMSTeamsPollCard({
|
|
30
|
+
question: "Lunch?",
|
|
31
|
+
options: ["Pizza", "Sushi"],
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
expect(card.pollId).toBeTruthy();
|
|
35
|
+
expect(card.fallbackText).toContain("Poll: Lunch?");
|
|
36
|
+
expect(card.fallbackText).toContain("1. Pizza");
|
|
37
|
+
expect(card.fallbackText).toContain("2. Sushi");
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it("extracts poll votes from activity values", () => {
|
|
41
|
+
const vote = extractMSTeamsPollVote({
|
|
42
|
+
value: {
|
|
43
|
+
openclawPollId: "poll-1",
|
|
44
|
+
choices: "0,1",
|
|
45
|
+
},
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
expect(vote).toEqual({
|
|
49
|
+
pollId: "poll-1",
|
|
50
|
+
selections: ["0", "1"],
|
|
51
|
+
});
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it("stores and records poll votes", async () => {
|
|
55
|
+
const home = await fs.promises.mkdtemp(path.join(os.tmpdir(), "openclaw-msteams-polls-"));
|
|
56
|
+
const store = createMSTeamsPollStoreFs({ homedir: () => home });
|
|
57
|
+
await store.createPoll({
|
|
58
|
+
id: "poll-2",
|
|
59
|
+
question: "Pick one",
|
|
60
|
+
options: ["A", "B"],
|
|
61
|
+
maxSelections: 1,
|
|
62
|
+
createdAt: new Date().toISOString(),
|
|
63
|
+
votes: {},
|
|
64
|
+
});
|
|
65
|
+
await store.recordVote({
|
|
66
|
+
pollId: "poll-2",
|
|
67
|
+
voterId: "user-1",
|
|
68
|
+
selections: ["0", "1"],
|
|
69
|
+
});
|
|
70
|
+
const stored = await store.getPoll("poll-2");
|
|
71
|
+
expect(stored?.votes["user-1"]).toEqual(["0"]);
|
|
72
|
+
});
|
|
73
|
+
});
|