mirror of
https://github.com/hansjone/dsh-im-ops.git
synced 2026-10-12 06:50:45 +08:00
feat: support inbound files across all channels
This commit is contained in:
parent
29bcb67a64
commit
0a26d7df92
45 changed files with 2651 additions and 343 deletions
|
|
@ -1,3 +1,4 @@
|
|||
import { fetchFileStream } from '../shared/file-download.mjs';
|
||||
import { fetchImageBuffer, ImagePromptError } from '../shared/image-prompt.mjs';
|
||||
|
||||
const DEFAULT_BASE_URL = 'https://slack.com/api/';
|
||||
|
|
@ -232,6 +233,18 @@ export class SlackApi {
|
|||
});
|
||||
}
|
||||
|
||||
async fileInfo({ fileId, signal } = {}) {
|
||||
const value = await this.#request('files.info', {
|
||||
tokenKind: 'bot',
|
||||
signal,
|
||||
body: { file: slackId(fileId, 'file id') },
|
||||
});
|
||||
if (!value?.file || typeof value.file !== 'object' || Array.isArray(value.file)) {
|
||||
throw new Error('Slack files.info returned no file object');
|
||||
}
|
||||
return value.file;
|
||||
}
|
||||
|
||||
postMessage({ channelId, text, threadTs, signal }) {
|
||||
return this.#request('chat.postMessage', {
|
||||
tokenKind: 'bot',
|
||||
|
|
@ -396,6 +409,14 @@ export class SlackApi {
|
|||
}
|
||||
|
||||
async downloadFile({ url, signal, maxBytes }) {
|
||||
return this.#downloadFile({ url, signal, maxBytes, stream: false });
|
||||
}
|
||||
|
||||
async downloadFileStream({ url, signal }) {
|
||||
return this.#downloadFile({ url, signal, stream: true });
|
||||
}
|
||||
|
||||
async #downloadFile({ url, signal, maxBytes, stream }) {
|
||||
if (!this.#botToken) throw new TypeError('Slack bot token is required for file download');
|
||||
const target = secureSlackFileUrl(url);
|
||||
const fetchSlackFile = async (requestUrl, options) => {
|
||||
|
|
@ -414,13 +435,15 @@ export class SlackApi {
|
|||
}
|
||||
return response;
|
||||
};
|
||||
return fetchImageBuffer(target, {
|
||||
const options = {
|
||||
fetchImpl: fetchSlackFile,
|
||||
headers: { authorization: `Bearer ${this.#botToken}` },
|
||||
signal,
|
||||
maxBytes,
|
||||
allowedHosts: SLACK_FILE_HOSTS,
|
||||
});
|
||||
};
|
||||
return stream
|
||||
? fetchFileStream(target, options)
|
||||
: fetchImageBuffer(target, { ...options, maxBytes });
|
||||
}
|
||||
|
||||
async #request(method, {
|
||||
|
|
@ -432,7 +455,7 @@ export class SlackApi {
|
|||
}) {
|
||||
const token = tokenKind === 'app' ? this.#appToken : this.#botToken;
|
||||
if (!token) throw new TypeError(`Slack ${tokenKind} token is required for ${method}`);
|
||||
const formEncoded = method === 'files.getUploadURLExternal';
|
||||
const formEncoded = method === 'files.getUploadURLExternal' || method === 'files.info';
|
||||
let response;
|
||||
try {
|
||||
response = await this.#fetch(new URL(method, this.#baseUrl), {
|
||||
|
|
|
|||
|
|
@ -43,10 +43,14 @@ function stripBotMention(value, botUserId) {
|
|||
.trim();
|
||||
}
|
||||
|
||||
function slackFileUrl(file) {
|
||||
return typeof file?.url_private_download === 'string' && file.url_private_download
|
||||
? file.url_private_download : file?.url_private;
|
||||
}
|
||||
|
||||
function slackImageSource(file, loadFile) {
|
||||
const mediaType = typeof file?.mimetype === 'string' ? file.mimetype.toLowerCase() : '';
|
||||
const url = typeof file?.url_private_download === 'string' && file.url_private_download
|
||||
? file.url_private_download : file?.url_private;
|
||||
const url = slackFileUrl(file);
|
||||
if (!IMAGE_MEDIA_TYPES.has(mediaType) || typeof url !== 'string' || !url) return null;
|
||||
return {
|
||||
name: typeof file.name === 'string' ? file.name : undefined,
|
||||
|
|
@ -56,9 +60,50 @@ function slackImageSource(file, loadFile) {
|
|||
};
|
||||
}
|
||||
|
||||
export function normalizeSlackEvent(payload, botUserId, { loadFile = async () => {
|
||||
throw new Error('Slack file downloader is unavailable');
|
||||
} } = {}) {
|
||||
function slackFileSource(file, loadFile, loadFileInfo) {
|
||||
const mediaType = typeof file?.mimetype === 'string' && file.mimetype
|
||||
? file.mimetype.toLowerCase() : undefined;
|
||||
const url = slackFileUrl(file);
|
||||
if (IMAGE_MEDIA_TYPES.has(mediaType)) return null;
|
||||
const requiresInfo = file?.file_access === 'check_file_info'
|
||||
&& typeof file?.id === 'string' && file.id;
|
||||
if ((typeof url !== 'string' || !url) && !requiresInfo) return null;
|
||||
return {
|
||||
name: typeof file?.name === 'string' && file.name
|
||||
? file.name : typeof file?.title === 'string' && file.title
|
||||
? file.title : requiresInfo ? file.id : 'slack-file',
|
||||
...(mediaType ? { mediaType } : {}),
|
||||
size: Number.isSafeInteger(file?.size) && file.size >= 0 ? file.size : undefined,
|
||||
load: async ({ signal } = {}) => {
|
||||
if (!requiresInfo) return loadFile(url, { signal });
|
||||
const resolved = await loadFileInfo(file.id, { signal });
|
||||
const resolvedUrl = slackFileUrl(resolved);
|
||||
if (typeof resolvedUrl !== 'string' || !resolvedUrl) {
|
||||
throw new Error('Slack files.info returned no downloadable URL');
|
||||
}
|
||||
const loaded = await loadFile(resolvedUrl, { signal });
|
||||
const name = typeof resolved?.name === 'string' && resolved.name
|
||||
? resolved.name : typeof resolved?.title === 'string' && resolved.title
|
||||
? resolved.title : file.id;
|
||||
const resolvedMediaType = typeof resolved?.mimetype === 'string' && resolved.mimetype
|
||||
? resolved.mimetype.toLowerCase() : undefined;
|
||||
if (Buffer.isBuffer(loaded) || loaded instanceof Uint8Array) {
|
||||
return { data: loaded, name, ...(resolvedMediaType ? { mediaType: resolvedMediaType } : {}) };
|
||||
}
|
||||
return {
|
||||
...loaded,
|
||||
name,
|
||||
...(resolvedMediaType ? { mediaType: resolvedMediaType } : {}),
|
||||
};
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export function normalizeSlackEvent(payload, botUserId, {
|
||||
loadFile = async () => { throw new Error('Slack file downloader is unavailable'); },
|
||||
loadFileStream = loadFile,
|
||||
loadFileInfo = async () => { throw new Error('Slack file metadata loader is unavailable'); },
|
||||
} = {}) {
|
||||
const event = payload?.event;
|
||||
if (!event || !payload?.event_id || !event.channel || !event.user || !event.ts) return null;
|
||||
const direct = event.type === 'message' && event.channel_type === 'im';
|
||||
|
|
@ -76,6 +121,9 @@ export function normalizeSlackEvent(payload, botUserId, { loadFile = async () =>
|
|||
images: Array.isArray(event.files)
|
||||
? event.files.map((file) => slackImageSource(file, loadFile)).filter(Boolean)
|
||||
: [],
|
||||
files: Array.isArray(event.files)
|
||||
? event.files.map((file) => slackFileSource(file, loadFileStream, loadFileInfo)).filter(Boolean)
|
||||
: [],
|
||||
addressed: direct || mentioned,
|
||||
replyTarget: {
|
||||
channelId: String(event.channel),
|
||||
|
|
@ -436,6 +484,8 @@ export class SlackRuntime {
|
|||
&& packet.payload.api_app_id !== this.#appId) return;
|
||||
const message = normalizeSlackEvent(packet.payload, this.#config.platformId.split(':')[1], {
|
||||
loadFile: (url, options) => this.#api.downloadFile({ url, ...options }),
|
||||
loadFileStream: (url, options) => this.#api.downloadFileStream({ url, ...options }),
|
||||
loadFileInfo: (fileId, options) => this.#api.fileInfo({ fileId, ...options }),
|
||||
});
|
||||
const bridge = this.#bridge;
|
||||
if (message && bridge) {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue