eniac-slack 0.1.14 → 0.1.16
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 +10 -0
- package/dist/utils/slack-files.d.ts +4 -2
- package/dist/utils/slack-files.d.ts.map +1 -1
- package/dist/utils/slack-files.js +99 -58
- package/dist/utils/slack-files.js.map +1 -1
- package/package.json +1 -1
- package/src/utils/slack-files.ts +116 -59
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,15 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.1.16] - 2026-03-12
|
|
4
|
+
|
|
5
|
+
- fix(slack): Slack 파일 다운로드 인증 실패 - redirect 수동 처리 (#25)
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
## [0.1.15] - 2026-03-12
|
|
9
|
+
|
|
10
|
+
- fix(slack): 이미지 로깅 강화 + magic bytes 불일치 허용 (#24)
|
|
11
|
+
|
|
12
|
+
|
|
3
13
|
## [0.1.14] - 2026-03-12
|
|
4
14
|
|
|
5
15
|
- fix(slack): sharp 정규화 + AsyncIterable로 이미지 직접 전달 (#23)
|
|
@@ -5,12 +5,14 @@ export interface SlackImageFile {
|
|
|
5
5
|
}
|
|
6
6
|
/**
|
|
7
7
|
* Download image files from a Slack message's `files` array.
|
|
8
|
-
*
|
|
9
|
-
* Only images with supported media types are returned; non-image files are skipped.
|
|
10
8
|
*/
|
|
11
9
|
export declare function downloadSlackImages(client: WebClient, files: Array<{
|
|
12
10
|
url_private_download?: string;
|
|
13
11
|
url_private?: string;
|
|
14
12
|
mimetype?: string;
|
|
13
|
+
name?: string;
|
|
14
|
+
filetype?: string;
|
|
15
|
+
size?: number;
|
|
16
|
+
id?: string;
|
|
15
17
|
}>): Promise<SlackImageFile[]>;
|
|
16
18
|
//# sourceMappingURL=slack-files.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"slack-files.d.ts","sourceRoot":"","sources":["../../src/utils/slack-files.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,gBAAgB,CAAC;AAGhD,MAAM,WAAW,cAAc;IAC7B,MAAM,EAAE,MAAM,CAAC;IACf,SAAS,EAAE,YAAY,GAAG,WAAW,GAAG,WAAW,GAAG,YAAY,CAAC;CACpE;
|
|
1
|
+
{"version":3,"file":"slack-files.d.ts","sourceRoot":"","sources":["../../src/utils/slack-files.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,gBAAgB,CAAC;AAGhD,MAAM,WAAW,cAAc;IAC7B,MAAM,EAAE,MAAM,CAAC;IACf,SAAS,EAAE,YAAY,GAAG,WAAW,GAAG,WAAW,GAAG,YAAY,CAAC;CACpE;AAmED;;GAEG;AACH,wBAAsB,mBAAmB,CACvC,MAAM,EAAE,SAAS,EACjB,KAAK,EAAE,KAAK,CAAC;IACX,oBAAoB,CAAC,EAAE,MAAM,CAAC;IAC9B,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,EAAE,CAAC,EAAE,MAAM,CAAC;CACb,CAAC,GACD,OAAO,CAAC,cAAc,EAAE,CAAC,CAkF3B"}
|
|
@@ -5,94 +5,135 @@ const SUPPORTED_TYPES = new Set([
|
|
|
5
5
|
"image/gif",
|
|
6
6
|
"image/webp",
|
|
7
7
|
]);
|
|
8
|
+
const MAX_IMAGE_DIMENSION = 1568; // Claude API recommended max
|
|
8
9
|
/**
|
|
9
|
-
*
|
|
10
|
+
* Detect actual image format from magic bytes.
|
|
10
11
|
*/
|
|
11
|
-
function
|
|
12
|
-
if (buf.length <
|
|
13
|
-
return
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
default:
|
|
25
|
-
return false;
|
|
26
|
-
}
|
|
12
|
+
function detectImageFormat(buf) {
|
|
13
|
+
if (buf.length < 12)
|
|
14
|
+
return null;
|
|
15
|
+
if (buf[0] === 0x89 && buf[1] === 0x50 && buf[2] === 0x4e && buf[3] === 0x47)
|
|
16
|
+
return "image/png";
|
|
17
|
+
if (buf[0] === 0xff && buf[1] === 0xd8 && buf[2] === 0xff)
|
|
18
|
+
return "image/jpeg";
|
|
19
|
+
if (buf[0] === 0x47 && buf[1] === 0x49 && buf[2] === 0x46)
|
|
20
|
+
return "image/gif";
|
|
21
|
+
if (buf[0] === 0x52 && buf[1] === 0x49 && buf[2] === 0x46 && buf[3] === 0x46
|
|
22
|
+
&& buf[8] === 0x57 && buf[9] === 0x45 && buf[10] === 0x42 && buf[11] === 0x50)
|
|
23
|
+
return "image/webp";
|
|
24
|
+
return null;
|
|
27
25
|
}
|
|
28
|
-
const MAX_IMAGE_DIMENSION = 1568; // Claude API recommended max
|
|
29
26
|
/**
|
|
30
|
-
*
|
|
31
|
-
*
|
|
27
|
+
* Download a file from Slack, handling cross-origin redirects that strip the Authorization header.
|
|
28
|
+
* Slack's file URLs often redirect to a CDN, and fetch() strips auth headers on cross-origin redirects.
|
|
32
29
|
*/
|
|
33
|
-
async function
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
})
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
30
|
+
async function fetchSlackFile(url, token) {
|
|
31
|
+
// First try: manual redirect to preserve auth through redirect chain
|
|
32
|
+
const resp = await fetch(url, {
|
|
33
|
+
headers: { Authorization: `Bearer ${token}` },
|
|
34
|
+
redirect: "manual",
|
|
35
|
+
});
|
|
36
|
+
console.log(`[slack-files] initial response: status=${resp.status}, content-type=${resp.headers.get("content-type")}, location=${resp.headers.get("location")?.substring(0, 80) ?? "none"}`);
|
|
37
|
+
// If redirect, follow it manually with auth header
|
|
38
|
+
if (resp.status >= 300 && resp.status < 400) {
|
|
39
|
+
const location = resp.headers.get("location");
|
|
40
|
+
if (location) {
|
|
41
|
+
console.log(`[slack-files] following redirect to: ${location.substring(0, 80)}...`);
|
|
42
|
+
const resp2 = await fetch(location, {
|
|
43
|
+
headers: { Authorization: `Bearer ${token}` },
|
|
44
|
+
});
|
|
45
|
+
console.log(`[slack-files] redirect response: status=${resp2.status}, content-type=${resp2.headers.get("content-type")}`);
|
|
46
|
+
const buf = Buffer.from(await resp2.arrayBuffer());
|
|
47
|
+
return { ok: resp2.ok, status: resp2.status, contentType: resp2.headers.get("content-type"), buffer: buf };
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
// No redirect or non-redirect response
|
|
51
|
+
if (resp.ok || resp.status === 200) {
|
|
52
|
+
const buf = Buffer.from(await resp.arrayBuffer());
|
|
53
|
+
return { ok: true, status: resp.status, contentType: resp.headers.get("content-type"), buffer: buf };
|
|
54
|
+
}
|
|
55
|
+
// If first approach failed (got HTML), try with token as query parameter
|
|
56
|
+
console.log(`[slack-files] Bearer auth failed (status=${resp.status}), trying token as query param`);
|
|
57
|
+
const urlWithToken = new URL(url);
|
|
58
|
+
urlWithToken.searchParams.set("t", token);
|
|
59
|
+
const resp3 = await fetch(urlWithToken.toString());
|
|
60
|
+
console.log(`[slack-files] query param response: status=${resp3.status}, content-type=${resp3.headers.get("content-type")}`);
|
|
61
|
+
const buf = Buffer.from(await resp3.arrayBuffer());
|
|
62
|
+
return { ok: resp3.ok, status: resp3.status, contentType: resp3.headers.get("content-type"), buffer: buf };
|
|
42
63
|
}
|
|
43
64
|
/**
|
|
44
65
|
* Download image files from a Slack message's `files` array.
|
|
45
|
-
*
|
|
46
|
-
* Only images with supported media types are returned; non-image files are skipped.
|
|
47
66
|
*/
|
|
48
67
|
export async function downloadSlackImages(client, files) {
|
|
49
68
|
const token = client.token;
|
|
50
|
-
if (!token)
|
|
69
|
+
if (!token) {
|
|
70
|
+
console.warn("[slack-files] no token found on WebClient");
|
|
51
71
|
return [];
|
|
72
|
+
}
|
|
73
|
+
console.log(`[slack-files] token type: ${token.substring(0, 5)}..., length=${token.length}`);
|
|
52
74
|
const results = [];
|
|
53
75
|
for (const file of files) {
|
|
54
76
|
const mime = file.mimetype;
|
|
55
|
-
|
|
77
|
+
console.log(`[slack-files] processing file: id=${file.id}, name=${file.name}, filetype=${file.filetype}, mimetype=${mime}, size=${file.size}`);
|
|
78
|
+
if (!mime || !SUPPORTED_TYPES.has(mime)) {
|
|
79
|
+
console.log(`[slack-files] skipping unsupported mime: ${mime}`);
|
|
56
80
|
continue;
|
|
81
|
+
}
|
|
57
82
|
const url = file.url_private_download ?? file.url_private;
|
|
58
|
-
if (!url)
|
|
83
|
+
if (!url) {
|
|
84
|
+
console.warn(`[slack-files] no download URL for file ${file.id}`);
|
|
59
85
|
continue;
|
|
86
|
+
}
|
|
87
|
+
console.log(`[slack-files] download URL: ${url}`);
|
|
60
88
|
try {
|
|
61
|
-
const
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
if (!resp.ok) {
|
|
65
|
-
console.warn(`[slack-files] failed to download ${url}: ${resp.status}`);
|
|
89
|
+
const { ok, status, contentType, buffer: buf } = await fetchSlackFile(url, token);
|
|
90
|
+
if (!ok) {
|
|
91
|
+
console.warn(`[slack-files] download failed: status=${status}`);
|
|
66
92
|
continue;
|
|
67
93
|
}
|
|
68
|
-
const
|
|
69
|
-
|
|
70
|
-
|
|
94
|
+
const hexDump = buf.subarray(0, 16).toString("hex").match(/../g)?.join(" ") ?? "";
|
|
95
|
+
const detectedFormat = detectImageFormat(buf);
|
|
96
|
+
console.log(`[slack-files] downloaded ${buf.length} bytes, content-type=${contentType}`);
|
|
97
|
+
console.log(`[slack-files] first 16 bytes: ${hexDump}`);
|
|
98
|
+
console.log(`[slack-files] declared mime=${mime}, detected format=${detectedFormat}`);
|
|
99
|
+
// If the data looks like HTML (Slack error/login page), log and skip
|
|
100
|
+
const head = buf.subarray(0, 50).toString("utf8");
|
|
101
|
+
if (head.includes("<!DOCTYPE") || head.includes("<html")) {
|
|
102
|
+
const preview = buf.subarray(0, 300).toString("utf8");
|
|
103
|
+
console.warn(`[slack-files] downloaded data is HTML, not an image! Preview: ${preview}`);
|
|
71
104
|
continue;
|
|
72
105
|
}
|
|
73
|
-
// Normalize
|
|
106
|
+
// Normalize with sharp (handles format detection internally)
|
|
74
107
|
try {
|
|
75
|
-
const
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
108
|
+
const metadata = await sharp(buf).metadata();
|
|
109
|
+
console.log(`[slack-files] sharp metadata: format=${metadata.format}, ${metadata.width}x${metadata.height}, channels=${metadata.channels}, space=${metadata.space}`);
|
|
110
|
+
const normalized = await sharp(buf)
|
|
111
|
+
.resize(MAX_IMAGE_DIMENSION, MAX_IMAGE_DIMENSION, {
|
|
112
|
+
fit: "inside",
|
|
113
|
+
withoutEnlargement: true,
|
|
114
|
+
})
|
|
115
|
+
.png()
|
|
116
|
+
.toBuffer();
|
|
117
|
+
const b64 = normalized.toString("base64");
|
|
118
|
+
console.log(`[slack-files] normalized: ${buf.length} → ${normalized.length} bytes, base64=${b64.length} chars`);
|
|
119
|
+
results.push({ base64: b64, mediaType: "image/png" });
|
|
83
120
|
}
|
|
84
|
-
catch (
|
|
85
|
-
console.
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
121
|
+
catch (sharpErr) {
|
|
122
|
+
console.error(`[slack-files] sharp failed:`, sharpErr);
|
|
123
|
+
if (detectedFormat && SUPPORTED_TYPES.has(detectedFormat)) {
|
|
124
|
+
console.log(`[slack-files] fallback: raw buffer with ${detectedFormat}`);
|
|
125
|
+
results.push({
|
|
126
|
+
base64: buf.toString("base64"),
|
|
127
|
+
mediaType: detectedFormat,
|
|
128
|
+
});
|
|
129
|
+
}
|
|
90
130
|
}
|
|
91
131
|
}
|
|
92
132
|
catch (err) {
|
|
93
|
-
console.
|
|
133
|
+
console.error(`[slack-files] error downloading file ${file.id}:`, err);
|
|
94
134
|
}
|
|
95
135
|
}
|
|
136
|
+
console.log(`[slack-files] total images processed: ${results.length}`);
|
|
96
137
|
return results;
|
|
97
138
|
}
|
|
98
139
|
//# sourceMappingURL=slack-files.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"slack-files.js","sourceRoot":"","sources":["../../src/utils/slack-files.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,MAAM,OAAO,CAAC;AAO1B,MAAM,eAAe,GAAG,IAAI,GAAG,CAAC;IAC9B,YAAY;IACZ,WAAW;IACX,WAAW;IACX,YAAY;CACb,CAAC,CAAC;AAEH;;GAEG;AACH,SAAS,
|
|
1
|
+
{"version":3,"file":"slack-files.js","sourceRoot":"","sources":["../../src/utils/slack-files.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
package/src/utils/slack-files.ts
CHANGED
|
@@ -13,101 +13,158 @@ const SUPPORTED_TYPES = new Set([
|
|
|
13
13
|
"image/webp",
|
|
14
14
|
]);
|
|
15
15
|
|
|
16
|
+
const MAX_IMAGE_DIMENSION = 1568; // Claude API recommended max
|
|
17
|
+
|
|
16
18
|
/**
|
|
17
|
-
*
|
|
19
|
+
* Detect actual image format from magic bytes.
|
|
18
20
|
*/
|
|
19
|
-
function
|
|
20
|
-
if (buf.length <
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
return buf[0] === 0x47 && buf[1] === 0x49 && buf[2] === 0x46;
|
|
28
|
-
case "image/webp":
|
|
29
|
-
return buf.length >= 12 && buf[0] === 0x52 && buf[1] === 0x49 && buf[2] === 0x46 && buf[3] === 0x46
|
|
30
|
-
&& buf[8] === 0x57 && buf[9] === 0x45 && buf[10] === 0x42 && buf[11] === 0x50;
|
|
31
|
-
default:
|
|
32
|
-
return false;
|
|
33
|
-
}
|
|
21
|
+
function detectImageFormat(buf: Buffer): string | null {
|
|
22
|
+
if (buf.length < 12) return null;
|
|
23
|
+
if (buf[0] === 0x89 && buf[1] === 0x50 && buf[2] === 0x4e && buf[3] === 0x47) return "image/png";
|
|
24
|
+
if (buf[0] === 0xff && buf[1] === 0xd8 && buf[2] === 0xff) return "image/jpeg";
|
|
25
|
+
if (buf[0] === 0x47 && buf[1] === 0x49 && buf[2] === 0x46) return "image/gif";
|
|
26
|
+
if (buf[0] === 0x52 && buf[1] === 0x49 && buf[2] === 0x46 && buf[3] === 0x46
|
|
27
|
+
&& buf[8] === 0x57 && buf[9] === 0x45 && buf[10] === 0x42 && buf[11] === 0x50) return "image/webp";
|
|
28
|
+
return null;
|
|
34
29
|
}
|
|
35
30
|
|
|
36
|
-
const MAX_IMAGE_DIMENSION = 1568; // Claude API recommended max
|
|
37
|
-
|
|
38
31
|
/**
|
|
39
|
-
*
|
|
40
|
-
*
|
|
32
|
+
* Download a file from Slack, handling cross-origin redirects that strip the Authorization header.
|
|
33
|
+
* Slack's file URLs often redirect to a CDN, and fetch() strips auth headers on cross-origin redirects.
|
|
41
34
|
*/
|
|
42
|
-
async function
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
35
|
+
async function fetchSlackFile(url: string, token: string): Promise<{ ok: boolean; status: number; contentType: string | null; buffer: Buffer }> {
|
|
36
|
+
// First try: manual redirect to preserve auth through redirect chain
|
|
37
|
+
const resp = await fetch(url, {
|
|
38
|
+
headers: { Authorization: `Bearer ${token}` },
|
|
39
|
+
redirect: "manual",
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
console.log(`[slack-files] initial response: status=${resp.status}, content-type=${resp.headers.get("content-type")}, location=${resp.headers.get("location")?.substring(0, 80) ?? "none"}`);
|
|
43
|
+
|
|
44
|
+
// If redirect, follow it manually with auth header
|
|
45
|
+
if (resp.status >= 300 && resp.status < 400) {
|
|
46
|
+
const location = resp.headers.get("location");
|
|
47
|
+
if (location) {
|
|
48
|
+
console.log(`[slack-files] following redirect to: ${location.substring(0, 80)}...`);
|
|
49
|
+
const resp2 = await fetch(location, {
|
|
50
|
+
headers: { Authorization: `Bearer ${token}` },
|
|
51
|
+
});
|
|
52
|
+
console.log(`[slack-files] redirect response: status=${resp2.status}, content-type=${resp2.headers.get("content-type")}`);
|
|
53
|
+
const buf = Buffer.from(await resp2.arrayBuffer());
|
|
54
|
+
return { ok: resp2.ok, status: resp2.status, contentType: resp2.headers.get("content-type"), buffer: buf };
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// No redirect or non-redirect response
|
|
59
|
+
if (resp.ok || resp.status === 200) {
|
|
60
|
+
const buf = Buffer.from(await resp.arrayBuffer());
|
|
61
|
+
return { ok: true, status: resp.status, contentType: resp.headers.get("content-type"), buffer: buf };
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// If first approach failed (got HTML), try with token as query parameter
|
|
65
|
+
console.log(`[slack-files] Bearer auth failed (status=${resp.status}), trying token as query param`);
|
|
66
|
+
const urlWithToken = new URL(url);
|
|
67
|
+
urlWithToken.searchParams.set("t", token);
|
|
68
|
+
const resp3 = await fetch(urlWithToken.toString());
|
|
69
|
+
console.log(`[slack-files] query param response: status=${resp3.status}, content-type=${resp3.headers.get("content-type")}`);
|
|
70
|
+
const buf = Buffer.from(await resp3.arrayBuffer());
|
|
71
|
+
return { ok: resp3.ok, status: resp3.status, contentType: resp3.headers.get("content-type"), buffer: buf };
|
|
51
72
|
}
|
|
52
73
|
|
|
53
74
|
/**
|
|
54
75
|
* Download image files from a Slack message's `files` array.
|
|
55
|
-
*
|
|
56
|
-
* Only images with supported media types are returned; non-image files are skipped.
|
|
57
76
|
*/
|
|
58
77
|
export async function downloadSlackImages(
|
|
59
78
|
client: WebClient,
|
|
60
|
-
files: Array<{
|
|
79
|
+
files: Array<{
|
|
80
|
+
url_private_download?: string;
|
|
81
|
+
url_private?: string;
|
|
82
|
+
mimetype?: string;
|
|
83
|
+
name?: string;
|
|
84
|
+
filetype?: string;
|
|
85
|
+
size?: number;
|
|
86
|
+
id?: string;
|
|
87
|
+
}>
|
|
61
88
|
): Promise<SlackImageFile[]> {
|
|
62
89
|
const token = (client as unknown as { token?: string }).token;
|
|
63
|
-
if (!token)
|
|
90
|
+
if (!token) {
|
|
91
|
+
console.warn("[slack-files] no token found on WebClient");
|
|
92
|
+
return [];
|
|
93
|
+
}
|
|
94
|
+
console.log(`[slack-files] token type: ${token.substring(0, 5)}..., length=${token.length}`);
|
|
64
95
|
|
|
65
96
|
const results: SlackImageFile[] = [];
|
|
66
97
|
|
|
67
98
|
for (const file of files) {
|
|
68
99
|
const mime = file.mimetype;
|
|
69
|
-
|
|
100
|
+
console.log(`[slack-files] processing file: id=${file.id}, name=${file.name}, filetype=${file.filetype}, mimetype=${mime}, size=${file.size}`);
|
|
101
|
+
|
|
102
|
+
if (!mime || !SUPPORTED_TYPES.has(mime)) {
|
|
103
|
+
console.log(`[slack-files] skipping unsupported mime: ${mime}`);
|
|
104
|
+
continue;
|
|
105
|
+
}
|
|
70
106
|
|
|
71
107
|
const url = file.url_private_download ?? file.url_private;
|
|
72
|
-
if (!url)
|
|
108
|
+
if (!url) {
|
|
109
|
+
console.warn(`[slack-files] no download URL for file ${file.id}`);
|
|
110
|
+
continue;
|
|
111
|
+
}
|
|
112
|
+
console.log(`[slack-files] download URL: ${url}`);
|
|
73
113
|
|
|
74
114
|
try {
|
|
75
|
-
const
|
|
76
|
-
headers: { Authorization: `Bearer ${token}` },
|
|
77
|
-
});
|
|
115
|
+
const { ok, status, contentType, buffer: buf } = await fetchSlackFile(url, token);
|
|
78
116
|
|
|
79
|
-
if (!
|
|
80
|
-
console.warn(`[slack-files]
|
|
117
|
+
if (!ok) {
|
|
118
|
+
console.warn(`[slack-files] download failed: status=${status}`);
|
|
81
119
|
continue;
|
|
82
120
|
}
|
|
83
121
|
|
|
84
|
-
const
|
|
85
|
-
|
|
86
|
-
|
|
122
|
+
const hexDump = buf.subarray(0, 16).toString("hex").match(/../g)?.join(" ") ?? "";
|
|
123
|
+
const detectedFormat = detectImageFormat(buf);
|
|
124
|
+
console.log(`[slack-files] downloaded ${buf.length} bytes, content-type=${contentType}`);
|
|
125
|
+
console.log(`[slack-files] first 16 bytes: ${hexDump}`);
|
|
126
|
+
console.log(`[slack-files] declared mime=${mime}, detected format=${detectedFormat}`);
|
|
127
|
+
|
|
128
|
+
// If the data looks like HTML (Slack error/login page), log and skip
|
|
129
|
+
const head = buf.subarray(0, 50).toString("utf8");
|
|
130
|
+
if (head.includes("<!DOCTYPE") || head.includes("<html")) {
|
|
131
|
+
const preview = buf.subarray(0, 300).toString("utf8");
|
|
132
|
+
console.warn(`[slack-files] downloaded data is HTML, not an image! Preview: ${preview}`);
|
|
87
133
|
continue;
|
|
88
134
|
}
|
|
89
135
|
|
|
90
|
-
// Normalize
|
|
136
|
+
// Normalize with sharp (handles format detection internally)
|
|
91
137
|
try {
|
|
92
|
-
const
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
138
|
+
const metadata = await sharp(buf).metadata();
|
|
139
|
+
console.log(`[slack-files] sharp metadata: format=${metadata.format}, ${metadata.width}x${metadata.height}, channels=${metadata.channels}, space=${metadata.space}`);
|
|
140
|
+
|
|
141
|
+
const normalized = await sharp(buf)
|
|
142
|
+
.resize(MAX_IMAGE_DIMENSION, MAX_IMAGE_DIMENSION, {
|
|
143
|
+
fit: "inside",
|
|
144
|
+
withoutEnlargement: true,
|
|
145
|
+
})
|
|
146
|
+
.png()
|
|
147
|
+
.toBuffer();
|
|
148
|
+
|
|
149
|
+
const b64 = normalized.toString("base64");
|
|
150
|
+
console.log(`[slack-files] normalized: ${buf.length} → ${normalized.length} bytes, base64=${b64.length} chars`);
|
|
151
|
+
|
|
152
|
+
results.push({ base64: b64, mediaType: "image/png" });
|
|
153
|
+
} catch (sharpErr) {
|
|
154
|
+
console.error(`[slack-files] sharp failed:`, sharpErr);
|
|
155
|
+
if (detectedFormat && SUPPORTED_TYPES.has(detectedFormat)) {
|
|
156
|
+
console.log(`[slack-files] fallback: raw buffer with ${detectedFormat}`);
|
|
157
|
+
results.push({
|
|
158
|
+
base64: buf.toString("base64"),
|
|
159
|
+
mediaType: detectedFormat as SlackImageFile["mediaType"],
|
|
160
|
+
});
|
|
161
|
+
}
|
|
106
162
|
}
|
|
107
163
|
} catch (err) {
|
|
108
|
-
console.
|
|
164
|
+
console.error(`[slack-files] error downloading file ${file.id}:`, err);
|
|
109
165
|
}
|
|
110
166
|
}
|
|
111
167
|
|
|
168
|
+
console.log(`[slack-files] total images processed: ${results.length}`);
|
|
112
169
|
return results;
|
|
113
170
|
}
|