From d849bbe491787ec2f89391d8c16db01484fe8b7c Mon Sep 17 00:00:00 2001 From: yuriassuncx Date: Sat, 13 Sep 2025 00:23:10 -0300 Subject: [PATCH 01/18] feat(slack): Enhance DM and file handling with new upload functionality and improved response structures --- slack/actions/dms/send.ts | 49 ++--- slack/actions/files/upload.ts | 112 +++++++++++ slack/client.ts | 280 ++++++++++++++++++++++++---- slack/loaders/conversations/open.ts | 51 +++++ slack/loaders/dms/history.ts | 11 +- slack/loaders/dms/list.ts | 2 +- slack/loaders/files/list.ts | 80 ++++++++ slack/manifest.gen.ts | 58 +++--- slack/utils/client.ts | 69 ++++++- 9 files changed, 613 insertions(+), 99 deletions(-) create mode 100644 slack/actions/files/upload.ts create mode 100644 slack/loaders/conversations/open.ts create mode 100644 slack/loaders/files/list.ts diff --git a/slack/actions/dms/send.ts b/slack/actions/dms/send.ts index 85e4b186..b00b5c41 100644 --- a/slack/actions/dms/send.ts +++ b/slack/actions/dms/send.ts @@ -17,6 +17,22 @@ export interface SendDmProps { blocks?: unknown[]; } +export interface SendDmResponse { + success: boolean; + message: string; + channelId?: string; + ts?: string; + messageData?: { + ok: boolean; + channel: string; + ts: string; + warning?: string; + response_metadata?: { + warnings?: string[]; + }; + }; +} + /** * @description Sends a direct message to a user * @action send-dm @@ -25,41 +41,32 @@ export default async function sendDm( props: SendDmProps, _req: Request, ctx: AppContext, -): Promise< - { success: boolean; message: string; channelId?: string; ts?: string } -> { +): Promise { try { - // Open a DM channel with the user - const channelResponse = await ctx.slack.openDmChannel(props.userId); - - if (!channelResponse.ok) { - return { - success: false, - message: `Failed to open DM channel: ${ - channelResponse.error || "Unknown error" - }`, - }; - } - - const channelId = channelResponse.data.channel.id; - - // Send the message to the DM channel - const messageResponse = await ctx.slack.postMessage(channelId, props.text, { + // Send message directly to the user ID (Slack automatically opens DM channel) + const messageResponse = await ctx.slack.postMessage(props.userId, props.text, { blocks: props.blocks, }); if (!messageResponse.ok) { return { success: false, - message: "Failed to send DM: Unknown error", + message: `Failed to send DM: ${messageResponse.error || "Unknown error"}`, }; } return { success: true, message: "DM sent successfully", - channelId, + channelId: messageResponse.channel, ts: messageResponse.ts, + messageData: { + ok: messageResponse.ok, + channel: messageResponse.channel, + ts: messageResponse.ts, + warning: messageResponse.warning, + response_metadata: messageResponse.response_metadata, + }, }; } catch (error: unknown) { const message = error instanceof Error ? error.message : String(error); diff --git a/slack/actions/files/upload.ts b/slack/actions/files/upload.ts new file mode 100644 index 00000000..2eb2697e --- /dev/null +++ b/slack/actions/files/upload.ts @@ -0,0 +1,112 @@ +import type { AppContext } from "../../mod.ts"; + +export interface UploadFileProps { + /** + * @description Channel ID or DM ID to send the file to (e.g., C123... or D123...) + */ + channels: string; + + /** + * @description File content as base64 string or File object + */ + file: string | File; + + /** + * @description Name of the file + */ + filename: string; + + /** + * @description Title of the file + */ + title?: string; + + /** + * @description Initial comment/message that accompanies the file + */ + initial_comment?: string; + + /** + * @description File type (optional, usually auto-detected) + */ + filetype?: string; +} + +export interface UploadFileResponse { + ok: boolean; + file?: { + id: string; + created: number; + timestamp: number; + name: string; + title: string; + mimetype: string; + filetype: string; + pretty_type: string; + user: string; + editable: boolean; + size: number; + mode: string; + is_external: boolean; + external_type: string; + is_public: boolean; + public_url_shared: boolean; + display_as_bot: boolean; + username: string; + url_private: string; + url_private_download: string; + permalink: string; + permalink_public?: string; + channels: string[]; + groups: string[]; + ims: string[]; + comments_count: number; + is_starred?: boolean; + }; + error?: string; + warning?: string; + response_metadata?: { + warnings?: string[]; + }; +} + +/** + * @description Uploads a file to Slack + * @action upload-file + */ +export default async function uploadFile( + props: UploadFileProps, + _req: Request, + ctx: AppContext, +): Promise { + try { + const response = await ctx.slack.uploadFile({ + channels: props.channels, + file: props.file, + filename: props.filename, + title: props.title, + initial_comment: props.initial_comment, + filetype: props.filetype, + }); + + if (!response.ok) { + return { + ok: false, + error: response.error || "Failed to upload file", + }; + } + + return { + ok: response.ok, + file: response.file, + warning: response.warning, + response_metadata: response.response_metadata, + }; + } catch (error) { + console.error("Error uploading file:", error); + return { + ok: false, + error: error instanceof Error ? error.message : "Unknown error", + }; + } +} \ No newline at end of file diff --git a/slack/client.ts b/slack/client.ts index 3e5db206..5669cbeb 100644 --- a/slack/client.ts +++ b/slack/client.ts @@ -18,24 +18,32 @@ export interface SlackResponse { */ export interface SlackChannel { id: string; - name: string; - is_channel: boolean; - is_private: boolean; + name?: string; + is_channel?: boolean; + is_private?: boolean; created: number; - creator: string; + creator?: string; is_archived: boolean; - is_general: boolean; - members: string[]; - topic: { + is_general?: boolean; + members?: string[]; + topic?: { value: string; creator: string; last_set: number; }; - purpose: { + purpose?: { value: string; creator: string; last_set: number; }; + // DM specific fields + is_im?: boolean; + is_org_shared?: boolean; + context_team_id?: string; + updated?: number; + user?: string; + is_user_deleted?: boolean; + priority?: number; } /** @@ -43,17 +51,77 @@ export interface SlackChannel { */ export interface SlackMessage { type: string; - user: string; + user?: string; text: string; ts: string; thread_ts?: string; reply_count?: number; + reply_users_count?: number; + latest_reply?: string; + reply_users?: string[]; + is_locked?: boolean; + subscribed?: boolean; files?: SlackFile[]; reactions?: Array<{ name: string; count: number; users: string[]; }>; + blocks?: Array<{ + type: string; + block_id: string; + elements: Array<{ + type: string; + elements?: Array<{ + type: string; + text: string; + }>; + }>; + }>; + subtype?: string; + edited?: { + user: string; + ts: string; + }; + team?: string; + bot_id?: string; + app_id?: string; + bot_profile?: SlackBotProfile; + assistant_app_thread?: { + title: string; + title_blocks: Array<{ + type: string; + block_id: string; + elements: Array<{ + type: string; + elements: Array<{ + type: string; + text: string; + }>; + }>; + }>; + artifacts: unknown[]; + context: Record; + }; + [key: string]: unknown; +} + +/** + * @description A bot profile in Slack + */ +export interface SlackBotProfile { + id: string; + app_id: string; + user_id: string; + name: string; + icons: { + image_36: string; + image_48: string; + image_72: string; + }; + deleted: boolean; + updated: number; + team_id: string; } /** @@ -69,6 +137,7 @@ export interface SlackFile { filetype: string; pretty_type: string; user: string; + user_team?: string; editable: boolean; size: number; mode: string; @@ -80,6 +149,7 @@ export interface SlackFile { username: string; url_private: string; url_private_download: string; + media_display_type?: string; thumb_64?: string; thumb_80?: string; thumb_360?: string; @@ -89,11 +159,29 @@ export interface SlackFile { thumb_480_w?: number; thumb_480_h?: number; thumb_160?: string; + thumb_720?: string; + thumb_720_w?: number; + thumb_720_h?: number; + thumb_800?: string; + thumb_800_w?: number; + thumb_800_h?: number; + thumb_960?: string; + thumb_960_w?: number; + thumb_960_h?: number; + thumb_1024?: string; + thumb_1024_w?: number; + thumb_1024_h?: number; + thumb_tiny?: string; image_exif_rotation?: number; original_w?: number; original_h?: number; permalink: string; - permalink_public: string; + permalink_public?: string; + channels: string[]; + groups: string[]; + ims: string[]; + comments_count: number; + is_starred?: boolean; } /** @@ -223,9 +311,17 @@ export class SlackClient { channelId: string, text: string, opts: { thread_ts?: string; blocks?: unknown[] } = {}, - ): Promise< - { channel: string; ts: string; message: SlackMessage; ok: boolean } - > { + ): Promise<{ + ok: boolean; + channel: string; + ts: string; + message: SlackMessage; + warning?: string; + response_metadata?: { + warnings?: string[]; + }; + error?: string; + }> { const payload: Record = { channel: channelId, text: text, @@ -302,7 +398,17 @@ export class SlackClient { async getChannelHistory( channelId: string, limit: number = 10, - ): Promise> { + ): Promise> { const params = new URLSearchParams({ channel: channelId, limit: limit.toString(), @@ -313,7 +419,21 @@ export class SlackClient { { headers: this.botHeaders }, ); - return response.json(); + const result = await response.json(); + return { + ok: result.ok, + error: result.error, + response_metadata: result.response_metadata, + data: { + messages: result.messages || [], + has_more: result.has_more, + pin_count: result.pin_count, + channel_actions_ts: result.channel_actions_ts, + channel_actions_count: result.channel_actions_count, + warning: result.warning, + response_metadata: result.response_metadata, + }, + }; } /** @@ -433,20 +553,26 @@ export class SlackClient { * @description Opens a direct message channel with a user * @param userId The user ID to open a DM with */ - async openDmChannel( - userId: string, - ): Promise> { + async openDmChannel(userId: string): Promise<{ + ok: boolean; + channel?: { id: string }; + error?: string; + no_op?: boolean; + already_open?: boolean; + warning?: string; + response_metadata?: { + warnings?: string[]; + }; + }> { const response = await fetch("https://slack.com/api/conversations.open", { method: "POST", headers: this.botHeaders, - body: JSON.stringify({ - users: userId, - }), + body: JSON.stringify({ users: userId }), }); return response.json(); } - + /** * @description Lists all direct message channels for the bot * @param limit Maximum number of DMs to return @@ -455,7 +581,7 @@ export class SlackClient { async listDmChannels( limit: number = 100, cursor?: string, - ): Promise> { + ): Promise> { const params = new URLSearchParams({ types: "im", limit: Math.min(limit, 100).toString(), @@ -470,22 +596,50 @@ export class SlackClient { { headers: this.botHeaders }, ); - return response.json(); + const result = await response.json(); + return { + ok: result.ok, + error: result.error, + response_metadata: result.response_metadata, + data: { + channels: result.channels || [], + response_metadata: result.response_metadata, + }, + }; } - + /** - * @description Gets information about a file - * @param fileId The ID of the file + * @description Lists files uploaded by a specific user + * @param userId The user ID whose files to list + * @param count Maximum number of files to return + * @param page Page number for pagination + * @param types Filter by file type */ - async getFileInfo( - fileId: string, - ): Promise> { + async listUserFiles( + userId: string, + count: number = 20, + page: number = 1, + types: string = 'all' + ): Promise<{ + ok: boolean; + files: SlackFile[]; + paging: { + count: number; + total: number; + page: number; + pages: number; + }; + error?: string; + }> { const params = new URLSearchParams({ - file: fileId, + user: userId, + count: count.toString(), + page: page.toString(), + types: types, }); const response = await fetch( - `https://slack.com/api/files.info?${params}`, + `https://slack.com/api/files.list?${params}`, { headers: this.botHeaders }, ); @@ -493,10 +647,64 @@ export class SlackClient { } /** - * @description Downloads a file from Slack - * @param fileUrl The URL of the file to download + * @description Uploads a file to Slack + * @param options Upload options including channels, file, filename, etc. */ - downloadFile(fileUrl: string): Promise { - return fetch(fileUrl, { headers: this.botHeaders }); + async uploadFile(options: { + channels: string; + file: string | File; + filename: string; + title?: string; + initial_comment?: string; + filetype?: string; + }): Promise<{ + ok: boolean; + file?: SlackFile; + error?: string; + warning?: string; + response_metadata?: { + warnings?: string[]; + }; + }> { + const formData = new FormData(); + formData.append("channels", options.channels); + formData.append("filename", options.filename); + + if (options.title) { + formData.append("title", options.title); + } + + if (options.initial_comment) { + formData.append("initial_comment", options.initial_comment); + } + + if (options.filetype) { + formData.append("filetype", options.filetype); + } + + // Handle file content + if (typeof options.file === "string") { + // Assume base64 string, convert to Blob + const byteCharacters = atob(options.file); + const byteNumbers = new Array(byteCharacters.length); + for (let i = 0; i < byteCharacters.length; i++) { + byteNumbers[i] = byteCharacters.charCodeAt(i); + } + const byteArray = new Uint8Array(byteNumbers); + const blob = new Blob([byteArray]); + formData.append("file", blob, options.filename); + } else { + formData.append("file", options.file, options.filename); + } + + const response = await fetch("https://slack.com/api/files.upload", { + method: "POST", + headers: { + Authorization: this.botHeaders.Authorization, + }, + body: formData, + }); + + return response.json(); } } diff --git a/slack/loaders/conversations/open.ts b/slack/loaders/conversations/open.ts new file mode 100644 index 00000000..00f931e9 --- /dev/null +++ b/slack/loaders/conversations/open.ts @@ -0,0 +1,51 @@ +import type { AppContext } from "../../mod.ts"; + +export interface Props { + /** + * @description The ID of the user to open a DM conversation with + */ + userId: string; +} + +export interface ConversationOpenResponse { + ok: boolean; + no_op?: boolean; + already_open?: boolean; + channel?: { + id: string; + }; + warning?: string; + response_metadata?: { + warnings?: string[]; + }; + error?: string; +} + +/** + * @description Opens a direct message conversation with a user + */ +export default async function openConversation( + props: Props, + _req: Request, + ctx: AppContext, +): Promise { + try { + const response = await ctx.slack.openDmChannel(props.userId); + + return { + ok: response.ok, + no_op: response.no_op, + already_open: response.already_open, + channel: response.channel, + warning: response.warning, + response_metadata: response.response_metadata, + error: response.error, + }; + } catch (error) { + console.error("Error opening conversation:", error); + return { + ok: false, + error: error instanceof Error ? error.message : "Unknown error", + }; + } +} \ No newline at end of file diff --git a/slack/loaders/dms/history.ts b/slack/loaders/dms/history.ts index 94d84c17..34c529b1 100644 --- a/slack/loaders/dms/history.ts +++ b/slack/loaders/dms/history.ts @@ -23,7 +23,6 @@ export default async function dmHistory( ctx: AppContext, ): Promise { try { - // First open or get the DM channel with the user const channelResponse = await ctx.slack.openDmChannel(props.userId); if (!channelResponse.ok) { @@ -31,10 +30,14 @@ export default async function dmHistory( return []; } - const channelId = channelResponse.data.channel.id; + const channelId = channelResponse.channel?.id; + if (!channelId) { + console.error("No channel ID returned for user", props.userId); + return []; + } + const limit = props.limit || 10; - // Get the history of this DM channel const historyResponse = await ctx.slack.getChannelHistory(channelId, limit); if (!historyResponse.ok) { @@ -42,7 +45,7 @@ export default async function dmHistory( return []; } - return historyResponse.data.messages; + return historyResponse.data.messages || []; } catch (error) { console.error("Error getting DM history:", error); return []; diff --git a/slack/loaders/dms/list.ts b/slack/loaders/dms/list.ts index bfacbbb3..318fb493 100644 --- a/slack/loaders/dms/list.ts +++ b/slack/loaders/dms/list.ts @@ -33,7 +33,7 @@ export default async function listDms( return { channels: dmResponse.data.channels, - next_cursor: dmResponse.response_metadata?.next_cursor, + next_cursor: dmResponse.data.response_metadata?.next_cursor, }; } catch (error) { console.error("Error listing DM channels:", error); diff --git a/slack/loaders/files/list.ts b/slack/loaders/files/list.ts new file mode 100644 index 00000000..f90ccea5 --- /dev/null +++ b/slack/loaders/files/list.ts @@ -0,0 +1,80 @@ +import type { AppContext } from "../../mod.ts"; +import { SlackFile } from "../../client.ts"; + +export interface Props { + /** + * @description The ID of the user whose files to list + */ + userId: string; + + /** + * @description Maximum number of files to return + * @default 20 + */ + count?: number; + + /** + * @description Page number for pagination + * @default 1 + */ + page?: number; + + /** + * @description Filter by file type (e.g., 'images', 'pdfs', 'all') + * @default 'all' + */ + types?: string; +} + +export interface FilesListResponse { + ok: boolean; + files: SlackFile[]; + paging: { + count: number; + total: number; + page: number; + pages: number; + }; + error?: string; +} + +/** + * @description Lists files uploaded by a specific user + */ +export default async function listUserFiles( + props: Props, + _req: Request, + ctx: AppContext, +): Promise { + try { + const response = await ctx.slack.listUserFiles( + props.userId, + props.count || 20, + props.page || 1, + props.types || 'all' + ); + + if (!response.ok) { + return { + ok: false, + files: [], + paging: { count: 0, total: 0, page: 1, pages: 0 }, + error: response.error || "Failed to list user files", + }; + } + + return { + ok: response.ok, + files: response.files || [], + paging: response.paging || { count: 0, total: 0, page: 1, pages: 0 }, + }; + } catch (error) { + console.error("Error listing user files:", error); + return { + ok: false, + files: [], + paging: { count: 0, total: 0, page: 1, pages: 0 }, + error: error instanceof Error ? error.message : "Unknown error", + }; + } +} \ No newline at end of file diff --git a/slack/manifest.gen.ts b/slack/manifest.gen.ts index 41b9de6f..88a95771 100644 --- a/slack/manifest.gen.ts +++ b/slack/manifest.gen.ts @@ -6,47 +6,49 @@ import * as $$$$$$$$$0 from "./actions/deco-chat/channels/invoke.ts"; import * as $$$$$$$$$1 from "./actions/deco-chat/channels/join.ts"; import * as $$$$$$$$$2 from "./actions/deco-chat/channels/leave.ts"; import * as $$$$$$$$$3 from "./actions/dms/send.ts"; -import * as $$$$$$$$$4 from "./actions/files/download.ts"; -import * as $$$$$$$$$5 from "./actions/files/info.ts"; -import * as $$$$$$$$$6 from "./actions/messages/post.ts"; -import * as $$$$$$$$$7 from "./actions/messages/react.ts"; -import * as $$$$$$$$$8 from "./actions/messages/threads/reply.ts"; -import * as $$$$$$$$$9 from "./actions/oauth/callback.ts"; -import * as $$$$$$$$$10 from "./actions/webhook/broker.ts"; +import * as $$$$$$$$$4 from "./actions/files/upload.ts"; +import * as $$$$$$$$$5 from "./actions/messages/post.ts"; +import * as $$$$$$$$$6 from "./actions/messages/react.ts"; +import * as $$$$$$$$$7 from "./actions/messages/threads/reply.ts"; +import * as $$$$$$$$$8 from "./actions/oauth/callback.ts"; +import * as $$$$$$$$$9 from "./actions/webhook/broker.ts"; import * as $$$0 from "./loaders/channels.ts"; import * as $$$1 from "./loaders/channels/history.ts"; -import * as $$$2 from "./loaders/deco-chat/channels/list.ts"; -import * as $$$3 from "./loaders/dms/history.ts"; -import * as $$$4 from "./loaders/dms/list.ts"; -import * as $$$5 from "./loaders/oauth/start.ts"; -import * as $$$6 from "./loaders/thread/replies.ts"; -import * as $$$7 from "./loaders/user/profile.ts"; -import * as $$$8 from "./loaders/users.ts"; +import * as $$$2 from "./loaders/conversations/open.ts"; +import * as $$$3 from "./loaders/deco-chat/channels/list.ts"; +import * as $$$4 from "./loaders/dms/history.ts"; +import * as $$$5 from "./loaders/dms/list.ts"; +import * as $$$6 from "./loaders/files/list.ts"; +import * as $$$7 from "./loaders/oauth/start.ts"; +import * as $$$8 from "./loaders/thread/replies.ts"; +import * as $$$9 from "./loaders/user/profile.ts"; +import * as $$$10 from "./loaders/users.ts"; const manifest = { "loaders": { "slack/loaders/channels.ts": $$$0, "slack/loaders/channels/history.ts": $$$1, - "slack/loaders/deco-chat/channels/list.ts": $$$2, - "slack/loaders/dms/history.ts": $$$3, - "slack/loaders/dms/list.ts": $$$4, - "slack/loaders/oauth/start.ts": $$$5, - "slack/loaders/thread/replies.ts": $$$6, - "slack/loaders/user/profile.ts": $$$7, - "slack/loaders/users.ts": $$$8, + "slack/loaders/conversations/open.ts": $$$2, + "slack/loaders/deco-chat/channels/list.ts": $$$3, + "slack/loaders/dms/history.ts": $$$4, + "slack/loaders/dms/list.ts": $$$5, + "slack/loaders/files/list.ts": $$$6, + "slack/loaders/oauth/start.ts": $$$7, + "slack/loaders/thread/replies.ts": $$$8, + "slack/loaders/user/profile.ts": $$$9, + "slack/loaders/users.ts": $$$10, }, "actions": { "slack/actions/deco-chat/channels/invoke.ts": $$$$$$$$$0, "slack/actions/deco-chat/channels/join.ts": $$$$$$$$$1, "slack/actions/deco-chat/channels/leave.ts": $$$$$$$$$2, "slack/actions/dms/send.ts": $$$$$$$$$3, - "slack/actions/files/download.ts": $$$$$$$$$4, - "slack/actions/files/info.ts": $$$$$$$$$5, - "slack/actions/messages/post.ts": $$$$$$$$$6, - "slack/actions/messages/react.ts": $$$$$$$$$7, - "slack/actions/messages/threads/reply.ts": $$$$$$$$$8, - "slack/actions/oauth/callback.ts": $$$$$$$$$9, - "slack/actions/webhook/broker.ts": $$$$$$$$$10, + "slack/actions/files/upload.ts": $$$$$$$$$4, + "slack/actions/messages/post.ts": $$$$$$$$$5, + "slack/actions/messages/react.ts": $$$$$$$$$6, + "slack/actions/messages/threads/reply.ts": $$$$$$$$$7, + "slack/actions/oauth/callback.ts": $$$$$$$$$8, + "slack/actions/webhook/broker.ts": $$$$$$$$$9, }, "name": "slack", "baseUrl": import.meta.url, diff --git a/slack/utils/client.ts b/slack/utils/client.ts index 2fef5370..225385b6 100644 --- a/slack/utils/client.ts +++ b/slack/utils/client.ts @@ -17,17 +17,29 @@ export interface SlackApiClient { team_id?: string; cursor?: string; }; - response: SlackResponse<{ channels: SlackChannel[] }>; + response: SlackResponse<{ + channels: SlackChannel[]; + response_metadata?: { + next_cursor?: string; + }; + }>; }; "POST /chat.postMessage": { json: { channel: string; text: string; thread_ts?: string; + blocks?: unknown[]; }; - response: SlackResponse< - { channel: string; ts: string; message: SlackMessage } - >; + response: SlackResponse<{ + channel: string; + ts: string; + message: SlackMessage; + warning?: string; + response_metadata?: { + warnings?: string[]; + }; + }>; }; "POST /reactions.add": { json: { @@ -42,7 +54,17 @@ export interface SlackApiClient { channel: string; limit?: string; }; - response: SlackResponse<{ messages: SlackMessage[] }>; + response: SlackResponse<{ + messages: SlackMessage[]; + has_more?: boolean; + pin_count?: number; + channel_actions_ts?: string | null; + channel_actions_count?: number; + warning?: string; + response_metadata?: { + warnings?: string[]; + }; + }>; }; "GET /conversations.replies": { searchParams: { @@ -69,13 +91,42 @@ export interface SlackApiClient { json: { users: string; }; - response: SlackResponse<{ channel: { id: string } }>; + response: SlackResponse<{ + channel: { id: string }; + no_op?: boolean; + already_open?: boolean; + warning?: string; + response_metadata?: { + warnings?: string[]; + }; + }>; }; - "GET /files.info": { + "GET /files.list": { searchParams: { - file: string; + user: string; + count?: string; + page?: string; + types?: string; }; - response: SlackResponse<{ file: SlackFile }>; + response: SlackResponse<{ + files: SlackFile[]; + paging: { + count: number; + total: number; + page: number; + pages: number; + }; + }>; + }; + "POST /files.upload": { + body: FormData; + response: SlackResponse<{ + file: SlackFile; + warning?: string; + response_metadata?: { + warnings?: string[]; + }; + }>; }; } From 761049376702f43ec44094ad0a5d4e0aacb0a151 Mon Sep 17 00:00:00 2001 From: yuriassuncx Date: Sat, 13 Sep 2025 00:29:28 -0300 Subject: [PATCH 02/18] refactor(slack): Remove unused file download and info actions --- slack/actions/files/download.ts | 72 --------------------------------- slack/actions/files/info.ts | 43 -------------------- 2 files changed, 115 deletions(-) delete mode 100644 slack/actions/files/download.ts delete mode 100644 slack/actions/files/info.ts diff --git a/slack/actions/files/download.ts b/slack/actions/files/download.ts deleted file mode 100644 index 7d0b25d9..00000000 --- a/slack/actions/files/download.ts +++ /dev/null @@ -1,72 +0,0 @@ -import type { AppContext } from "../../mod.ts"; - -export interface DownloadFileProps { - /** - * @description The URL of the file to download - */ - fileUrl: string; -} - -/** - * @description Downloads a file from Slack and returns it as a base64 string - */ -export default async function downloadFile( - props: DownloadFileProps, - _req: Request, - ctx: AppContext, -): Promise< - { success: boolean; data?: string; contentType?: string; message?: string } -> { - try { - // Host allowlist to prevent SSRF: only allow Slack-hosted URLs - const url = new URL(props.fileUrl); - const host = url.hostname.toLowerCase(); - const allowed = (h: string) => - h === "slack.com" || - h.endsWith(".slack.com") || - h === "slack-files.com" || - h.endsWith(".slack-files.com") || - h.endsWith(".slack-edge.com"); - if (url.protocol !== "https:" || !allowed(host)) { - return { - success: false, - message: "Only Slack-hosted HTTPS file URLs are allowed.", - }; - } - const fileResponse = await ctx.slack.downloadFile(props.fileUrl); - if (!fileResponse.ok) { - return { - success: false, - message: `Failed to download file: ${ - fileResponse.statusText || "Unknown error" - }`, - }; - } - - const contentType = fileResponse.headers.get("content-type") || - "application/octet-stream"; - const arrayBuffer = await fileResponse.arrayBuffer(); - const bytes = new Uint8Array(arrayBuffer); - - // Convert to base64 - const base64 = btoa( - Array(bytes.length) - .fill("") - .map((_, i) => String.fromCharCode(bytes[i])) - .join(""), - ); - - return { - success: true, - data: base64, - contentType, - }; - } catch (error: unknown) { - const message = error instanceof Error ? error.message : String(error); - console.error("Error downloading file:", error); - return { - success: false, - message: `Error downloading file: ${message}`, - }; - } -} diff --git a/slack/actions/files/info.ts b/slack/actions/files/info.ts deleted file mode 100644 index a8901cbb..00000000 --- a/slack/actions/files/info.ts +++ /dev/null @@ -1,43 +0,0 @@ -import type { AppContext } from "../../mod.ts"; -import { SlackFile } from "../../client.ts"; - -export interface FileInfoProps { - /** - * @description The ID of the file to get information about - */ - fileId: string; -} - -/** - * @description Gets detailed information about a file - */ -export default async function fileInfo( - props: FileInfoProps, - _req: Request, - ctx: AppContext, -): Promise<{ success: boolean; file?: SlackFile; message?: string }> { - try { - const fileResponse = await ctx.slack.getFileInfo(props.fileId); - - if (!fileResponse.ok) { - return { - success: false, - message: `Failed to get file info: ${ - fileResponse.error || "Unknown error" - }`, - }; - } - - return { - success: true, - file: fileResponse.data.file, - }; - } catch (error: unknown) { - const message = error instanceof Error ? error.message : String(error); - console.error("Error getting file info:", error); - return { - success: false, - message: `Error getting file info: ${message}`, - }; - } -} From 964801865cd2bca9c7a91a8353bbd283204a5993 Mon Sep 17 00:00:00 2001 From: yuriassuncx Date: Sat, 13 Sep 2025 00:36:08 -0300 Subject: [PATCH 03/18] refactor(slack): Reorder and optimize SCOPES in constants.ts --- slack/utils/constants.ts | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/slack/utils/constants.ts b/slack/utils/constants.ts index 3cf972c6..6eac7e80 100644 --- a/slack/utils/constants.ts +++ b/slack/utils/constants.ts @@ -1,18 +1,19 @@ export const SCOPES = [ - "channels:read", - "chat:write", - "reactions:write", - "channels:history", - "users:read", - "users:read.email", "app_mentions:read", + "channels:history", "channels:join", + "channels:read", + "chat:write", + "files:read", + "files:write", "groups:read", "im:history", "im:read", "im:write", - "files:read", - "files:write", + "mpim:read", + "reactions:write", + "users:read", + "users:read.email", ]; export const API_URL = "https://slack.com/api"; From 9479dcb815c4ab411befd53dd1b2dbf2429c2409 Mon Sep 17 00:00:00 2001 From: "@yuri_assuncx" Date: Sat, 13 Sep 2025 00:39:03 -0300 Subject: [PATCH 04/18] fix: improve openDmChannel function suggested by coderabbitai Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> --- slack/loaders/dms/history.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/slack/loaders/dms/history.ts b/slack/loaders/dms/history.ts index 34c529b1..7f3a6a1d 100644 --- a/slack/loaders/dms/history.ts +++ b/slack/loaders/dms/history.ts @@ -30,12 +30,12 @@ export default async function dmHistory( return []; } - const channelId = channelResponse.channel?.id; + const channelId = + channelResponse.data?.channel?.id ?? channelResponse.channel?.id; if (!channelId) { console.error("No channel ID returned for user", props.userId); return []; } - const limit = props.limit || 10; const historyResponse = await ctx.slack.getChannelHistory(channelId, limit); From eb6f3573563ae91d6d5593091383fb1efde3a66c Mon Sep 17 00:00:00 2001 From: yuriassuncx Date: Sat, 13 Sep 2025 00:46:52 -0300 Subject: [PATCH 05/18] feat(slack): add pagination support to channel and DM history retrieval --- slack/client.ts | 13 +++++++------ slack/loaders/channels/history.ts | 8 ++++++-- slack/loaders/dms/history.ts | 3 +-- slack/loaders/dms/list.ts | 2 +- slack/utils/client.ts | 7 +------ 5 files changed, 16 insertions(+), 17 deletions(-) diff --git a/slack/client.ts b/slack/client.ts index 5669cbeb..782e78c0 100644 --- a/slack/client.ts +++ b/slack/client.ts @@ -394,10 +394,12 @@ export class SlackClient { * @description Gets message history from a channel * @param channelId The channel ID * @param limit Maximum number of messages to return + * @param cursor Pagination cursor for next page */ async getChannelHistory( channelId: string, limit: number = 10, + cursor?: string, ): Promise> { const params = new URLSearchParams({ channel: channelId, limit: limit.toString(), }); + if (cursor) { + params.append("cursor", cursor); + } + const response = await fetch( `https://slack.com/api/conversations.history?${params}`, { headers: this.botHeaders }, @@ -431,7 +434,6 @@ export class SlackClient { channel_actions_ts: result.channel_actions_ts, channel_actions_count: result.channel_actions_count, warning: result.warning, - response_metadata: result.response_metadata, }, }; } @@ -581,7 +583,7 @@ export class SlackClient { async listDmChannels( limit: number = 100, cursor?: string, - ): Promise> { + ): Promise> { const params = new URLSearchParams({ types: "im", limit: Math.min(limit, 100).toString(), @@ -603,7 +605,6 @@ export class SlackClient { response_metadata: result.response_metadata, data: { channels: result.channels || [], - response_metadata: result.response_metadata, }, }; } diff --git a/slack/loaders/channels/history.ts b/slack/loaders/channels/history.ts index 6c8f3dcf..09c198d1 100644 --- a/slack/loaders/channels/history.ts +++ b/slack/loaders/channels/history.ts @@ -11,6 +11,10 @@ export interface Props { * @default 10 */ limit?: number; + /** + * @description Pagination cursor for next page + */ + cursor?: string; } /** @@ -23,6 +27,6 @@ export default async function getChannelHistory( _req: Request, ctx: AppContext, ): Promise> { - const { channelId, limit } = props; - return await ctx.slack.getChannelHistory(channelId, limit); + const { channelId, limit, cursor } = props; + return await ctx.slack.getChannelHistory(channelId, limit, cursor); } diff --git a/slack/loaders/dms/history.ts b/slack/loaders/dms/history.ts index 7f3a6a1d..2916e0c9 100644 --- a/slack/loaders/dms/history.ts +++ b/slack/loaders/dms/history.ts @@ -30,8 +30,7 @@ export default async function dmHistory( return []; } - const channelId = - channelResponse.data?.channel?.id ?? channelResponse.channel?.id; + const channelId = channelResponse.channel?.id; if (!channelId) { console.error("No channel ID returned for user", props.userId); return []; diff --git a/slack/loaders/dms/list.ts b/slack/loaders/dms/list.ts index 318fb493..bfacbbb3 100644 --- a/slack/loaders/dms/list.ts +++ b/slack/loaders/dms/list.ts @@ -33,7 +33,7 @@ export default async function listDms( return { channels: dmResponse.data.channels, - next_cursor: dmResponse.data.response_metadata?.next_cursor, + next_cursor: dmResponse.response_metadata?.next_cursor, }; } catch (error) { console.error("Error listing DM channels:", error); diff --git a/slack/utils/client.ts b/slack/utils/client.ts index 225385b6..d553952d 100644 --- a/slack/utils/client.ts +++ b/slack/utils/client.ts @@ -19,9 +19,6 @@ export interface SlackApiClient { }; response: SlackResponse<{ channels: SlackChannel[]; - response_metadata?: { - next_cursor?: string; - }; }>; }; "POST /chat.postMessage": { @@ -53,6 +50,7 @@ export interface SlackApiClient { searchParams: { channel: string; limit?: string; + cursor?: string; }; response: SlackResponse<{ messages: SlackMessage[]; @@ -61,9 +59,6 @@ export interface SlackApiClient { channel_actions_ts?: string | null; channel_actions_count?: number; warning?: string; - response_metadata?: { - warnings?: string[]; - }; }>; }; "GET /conversations.replies": { From 9b7395b42d8954386e62d6b982529a43c58d7207 Mon Sep 17 00:00:00 2001 From: yuriassuncx Date: Sat, 13 Sep 2025 00:51:01 -0300 Subject: [PATCH 06/18] feat(slack): enhance file upload and message posting with improved response structures and pagination support --- slack/actions/files/upload.ts | 31 ++----------------------------- slack/actions/messages/post.ts | 14 +++++++++++--- slack/loaders/channels/history.ts | 9 ++++++++- slack/loaders/dms/history.ts | 7 ++++++- 4 files changed, 27 insertions(+), 34 deletions(-) diff --git a/slack/actions/files/upload.ts b/slack/actions/files/upload.ts index 2eb2697e..7fa994ed 100644 --- a/slack/actions/files/upload.ts +++ b/slack/actions/files/upload.ts @@ -1,4 +1,5 @@ import type { AppContext } from "../../mod.ts"; +import { SlackFile } from "../../client.ts"; export interface UploadFileProps { /** @@ -34,35 +35,7 @@ export interface UploadFileProps { export interface UploadFileResponse { ok: boolean; - file?: { - id: string; - created: number; - timestamp: number; - name: string; - title: string; - mimetype: string; - filetype: string; - pretty_type: string; - user: string; - editable: boolean; - size: number; - mode: string; - is_external: boolean; - external_type: string; - is_public: boolean; - public_url_shared: boolean; - display_as_bot: boolean; - username: string; - url_private: string; - url_private_download: string; - permalink: string; - permalink_public?: string; - channels: string[]; - groups: string[]; - ims: string[]; - comments_count: number; - is_starred?: boolean; - }; + file?: SlackFile; error?: string; warning?: string; response_metadata?: { diff --git a/slack/actions/messages/post.ts b/slack/actions/messages/post.ts index b2a7de97..d56793e3 100644 --- a/slack/actions/messages/post.ts +++ b/slack/actions/messages/post.ts @@ -21,9 +21,17 @@ export default async function postMessage( props: Props, _req: Request, ctx: AppContext, -): Promise< - { channel: string; ts: string; message: SlackMessage } -> { +): Promise<{ + ok: boolean; + channel: string; + ts: string; + message: SlackMessage; + warning?: string; + response_metadata?: { + warnings?: string[]; + }; + error?: string; +}> { const { channelId, text } = props; return await ctx.slack.postMessage(channelId, text); } diff --git a/slack/loaders/channels/history.ts b/slack/loaders/channels/history.ts index 09c198d1..1b1e94da 100644 --- a/slack/loaders/channels/history.ts +++ b/slack/loaders/channels/history.ts @@ -26,7 +26,14 @@ export default async function getChannelHistory( props: Props, _req: Request, ctx: AppContext, -): Promise> { +): Promise> { const { channelId, limit, cursor } = props; return await ctx.slack.getChannelHistory(channelId, limit, cursor); } diff --git a/slack/loaders/dms/history.ts b/slack/loaders/dms/history.ts index 2916e0c9..278e5ef6 100644 --- a/slack/loaders/dms/history.ts +++ b/slack/loaders/dms/history.ts @@ -12,6 +12,11 @@ export interface Props { * @default 10 */ limit?: number; + + /** + * @description Pagination cursor for next page + */ + cursor?: string; } /** @@ -37,7 +42,7 @@ export default async function dmHistory( } const limit = props.limit || 10; - const historyResponse = await ctx.slack.getChannelHistory(channelId, limit); + const historyResponse = await ctx.slack.getChannelHistory(channelId, limit, props.cursor); if (!historyResponse.ok) { console.error("Failed to get DM history:", historyResponse.error); From 92b49f10885c7a1a468409e0a77c2228f1d043e0 Mon Sep 17 00:00:00 2001 From: yuriassuncx Date: Sat, 13 Sep 2025 01:02:27 -0300 Subject: [PATCH 07/18] feat(slack): add titles to various actions for improved clarity and documentation --- slack/actions/deco-chat/channels/invoke.ts | 1 + slack/actions/deco-chat/channels/join.ts | 1 + slack/actions/deco-chat/channels/leave.ts | 1 + slack/actions/dms/send.ts | 2 ++ slack/actions/files/upload.ts | 2 ++ slack/loaders/conversations/open.ts | 2 ++ slack/loaders/deco-chat/channels/list.ts | 3 ++- slack/loaders/dms/history.ts | 2 ++ slack/loaders/dms/list.ts | 2 ++ slack/loaders/files/list.ts | 2 ++ 10 files changed, 17 insertions(+), 1 deletion(-) diff --git a/slack/actions/deco-chat/channels/invoke.ts b/slack/actions/deco-chat/channels/invoke.ts index d25a9ee5..c70b2221 100644 --- a/slack/actions/deco-chat/channels/invoke.ts +++ b/slack/actions/deco-chat/channels/invoke.ts @@ -4,6 +4,7 @@ import type { AppContext, SlackWebhookPayload } from "../../../mod.ts"; /** * @name DECO_CHAT_CHANNELS_INVOKE + * @title Deco Chat Channel Invoke * @description This action is triggered when slack sends a webhook event */ export default async function invoke( diff --git a/slack/actions/deco-chat/channels/join.ts b/slack/actions/deco-chat/channels/join.ts index dc485d43..c203ed41 100644 --- a/slack/actions/deco-chat/channels/join.ts +++ b/slack/actions/deco-chat/channels/join.ts @@ -3,6 +3,7 @@ import type { AppContext } from "../../../mod.ts"; /** * @name DECO_CHAT_CHANNELS_JOIN + * @title Deco Chat Channel Join * @description This action is triggered when channel is selected */ export default async function join( diff --git a/slack/actions/deco-chat/channels/leave.ts b/slack/actions/deco-chat/channels/leave.ts index 6e04c6a8..b4111fe6 100644 --- a/slack/actions/deco-chat/channels/leave.ts +++ b/slack/actions/deco-chat/channels/leave.ts @@ -3,6 +3,7 @@ import type { AppContext } from "../../../mod.ts"; /** * @name DECO_CHAT_CHANNELS_LEAVE + * @title Deco Chat Channel Leave * @description This action is triggered when slack integration is left */ export default async function leave( diff --git a/slack/actions/dms/send.ts b/slack/actions/dms/send.ts index b00b5c41..53159560 100644 --- a/slack/actions/dms/send.ts +++ b/slack/actions/dms/send.ts @@ -34,6 +34,8 @@ export interface SendDmResponse { } /** + * @name DMS_SEND + * @title Send DM * @description Sends a direct message to a user * @action send-dm */ diff --git a/slack/actions/files/upload.ts b/slack/actions/files/upload.ts index 7fa994ed..29220e55 100644 --- a/slack/actions/files/upload.ts +++ b/slack/actions/files/upload.ts @@ -44,6 +44,8 @@ export interface UploadFileResponse { } /** + * @name FILES_UPLOAD + * @title Upload File * @description Uploads a file to Slack * @action upload-file */ diff --git a/slack/loaders/conversations/open.ts b/slack/loaders/conversations/open.ts index 00f931e9..07b88c7c 100644 --- a/slack/loaders/conversations/open.ts +++ b/slack/loaders/conversations/open.ts @@ -22,6 +22,8 @@ export interface ConversationOpenResponse { } /** + * @name CONVERSATIONS_OPEN + * @title Open DM Conversation * @description Opens a direct message conversation with a user */ export default async function openConversation( diff --git a/slack/loaders/deco-chat/channels/list.ts b/slack/loaders/deco-chat/channels/list.ts index 2e126f61..b2a13325 100644 --- a/slack/loaders/deco-chat/channels/list.ts +++ b/slack/loaders/deco-chat/channels/list.ts @@ -7,6 +7,7 @@ import type { AppContext } from "../../../mod.ts"; export const DECO_CHAT_CHANNEL_ID = "@deco.chat"; /** * @name DECO_CHAT_CHANNELS_LIST + * @title Deco Chat Channels List * @description This action is triggered when slack channels are needed */ export default async function list( @@ -19,7 +20,7 @@ export default async function list( channels: [ ...channels.channels.map((ch) => { return { - label: ch.name, + label: ch.name ?? "", value: ch.id, }; }), diff --git a/slack/loaders/dms/history.ts b/slack/loaders/dms/history.ts index 278e5ef6..73ea843d 100644 --- a/slack/loaders/dms/history.ts +++ b/slack/loaders/dms/history.ts @@ -20,6 +20,8 @@ export interface Props { } /** + * @name DMS_HISTORY + * @title DM Conversation History * @description Lists messages in a direct message conversation with a user */ export default async function dmHistory( diff --git a/slack/loaders/dms/list.ts b/slack/loaders/dms/list.ts index bfacbbb3..1bf161d5 100644 --- a/slack/loaders/dms/list.ts +++ b/slack/loaders/dms/list.ts @@ -15,6 +15,8 @@ export interface Props { } /** + * @name DMS_LIST + * @title List DM Channels * @description Lists all direct message channels for the bot */ export default async function listDms( diff --git a/slack/loaders/files/list.ts b/slack/loaders/files/list.ts index f90ccea5..5e22e288 100644 --- a/slack/loaders/files/list.ts +++ b/slack/loaders/files/list.ts @@ -39,6 +39,8 @@ export interface FilesListResponse { } /** + * @name FILES_LIST + * @title List User Files * @description Lists files uploaded by a specific user */ export default async function listUserFiles( From f52a5c42767209f452d0ccfe951a7e159cc4344b Mon Sep 17 00:00:00 2001 From: "@yuri_assuncx" Date: Sat, 13 Sep 2025 01:14:40 -0300 Subject: [PATCH 08/18] fix: improvements suggested by coderabbitai Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> --- slack/client.ts | 21 +++++++++++++++------ 1 file changed, 15 insertions(+), 6 deletions(-) diff --git a/slack/client.ts b/slack/client.ts index 782e78c0..8e8bab34 100644 --- a/slack/client.ts +++ b/slack/client.ts @@ -311,17 +311,26 @@ export class SlackClient { channelId: string, text: string, opts: { thread_ts?: string; blocks?: unknown[] } = {}, - ): Promise<{ - ok: boolean; + ): Promise> { + // ... existing logic that makes the HTTP request and sets `response` ... + const result = await response.json(); + return { + ok: result.ok, + error: result.error, + response_metadata: result.response_metadata, + data: { + channel: result.channel, + ts: result.ts, + message: result.message, + warning: result.warning, + }, }; - error?: string; - }> { + } const payload: Record = { channel: channelId, text: text, From 433be8fd7f8e15aae9957652f1992897891c0b9c Mon Sep 17 00:00:00 2001 From: yuriassuncx Date: Sun, 14 Sep 2025 12:08:55 -0300 Subject: [PATCH 09/18] feat(slack): enhance Slack API response handling with improved data structures and optional fields --- slack/client.ts | 210 ++++++++++++++++++++++++++++++++++-------------- 1 file changed, 151 insertions(+), 59 deletions(-) diff --git a/slack/client.ts b/slack/client.ts index 8e8bab34..2cf24b0d 100644 --- a/slack/client.ts +++ b/slack/client.ts @@ -9,6 +9,7 @@ export interface SlackResponse { error?: string; response_metadata?: { next_cursor?: string; + warnings?: string[]; }; data: T; } @@ -177,10 +178,10 @@ export interface SlackFile { original_h?: number; permalink: string; permalink_public?: string; - channels: string[]; - groups: string[]; - ims: string[]; - comments_count: number; + channels?: string[]; + groups?: string[]; + ims?: string[]; + comments_count?: number; is_starred?: boolean; } @@ -280,7 +281,7 @@ export class SlackClient { { headers: this.botHeaders }, ); - return response.json(); + return await response.json(); } /** @@ -298,7 +299,15 @@ export class SlackClient { }), }); - return response.json(); + const result = await response.json(); + return { + ok: result.ok, + error: result.error, + response_metadata: result.response_metadata, + data: { + channel: result.channel, + }, + }; } /** @@ -317,20 +326,6 @@ export class SlackClient { message: SlackMessage; warning?: string; }>> { - // ... existing logic that makes the HTTP request and sets `response` ... - const result = await response.json(); - return { - ok: result.ok, - error: result.error, - response_metadata: result.response_metadata, - data: { - channel: result.channel, - ts: result.ts, - message: result.message, - warning: result.warning, - }, - }; - } const payload: Record = { channel: channelId, text: text, @@ -346,7 +341,18 @@ export class SlackClient { body: JSON.stringify(payload), }); - return response.json(); + const result = await response.json(); + return { + ok: result.ok, + error: result.error, + response_metadata: result.response_metadata, + data: { + channel: result.channel, + ts: result.ts, + message: result.message, + warning: result.warning, + }, + }; } /** @@ -372,7 +378,17 @@ export class SlackClient { }), }); - return response.json(); + const result = await response.json(); + return { + ok: result.ok, + error: result.error, + response_metadata: result.response_metadata, + data: { + channel: result.channel, + ts: result.ts, + message: result.message, + }, + }; } /** @@ -396,7 +412,16 @@ export class SlackClient { }), }); - return response.json(); + const result = await response.json(); + return { + ok: result.ok, + error: result.error, + response_metadata: result.response_metadata, + data: { + channel: result.channel || channelId, + ts: result.ts || timestamp, + }, + }; } /** @@ -466,7 +491,15 @@ export class SlackClient { { headers: this.botHeaders }, ); - return response.json(); + const result = await response.json(); + return { + ok: result.ok, + error: result.error, + response_metadata: result.response_metadata, + data: { + messages: result.messages || [], + }, + }; } /** @@ -493,7 +526,15 @@ export class SlackClient { headers: this.botHeaders, }); - return response.json(); + const result = await response.json(); + return { + ok: result.ok, + error: result.error, + response_metadata: result.response_metadata, + data: { + members: result.members || [], + }, + }; } /** @@ -513,7 +554,15 @@ export class SlackClient { { headers: this.botHeaders }, ); - return response.json(); + const result = await response.json(); + return { + ok: result.ok, + error: result.error, + response_metadata: result.response_metadata, + data: { + profile: result.profile || {}, + }, + }; } /** @@ -524,7 +573,13 @@ export class SlackClient { headers: this.botHeaders, }); - return response.json(); + const result = await response.json(); + return { + ok: result.ok, + error: result.error, + response_metadata: result.response_metadata, + data: result, + }; } /** @@ -539,9 +594,11 @@ export class SlackClient { ts: string, text: string, opts: { thread_ts?: string; blocks?: unknown[] } = {}, - ): Promise< - { channel: string; ts: string; message: SlackMessage; ok: boolean } - > { + ): Promise> { const payload: Record = { channel: channelId, ts: ts, @@ -557,31 +614,48 @@ export class SlackClient { headers: this.botHeaders, body: JSON.stringify(payload), }); - return response.json(); + + const result = await response.json(); + return { + ok: result.ok, + error: result.error, + response_metadata: result.response_metadata, + data: { + channel: result.channel, + ts: result.ts, + message: result.message, + }, + }; } /** * @description Opens a direct message channel with a user * @param userId The user ID to open a DM with */ - async openDmChannel(userId: string): Promise<{ - ok: boolean; + async openDmChannel(userId: string): Promise { + }>> { const response = await fetch("https://slack.com/api/conversations.open", { method: "POST", headers: this.botHeaders, body: JSON.stringify({ users: userId }), }); - return response.json(); + const result = await response.json(); + return { + ok: result.ok, + error: result.error, + response_metadata: result.response_metadata, + data: { + channel: result.channel, + no_op: result.no_op, + already_open: result.already_open, + warning: result.warning, + }, + }; } /** @@ -630,8 +704,7 @@ export class SlackClient { count: number = 20, page: number = 1, types: string = 'all' - ): Promise<{ - ok: boolean; + ): Promise { + }>> { const params = new URLSearchParams({ user: userId, count: count.toString(), @@ -653,7 +725,21 @@ export class SlackClient { { headers: this.botHeaders }, ); - return response.json(); + const result = await response.json(); + return { + ok: result.ok, + error: result.error, + response_metadata: result.response_metadata, + data: { + files: result.files || [], + paging: result.paging || { + count: 0, + total: 0, + page: 1, + pages: 0, + }, + }, + }; } /** @@ -667,15 +753,10 @@ export class SlackClient { title?: string; initial_comment?: string; filetype?: string; - }): Promise<{ - ok: boolean; + }): Promise { + }>> { const formData = new FormData(); formData.append("channels", options.channels); formData.append("filename", options.filename); @@ -694,14 +775,16 @@ export class SlackClient { // Handle file content if (typeof options.file === "string") { - // Assume base64 string, convert to Blob - const byteCharacters = atob(options.file); - const byteNumbers = new Array(byteCharacters.length); - for (let i = 0; i < byteCharacters.length; i++) { - byteNumbers[i] = byteCharacters.charCodeAt(i); + // Accept raw base64 or data URL: data:;base64, + let input = options.file; + let mime: string | undefined; + const m = /^data:([^;]+);base64,/.exec(input); + if (m) { + mime = m[1]; + input = input.slice(m[0].length); } - const byteArray = new Uint8Array(byteNumbers); - const blob = new Blob([byteArray]); + const bytes = Uint8Array.from(atob(input), (c) => c.charCodeAt(0)); + const blob = new Blob([bytes], mime ? { type: mime } : undefined); formData.append("file", blob, options.filename); } else { formData.append("file", options.file, options.filename); @@ -715,6 +798,15 @@ export class SlackClient { body: formData, }); - return response.json(); + const result = await response.json(); + return { + ok: result.ok, + error: result.error, + response_metadata: result.response_metadata, + data: { + file: result.file, + warning: result.warning, + }, + }; } } From 8d9e729f0ddf5af20a9d498f2adf243f1fa11ad0 Mon Sep 17 00:00:00 2001 From: "@yuri_assuncx" Date: Sun, 14 Sep 2025 12:55:53 -0300 Subject: [PATCH 10/18] feat: new improvements suggested by coderabbit Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> --- slack/client.ts | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/slack/client.ts b/slack/client.ts index 2cf24b0d..49880895 100644 --- a/slack/client.ts +++ b/slack/client.ts @@ -747,18 +747,19 @@ export class SlackClient { * @param options Upload options including channels, file, filename, etc. */ async uploadFile(options: { - channels: string; + channels?: string; file: string | File; filename: string; title?: string; initial_comment?: string; filetype?: string; + thread_ts?: string; }): Promise> { const formData = new FormData(); - formData.append("channels", options.channels); + if (options.channels) formData.append("channels", options.channels); formData.append("filename", options.filename); if (options.title) { @@ -773,6 +774,8 @@ export class SlackClient { formData.append("filetype", options.filetype); } + if (options.thread_ts) formData.append("thread_ts", options.thread_ts); + // Handle file content if (typeof options.file === "string") { // Accept raw base64 or data URL: data:;base64, @@ -783,7 +786,11 @@ export class SlackClient { mime = m[1]; input = input.slice(m[0].length); } - const bytes = Uint8Array.from(atob(input), (c) => c.charCodeAt(0)); + const bin = (typeof atob === "function") + ? atob(input) + // @ts-ignore Node fallback + : Buffer.from(input, "base64").toString("binary"); + const bytes = Uint8Array.from(bin, (c) => c.charCodeAt(0)); const blob = new Blob([bytes], mime ? { type: mime } : undefined); formData.append("file", blob, options.filename); } else { From 5d770cdb5a7758f7a3b84c30a6bc9588ddea7b94 Mon Sep 17 00:00:00 2001 From: yuriassuncx Date: Sun, 14 Sep 2025 17:24:15 -0300 Subject: [PATCH 11/18] feat(slack): implement new file upload V2 API and update documentation for migration --- slack/README.md | 29 +++++- slack/actions/files/upload.ts | 91 +++++++++++++++-- slack/actions/files/uploadV2.ts | 94 ++++++++++++++++++ slack/client.ts | 166 +++++++++++++++++++++++++++++++- slack/manifest.gen.ts | 22 +++-- 5 files changed, 381 insertions(+), 21 deletions(-) create mode 100644 slack/actions/files/uploadV2.ts diff --git a/slack/README.md b/slack/README.md index b67f68bf..6984c687 100644 --- a/slack/README.md +++ b/slack/README.md @@ -9,7 +9,18 @@ A Deco app for integrating with Slack using OAuth 2.0 authentication. This app a - 📋 **Channel Operations** - List and interact with workspace channels - 👥 **User Management** - Get user information and profiles - 🎯 **Reactions** - Add emoji reactions to messages -- 🔄 **Automatic Token Refresh** - Handles token expiration automatically +- � **File Upload V2** - Upload files using the new Slack API (files.upload sunset Nov 12, 2025) +- �🔄 **Automatic Token Refresh** - Handles token expiration automatically + +## ⚠️ IMPORTANT: File Upload API Migration + +**The Slack `files.upload` API will be sunset on November 12, 2025.** This app now includes: + +- ✅ **New V2 Upload API** - Using `files.getUploadURLExternal` + `files.completeUploadExternal` +- ⚠️ **Legacy API Support** - With deprecation warnings (will be removed) +- 📖 **Migration Guide** - See [UPLOAD_MIGRATION.md](./UPLOAD_MIGRATION.md) for details + +**Action Required**: Update your file upload code to use `uploadFileV2()` method before November 12, 2025. ## Setup Instructions @@ -78,6 +89,22 @@ After OAuth setup, the app will automatically: ### Reactions - `addReaction(channelId, timestamp, reaction)` - Add emoji reaction +### File Upload +- `uploadFileV2(options)` - Upload files using new V2 API (recommended) +- `uploadFile(options)` - Upload files using legacy API (deprecated, shows warning) + +**New V2 Upload Example:** +```typescript +const response = await slack.uploadFileV2({ + channels: "C1234567890", // Optional + file: fileBlob, // Supports Uint8Array, Blob, File, base64, data URL + filename: "document.pdf", + title: "Important Document", + thread_ts: "1234567890.123456", // Optional - upload to thread + initial_comment: "Here's the document" +}); +``` + ## Migration from Bot Token If you're migrating from the previous bot token approach: diff --git a/slack/actions/files/upload.ts b/slack/actions/files/upload.ts index 29220e55..75f5aa91 100644 --- a/slack/actions/files/upload.ts +++ b/slack/actions/files/upload.ts @@ -8,9 +8,9 @@ export interface UploadFileProps { channels: string; /** - * @description File content as base64 string or File object + * @description File content as base64 string, data URL, Uint8Array, Blob, or File object */ - file: string | File; + file: string | Uint8Array | Blob | File; /** * @description Name of the file @@ -28,14 +28,34 @@ export interface UploadFileProps { initial_comment?: string; /** - * @description File type (optional, usually auto-detected) + * @description File type (optional, usually auto-detected) - only used with legacy API */ filetype?: string; + + /** + * @description Thread timestamp to upload file to a specific thread + */ + thread_ts?: string; + + /** + * @description Use legacy files.upload API instead of the new V2 API (not recommended) + * @default false + */ + useLegacyApi?: boolean; } export interface UploadFileResponse { ok: boolean; - file?: SlackFile; + files?: Array<{ + id: string; + title?: string; + name?: string; + mimetype?: string; + filetype?: string; + permalink?: string; + url_private?: string; + }>; + file?: SlackFile; // Legacy field for backwards compatibility error?: string; warning?: string; response_metadata?: { @@ -46,7 +66,7 @@ export interface UploadFileResponse { /** * @name FILES_UPLOAD * @title Upload File - * @description Uploads a file to Slack + * @description Uploads a file to Slack using the new V2 API by default, with fallback to legacy API * @action upload-file */ export default async function uploadFile( @@ -55,9 +75,53 @@ export default async function uploadFile( ctx: AppContext, ): Promise { try { + // Use V2 API by default unless explicitly requested to use legacy + if (!props.useLegacyApi) { + const response = await ctx.slack.uploadFileV2({ + channels: props.channels, + file: props.file, + filename: props.filename, + title: props.title, + thread_ts: props.thread_ts, + initial_comment: props.initial_comment, + }); + + if (!response.ok) { + return { + ok: false, + files: [], + error: response.error || "Failed to upload file with V2 API", + }; + } + + return { + ok: response.ok, + files: response.data.files, + // For backwards compatibility, set file to first uploaded file + file: response.data.files[0] ? { + id: response.data.files[0].id, + name: response.data.files[0].name || props.filename, + title: response.data.files[0].title || props.title || props.filename, + mimetype: response.data.files[0].mimetype || "", + filetype: response.data.files[0].filetype || "", + permalink: response.data.files[0].permalink || "", + url_private: response.data.files[0].url_private || "", + } as SlackFile : undefined, + response_metadata: response.response_metadata, + }; + } + + // Legacy API fallback + if (typeof props.file !== "string" && !(props.file instanceof File)) { + return { + ok: false, + error: "Legacy API only supports string (base64) or File objects. Use V2 API for other file types.", + }; + } + const response = await ctx.slack.uploadFile({ channels: props.channels, - file: props.file, + file: props.file as string | File, filename: props.filename, title: props.title, initial_comment: props.initial_comment, @@ -67,14 +131,23 @@ export default async function uploadFile( if (!response.ok) { return { ok: false, - error: response.error || "Failed to upload file", + error: response.error || "Failed to upload file with legacy API", }; } return { ok: response.ok, - file: response.file, - warning: response.warning, + file: response.data.file, + files: response.data.file ? [{ + id: response.data.file.id, + name: response.data.file.name, + title: response.data.file.title, + mimetype: response.data.file.mimetype, + filetype: response.data.file.filetype, + permalink: response.data.file.permalink, + url_private: response.data.file.url_private, + }] : [], + warning: response.data.warning, response_metadata: response.response_metadata, }; } catch (error) { diff --git a/slack/actions/files/uploadV2.ts b/slack/actions/files/uploadV2.ts new file mode 100644 index 00000000..5199ebec --- /dev/null +++ b/slack/actions/files/uploadV2.ts @@ -0,0 +1,94 @@ +import type { AppContext } from "../../mod.ts"; + +export interface UploadFileV2Props { + /** + * @description Channel ID or DM ID to send the file to (optional, if not provided file won't be shared to a channel) + */ + channels?: string; + + /** + * @description File content as base64 string, data URL, Uint8Array, Blob, or File object + */ + file: string | Uint8Array | Blob | File; + + /** + * @description Name of the file + */ + filename: string; + + /** + * @description Title of the file + */ + title?: string; + + /** + * @description Thread timestamp to upload file to a specific thread (requires channels to be set) + */ + thread_ts?: string; + + /** + * @description Initial comment/message that accompanies the file + */ + initial_comment?: string; +} + +export interface UploadFileV2Response { + ok: boolean; + files: Array<{ + id: string; + title?: string; + name?: string; + mimetype?: string; + filetype?: string; + permalink?: string; + url_private?: string; + }>; + error?: string; + response_metadata?: { + warnings?: string[]; + }; +} + +/** + * @name FILES_UPLOAD_V2 + * @title Upload File (V2) + * @description Uploads a file to Slack using the new V2 API (files.getUploadURLExternal + files.completeUploadExternal) + * @action upload-file-v2 + */ +export default async function uploadFileV2( + props: UploadFileV2Props, + _req: Request, + ctx: AppContext, +): Promise { + try { + const response = await ctx.slack.uploadFileV2({ + channels: props.channels, + file: props.file, + filename: props.filename, + title: props.title, + thread_ts: props.thread_ts, + initial_comment: props.initial_comment, + }); + + if (!response.ok) { + return { + ok: false, + files: [], + error: response.error || "Failed to upload file", + }; + } + + return { + ok: response.ok, + files: response.data.files, + response_metadata: response.response_metadata, + }; + } catch (error) { + console.error("Error uploading file with V2 API:", error); + return { + ok: false, + files: [], + error: error instanceof Error ? error.message : "Unknown error", + }; + } +} \ No newline at end of file diff --git a/slack/client.ts b/slack/client.ts index 2cf24b0d..17743890 100644 --- a/slack/client.ts +++ b/slack/client.ts @@ -233,6 +233,36 @@ export interface SlackAuthTestResponse { export type ChannelType = "public_channel" | "private_channel" | "mpim" | "im"; +/** + * @description Response from files.getUploadURLExternal endpoint + */ +export interface SlackUploadURLResponse { + ok: boolean; + error?: string; + upload_url?: string; + file_id?: string; +} + +/** + * @description Response from files.completeUploadExternal endpoint + */ +export interface SlackCompleteUploadResponse { + ok: boolean; + error?: string; + files?: Array<{ + id: string; + title?: string; + name?: string; + mimetype?: string; + filetype?: string; + permalink?: string; + url_private?: string; + }>; + response_metadata?: { + warnings?: string[]; + }; +} + /** * @description Client for interacting with Slack APIs */ @@ -741,9 +771,136 @@ export class SlackClient { }, }; } + + /** + * @description Uploads a file to Slack using the new v2 API (files.getUploadURLExternal + files.completeUploadExternal) + * @param options Upload options including channel, file, filename, etc. + */ + async uploadFileV2(options: { + channels?: string; + file: Uint8Array | Blob | string | File; + filename: string; + title?: string; + thread_ts?: string; + initial_comment?: string; + }): Promise; + }>> { + // Convert file to Blob/Uint8Array for size calculation + let fileBlob: Blob; + let fileSize: number; + + if (typeof options.file === "string") { + // Handle base64 or data URL + let input = options.file; + let mime: string | undefined; + const m = /^data:([^;]+);base64,/.exec(input); + if (m) { + mime = m[1]; + input = input.slice(m[0].length); + } + const bytes = Uint8Array.from(atob(input), (c) => c.charCodeAt(0)); + fileBlob = new Blob([bytes], mime ? { type: mime } : undefined); + fileSize = bytes.byteLength; + } else if (options.file instanceof Uint8Array) { + fileBlob = new Blob([options.file]); + fileSize = options.file.byteLength; + } else { + // Assume it's a Blob or File + fileBlob = options.file as Blob; + fileSize = (options.file as Blob).size; + } + + // Step 1: Get upload URL + const getUrlResponse = await fetch("https://slack.com/api/files.getUploadURLExternal", { + method: "POST", + headers: this.botHeaders, + body: JSON.stringify({ + filename: options.filename, + length: fileSize, + }), + }); + + const getUrlResult: SlackUploadURLResponse = await getUrlResponse.json(); + if (!getUrlResult.ok) { + return { + ok: false, + error: getUrlResult.error || "Failed to get upload URL", + data: { files: [] }, + }; + } + + if (!getUrlResult.upload_url || !getUrlResult.file_id) { + return { + ok: false, + error: "Invalid response from files.getUploadURLExternal", + data: { files: [] }, + }; + } + + // Step 2: Upload file to the provided URL (no authorization header) + const uploadResponse = await fetch(getUrlResult.upload_url, { + method: "POST", + body: fileBlob, + }); + + if (!uploadResponse.ok) { + return { + ok: false, + error: `File upload failed: ${uploadResponse.statusText}`, + data: { files: [] }, + }; + } + + // Step 3: Complete the upload + const completePayload: Record = { + files: [{ + id: getUrlResult.file_id, + title: options.title || options.filename, + }], + }; + + if (options.channels) { + completePayload.channel_id = options.channels; + } + + if (options.thread_ts) { + completePayload.thread_ts = options.thread_ts; + } + + if (options.initial_comment) { + completePayload.initial_comment = options.initial_comment; + } + + const completeResponse = await fetch("https://slack.com/api/files.completeUploadExternal", { + method: "POST", + headers: this.botHeaders, + body: JSON.stringify(completePayload), + }); + + const completeResult: SlackCompleteUploadResponse = await completeResponse.json(); + + return { + ok: completeResult.ok, + error: completeResult.error, + response_metadata: completeResult.response_metadata, + data: { + files: completeResult.files || [], + }, + }; + } /** - * @description Uploads a file to Slack + * @description Uploads a file to Slack using the legacy files.upload API + * @deprecated This method uses files.upload which will be sunset on November 12, 2025. Use uploadFileV2 instead. * @param options Upload options including channels, file, filename, etc. */ async uploadFile(options: { @@ -757,6 +914,13 @@ export class SlackClient { file?: SlackFile; warning?: string; }>> { + // Deprecation warning + console.warn( + "⚠️ DEPRECATION WARNING: files.upload API will be sunset on November 12, 2025. " + + "Please migrate to uploadFileV2() which uses the new files.getUploadURLExternal + files.completeUploadExternal flow. " + + "See: https://docs.slack.dev/reference/methods/files.getUploadURLExternal" + ); + const formData = new FormData(); formData.append("channels", options.channels); formData.append("filename", options.filename); diff --git a/slack/manifest.gen.ts b/slack/manifest.gen.ts index 88a95771..61da84c1 100644 --- a/slack/manifest.gen.ts +++ b/slack/manifest.gen.ts @@ -7,11 +7,12 @@ import * as $$$$$$$$$1 from "./actions/deco-chat/channels/join.ts"; import * as $$$$$$$$$2 from "./actions/deco-chat/channels/leave.ts"; import * as $$$$$$$$$3 from "./actions/dms/send.ts"; import * as $$$$$$$$$4 from "./actions/files/upload.ts"; -import * as $$$$$$$$$5 from "./actions/messages/post.ts"; -import * as $$$$$$$$$6 from "./actions/messages/react.ts"; -import * as $$$$$$$$$7 from "./actions/messages/threads/reply.ts"; -import * as $$$$$$$$$8 from "./actions/oauth/callback.ts"; -import * as $$$$$$$$$9 from "./actions/webhook/broker.ts"; +import * as $$$$$$$$$5 from "./actions/files/uploadV2.ts"; +import * as $$$$$$$$$6 from "./actions/messages/post.ts"; +import * as $$$$$$$$$7 from "./actions/messages/react.ts"; +import * as $$$$$$$$$8 from "./actions/messages/threads/reply.ts"; +import * as $$$$$$$$$9 from "./actions/oauth/callback.ts"; +import * as $$$$$$$$$10 from "./actions/webhook/broker.ts"; import * as $$$0 from "./loaders/channels.ts"; import * as $$$1 from "./loaders/channels/history.ts"; import * as $$$2 from "./loaders/conversations/open.ts"; @@ -44,11 +45,12 @@ const manifest = { "slack/actions/deco-chat/channels/leave.ts": $$$$$$$$$2, "slack/actions/dms/send.ts": $$$$$$$$$3, "slack/actions/files/upload.ts": $$$$$$$$$4, - "slack/actions/messages/post.ts": $$$$$$$$$5, - "slack/actions/messages/react.ts": $$$$$$$$$6, - "slack/actions/messages/threads/reply.ts": $$$$$$$$$7, - "slack/actions/oauth/callback.ts": $$$$$$$$$8, - "slack/actions/webhook/broker.ts": $$$$$$$$$9, + "slack/actions/files/uploadV2.ts": $$$$$$$$$5, + "slack/actions/messages/post.ts": $$$$$$$$$6, + "slack/actions/messages/react.ts": $$$$$$$$$7, + "slack/actions/messages/threads/reply.ts": $$$$$$$$$8, + "slack/actions/oauth/callback.ts": $$$$$$$$$9, + "slack/actions/webhook/broker.ts": $$$$$$$$$10, }, "name": "slack", "baseUrl": import.meta.url, From e33e51a97c0b11b1f59315c4844b9ec9e340af08 Mon Sep 17 00:00:00 2001 From: yuriassuncx Date: Sun, 14 Sep 2025 17:31:46 -0300 Subject: [PATCH 12/18] refactor(slack): update response handling to use new data structure in DM and file listing actions --- slack/actions/dms/send.ts | 10 +++++----- slack/actions/messages/post.ts | 23 +++++++++++++---------- slack/client.ts | 26 +++++++++++++++----------- slack/loaders/channels.ts | 34 +++++++++++++++++++++++++--------- slack/loaders/files/list.ts | 4 ++-- 5 files changed, 60 insertions(+), 37 deletions(-) diff --git a/slack/actions/dms/send.ts b/slack/actions/dms/send.ts index 53159560..194d6a45 100644 --- a/slack/actions/dms/send.ts +++ b/slack/actions/dms/send.ts @@ -60,13 +60,13 @@ export default async function sendDm( return { success: true, message: "DM sent successfully", - channelId: messageResponse.channel, - ts: messageResponse.ts, + channelId: messageResponse.data.channel, + ts: messageResponse.data.ts, messageData: { ok: messageResponse.ok, - channel: messageResponse.channel, - ts: messageResponse.ts, - warning: messageResponse.warning, + channel: messageResponse.data.channel, + ts: messageResponse.data.ts, + warning: messageResponse.data.warning, response_metadata: messageResponse.response_metadata, }, }; diff --git a/slack/actions/messages/post.ts b/slack/actions/messages/post.ts index d56793e3..9f3e7593 100644 --- a/slack/actions/messages/post.ts +++ b/slack/actions/messages/post.ts @@ -1,4 +1,4 @@ -import type { SlackMessage } from "../../client.ts"; +import type { SlackMessage, SlackResponse } from "../../client.ts"; import type { AppContext } from "../../mod.ts"; export interface Props { @@ -10,6 +10,14 @@ export interface Props { * @description The message text to post */ text: string; + /** + * @description Thread timestamp to reply to a specific thread + */ + thread_ts?: string; + /** + * @description Blocks for rich formatting (Block Kit) + */ + blocks?: unknown[]; } /** @@ -21,17 +29,12 @@ export default async function postMessage( props: Props, _req: Request, ctx: AppContext, -): Promise<{ - ok: boolean; +): Promise { - const { channelId, text } = props; - return await ctx.slack.postMessage(channelId, text); +}>> { + const { channelId, text, thread_ts, blocks } = props; + return await ctx.slack.postMessage(channelId, text, { thread_ts, blocks }); } diff --git a/slack/client.ts b/slack/client.ts index 45f7d762..73f39f48 100644 --- a/slack/client.ts +++ b/slack/client.ts @@ -292,9 +292,9 @@ export class SlackClient { limit: number = 1000, cursor?: string, types: ChannelType[] = ["public_channel", "private_channel"], - ): Promise< - { channels: SlackChannel[]; response_metadata?: { next_cursor?: string } } - > { + ): Promise> { const params = new URLSearchParams({ types: types.join(","), exclude_archived: "false", @@ -311,7 +311,15 @@ export class SlackClient { { headers: this.botHeaders }, ); - return await response.json(); + const result = await response.json(); + return { + ok: result.ok, + error: result.error, + response_metadata: result.response_metadata, + data: { + channels: result.channels || [], + }, + }; } /** @@ -904,7 +912,7 @@ export class SlackClient { * @param options Upload options including channels, file, filename, etc. */ async uploadFile(options: { - channels?: string; + channels: string; file: string | File; filename: string; title?: string; @@ -923,7 +931,7 @@ export class SlackClient { ); const formData = new FormData(); - if (options.channels) formData.append("channels", options.channels); + formData.append("channels", options.channels); formData.append("filename", options.filename); if (options.title) { @@ -950,11 +958,7 @@ export class SlackClient { mime = m[1]; input = input.slice(m[0].length); } - const bin = (typeof atob === "function") - ? atob(input) - // @ts-ignore Node fallback - : Buffer.from(input, "base64").toString("binary"); - const bytes = Uint8Array.from(bin, (c) => c.charCodeAt(0)); + const bytes = Uint8Array.from(atob(input), (c) => c.charCodeAt(0)); const blob = new Blob([bytes], mime ? { type: mime } : undefined); formData.append("file", blob, options.filename); } else { diff --git a/slack/loaders/channels.ts b/slack/loaders/channels.ts index 5cb08c3c..77f3ee57 100644 --- a/slack/loaders/channels.ts +++ b/slack/loaders/channels.ts @@ -1,4 +1,4 @@ -import type { ChannelType, SlackChannel } from "../client.ts"; +import type { ChannelType, SlackChannel, SlackResponse } from "../client.ts"; import type { AppContext } from "../mod.ts"; export interface Props { @@ -27,7 +27,7 @@ export default async function listChannels( props: Props, _req: Request, ctx: AppContext, -): Promise<{ channels: SlackChannel[] }> { +): Promise> { const { limit, cursor, types } = props; const teamId = ctx.teamId; @@ -39,26 +39,42 @@ export default async function listChannels( if (!limit) { // fetch all channels in loop - const allChannels = []; + const allChannels: SlackChannel[] = []; let nextCursor = cursor; while (true) { try { const response = await ctx.slack.getChannels( teamId, - limit, + 1000, // Use max limit for pagination nextCursor, types, ); - allChannels.push(...response.channels); + + if (!response.ok) { + return { + ok: false, + error: response.error || "Failed to fetch channels", + data: { channels: [] }, + }; + } + + allChannels.push(...response.data.channels); nextCursor = response.response_metadata?.next_cursor; - if (!nextCursor || response.channels.length === 0) { + if (!nextCursor || response.data.channels.length === 0) { break; } - } catch { - break; + } catch (error) { + return { + ok: false, + error: error instanceof Error ? error.message : "Unknown error", + data: { channels: [] }, + }; } } - return { channels: allChannels }; + return { + ok: true, + data: { channels: allChannels }, + }; } return await ctx.slack.getChannels(teamId, limit, cursor, types); diff --git a/slack/loaders/files/list.ts b/slack/loaders/files/list.ts index 5e22e288..a6f9e27d 100644 --- a/slack/loaders/files/list.ts +++ b/slack/loaders/files/list.ts @@ -67,8 +67,8 @@ export default async function listUserFiles( return { ok: response.ok, - files: response.files || [], - paging: response.paging || { count: 0, total: 0, page: 1, pages: 0 }, + files: response.data.files || [], + paging: response.data.paging || { count: 0, total: 0, page: 1, pages: 0 }, }; } catch (error) { console.error("Error listing user files:", error); From fa3635c6f0ca5c371aa72f236798ddb2933975ae Mon Sep 17 00:00:00 2001 From: "@yuri_assuncx" Date: Mon, 15 Sep 2025 17:54:12 -0300 Subject: [PATCH 13/18] feat: new improvements added by coderabbit Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> --- slack/actions/files/upload.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/slack/actions/files/upload.ts b/slack/actions/files/upload.ts index 75f5aa91..234e1487 100644 --- a/slack/actions/files/upload.ts +++ b/slack/actions/files/upload.ts @@ -126,6 +126,7 @@ export default async function uploadFile( title: props.title, initial_comment: props.initial_comment, filetype: props.filetype, + thread_ts: props.thread_ts, }); if (!response.ok) { From c4d719febb0dfa5b5606b64605ab8b471f529685 Mon Sep 17 00:00:00 2001 From: yuriassuncx Date: Mon, 15 Sep 2025 17:58:31 -0300 Subject: [PATCH 14/18] refactor(slack): update UploadFileResponse to expose a safe subset of SlackFile for compatibility --- slack/actions/files/upload.ts | 28 +++++++++++++++++----------- 1 file changed, 17 insertions(+), 11 deletions(-) diff --git a/slack/actions/files/upload.ts b/slack/actions/files/upload.ts index 234e1487..efb80268 100644 --- a/slack/actions/files/upload.ts +++ b/slack/actions/files/upload.ts @@ -55,7 +55,11 @@ export interface UploadFileResponse { permalink?: string; url_private?: string; }>; - file?: SlackFile; // Legacy field for backwards compatibility + // For compatibility, expose a safe subset of SlackFile + file?: Pick< + SlackFile, + "id" | "name" | "title" | "mimetype" | "filetype" | "permalink" | "url_private" + >; error?: string; warning?: string; response_metadata?: { @@ -97,16 +101,18 @@ export default async function uploadFile( return { ok: response.ok, files: response.data.files, - // For backwards compatibility, set file to first uploaded file - file: response.data.files[0] ? { - id: response.data.files[0].id, - name: response.data.files[0].name || props.filename, - title: response.data.files[0].title || props.title || props.filename, - mimetype: response.data.files[0].mimetype || "", - filetype: response.data.files[0].filetype || "", - permalink: response.data.files[0].permalink || "", - url_private: response.data.files[0].url_private || "", - } as SlackFile : undefined, + // For backwards compatibility, expose only safe subset of SlackFile + file: response.data.files[0] + ? { + id: response.data.files[0].id, + name: response.data.files[0].name || props.filename, + title: response.data.files[0].title || props.title || props.filename, + mimetype: response.data.files[0].mimetype || "", + filetype: response.data.files[0].filetype || "", + permalink: response.data.files[0].permalink || "", + url_private: response.data.files[0].url_private || "", + } + : undefined, response_metadata: response.response_metadata, }; } From 6dd38c4db5cb438f32cbf988270593ea2251d2ed Mon Sep 17 00:00:00 2001 From: "@yuri_assuncx" Date: Mon, 15 Sep 2025 18:09:08 -0300 Subject: [PATCH 15/18] feat: new improvements to upload action Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> --- slack/actions/files/upload.ts | 29 ++++++++++++++++++++--------- 1 file changed, 20 insertions(+), 9 deletions(-) diff --git a/slack/actions/files/upload.ts b/slack/actions/files/upload.ts index efb80268..e2e68398 100644 --- a/slack/actions/files/upload.ts +++ b/slack/actions/files/upload.ts @@ -98,23 +98,34 @@ export default async function uploadFile( }; } + const first = response.data?.files?.[0]; return { ok: response.ok, - files: response.data.files, + files: + response.data?.files?.map((f) => ({ + id: f.id, + name: f.name ?? props.filename, + title: f.title ?? props.title ?? props.filename, + mimetype: f.mimetype ?? "", + filetype: f.filetype ?? "", + permalink: f.permalink ?? "", + url_private: f.url_private ?? "", + })) ?? [], // For backwards compatibility, expose only safe subset of SlackFile - file: response.data.files[0] + file: first ? { - id: response.data.files[0].id, - name: response.data.files[0].name || props.filename, - title: response.data.files[0].title || props.title || props.filename, - mimetype: response.data.files[0].mimetype || "", - filetype: response.data.files[0].filetype || "", - permalink: response.data.files[0].permalink || "", - url_private: response.data.files[0].url_private || "", + id: first.id, + name: first.name ?? props.filename, + title: first.title ?? props.title ?? props.filename, + mimetype: first.mimetype ?? "", + filetype: first.filetype ?? "", + permalink: first.permalink ?? "", + url_private: first.url_private ?? "", } : undefined, response_metadata: response.response_metadata, }; + }; } // Legacy API fallback From e7d4e1662d98f6ed8df683b7eed3a00303dcaa20 Mon Sep 17 00:00:00 2001 From: "@yuri_assuncx" Date: Mon, 15 Sep 2025 18:09:48 -0300 Subject: [PATCH 16/18] fix: new improvements added to file action by coderrabit Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> --- slack/actions/files/upload.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/slack/actions/files/upload.ts b/slack/actions/files/upload.ts index e2e68398..e989bd07 100644 --- a/slack/actions/files/upload.ts +++ b/slack/actions/files/upload.ts @@ -129,7 +129,10 @@ export default async function uploadFile( } // Legacy API fallback - if (typeof props.file !== "string" && !(props.file instanceof File)) { + if ( + typeof props.file !== "string" && + !(typeof File !== "undefined" && props.file instanceof File) + ) { return { ok: false, error: "Legacy API only supports string (base64) or File objects. Use V2 API for other file types.", From 3f5072d22211f5f7d4f9b0f91160a3f9e7cafe29 Mon Sep 17 00:00:00 2001 From: yuriassuncx Date: Mon, 15 Sep 2025 18:12:18 -0300 Subject: [PATCH 17/18] fix: remove unnecessary closing braces in uploadFile function --- slack/actions/files/upload.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/slack/actions/files/upload.ts b/slack/actions/files/upload.ts index e989bd07..79da7f17 100644 --- a/slack/actions/files/upload.ts +++ b/slack/actions/files/upload.ts @@ -125,8 +125,7 @@ export default async function uploadFile( : undefined, response_metadata: response.response_metadata, }; - }; - } + } // Legacy API fallback if ( From 85f7de745f39a73ff03556c1a8a0fc4ff79e484f Mon Sep 17 00:00:00 2001 From: yuriassuncx Date: Mon, 15 Sep 2025 18:16:38 -0300 Subject: [PATCH 18/18] fix: adjust default channel limit to 100 for consistency in Slack API calls --- slack/client.ts | 2 +- slack/loaders/channels.ts | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/slack/client.ts b/slack/client.ts index 73f39f48..89672fa2 100644 --- a/slack/client.ts +++ b/slack/client.ts @@ -289,7 +289,7 @@ export class SlackClient { */ async getChannels( teamId: string, - limit: number = 1000, + limit: number = 100, cursor?: string, types: ChannelType[] = ["public_channel", "private_channel"], ): Promise> { - const { limit, cursor, types } = props; + const { limit = 100, cursor, types } = props; const teamId = ctx.teamId; if (!teamId) { @@ -45,7 +45,7 @@ export default async function listChannels( try { const response = await ctx.slack.getChannels( teamId, - 1000, // Use max limit for pagination + limit, nextCursor, types, );