@cuylabs/channel-slack 0.4.0 → 0.5.0
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/README.md +20 -12
- package/dist/bolt.d.ts +30 -1
- package/dist/bolt.js +163 -0
- package/dist/chunk-CMR6B76C.js +664 -0
- package/dist/{chunk-FX2JOVX5.js → chunk-IDVDMJ5U.js} +262 -1
- package/dist/diagnostics.d.ts +21 -104
- package/dist/diagnostics.js +21 -3
- package/dist/index.d.ts +1 -1
- package/dist/index.js +7 -1
- package/dist/inspect-BpY5JA0K.d.ts +128 -0
- package/dist/policy.d.ts +47 -1
- package/dist/policy.js +7 -1
- package/dist/setup.d.ts +1 -1
- package/dist/setup.js +1 -1
- package/docs/concepts/bolt-runtime.md +26 -0
- package/docs/concepts/message-policy.md +32 -0
- package/docs/reference/channel-slack-boundary.md +2 -2
- package/docs/reference/exports.md +12 -12
- package/package.json +1 -1
- package/dist/chunk-BODPT4I6.js +0 -322
|
@@ -0,0 +1,664 @@
|
|
|
1
|
+
// src/policy/message/keys.ts
|
|
2
|
+
function createSlackMessagePolicyMessageKey(activity) {
|
|
3
|
+
if (!activity.messageTs) {
|
|
4
|
+
return void 0;
|
|
5
|
+
}
|
|
6
|
+
return `${activity.teamId ?? "unknown"}:${activity.channelId}:${activity.messageTs}`;
|
|
7
|
+
}
|
|
8
|
+
function createSlackMessagePolicyThreadKey(activity) {
|
|
9
|
+
return `${activity.teamId ?? "unknown"}:${activity.channelId}:${activity.threadTs ?? activity.messageTs ?? "unthreaded"}`;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
// src/policy/message/state-store.ts
|
|
13
|
+
var DEFAULT_MAX_MENTIONED_THREADS = 1e4;
|
|
14
|
+
var DEFAULT_MAX_ACCEPTED_MESSAGES = 1e4;
|
|
15
|
+
function createInMemorySlackMessagePolicyStateStore({
|
|
16
|
+
maxAcceptedMessages = DEFAULT_MAX_ACCEPTED_MESSAGES,
|
|
17
|
+
maxMentionedThreads = DEFAULT_MAX_MENTIONED_THREADS
|
|
18
|
+
} = {}) {
|
|
19
|
+
const mentionedThreads = /* @__PURE__ */ new Map();
|
|
20
|
+
const mentionedThreadOrder = [];
|
|
21
|
+
const acceptedMessageKeys = /* @__PURE__ */ new Set();
|
|
22
|
+
const acceptedMessageOrder = [];
|
|
23
|
+
function claimAcceptedMessageKey(key) {
|
|
24
|
+
if (acceptedMessageKeys.has(key)) {
|
|
25
|
+
return false;
|
|
26
|
+
}
|
|
27
|
+
acceptedMessageKeys.add(key);
|
|
28
|
+
acceptedMessageOrder.push(key);
|
|
29
|
+
trimSetByOrder(
|
|
30
|
+
acceptedMessageKeys,
|
|
31
|
+
acceptedMessageOrder,
|
|
32
|
+
maxAcceptedMessages
|
|
33
|
+
);
|
|
34
|
+
return true;
|
|
35
|
+
}
|
|
36
|
+
return {
|
|
37
|
+
getMentionedThread(key) {
|
|
38
|
+
return mentionedThreads.get(key);
|
|
39
|
+
},
|
|
40
|
+
hasAcceptedMessage(key) {
|
|
41
|
+
return acceptedMessageKeys.has(key);
|
|
42
|
+
},
|
|
43
|
+
claimAcceptedMessage(key) {
|
|
44
|
+
return claimAcceptedMessageKey(key);
|
|
45
|
+
},
|
|
46
|
+
rememberMentionedThread(key, state) {
|
|
47
|
+
if (!mentionedThreads.has(key)) {
|
|
48
|
+
mentionedThreadOrder.push(key);
|
|
49
|
+
}
|
|
50
|
+
mentionedThreads.set(key, state);
|
|
51
|
+
trimMapByOrder(
|
|
52
|
+
mentionedThreads,
|
|
53
|
+
mentionedThreadOrder,
|
|
54
|
+
maxMentionedThreads
|
|
55
|
+
);
|
|
56
|
+
},
|
|
57
|
+
rememberAcceptedMessage(key) {
|
|
58
|
+
claimAcceptedMessageKey(key);
|
|
59
|
+
}
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
function trimMapByOrder(map, order, maxEntries) {
|
|
63
|
+
if (!Number.isFinite(maxEntries) || maxEntries <= 0) {
|
|
64
|
+
map.clear();
|
|
65
|
+
order.length = 0;
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
while (order.length > maxEntries) {
|
|
69
|
+
const oldest = order.shift();
|
|
70
|
+
if (oldest !== void 0) {
|
|
71
|
+
map.delete(oldest);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
function trimSetByOrder(set, order, maxEntries) {
|
|
76
|
+
if (!Number.isFinite(maxEntries) || maxEntries <= 0) {
|
|
77
|
+
set.clear();
|
|
78
|
+
order.length = 0;
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
while (order.length > maxEntries) {
|
|
82
|
+
const oldest = order.shift();
|
|
83
|
+
if (oldest !== void 0) {
|
|
84
|
+
set.delete(oldest);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// src/policy/message/async-resolver.ts
|
|
90
|
+
function createAsyncSlackMessagePolicyResolver(config = {}) {
|
|
91
|
+
const allowedChannelIds = new Set(config.allowedChannelIds ?? []);
|
|
92
|
+
const messagePolicy = config.messagePolicy ?? "disabled";
|
|
93
|
+
const threadReplyPolicy = config.threadReplyPolicy ?? "mention-required";
|
|
94
|
+
const stateStore = config.stateStore ?? createInMemorySlackMessagePolicyStateStore({
|
|
95
|
+
maxAcceptedMessages: config.maxAcceptedMessages,
|
|
96
|
+
maxMentionedThreads: config.maxMentionedThreads
|
|
97
|
+
});
|
|
98
|
+
async function rememberMentionedThread(activity) {
|
|
99
|
+
const key = createSlackMessagePolicyThreadKey(activity);
|
|
100
|
+
const originalUserId = activity.parentUserId ?? activity.userId;
|
|
101
|
+
await stateStore.rememberMentionedThread(
|
|
102
|
+
key,
|
|
103
|
+
{ originalUserId },
|
|
104
|
+
{
|
|
105
|
+
activity,
|
|
106
|
+
key
|
|
107
|
+
}
|
|
108
|
+
);
|
|
109
|
+
}
|
|
110
|
+
async function claimAcceptedMessage(activity) {
|
|
111
|
+
const key = createSlackMessagePolicyMessageKey(activity);
|
|
112
|
+
if (!key) {
|
|
113
|
+
return true;
|
|
114
|
+
}
|
|
115
|
+
const context = { activity, key };
|
|
116
|
+
if (stateStore.claimAcceptedMessage) {
|
|
117
|
+
return await stateStore.claimAcceptedMessage(key, context);
|
|
118
|
+
}
|
|
119
|
+
if (await stateStore.hasAcceptedMessage(key, context)) {
|
|
120
|
+
return false;
|
|
121
|
+
}
|
|
122
|
+
await stateStore.rememberAcceptedMessage(key, context);
|
|
123
|
+
return true;
|
|
124
|
+
}
|
|
125
|
+
async function isDuplicate(activity) {
|
|
126
|
+
const key = createSlackMessagePolicyMessageKey(activity);
|
|
127
|
+
return Boolean(
|
|
128
|
+
key && await stateStore.hasAcceptedMessage(key, { activity, key })
|
|
129
|
+
);
|
|
130
|
+
}
|
|
131
|
+
async function accept(activity, reason) {
|
|
132
|
+
if (!await claimAcceptedMessage(activity)) {
|
|
133
|
+
return reject(activity, "duplicate-message-event");
|
|
134
|
+
}
|
|
135
|
+
return {
|
|
136
|
+
accepted: true,
|
|
137
|
+
activity,
|
|
138
|
+
reason,
|
|
139
|
+
text: activity.text
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
function reject(activity, reason) {
|
|
143
|
+
return {
|
|
144
|
+
accepted: false,
|
|
145
|
+
activity,
|
|
146
|
+
reason
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
async function resolve(activity) {
|
|
150
|
+
if (activity.isMention) {
|
|
151
|
+
await rememberMentionedThread(activity);
|
|
152
|
+
return accept(activity, "bot-mentioned");
|
|
153
|
+
}
|
|
154
|
+
if (await isDuplicate(activity)) {
|
|
155
|
+
return reject(activity, "duplicate-message-event");
|
|
156
|
+
}
|
|
157
|
+
if (activity.channelType === "dm") {
|
|
158
|
+
return accept(activity, "direct-message");
|
|
159
|
+
}
|
|
160
|
+
if (messagePolicy === "disabled") {
|
|
161
|
+
return reject(activity, "passive-channel-messages-disabled");
|
|
162
|
+
}
|
|
163
|
+
if (messagePolicy === "any-added-channel") {
|
|
164
|
+
return accept(activity, "any-added-channel");
|
|
165
|
+
}
|
|
166
|
+
if (messagePolicy === "allowed-channels") {
|
|
167
|
+
return allowedChannelIds.has(activity.channelId) ? accept(activity, "allowed-channel") : reject(activity, "channel-not-allowed");
|
|
168
|
+
}
|
|
169
|
+
const threadKey = createSlackMessagePolicyThreadKey(activity);
|
|
170
|
+
const threadState = await stateStore.getMentionedThread(threadKey, {
|
|
171
|
+
activity,
|
|
172
|
+
key: threadKey
|
|
173
|
+
});
|
|
174
|
+
if (!threadState) {
|
|
175
|
+
return reject(activity, "thread-not-mentioned");
|
|
176
|
+
}
|
|
177
|
+
if (threadReplyPolicy === "mention-required") {
|
|
178
|
+
return reject(activity, "thread-reply-mention-required");
|
|
179
|
+
}
|
|
180
|
+
if (threadReplyPolicy === "original-user" && threadState.originalUserId && activity.userId !== threadState.originalUserId) {
|
|
181
|
+
return reject(activity, "thread-reply-not-original-user");
|
|
182
|
+
}
|
|
183
|
+
return accept(activity, "mentioned-thread-reply");
|
|
184
|
+
}
|
|
185
|
+
return {
|
|
186
|
+
resolve,
|
|
187
|
+
async resolveMessage(activity) {
|
|
188
|
+
const decision = await resolve(activity);
|
|
189
|
+
return decision.accepted ? decision.text : void 0;
|
|
190
|
+
}
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// src/policy/message/postgres-state-store.ts
|
|
195
|
+
var DEFAULT_MENTIONED_THREADS_TABLE = "channel_slack_message_policy_threads";
|
|
196
|
+
var DEFAULT_ACCEPTED_MESSAGES_TABLE = "channel_slack_message_policy_messages";
|
|
197
|
+
var DEFAULT_ACCEPTED_MESSAGE_RETENTION_MS = 7 * 24 * 60 * 60 * 1e3;
|
|
198
|
+
var DEFAULT_MENTIONED_THREAD_RETENTION_MS = 30 * 24 * 60 * 60 * 1e3;
|
|
199
|
+
var DEFAULT_PRUNE_BATCH_SIZE = 1e3;
|
|
200
|
+
var DEFAULT_PRUNE_INTERVAL_MS = 6 * 60 * 60 * 1e3;
|
|
201
|
+
function createPostgresSlackMessagePolicyStateStore({
|
|
202
|
+
acceptedMessageRetentionMs = DEFAULT_ACCEPTED_MESSAGE_RETENTION_MS,
|
|
203
|
+
acceptedMessagesTableName = DEFAULT_ACCEPTED_MESSAGES_TABLE,
|
|
204
|
+
client,
|
|
205
|
+
connectionString,
|
|
206
|
+
ensureSchema = true,
|
|
207
|
+
mentionedThreadRetentionMs = DEFAULT_MENTIONED_THREAD_RETENTION_MS,
|
|
208
|
+
mentionedThreadsTableName = DEFAULT_MENTIONED_THREADS_TABLE,
|
|
209
|
+
onPruneError,
|
|
210
|
+
pruneBatchSize = DEFAULT_PRUNE_BATCH_SIZE,
|
|
211
|
+
pruneIntervalMs = DEFAULT_PRUNE_INTERVAL_MS,
|
|
212
|
+
schema
|
|
213
|
+
}) {
|
|
214
|
+
let activeClient = client;
|
|
215
|
+
let ownsClient = false;
|
|
216
|
+
let initialized;
|
|
217
|
+
let pruneTimer;
|
|
218
|
+
async function getClient() {
|
|
219
|
+
if (activeClient) {
|
|
220
|
+
return activeClient;
|
|
221
|
+
}
|
|
222
|
+
if (!connectionString) {
|
|
223
|
+
throw new Error(
|
|
224
|
+
"connectionString is required when a Postgres Slack message policy state client is not provided"
|
|
225
|
+
);
|
|
226
|
+
}
|
|
227
|
+
const Pool = await importPostgresPoolConstructor();
|
|
228
|
+
activeClient = new Pool({ connectionString });
|
|
229
|
+
ownsClient = true;
|
|
230
|
+
return activeClient;
|
|
231
|
+
}
|
|
232
|
+
async function ensureInitialized() {
|
|
233
|
+
const currentClient = await getClient();
|
|
234
|
+
initialized ??= initializePostgresSlackMessagePolicyState({
|
|
235
|
+
acceptedMessagesTableName,
|
|
236
|
+
client: currentClient,
|
|
237
|
+
ensureSchema,
|
|
238
|
+
mentionedThreadsTableName,
|
|
239
|
+
schema
|
|
240
|
+
});
|
|
241
|
+
await initialized;
|
|
242
|
+
startPruneTimer();
|
|
243
|
+
return currentClient;
|
|
244
|
+
}
|
|
245
|
+
function startPruneTimer() {
|
|
246
|
+
if (pruneTimer || pruneIntervalMs <= 0) {
|
|
247
|
+
return;
|
|
248
|
+
}
|
|
249
|
+
pruneTimer = setInterval(() => {
|
|
250
|
+
void prune().catch((error) => {
|
|
251
|
+
onPruneError?.(error);
|
|
252
|
+
});
|
|
253
|
+
}, pruneIntervalMs);
|
|
254
|
+
pruneTimer.unref?.();
|
|
255
|
+
}
|
|
256
|
+
async function prune() {
|
|
257
|
+
const currentClient = await ensureInitialized();
|
|
258
|
+
return prunePostgresSlackMessagePolicyState({
|
|
259
|
+
acceptedMessageRetentionMs,
|
|
260
|
+
acceptedMessagesTableName,
|
|
261
|
+
client: currentClient,
|
|
262
|
+
mentionedThreadRetentionMs,
|
|
263
|
+
mentionedThreadsTableName,
|
|
264
|
+
pruneBatchSize,
|
|
265
|
+
schema
|
|
266
|
+
});
|
|
267
|
+
}
|
|
268
|
+
async function claimAcceptedMessage(key, context) {
|
|
269
|
+
const currentClient = await ensureInitialized();
|
|
270
|
+
const result = await currentClient.query(
|
|
271
|
+
`INSERT INTO ${qualifiedTableName({
|
|
272
|
+
schema,
|
|
273
|
+
tableName: acceptedMessagesTableName
|
|
274
|
+
})} (
|
|
275
|
+
key,
|
|
276
|
+
team_id,
|
|
277
|
+
channel_id,
|
|
278
|
+
thread_ts,
|
|
279
|
+
message_ts,
|
|
280
|
+
user_id
|
|
281
|
+
)
|
|
282
|
+
VALUES ($1::text, $2::text, $3::text, $4::text, $5::text, $6::text)
|
|
283
|
+
ON CONFLICT (key) DO NOTHING
|
|
284
|
+
RETURNING key`,
|
|
285
|
+
[
|
|
286
|
+
key,
|
|
287
|
+
context.activity.teamId ?? null,
|
|
288
|
+
context.activity.channelId,
|
|
289
|
+
context.activity.threadTs ?? null,
|
|
290
|
+
context.activity.messageTs ?? null,
|
|
291
|
+
context.activity.userId
|
|
292
|
+
]
|
|
293
|
+
);
|
|
294
|
+
return result.rows.length > 0;
|
|
295
|
+
}
|
|
296
|
+
return {
|
|
297
|
+
async getMentionedThread(key) {
|
|
298
|
+
const currentClient = await ensureInitialized();
|
|
299
|
+
const result = await currentClient.query(
|
|
300
|
+
`SELECT original_user_id FROM ${qualifiedTableName({
|
|
301
|
+
schema,
|
|
302
|
+
tableName: mentionedThreadsTableName
|
|
303
|
+
})} WHERE key = $1::text`,
|
|
304
|
+
[key]
|
|
305
|
+
);
|
|
306
|
+
const row = result.rows[0];
|
|
307
|
+
if (!row) {
|
|
308
|
+
return void 0;
|
|
309
|
+
}
|
|
310
|
+
return row.original_user_id ? { originalUserId: row.original_user_id } : {};
|
|
311
|
+
},
|
|
312
|
+
async hasAcceptedMessage(key) {
|
|
313
|
+
const currentClient = await ensureInitialized();
|
|
314
|
+
const result = await currentClient.query(
|
|
315
|
+
`SELECT 1 FROM ${qualifiedTableName({
|
|
316
|
+
schema,
|
|
317
|
+
tableName: acceptedMessagesTableName
|
|
318
|
+
})} WHERE key = $1::text LIMIT 1`,
|
|
319
|
+
[key]
|
|
320
|
+
);
|
|
321
|
+
return result.rows.length > 0;
|
|
322
|
+
},
|
|
323
|
+
claimAcceptedMessage,
|
|
324
|
+
async rememberMentionedThread(key, state, context) {
|
|
325
|
+
const currentClient = await ensureInitialized();
|
|
326
|
+
await currentClient.query(
|
|
327
|
+
`INSERT INTO ${qualifiedTableName({
|
|
328
|
+
schema,
|
|
329
|
+
tableName: mentionedThreadsTableName
|
|
330
|
+
})} (
|
|
331
|
+
key,
|
|
332
|
+
team_id,
|
|
333
|
+
channel_id,
|
|
334
|
+
thread_ts,
|
|
335
|
+
original_user_id,
|
|
336
|
+
last_message_ts,
|
|
337
|
+
updated_at
|
|
338
|
+
)
|
|
339
|
+
VALUES ($1::text, $2::text, $3::text, $4::text, $5::text, $6::text, now())
|
|
340
|
+
ON CONFLICT (key) DO UPDATE SET
|
|
341
|
+
team_id = EXCLUDED.team_id,
|
|
342
|
+
channel_id = EXCLUDED.channel_id,
|
|
343
|
+
thread_ts = EXCLUDED.thread_ts,
|
|
344
|
+
original_user_id = EXCLUDED.original_user_id,
|
|
345
|
+
last_message_ts = EXCLUDED.last_message_ts,
|
|
346
|
+
updated_at = now()`,
|
|
347
|
+
[
|
|
348
|
+
key,
|
|
349
|
+
context.activity.teamId ?? null,
|
|
350
|
+
context.activity.channelId,
|
|
351
|
+
context.activity.threadTs ?? context.activity.messageTs ?? null,
|
|
352
|
+
state.originalUserId ?? null,
|
|
353
|
+
context.activity.messageTs ?? null
|
|
354
|
+
]
|
|
355
|
+
);
|
|
356
|
+
},
|
|
357
|
+
async rememberAcceptedMessage(key, context) {
|
|
358
|
+
await claimAcceptedMessage(key, context);
|
|
359
|
+
},
|
|
360
|
+
prune,
|
|
361
|
+
async close() {
|
|
362
|
+
if (pruneTimer) {
|
|
363
|
+
clearInterval(pruneTimer);
|
|
364
|
+
pruneTimer = void 0;
|
|
365
|
+
}
|
|
366
|
+
if (ownsClient) {
|
|
367
|
+
await activeClient?.end?.();
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
};
|
|
371
|
+
}
|
|
372
|
+
async function initializePostgresSlackMessagePolicyState({
|
|
373
|
+
acceptedMessagesTableName = DEFAULT_ACCEPTED_MESSAGES_TABLE,
|
|
374
|
+
client,
|
|
375
|
+
ensureSchema = true,
|
|
376
|
+
mentionedThreadsTableName = DEFAULT_MENTIONED_THREADS_TABLE,
|
|
377
|
+
schema
|
|
378
|
+
}) {
|
|
379
|
+
if (schema && ensureSchema) {
|
|
380
|
+
await client.query(
|
|
381
|
+
`CREATE SCHEMA IF NOT EXISTS ${quoteIdentifier(schema)}`
|
|
382
|
+
);
|
|
383
|
+
}
|
|
384
|
+
const mentionedThreadsTable = qualifiedTableName({
|
|
385
|
+
schema,
|
|
386
|
+
tableName: mentionedThreadsTableName
|
|
387
|
+
});
|
|
388
|
+
const acceptedMessagesTable = qualifiedTableName({
|
|
389
|
+
schema,
|
|
390
|
+
tableName: acceptedMessagesTableName
|
|
391
|
+
});
|
|
392
|
+
await client.query(`
|
|
393
|
+
CREATE TABLE IF NOT EXISTS ${mentionedThreadsTable} (
|
|
394
|
+
key text PRIMARY KEY,
|
|
395
|
+
team_id text,
|
|
396
|
+
channel_id text NOT NULL,
|
|
397
|
+
thread_ts text,
|
|
398
|
+
original_user_id text,
|
|
399
|
+
last_message_ts text,
|
|
400
|
+
updated_at timestamptz NOT NULL DEFAULT now()
|
|
401
|
+
)
|
|
402
|
+
`);
|
|
403
|
+
await client.query(`
|
|
404
|
+
CREATE TABLE IF NOT EXISTS ${acceptedMessagesTable} (
|
|
405
|
+
key text PRIMARY KEY,
|
|
406
|
+
team_id text,
|
|
407
|
+
channel_id text NOT NULL,
|
|
408
|
+
thread_ts text,
|
|
409
|
+
message_ts text,
|
|
410
|
+
user_id text NOT NULL,
|
|
411
|
+
created_at timestamptz NOT NULL DEFAULT now()
|
|
412
|
+
)
|
|
413
|
+
`);
|
|
414
|
+
const mentionedIndexPrefix = slackPolicyIndexPrefix({
|
|
415
|
+
schema,
|
|
416
|
+
tableName: mentionedThreadsTableName
|
|
417
|
+
});
|
|
418
|
+
const acceptedIndexPrefix = slackPolicyIndexPrefix({
|
|
419
|
+
schema,
|
|
420
|
+
tableName: acceptedMessagesTableName
|
|
421
|
+
});
|
|
422
|
+
await client.query(
|
|
423
|
+
`CREATE INDEX IF NOT EXISTS ${quoteIdentifier(
|
|
424
|
+
`${mentionedIndexPrefix}_updated_idx`
|
|
425
|
+
)} ON ${mentionedThreadsTable} (updated_at DESC)`
|
|
426
|
+
);
|
|
427
|
+
await client.query(
|
|
428
|
+
`CREATE INDEX IF NOT EXISTS ${quoteIdentifier(
|
|
429
|
+
`${mentionedIndexPrefix}_channel_thread_idx`
|
|
430
|
+
)} ON ${mentionedThreadsTable} (channel_id, thread_ts)`
|
|
431
|
+
);
|
|
432
|
+
await client.query(
|
|
433
|
+
`CREATE INDEX IF NOT EXISTS ${quoteIdentifier(
|
|
434
|
+
`${acceptedIndexPrefix}_created_idx`
|
|
435
|
+
)} ON ${acceptedMessagesTable} (created_at DESC)`
|
|
436
|
+
);
|
|
437
|
+
await client.query(
|
|
438
|
+
`CREATE INDEX IF NOT EXISTS ${quoteIdentifier(
|
|
439
|
+
`${acceptedIndexPrefix}_channel_message_idx`
|
|
440
|
+
)} ON ${acceptedMessagesTable} (channel_id, message_ts)`
|
|
441
|
+
);
|
|
442
|
+
}
|
|
443
|
+
async function prunePostgresSlackMessagePolicyState({
|
|
444
|
+
acceptedMessageRetentionMs = DEFAULT_ACCEPTED_MESSAGE_RETENTION_MS,
|
|
445
|
+
acceptedMessagesTableName = DEFAULT_ACCEPTED_MESSAGES_TABLE,
|
|
446
|
+
client,
|
|
447
|
+
mentionedThreadRetentionMs = DEFAULT_MENTIONED_THREAD_RETENTION_MS,
|
|
448
|
+
mentionedThreadsTableName = DEFAULT_MENTIONED_THREADS_TABLE,
|
|
449
|
+
pruneBatchSize = DEFAULT_PRUNE_BATCH_SIZE,
|
|
450
|
+
schema
|
|
451
|
+
}) {
|
|
452
|
+
const batchSize = Math.max(1, Math.floor(pruneBatchSize));
|
|
453
|
+
const mentionedThreadsDeleted = mentionedThreadRetentionMs > 0 ? await deleteOldRows({
|
|
454
|
+
batchSize,
|
|
455
|
+
client,
|
|
456
|
+
retentionMs: mentionedThreadRetentionMs,
|
|
457
|
+
tableName: qualifiedTableName({
|
|
458
|
+
schema,
|
|
459
|
+
tableName: mentionedThreadsTableName
|
|
460
|
+
}),
|
|
461
|
+
timestampColumn: "updated_at"
|
|
462
|
+
}) : 0;
|
|
463
|
+
const acceptedMessagesDeleted = acceptedMessageRetentionMs > 0 ? await deleteOldRows({
|
|
464
|
+
batchSize,
|
|
465
|
+
client,
|
|
466
|
+
retentionMs: acceptedMessageRetentionMs,
|
|
467
|
+
tableName: qualifiedTableName({
|
|
468
|
+
schema,
|
|
469
|
+
tableName: acceptedMessagesTableName
|
|
470
|
+
}),
|
|
471
|
+
timestampColumn: "created_at"
|
|
472
|
+
}) : 0;
|
|
473
|
+
return { acceptedMessagesDeleted, mentionedThreadsDeleted };
|
|
474
|
+
}
|
|
475
|
+
async function deleteOldRows({
|
|
476
|
+
batchSize,
|
|
477
|
+
client,
|
|
478
|
+
retentionMs,
|
|
479
|
+
tableName,
|
|
480
|
+
timestampColumn
|
|
481
|
+
}) {
|
|
482
|
+
const result = await client.query(
|
|
483
|
+
`WITH expired AS (
|
|
484
|
+
SELECT key
|
|
485
|
+
FROM ${tableName}
|
|
486
|
+
WHERE ${quoteIdentifier(timestampColumn)} < now() - ($1::bigint * interval '1 millisecond')
|
|
487
|
+
ORDER BY ${quoteIdentifier(timestampColumn)} ASC
|
|
488
|
+
LIMIT $2::integer
|
|
489
|
+
)
|
|
490
|
+
DELETE FROM ${tableName} target
|
|
491
|
+
USING expired
|
|
492
|
+
WHERE target.key = expired.key`,
|
|
493
|
+
[Math.max(1, Math.floor(retentionMs)), batchSize]
|
|
494
|
+
);
|
|
495
|
+
return result.rowCount ?? 0;
|
|
496
|
+
}
|
|
497
|
+
function qualifiedTableName({
|
|
498
|
+
schema,
|
|
499
|
+
tableName
|
|
500
|
+
}) {
|
|
501
|
+
return schema ? `${quoteIdentifier(schema)}.${quoteIdentifier(tableName)}` : quoteIdentifier(tableName);
|
|
502
|
+
}
|
|
503
|
+
function quoteIdentifier(value) {
|
|
504
|
+
return `"${value.replace(/"/g, '""')}"`;
|
|
505
|
+
}
|
|
506
|
+
function slackPolicyIndexPrefix({
|
|
507
|
+
schema,
|
|
508
|
+
tableName
|
|
509
|
+
}) {
|
|
510
|
+
const raw = [schema, tableName].filter(Boolean).join("_");
|
|
511
|
+
return raw.replace(/[^A-Za-z0-9_]+/g, "_").replace(/^_+|_+$/g, "").slice(0, 40) || "channel_slack_policy";
|
|
512
|
+
}
|
|
513
|
+
async function importPostgresPoolConstructor() {
|
|
514
|
+
const dynamicImport = new Function(
|
|
515
|
+
"specifier",
|
|
516
|
+
"return import(specifier)"
|
|
517
|
+
);
|
|
518
|
+
try {
|
|
519
|
+
const pg = await dynamicImport("pg");
|
|
520
|
+
return pg.Pool;
|
|
521
|
+
} catch (error) {
|
|
522
|
+
throw new Error(
|
|
523
|
+
`The "pg" package is required when using connectionString with createPostgresSlackMessagePolicyStateStore. Install pg or pass a client. ${formatImportError(
|
|
524
|
+
error
|
|
525
|
+
)}`
|
|
526
|
+
);
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
function formatImportError(error) {
|
|
530
|
+
return error instanceof Error ? error.message : String(error);
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
// src/policy/message/passive.ts
|
|
534
|
+
function shouldRegisterSlackPassiveChannelMessages(config = {}) {
|
|
535
|
+
const messagePolicy = config.messagePolicy ?? "disabled";
|
|
536
|
+
const threadReplyPolicy = config.threadReplyPolicy ?? "mention-required";
|
|
537
|
+
if (messagePolicy === "disabled") {
|
|
538
|
+
return false;
|
|
539
|
+
}
|
|
540
|
+
if (messagePolicy === "mentioned-threads" && threadReplyPolicy === "mention-required") {
|
|
541
|
+
return false;
|
|
542
|
+
}
|
|
543
|
+
if (messagePolicy === "allowed-channels") {
|
|
544
|
+
return Boolean(config.allowedChannelIds?.length);
|
|
545
|
+
}
|
|
546
|
+
return true;
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
// src/policy/message/resolver.ts
|
|
550
|
+
function createSlackMessagePolicyResolver(config = {}) {
|
|
551
|
+
const allowedChannelIds = new Set(config.allowedChannelIds ?? []);
|
|
552
|
+
const messagePolicy = config.messagePolicy ?? "disabled";
|
|
553
|
+
const threadReplyPolicy = config.threadReplyPolicy ?? "mention-required";
|
|
554
|
+
const stateStore = config.stateStore ?? createInMemorySlackMessagePolicyStateStore({
|
|
555
|
+
maxAcceptedMessages: config.maxAcceptedMessages,
|
|
556
|
+
maxMentionedThreads: config.maxMentionedThreads
|
|
557
|
+
});
|
|
558
|
+
function rememberMentionedThread(activity) {
|
|
559
|
+
const key = createSlackMessagePolicyThreadKey(activity);
|
|
560
|
+
const originalUserId = activity.parentUserId ?? activity.userId;
|
|
561
|
+
stateStore.rememberMentionedThread(
|
|
562
|
+
key,
|
|
563
|
+
{ originalUserId },
|
|
564
|
+
{
|
|
565
|
+
activity,
|
|
566
|
+
key
|
|
567
|
+
}
|
|
568
|
+
);
|
|
569
|
+
}
|
|
570
|
+
function claimAcceptedMessage(activity) {
|
|
571
|
+
const key = createSlackMessagePolicyMessageKey(activity);
|
|
572
|
+
if (!key) {
|
|
573
|
+
return true;
|
|
574
|
+
}
|
|
575
|
+
const context = { activity, key };
|
|
576
|
+
if (stateStore.claimAcceptedMessage) {
|
|
577
|
+
return stateStore.claimAcceptedMessage(key, context);
|
|
578
|
+
}
|
|
579
|
+
if (stateStore.hasAcceptedMessage(key, context)) {
|
|
580
|
+
return false;
|
|
581
|
+
}
|
|
582
|
+
stateStore.rememberAcceptedMessage(key, context);
|
|
583
|
+
return true;
|
|
584
|
+
}
|
|
585
|
+
function isDuplicate(activity) {
|
|
586
|
+
const key = createSlackMessagePolicyMessageKey(activity);
|
|
587
|
+
return Boolean(
|
|
588
|
+
key && stateStore.hasAcceptedMessage(key, { activity, key })
|
|
589
|
+
);
|
|
590
|
+
}
|
|
591
|
+
function accept(activity, reason) {
|
|
592
|
+
if (!claimAcceptedMessage(activity)) {
|
|
593
|
+
return reject(activity, "duplicate-message-event");
|
|
594
|
+
}
|
|
595
|
+
return {
|
|
596
|
+
accepted: true,
|
|
597
|
+
activity,
|
|
598
|
+
reason,
|
|
599
|
+
text: activity.text
|
|
600
|
+
};
|
|
601
|
+
}
|
|
602
|
+
function reject(activity, reason) {
|
|
603
|
+
return {
|
|
604
|
+
accepted: false,
|
|
605
|
+
activity,
|
|
606
|
+
reason
|
|
607
|
+
};
|
|
608
|
+
}
|
|
609
|
+
function resolve(activity) {
|
|
610
|
+
if (activity.isMention) {
|
|
611
|
+
rememberMentionedThread(activity);
|
|
612
|
+
return accept(activity, "bot-mentioned");
|
|
613
|
+
}
|
|
614
|
+
if (isDuplicate(activity)) {
|
|
615
|
+
return reject(activity, "duplicate-message-event");
|
|
616
|
+
}
|
|
617
|
+
if (activity.channelType === "dm") {
|
|
618
|
+
return accept(activity, "direct-message");
|
|
619
|
+
}
|
|
620
|
+
if (messagePolicy === "disabled") {
|
|
621
|
+
return reject(activity, "passive-channel-messages-disabled");
|
|
622
|
+
}
|
|
623
|
+
if (messagePolicy === "any-added-channel") {
|
|
624
|
+
return accept(activity, "any-added-channel");
|
|
625
|
+
}
|
|
626
|
+
if (messagePolicy === "allowed-channels") {
|
|
627
|
+
return allowedChannelIds.has(activity.channelId) ? accept(activity, "allowed-channel") : reject(activity, "channel-not-allowed");
|
|
628
|
+
}
|
|
629
|
+
const threadKey = createSlackMessagePolicyThreadKey(activity);
|
|
630
|
+
const threadState = stateStore.getMentionedThread(threadKey, {
|
|
631
|
+
activity,
|
|
632
|
+
key: threadKey
|
|
633
|
+
});
|
|
634
|
+
if (!threadState) {
|
|
635
|
+
return reject(activity, "thread-not-mentioned");
|
|
636
|
+
}
|
|
637
|
+
if (threadReplyPolicy === "mention-required") {
|
|
638
|
+
return reject(activity, "thread-reply-mention-required");
|
|
639
|
+
}
|
|
640
|
+
if (threadReplyPolicy === "original-user" && threadState.originalUserId && activity.userId !== threadState.originalUserId) {
|
|
641
|
+
return reject(activity, "thread-reply-not-original-user");
|
|
642
|
+
}
|
|
643
|
+
return accept(activity, "mentioned-thread-reply");
|
|
644
|
+
}
|
|
645
|
+
return {
|
|
646
|
+
resolve,
|
|
647
|
+
resolveMessage(activity) {
|
|
648
|
+
const decision = resolve(activity);
|
|
649
|
+
return decision.accepted ? decision.text : void 0;
|
|
650
|
+
}
|
|
651
|
+
};
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
export {
|
|
655
|
+
createSlackMessagePolicyMessageKey,
|
|
656
|
+
createSlackMessagePolicyThreadKey,
|
|
657
|
+
createInMemorySlackMessagePolicyStateStore,
|
|
658
|
+
createAsyncSlackMessagePolicyResolver,
|
|
659
|
+
createPostgresSlackMessagePolicyStateStore,
|
|
660
|
+
initializePostgresSlackMessagePolicyState,
|
|
661
|
+
prunePostgresSlackMessagePolicyState,
|
|
662
|
+
shouldRegisterSlackPassiveChannelMessages,
|
|
663
|
+
createSlackMessagePolicyResolver
|
|
664
|
+
};
|