niahere 0.3.2 → 0.3.3
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/package.json +1 -1
- package/src/channels/slack/attachments.ts +142 -0
- package/src/channels/slack/watch.ts +73 -0
- package/src/channels/slack.ts +10 -194
- package/src/channels/telegram.ts +214 -205
- package/src/mcp/tools/index.ts +9 -0
- package/src/mcp/tools/jobs.ts +145 -0
- package/src/mcp/tools/messages.ts +25 -0
- package/src/mcp/tools/misc.ts +63 -0
- package/src/mcp/tools/send.ts +202 -0
- package/src/mcp/tools/watch.ts +50 -0
- package/src/mcp/tools.ts +0 -481
package/package.json
CHANGED
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Disk-backed cache for Slack file attachments. Slack file URLs expire and
|
|
3
|
+
* require Authorization on download, so we fetch once per (scope, url),
|
|
4
|
+
* write the bytes + metadata to `~/.niahere/tmp/attachments/<scope>/`, and
|
|
5
|
+
* read from disk on subsequent references. Survives daemon restarts via
|
|
6
|
+
* the metadata sidecar.
|
|
7
|
+
*/
|
|
8
|
+
import { createHash } from "crypto";
|
|
9
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "fs";
|
|
10
|
+
import { join } from "path";
|
|
11
|
+
import type { Attachment, AttachmentType } from "../../types";
|
|
12
|
+
import { classifyMime, prepareImage, validateAttachment } from "../../utils/attachment";
|
|
13
|
+
import { getNiaHome } from "../../utils/paths";
|
|
14
|
+
import { log } from "../../utils/log";
|
|
15
|
+
|
|
16
|
+
interface CachedFile {
|
|
17
|
+
path: string;
|
|
18
|
+
type: AttachmentType;
|
|
19
|
+
mimeType: string;
|
|
20
|
+
filename?: string;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function urlHash(url: string): string {
|
|
24
|
+
return createHash("sha256").update(url).digest("hex").slice(0, 16);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function safeExtension(filename?: string): string {
|
|
28
|
+
const ext = filename?.split(".").pop();
|
|
29
|
+
return ext && /^[a-zA-Z0-9]{1,16}$/.test(ext) ? ext : "bin";
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function cacheExtension(filename: string | undefined, mime: string, attType: AttachmentType): string {
|
|
33
|
+
if (attType === "image" && mime !== "image/gif") return "jpg";
|
|
34
|
+
return safeExtension(filename);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function loadCached(entry: CachedFile): Attachment {
|
|
38
|
+
return {
|
|
39
|
+
type: entry.type,
|
|
40
|
+
data: readFileSync(entry.path),
|
|
41
|
+
mimeType: entry.mimeType,
|
|
42
|
+
filename: entry.filename,
|
|
43
|
+
sourcePath: entry.path,
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export class SlackAttachmentCache {
|
|
48
|
+
private readonly attachRoot: string;
|
|
49
|
+
private readonly fileIndex = new Map<string, CachedFile>();
|
|
50
|
+
|
|
51
|
+
constructor(private readonly botToken: string) {
|
|
52
|
+
this.attachRoot = join(getNiaHome(), "tmp", "attachments");
|
|
53
|
+
mkdirSync(this.attachRoot, { recursive: true });
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
async extract(files: any[], scope: string): Promise<Attachment[]> {
|
|
57
|
+
const attachments: Attachment[] = [];
|
|
58
|
+
const scopedDir = this.dirForScope(scope);
|
|
59
|
+
|
|
60
|
+
for (const file of files) {
|
|
61
|
+
const mime = file.mimetype || "application/octet-stream";
|
|
62
|
+
const attType = classifyMime(mime);
|
|
63
|
+
if (!attType) continue;
|
|
64
|
+
if (!file.url_private_download) continue;
|
|
65
|
+
|
|
66
|
+
const indexedKey = `${scope}:${file.url_private_download}`;
|
|
67
|
+
const cached = this.fileIndex.get(indexedKey);
|
|
68
|
+
if (cached && existsSync(cached.path)) {
|
|
69
|
+
attachments.push(loadCached(cached));
|
|
70
|
+
continue;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const hash = urlHash(file.url_private_download);
|
|
74
|
+
const ext = cacheExtension(file.name, mime, attType);
|
|
75
|
+
const diskPath = join(scopedDir, `${hash}.${ext}`);
|
|
76
|
+
const metaPath = join(scopedDir, `${hash}.meta.json`);
|
|
77
|
+
|
|
78
|
+
// Re-load from disk if a prior daemon run already wrote this file.
|
|
79
|
+
if (existsSync(diskPath) && existsSync(metaPath)) {
|
|
80
|
+
try {
|
|
81
|
+
const meta = JSON.parse(readFileSync(metaPath, "utf8"));
|
|
82
|
+
const entry: CachedFile = {
|
|
83
|
+
path: diskPath,
|
|
84
|
+
type: meta.type || attType,
|
|
85
|
+
mimeType: meta.mimeType || mime,
|
|
86
|
+
filename: meta.filename || file.name,
|
|
87
|
+
};
|
|
88
|
+
this.fileIndex.set(indexedKey, entry);
|
|
89
|
+
attachments.push(loadCached(entry));
|
|
90
|
+
continue;
|
|
91
|
+
} catch {
|
|
92
|
+
// Corrupt meta — re-download.
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
try {
|
|
97
|
+
const raw = await this.download(file.url_private_download);
|
|
98
|
+
const error = validateAttachment(raw);
|
|
99
|
+
if (error) {
|
|
100
|
+
log.warn({ file: file.name, error }, "skipping slack attachment");
|
|
101
|
+
continue;
|
|
102
|
+
}
|
|
103
|
+
let data = raw;
|
|
104
|
+
let finalMime = mime;
|
|
105
|
+
if (attType === "image") {
|
|
106
|
+
const prepared = await prepareImage(raw, mime);
|
|
107
|
+
data = prepared.data;
|
|
108
|
+
finalMime = prepared.mimeType;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
writeFileSync(diskPath, data);
|
|
112
|
+
writeFileSync(metaPath, JSON.stringify({ type: attType, mimeType: finalMime, filename: file.name }));
|
|
113
|
+
const entry: CachedFile = { path: diskPath, type: attType, mimeType: finalMime, filename: file.name };
|
|
114
|
+
this.fileIndex.set(indexedKey, entry);
|
|
115
|
+
|
|
116
|
+
attachments.push({
|
|
117
|
+
type: attType,
|
|
118
|
+
data,
|
|
119
|
+
mimeType: finalMime,
|
|
120
|
+
filename: file.name,
|
|
121
|
+
sourcePath: diskPath,
|
|
122
|
+
});
|
|
123
|
+
} catch (err) {
|
|
124
|
+
log.warn({ err, file: file.name }, "failed to download slack file");
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
return attachments;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
private dirForScope(scope: string): string {
|
|
131
|
+
const safeScope = scope.replace(/[^a-zA-Z0-9._-]/g, "_");
|
|
132
|
+
const dir = join(this.attachRoot, safeScope);
|
|
133
|
+
mkdirSync(dir, { recursive: true });
|
|
134
|
+
return dir;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
private async download(url: string): Promise<Buffer> {
|
|
138
|
+
const resp = await fetch(url, { headers: { Authorization: `Bearer ${this.botToken}` } });
|
|
139
|
+
if (!resp.ok) throw new Error(`Slack file download failed: ${resp.status}`);
|
|
140
|
+
return Buffer.from(await resp.arrayBuffer());
|
|
141
|
+
}
|
|
142
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Slack watch channels: hot-reloads `channels.slack.watch` entries from
|
|
3
|
+
* config.yaml (plus any behavior files they reference) on each inbound
|
|
4
|
+
* message, gated by an mtime check so we don't re-parse config on every
|
|
5
|
+
* call. Keyed by `channel_id` (the part before `#` in the config key).
|
|
6
|
+
*/
|
|
7
|
+
import { statSync } from "fs";
|
|
8
|
+
import { getConfig, resetConfig } from "../../utils/config";
|
|
9
|
+
import { getPaths } from "../../utils/paths";
|
|
10
|
+
import { log } from "../../utils/log";
|
|
11
|
+
import { resolveWatchBehavior } from "../../utils/watches";
|
|
12
|
+
|
|
13
|
+
export interface WatchEntry {
|
|
14
|
+
name: string;
|
|
15
|
+
behavior: string;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function maxMtime(paths: string[]): number {
|
|
19
|
+
let max = 0;
|
|
20
|
+
for (const p of paths) {
|
|
21
|
+
try {
|
|
22
|
+
const m = statSync(p).mtimeMs;
|
|
23
|
+
if (m > max) max = m;
|
|
24
|
+
} catch {
|
|
25
|
+
// missing file — ignore
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
return max;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export class SlackWatchReloader {
|
|
32
|
+
private cache = new Map<string, WatchEntry>();
|
|
33
|
+
private filePaths: string[] = [];
|
|
34
|
+
private lastReloadMtime = 0;
|
|
35
|
+
|
|
36
|
+
/** Re-parse config + behavior files if any have been modified since the last read. */
|
|
37
|
+
reload(): Map<string, WatchEntry> {
|
|
38
|
+
const configPath = getPaths().config;
|
|
39
|
+
const mtime = maxMtime([configPath, ...this.filePaths]);
|
|
40
|
+
if (mtime === 0) return this.cache;
|
|
41
|
+
if (mtime === this.lastReloadMtime) return this.cache;
|
|
42
|
+
|
|
43
|
+
resetConfig();
|
|
44
|
+
const watch = getConfig().channels.slack.watch;
|
|
45
|
+
const fresh = new Map<string, WatchEntry>();
|
|
46
|
+
const freshFiles: string[] = [];
|
|
47
|
+
|
|
48
|
+
if (watch) {
|
|
49
|
+
for (const [key, entry] of Object.entries(watch)) {
|
|
50
|
+
if (!entry.enabled) continue;
|
|
51
|
+
const hashIdx = key.indexOf("#");
|
|
52
|
+
if (hashIdx === -1) {
|
|
53
|
+
log.warn({ channel: key }, "slack: watch key must use channel_id#name format, skipping");
|
|
54
|
+
continue;
|
|
55
|
+
}
|
|
56
|
+
const id = key.slice(0, hashIdx);
|
|
57
|
+
const name = key.slice(hashIdx + 1);
|
|
58
|
+
const resolved = resolveWatchBehavior(entry.behavior, name);
|
|
59
|
+
if (resolved.filePath) freshFiles.push(resolved.filePath);
|
|
60
|
+
fresh.set(id, { name, behavior: resolved.behavior });
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
if (fresh.size !== this.cache.size) {
|
|
65
|
+
log.info({ count: fresh.size }, "slack: watch channels reloaded");
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
this.cache = fresh;
|
|
69
|
+
this.filePaths = freshFiles;
|
|
70
|
+
this.lastReloadMtime = maxMtime([configPath, ...freshFiles]);
|
|
71
|
+
return this.cache;
|
|
72
|
+
}
|
|
73
|
+
}
|
package/src/channels/slack.ts
CHANGED
|
@@ -1,18 +1,14 @@
|
|
|
1
1
|
import { App } from "@slack/bolt";
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import { createHash } from "crypto";
|
|
5
|
-
import type { Channel, ChatState, Attachment, AttachmentType, Outbound, Recipient } from "../types";
|
|
6
|
-
import { getConfig, updateRawConfig, resetConfig } from "../utils/config";
|
|
2
|
+
import type { Channel, ChatState, Attachment, Outbound, Recipient } from "../types";
|
|
3
|
+
import { getConfig, updateRawConfig } from "../utils/config";
|
|
7
4
|
import { relativeTime } from "../utils/format";
|
|
8
5
|
import { runMigrations } from "../db/migrate";
|
|
9
6
|
import { Session, Message } from "../db/models";
|
|
10
7
|
import { log } from "../utils/log";
|
|
11
8
|
import { getMcpServers } from "../mcp";
|
|
12
|
-
import { getNiaHome, getPaths } from "../utils/paths";
|
|
13
|
-
import { classifyMime, validateAttachment, prepareImage } from "../utils/attachment";
|
|
14
|
-
import { resolveWatchBehavior } from "../utils/watches";
|
|
15
9
|
import { chainLock, openChatEngine, rotateRoom } from "./common/chat-session";
|
|
10
|
+
import { SlackAttachmentCache } from "./slack/attachments";
|
|
11
|
+
import { SlackWatchReloader } from "./slack/watch";
|
|
16
12
|
|
|
17
13
|
/** Strip markdown backticks so sentinel tokens like [NO_REPLY] match even when the LLM wraps them. */
|
|
18
14
|
function cleanSentinel(text: string): string {
|
|
@@ -141,59 +137,8 @@ class SlackChannel implements Channel {
|
|
|
141
137
|
|
|
142
138
|
let botUserId: string | undefined;
|
|
143
139
|
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
let watchCache: Map<string, { name: string; behavior: string }> = new Map();
|
|
147
|
-
let watchFilePaths: string[] = [];
|
|
148
|
-
let lastReloadMtime = 0;
|
|
149
|
-
|
|
150
|
-
function maxMtime(paths: string[]): number {
|
|
151
|
-
let max = 0;
|
|
152
|
-
for (const p of paths) {
|
|
153
|
-
try {
|
|
154
|
-
const m = statSync(p).mtimeMs;
|
|
155
|
-
if (m > max) max = m;
|
|
156
|
-
} catch {
|
|
157
|
-
// ignore missing files
|
|
158
|
-
}
|
|
159
|
-
}
|
|
160
|
-
return max;
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
function reloadWatchChannels(): Map<string, { name: string; behavior: string }> {
|
|
164
|
-
const configPath = getPaths().config;
|
|
165
|
-
const mtime = maxMtime([configPath, ...watchFilePaths]);
|
|
166
|
-
if (mtime === 0) return watchCache;
|
|
167
|
-
if (mtime === lastReloadMtime) return watchCache;
|
|
168
|
-
|
|
169
|
-
resetConfig(); // clear cached config so getConfig() re-reads from disk
|
|
170
|
-
const cfg = getConfig();
|
|
171
|
-
const watch = cfg.channels.slack.watch;
|
|
172
|
-
const fresh = new Map<string, { name: string; behavior: string }>();
|
|
173
|
-
const freshFiles: string[] = [];
|
|
174
|
-
if (watch) {
|
|
175
|
-
for (const [key, entry] of Object.entries(watch)) {
|
|
176
|
-
if (!entry.enabled) continue;
|
|
177
|
-
const hashIdx = key.indexOf("#");
|
|
178
|
-
if (hashIdx === -1) {
|
|
179
|
-
log.warn({ channel: key }, "slack: watch key must use channel_id#name format, skipping");
|
|
180
|
-
continue;
|
|
181
|
-
}
|
|
182
|
-
const id = key.slice(0, hashIdx);
|
|
183
|
-
const name = key.slice(hashIdx + 1);
|
|
184
|
-
const resolved = resolveWatchBehavior(entry.behavior, name);
|
|
185
|
-
if (resolved.filePath) freshFiles.push(resolved.filePath);
|
|
186
|
-
fresh.set(id, { name, behavior: resolved.behavior });
|
|
187
|
-
}
|
|
188
|
-
}
|
|
189
|
-
if (fresh.size !== watchCache.size) {
|
|
190
|
-
log.info({ count: fresh.size }, "slack: watch channels reloaded");
|
|
191
|
-
}
|
|
192
|
-
watchCache = fresh;
|
|
193
|
-
watchFilePaths = freshFiles;
|
|
194
|
-
lastReloadMtime = maxMtime([configPath, ...freshFiles]);
|
|
195
|
-
return watchCache;
|
|
196
|
-
}
|
|
140
|
+
const watchReloader = new SlackWatchReloader();
|
|
141
|
+
const attachmentCache = new SlackAttachmentCache(botToken);
|
|
197
142
|
|
|
198
143
|
// Slash command: /nia
|
|
199
144
|
app.command("/nia", async ({ command, ack, respond }) => {
|
|
@@ -242,135 +187,6 @@ class SlackChannel implements Channel {
|
|
|
242
187
|
await respond("New conversation started.");
|
|
243
188
|
});
|
|
244
189
|
|
|
245
|
-
// Disk-backed file cache: download once, read from disk on subsequent requests
|
|
246
|
-
const attachRoot = join(getNiaHome(), "tmp", "attachments");
|
|
247
|
-
mkdirSync(attachRoot, { recursive: true });
|
|
248
|
-
|
|
249
|
-
interface CachedFile {
|
|
250
|
-
path: string;
|
|
251
|
-
type: AttachmentType;
|
|
252
|
-
mimeType: string;
|
|
253
|
-
filename?: string;
|
|
254
|
-
}
|
|
255
|
-
const fileIndex = new Map<string, CachedFile>();
|
|
256
|
-
|
|
257
|
-
function urlHash(url: string): string {
|
|
258
|
-
return createHash("sha256").update(url).digest("hex").slice(0, 16);
|
|
259
|
-
}
|
|
260
|
-
|
|
261
|
-
function loadCached(entry: CachedFile): Attachment {
|
|
262
|
-
return {
|
|
263
|
-
type: entry.type,
|
|
264
|
-
data: readFileSync(entry.path),
|
|
265
|
-
mimeType: entry.mimeType,
|
|
266
|
-
filename: entry.filename,
|
|
267
|
-
sourcePath: entry.path,
|
|
268
|
-
};
|
|
269
|
-
}
|
|
270
|
-
|
|
271
|
-
async function downloadSlackFile(url: string): Promise<Buffer> {
|
|
272
|
-
const resp = await fetch(url, {
|
|
273
|
-
headers: { Authorization: `Bearer ${botToken}` },
|
|
274
|
-
});
|
|
275
|
-
if (!resp.ok) throw new Error(`Slack file download failed: ${resp.status}`);
|
|
276
|
-
return Buffer.from(await resp.arrayBuffer());
|
|
277
|
-
}
|
|
278
|
-
|
|
279
|
-
function cacheDirForScope(scope: string): string {
|
|
280
|
-
const safeScope = scope.replace(/[^a-zA-Z0-9._-]/g, "_");
|
|
281
|
-
const dir = join(attachRoot, safeScope);
|
|
282
|
-
mkdirSync(dir, { recursive: true });
|
|
283
|
-
return dir;
|
|
284
|
-
}
|
|
285
|
-
|
|
286
|
-
function cacheKey(scope: string, url: string): string {
|
|
287
|
-
return `${scope}:${url}`;
|
|
288
|
-
}
|
|
289
|
-
|
|
290
|
-
function safeExtension(filename?: string): string {
|
|
291
|
-
const ext = filename?.split(".").pop();
|
|
292
|
-
return ext && /^[a-zA-Z0-9]{1,16}$/.test(ext) ? ext : "bin";
|
|
293
|
-
}
|
|
294
|
-
|
|
295
|
-
function cacheExtension(filename: string | undefined, mime: string, attType: AttachmentType): string {
|
|
296
|
-
if (attType === "image" && mime !== "image/gif") return "jpg";
|
|
297
|
-
return safeExtension(filename);
|
|
298
|
-
}
|
|
299
|
-
|
|
300
|
-
async function extractSlackAttachments(files: any[], scope: string): Promise<Attachment[]> {
|
|
301
|
-
const attachments: Attachment[] = [];
|
|
302
|
-
const scopedAttachDir = cacheDirForScope(scope);
|
|
303
|
-
for (const file of files) {
|
|
304
|
-
const mime = file.mimetype || "application/octet-stream";
|
|
305
|
-
const attType = classifyMime(mime);
|
|
306
|
-
if (!attType) continue;
|
|
307
|
-
if (!file.url_private_download) continue;
|
|
308
|
-
|
|
309
|
-
// Check in-memory index first
|
|
310
|
-
const indexedKey = cacheKey(scope, file.url_private_download);
|
|
311
|
-
const cached = fileIndex.get(indexedKey);
|
|
312
|
-
if (cached && existsSync(cached.path)) {
|
|
313
|
-
attachments.push(loadCached(cached));
|
|
314
|
-
continue;
|
|
315
|
-
}
|
|
316
|
-
|
|
317
|
-
// Check disk (survives daemon restarts) — scoped by Slack room/thread.
|
|
318
|
-
const hash = urlHash(file.url_private_download);
|
|
319
|
-
const ext = cacheExtension(file.name, mime, attType);
|
|
320
|
-
const diskPath = join(scopedAttachDir, `${hash}.${ext}`);
|
|
321
|
-
const metaPath = join(scopedAttachDir, `${hash}.meta.json`);
|
|
322
|
-
if (existsSync(diskPath) && existsSync(metaPath)) {
|
|
323
|
-
try {
|
|
324
|
-
const meta = JSON.parse(readFileSync(metaPath, "utf8"));
|
|
325
|
-
const entry: CachedFile = {
|
|
326
|
-
path: diskPath,
|
|
327
|
-
type: meta.type || attType,
|
|
328
|
-
mimeType: meta.mimeType || mime,
|
|
329
|
-
filename: meta.filename || file.name,
|
|
330
|
-
};
|
|
331
|
-
fileIndex.set(indexedKey, entry);
|
|
332
|
-
attachments.push(loadCached(entry));
|
|
333
|
-
continue;
|
|
334
|
-
} catch {
|
|
335
|
-
// Corrupt meta — re-download
|
|
336
|
-
}
|
|
337
|
-
}
|
|
338
|
-
|
|
339
|
-
try {
|
|
340
|
-
const data = await downloadSlackFile(file.url_private_download);
|
|
341
|
-
const error = validateAttachment(data);
|
|
342
|
-
if (error) {
|
|
343
|
-
log.warn({ file: file.name, error }, "skipping slack attachment");
|
|
344
|
-
continue;
|
|
345
|
-
}
|
|
346
|
-
let finalData = data;
|
|
347
|
-
let finalMime = mime;
|
|
348
|
-
if (attType === "image") {
|
|
349
|
-
const prepared = await prepareImage(data, mime);
|
|
350
|
-
finalData = prepared.data;
|
|
351
|
-
finalMime = prepared.mimeType;
|
|
352
|
-
}
|
|
353
|
-
|
|
354
|
-
// Save file + metadata to disk
|
|
355
|
-
writeFileSync(diskPath, finalData);
|
|
356
|
-
writeFileSync(metaPath, JSON.stringify({ type: attType, mimeType: finalMime, filename: file.name }));
|
|
357
|
-
const entry: CachedFile = { path: diskPath, type: attType, mimeType: finalMime, filename: file.name };
|
|
358
|
-
fileIndex.set(indexedKey, entry);
|
|
359
|
-
|
|
360
|
-
attachments.push({
|
|
361
|
-
type: attType,
|
|
362
|
-
data: finalData,
|
|
363
|
-
mimeType: finalMime,
|
|
364
|
-
filename: file.name,
|
|
365
|
-
sourcePath: diskPath,
|
|
366
|
-
});
|
|
367
|
-
} catch (err) {
|
|
368
|
-
log.warn({ err, file: file.name }, "failed to download slack file");
|
|
369
|
-
}
|
|
370
|
-
}
|
|
371
|
-
return attachments;
|
|
372
|
-
}
|
|
373
|
-
|
|
374
190
|
// Handle messages (DMs + @mentions)
|
|
375
191
|
app.message(async ({ message, say, client }) => {
|
|
376
192
|
if (message.subtype && message.subtype !== "file_share") return;
|
|
@@ -435,7 +251,7 @@ class SlackChannel implements Channel {
|
|
|
435
251
|
}
|
|
436
252
|
|
|
437
253
|
// Check if this is a watched channel (hot-reloads from config.yaml via mtime)
|
|
438
|
-
const currentWatch =
|
|
254
|
+
const currentWatch = watchReloader.reload();
|
|
439
255
|
const watchConfig = currentWatch.get(msg.channel);
|
|
440
256
|
const isWatched = !!watchConfig;
|
|
441
257
|
|
|
@@ -497,7 +313,7 @@ class SlackChannel implements Channel {
|
|
|
497
313
|
// Download any file attachments
|
|
498
314
|
let attachments: Attachment[] | undefined;
|
|
499
315
|
if (hasFiles) {
|
|
500
|
-
attachments = await
|
|
316
|
+
attachments = await attachmentCache.extract(msg.files!, roomPrefix(key));
|
|
501
317
|
}
|
|
502
318
|
|
|
503
319
|
if (!text && (!attachments || attachments.length === 0)) return;
|
|
@@ -534,7 +350,7 @@ class SlackChannel implements Channel {
|
|
|
534
350
|
const messagesWithFiles = priorMessages.filter((m: any) => m.files?.length > 0);
|
|
535
351
|
let threadFilesAdded = 0;
|
|
536
352
|
for (const m of messagesWithFiles) {
|
|
537
|
-
const extracted = await
|
|
353
|
+
const extracted = await attachmentCache.extract(m.files || [], roomPrefix(key));
|
|
538
354
|
for (const att of extracted) {
|
|
539
355
|
attachments.push(att);
|
|
540
356
|
threadFilesAdded++;
|
|
@@ -673,7 +489,7 @@ class SlackChannel implements Channel {
|
|
|
673
489
|
}
|
|
674
490
|
|
|
675
491
|
// Initial watch channel load
|
|
676
|
-
|
|
492
|
+
watchReloader.reload();
|
|
677
493
|
|
|
678
494
|
log.info("slack bot started (Socket Mode)");
|
|
679
495
|
this.app = app;
|