feat(slack): canvas related operations (#4306)

* feat(slack): canvas related operations

* extract shared code
This commit is contained in:
Vikhyath Mondreti
2026-04-27 12:00:40 -07:00
committed by GitHub
parent 76ad59fd7d
commit 2a52141d2f
11 changed files with 887 additions and 2 deletions

View File

@@ -46,6 +46,10 @@ export const SlackBlock: BlockConfig<SlackResponse> = {
{ label: 'Get User Presence', id: 'get_user_presence' },
{ label: 'Edit Canvas', id: 'edit_canvas' },
{ label: 'Create Channel Canvas', id: 'create_channel_canvas' },
{ label: 'Get Canvas Info', id: 'get_canvas' },
{ label: 'List Canvases', id: 'list_canvases' },
{ label: 'Lookup Canvas Sections', id: 'lookup_canvas_sections' },
{ label: 'Delete Canvas', id: 'delete_canvas' },
{ label: 'Create Conversation', id: 'create_conversation' },
{ label: 'Invite to Conversation', id: 'invite_to_conversation' },
{ label: 'Open View', id: 'open_view' },
@@ -146,6 +150,10 @@ export const SlackBlock: BlockConfig<SlackResponse> = {
'get_user',
'get_user_presence',
'edit_canvas',
'get_canvas',
'list_canvases',
'lookup_canvas_sections',
'delete_canvas',
'create_conversation',
'open_view',
'update_view',
@@ -182,6 +190,10 @@ export const SlackBlock: BlockConfig<SlackResponse> = {
'get_user',
'get_user_presence',
'edit_canvas',
'get_canvas',
'list_canvases',
'lookup_canvas_sections',
'delete_canvas',
'create_conversation',
'open_view',
'update_view',
@@ -820,6 +832,132 @@ Return ONLY the timestamp string - no explanations, no quotes, no extra text.`,
value: 'create_channel_canvas',
},
},
// Get Canvas specific fields
{
id: 'getCanvasId',
title: 'Canvas ID',
type: 'short-input',
placeholder: 'Enter canvas ID (e.g., F1234ABCD)',
condition: {
field: 'operation',
value: 'get_canvas',
},
required: true,
},
// List Canvases specific fields
{
id: 'canvasListChannel',
title: 'Channel ID',
type: 'short-input',
placeholder: 'Optional channel filter (e.g., C1234567890)',
condition: {
field: 'operation',
value: 'list_canvases',
},
mode: 'advanced',
},
{
id: 'canvasListCount',
title: 'Canvas Limit',
type: 'short-input',
placeholder: '100',
condition: {
field: 'operation',
value: 'list_canvases',
},
mode: 'advanced',
},
{
id: 'canvasListPage',
title: 'Page',
type: 'short-input',
placeholder: '1',
condition: {
field: 'operation',
value: 'list_canvases',
},
mode: 'advanced',
},
{
id: 'canvasListUser',
title: 'User ID',
type: 'short-input',
placeholder: 'Optional creator filter (e.g., U1234567890)',
condition: {
field: 'operation',
value: 'list_canvases',
},
mode: 'advanced',
},
{
id: 'canvasListTsFrom',
title: 'Created After',
type: 'short-input',
placeholder: 'Unix timestamp (e.g., 123456789)',
condition: {
field: 'operation',
value: 'list_canvases',
},
mode: 'advanced',
},
{
id: 'canvasListTsTo',
title: 'Created Before',
type: 'short-input',
placeholder: 'Unix timestamp (e.g., 123456789)',
condition: {
field: 'operation',
value: 'list_canvases',
},
mode: 'advanced',
},
{
id: 'canvasListTeamId',
title: 'Team ID',
type: 'short-input',
placeholder: 'Encoded team ID (org tokens only)',
condition: {
field: 'operation',
value: 'list_canvases',
},
mode: 'advanced',
},
// Lookup Canvas Sections specific fields
{
id: 'lookupCanvasId',
title: 'Canvas ID',
type: 'short-input',
placeholder: 'Enter canvas ID (e.g., F1234ABCD)',
condition: {
field: 'operation',
value: 'lookup_canvas_sections',
},
required: true,
},
{
id: 'sectionCriteria',
title: 'Section Criteria',
type: 'code',
language: 'json',
placeholder: '{"section_types":["h1"],"contains_text":"Roadmap"}',
condition: {
field: 'operation',
value: 'lookup_canvas_sections',
},
required: true,
},
// Delete Canvas specific fields
{
id: 'deleteCanvasId',
title: 'Canvas ID',
type: 'short-input',
placeholder: 'Enter canvas ID (e.g., F1234ABCD)',
condition: {
field: 'operation',
value: 'delete_canvas',
},
required: true,
},
// Create Conversation specific fields
{
id: 'conversationName',
@@ -1058,6 +1196,10 @@ Do not include any explanations, markdown formatting, or other text outside the
'slack_get_user_presence',
'slack_edit_canvas',
'slack_create_channel_canvas',
'slack_get_canvas',
'slack_list_canvases',
'slack_lookup_canvas_sections',
'slack_delete_canvas',
'slack_create_conversation',
'slack_invite_to_conversation',
'slack_open_view',
@@ -1106,6 +1248,14 @@ Do not include any explanations, markdown formatting, or other text outside the
return 'slack_edit_canvas'
case 'create_channel_canvas':
return 'slack_create_channel_canvas'
case 'get_canvas':
return 'slack_get_canvas'
case 'list_canvases':
return 'slack_list_canvases'
case 'lookup_canvas_sections':
return 'slack_lookup_canvas_sections'
case 'delete_canvas':
return 'slack_delete_canvas'
case 'create_conversation':
return 'slack_create_conversation'
case 'invite_to_conversation':
@@ -1164,6 +1314,17 @@ Do not include any explanations, markdown formatting, or other text outside the
canvasTitle,
channelCanvasTitle,
channelCanvasContent,
getCanvasId,
canvasListChannel,
canvasListCount,
canvasListPage,
canvasListUser,
canvasListTsFrom,
canvasListTsTo,
canvasListTeamId,
lookupCanvasId,
sectionCriteria,
deleteCanvasId,
conversationName,
isPrivate,
teamId,
@@ -1343,6 +1504,49 @@ Do not include any explanations, markdown formatting, or other text outside the
}
break
case 'get_canvas':
baseParams.canvasId = getCanvasId
break
case 'list_canvases':
if (canvasListChannel) {
baseParams.channel = String(canvasListChannel).trim()
}
if (canvasListCount) {
const parsedCount = Number.parseInt(canvasListCount, 10)
if (!Number.isNaN(parsedCount) && parsedCount > 0) {
baseParams.count = parsedCount
}
}
if (canvasListPage) {
const parsedPage = Number.parseInt(canvasListPage, 10)
if (!Number.isNaN(parsedPage) && parsedPage > 0) {
baseParams.page = parsedPage
}
}
if (canvasListUser) {
baseParams.user = String(canvasListUser).trim()
}
if (canvasListTsFrom) {
baseParams.tsFrom = String(canvasListTsFrom).trim()
}
if (canvasListTsTo) {
baseParams.tsTo = String(canvasListTsTo).trim()
}
if (canvasListTeamId) {
baseParams.teamId = String(canvasListTeamId).trim()
}
break
case 'lookup_canvas_sections':
baseParams.canvasId = lookupCanvasId
baseParams.criteria = sectionCriteria
break
case 'delete_canvas':
baseParams.canvasId = deleteCanvasId
break
case 'create_conversation':
baseParams.name = conversationName
baseParams.isPrivate = isPrivate === 'true'
@@ -1461,6 +1665,24 @@ Do not include any explanations, markdown formatting, or other text outside the
// Create Channel Canvas inputs
channelCanvasTitle: { type: 'string', description: 'Title for channel canvas' },
channelCanvasContent: { type: 'string', description: 'Content for channel canvas' },
// Canvas management inputs
getCanvasId: { type: 'string', description: 'Canvas ID to retrieve' },
canvasListChannel: { type: 'string', description: 'Optional channel filter for canvases' },
canvasListCount: { type: 'string', description: 'Maximum number of canvases to return' },
canvasListPage: { type: 'string', description: 'Canvas list page number' },
canvasListUser: { type: 'string', description: 'Optional canvas creator user filter' },
canvasListTsFrom: {
type: 'string',
description: 'Filter canvases created after this timestamp',
},
canvasListTsTo: {
type: 'string',
description: 'Filter canvases created before this timestamp',
},
canvasListTeamId: { type: 'string', description: 'Encoded team ID for org tokens' },
lookupCanvasId: { type: 'string', description: 'Canvas ID to search for sections' },
sectionCriteria: { type: 'json', description: 'Canvas section lookup criteria' },
deleteCanvasId: { type: 'string', description: 'Canvas ID to delete' },
// Create Conversation inputs
conversationName: { type: 'string', description: 'Name for the new channel' },
isPrivate: { type: 'string', description: 'Create as private channel (true/false)' },
@@ -1511,6 +1733,26 @@ Do not include any explanations, markdown formatting, or other text outside the
// slack_canvas outputs
canvas_id: { type: 'string', description: 'Canvas identifier for created canvases' },
title: { type: 'string', description: 'Canvas title' },
canvas: {
type: 'json',
description: 'Canvas file metadata returned by Slack',
},
canvases: {
type: 'json',
description: 'Array of canvas file objects returned by Slack',
},
paging: {
type: 'json',
description: 'Pagination information for listed canvases',
},
sections: {
type: 'json',
description: 'Canvas section IDs returned by Slack section lookup',
},
ok: {
type: 'boolean',
description: 'Whether Slack completed the canvas operation successfully',
},
// slack_message_reader outputs (read operation)
messages: {

View File

@@ -704,7 +704,7 @@ export const OAUTH_PROVIDERS: Record<string, OAuthProviderConfig> = {
services: {
slack: {
name: 'Slack',
description: 'Send messages using a bot for Slack.',
description: 'Use Slack messaging, files, reactions, views, and canvases.',
providerId: 'slack',
icon: SlackIcon,
baseProviderIcon: SlackIcon,
@@ -722,6 +722,7 @@ export const OAUTH_PROVIDERS: Record<string, OAuthProviderConfig> = {
// TODO: Add 'users:read.email' once Slack app review is approved
'files:write',
'files:read',
'canvases:read',
'canvases:write',
'reactions:write',
],

View File

@@ -278,7 +278,8 @@ export const SCOPE_DESCRIPTIONS: Record<string, string> = {
'users:read.email': 'View user email addresses',
'files:write': 'Upload files',
'files:read': 'Download and read files',
'canvases:write': 'Create canvas documents',
'canvases:read': 'Read canvas sections',
'canvases:write': 'Create, edit, and delete canvas documents',
'reactions:write': 'Add emoji reactions to messages',
// Webflow scopes

View File

@@ -2364,19 +2364,23 @@ import {
slackCanvasTool,
slackCreateChannelCanvasTool,
slackCreateConversationTool,
slackDeleteCanvasTool,
slackDeleteMessageTool,
slackDownloadTool,
slackEditCanvasTool,
slackEphemeralMessageTool,
slackGetCanvasTool,
slackGetChannelInfoTool,
slackGetMessageTool,
slackGetThreadTool,
slackGetUserPresenceTool,
slackGetUserTool,
slackInviteToConversationTool,
slackListCanvasesTool,
slackListChannelsTool,
slackListMembersTool,
slackListUsersTool,
slackLookupCanvasSectionsTool,
slackMessageReaderTool,
slackMessageTool,
slackOpenViewTool,
@@ -3360,6 +3364,10 @@ export const tools: Record<string, ToolConfig> = {
slack_publish_view: slackPublishViewTool,
slack_edit_canvas: slackEditCanvasTool,
slack_create_channel_canvas: slackCreateChannelCanvasTool,
slack_get_canvas: slackGetCanvasTool,
slack_list_canvases: slackListCanvasesTool,
slack_lookup_canvas_sections: slackLookupCanvasSectionsTool,
slack_delete_canvas: slackDeleteCanvasTool,
slack_create_conversation: slackCreateConversationTool,
slack_invite_to_conversation: slackInviteToConversationTool,
github_repo_info: githubRepoInfoTool,

View File

@@ -0,0 +1,79 @@
import type { SlackDeleteCanvasParams, SlackDeleteCanvasResponse } from '@/tools/slack/types'
import type { ToolConfig } from '@/tools/types'
export const slackDeleteCanvasTool: ToolConfig<SlackDeleteCanvasParams, SlackDeleteCanvasResponse> =
{
id: 'slack_delete_canvas',
name: 'Slack Delete Canvas',
description: 'Delete a Slack canvas by its canvas ID',
version: '1.0.0',
oauth: {
required: true,
provider: 'slack',
},
params: {
authMethod: {
type: 'string',
required: false,
visibility: 'user-only',
description: 'Authentication method: oauth or bot_token',
},
botToken: {
type: 'string',
required: false,
visibility: 'user-only',
description: 'Bot token for Custom Bot',
},
accessToken: {
type: 'string',
required: false,
visibility: 'hidden',
description: 'OAuth access token or bot token for Slack API',
},
canvasId: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'Canvas ID to delete (e.g., F1234ABCD)',
},
},
request: {
url: 'https://slack.com/api/canvases.delete',
method: 'POST',
headers: (params: SlackDeleteCanvasParams) => ({
'Content-Type': 'application/json',
Authorization: `Bearer ${params.accessToken || params.botToken}`,
}),
body: (params: SlackDeleteCanvasParams) => ({
canvas_id: params.canvasId.trim(),
}),
},
transformResponse: async (response: Response) => {
const data = await response.json()
if (!data.ok) {
if (data.error === 'canvas_not_found') {
throw new Error('Canvas not found or not visible to the authenticated Slack user or bot.')
}
if (data.error === 'canvas_deleting_disabled') {
throw new Error('Canvas deletion is disabled for this workspace.')
}
throw new Error(data.error || 'Failed to delete canvas')
}
return {
success: true,
output: {
ok: data.ok,
},
}
},
outputs: {
ok: { type: 'boolean', description: 'Whether Slack deleted the canvas successfully' },
},
}

View File

@@ -0,0 +1,85 @@
import type { SlackGetCanvasParams, SlackGetCanvasResponse } from '@/tools/slack/types'
import { CANVAS_FILE_OUTPUT_PROPERTIES } from '@/tools/slack/types'
import { mapCanvasFile } from '@/tools/slack/utils'
import type { ToolConfig } from '@/tools/types'
export const slackGetCanvasTool: ToolConfig<SlackGetCanvasParams, SlackGetCanvasResponse> = {
id: 'slack_get_canvas',
name: 'Slack Get Canvas Info',
description: 'Get Slack canvas file metadata by canvas ID',
version: '1.0.0',
oauth: {
required: true,
provider: 'slack',
},
params: {
authMethod: {
type: 'string',
required: false,
visibility: 'user-only',
description: 'Authentication method: oauth or bot_token',
},
botToken: {
type: 'string',
required: false,
visibility: 'user-only',
description: 'Bot token for Custom Bot',
},
accessToken: {
type: 'string',
required: false,
visibility: 'hidden',
description: 'OAuth access token or bot token for Slack API',
},
canvasId: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'Canvas file ID to retrieve (e.g., F1234ABCD)',
},
},
request: {
url: (params: SlackGetCanvasParams) => {
const url = new URL('https://slack.com/api/files.info')
url.searchParams.append('file', params.canvasId.trim())
return url.toString()
},
method: 'GET',
headers: (params: SlackGetCanvasParams) => ({
'Content-Type': 'application/json',
Authorization: `Bearer ${params.accessToken || params.botToken}`,
}),
},
transformResponse: async (response: Response) => {
const data = await response.json()
if (!data.ok) {
if (data.error === 'file_not_found') {
throw new Error('Canvas not found. Please check the canvas ID and try again.')
}
if (data.error === 'not_visible') {
throw new Error('Canvas is not visible to the authenticated Slack user or bot.')
}
throw new Error(data.error || 'Failed to get canvas from Slack')
}
return {
success: true,
output: {
canvas: mapCanvasFile(data.file),
},
}
},
outputs: {
canvas: {
type: 'object',
description: 'Canvas file information returned by Slack',
properties: CANVAS_FILE_OUTPUT_PROPERTIES,
},
},
}

View File

@@ -2,19 +2,23 @@ import { slackAddReactionTool } from '@/tools/slack/add_reaction'
import { slackCanvasTool } from '@/tools/slack/canvas'
import { slackCreateChannelCanvasTool } from '@/tools/slack/create_channel_canvas'
import { slackCreateConversationTool } from '@/tools/slack/create_conversation'
import { slackDeleteCanvasTool } from '@/tools/slack/delete_canvas'
import { slackDeleteMessageTool } from '@/tools/slack/delete_message'
import { slackDownloadTool } from '@/tools/slack/download'
import { slackEditCanvasTool } from '@/tools/slack/edit_canvas'
import { slackEphemeralMessageTool } from '@/tools/slack/ephemeral_message'
import { slackGetCanvasTool } from '@/tools/slack/get_canvas'
import { slackGetChannelInfoTool } from '@/tools/slack/get_channel_info'
import { slackGetMessageTool } from '@/tools/slack/get_message'
import { slackGetThreadTool } from '@/tools/slack/get_thread'
import { slackGetUserTool } from '@/tools/slack/get_user'
import { slackGetUserPresenceTool } from '@/tools/slack/get_user_presence'
import { slackInviteToConversationTool } from '@/tools/slack/invite_to_conversation'
import { slackListCanvasesTool } from '@/tools/slack/list_canvases'
import { slackListChannelsTool } from '@/tools/slack/list_channels'
import { slackListMembersTool } from '@/tools/slack/list_members'
import { slackListUsersTool } from '@/tools/slack/list_users'
import { slackLookupCanvasSectionsTool } from '@/tools/slack/lookup_canvas_sections'
import { slackMessageTool } from '@/tools/slack/message'
import { slackMessageReaderTool } from '@/tools/slack/message_reader'
import { slackOpenViewTool } from '@/tools/slack/open_view'
@@ -29,6 +33,10 @@ export {
slackCanvasTool,
slackCreateConversationTool,
slackCreateChannelCanvasTool,
slackGetCanvasTool,
slackListCanvasesTool,
slackLookupCanvasSectionsTool,
slackDeleteCanvasTool,
slackMessageReaderTool,
slackDownloadTool,
slackEditCanvasTool,
@@ -51,3 +59,5 @@ export {
slackGetThreadTool,
slackInviteToConversationTool,
}
export * from './types'

View File

@@ -0,0 +1,142 @@
import type { SlackListCanvasesParams, SlackListCanvasesResponse } from '@/tools/slack/types'
import { CANVAS_FILE_OUTPUT_PROPERTIES, CANVAS_PAGING_OUTPUT_PROPERTIES } from '@/tools/slack/types'
import { mapCanvasFile } from '@/tools/slack/utils'
import type { ToolConfig } from '@/tools/types'
export const slackListCanvasesTool: ToolConfig<SlackListCanvasesParams, SlackListCanvasesResponse> =
{
id: 'slack_list_canvases',
name: 'Slack List Canvases',
description: 'List Slack canvases available to the authenticated user or bot',
version: '1.0.0',
oauth: {
required: true,
provider: 'slack',
},
params: {
authMethod: {
type: 'string',
required: false,
visibility: 'user-only',
description: 'Authentication method: oauth or bot_token',
},
botToken: {
type: 'string',
required: false,
visibility: 'user-only',
description: 'Bot token for Custom Bot',
},
accessToken: {
type: 'string',
required: false,
visibility: 'hidden',
description: 'OAuth access token or bot token for Slack API',
},
channel: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Filter canvases appearing in a specific channel ID',
},
count: {
type: 'number',
required: false,
visibility: 'user-or-llm',
description: 'Number of canvases to return per page',
},
page: {
type: 'number',
required: false,
visibility: 'user-or-llm',
description: 'Page number to return',
},
user: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Filter canvases created by a single user ID',
},
tsFrom: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Filter canvases created after this Unix timestamp',
},
tsTo: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Filter canvases created before this Unix timestamp',
},
teamId: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Encoded team ID, required when using an org-level token',
},
},
request: {
url: (params: SlackListCanvasesParams) => {
const url = new URL('https://slack.com/api/files.list')
url.searchParams.append('types', 'canvas')
if (params.channel) url.searchParams.append('channel', params.channel.trim())
if (params.count) url.searchParams.append('count', String(params.count))
if (params.page) url.searchParams.append('page', String(params.page))
if (params.user) url.searchParams.append('user', params.user.trim())
if (params.tsFrom) url.searchParams.append('ts_from', params.tsFrom.trim())
if (params.tsTo) url.searchParams.append('ts_to', params.tsTo.trim())
if (params.teamId) url.searchParams.append('team_id', params.teamId.trim())
return url.toString()
},
method: 'GET',
headers: (params: SlackListCanvasesParams) => ({
'Content-Type': 'application/json',
Authorization: `Bearer ${params.accessToken || params.botToken}`,
}),
},
transformResponse: async (response: Response) => {
const data = await response.json()
if (!data.ok) {
if (data.error === 'unknown_type') {
throw new Error('Slack did not recognize the canvas file type filter.')
}
throw new Error(data.error || 'Failed to list canvases from Slack')
}
return {
success: true,
output: {
canvases: (data.files ?? []).map(mapCanvasFile),
paging: {
count: data.paging?.count ?? 0,
total: data.paging?.total ?? 0,
page: data.paging?.page ?? 0,
pages: data.paging?.pages ?? 0,
},
},
}
},
outputs: {
canvases: {
type: 'array',
description: 'Canvas file objects returned by Slack',
items: {
type: 'object',
properties: CANVAS_FILE_OUTPUT_PROPERTIES,
},
},
paging: {
type: 'object',
description: 'Pagination information from Slack',
properties: CANVAS_PAGING_OUTPUT_PROPERTIES,
},
},
}

View File

@@ -0,0 +1,114 @@
import type {
SlackLookupCanvasSectionsParams,
SlackLookupCanvasSectionsResponse,
} from '@/tools/slack/types'
import { CANVAS_SECTION_OUTPUT_PROPERTIES } from '@/tools/slack/types'
import type { ToolConfig } from '@/tools/types'
const parseCriteria = (criteria: SlackLookupCanvasSectionsParams['criteria']) => {
if (typeof criteria !== 'string') {
return criteria
}
try {
return JSON.parse(criteria)
} catch {
throw new Error('Canvas section criteria must be a valid JSON object')
}
}
export const slackLookupCanvasSectionsTool: ToolConfig<
SlackLookupCanvasSectionsParams,
SlackLookupCanvasSectionsResponse
> = {
id: 'slack_lookup_canvas_sections',
name: 'Slack Lookup Canvas Sections',
description: 'Find Slack canvas section IDs matching criteria for later edits',
version: '1.0.0',
oauth: {
required: true,
provider: 'slack',
},
params: {
authMethod: {
type: 'string',
required: false,
visibility: 'user-only',
description: 'Authentication method: oauth or bot_token',
},
botToken: {
type: 'string',
required: false,
visibility: 'user-only',
description: 'Bot token for Custom Bot',
},
accessToken: {
type: 'string',
required: false,
visibility: 'hidden',
description: 'OAuth access token or bot token for Slack API',
},
canvasId: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'Canvas ID to search (e.g., F1234ABCD)',
},
criteria: {
type: 'json',
required: true,
visibility: 'user-or-llm',
description:
'Section lookup criteria, such as {"section_types":["h1"],"contains_text":"Roadmap"}',
},
},
request: {
url: 'https://slack.com/api/canvases.sections.lookup',
method: 'POST',
headers: (params: SlackLookupCanvasSectionsParams) => ({
'Content-Type': 'application/json',
Authorization: `Bearer ${params.accessToken || params.botToken}`,
}),
body: (params: SlackLookupCanvasSectionsParams) => ({
canvas_id: params.canvasId.trim(),
criteria: parseCriteria(params.criteria),
}),
},
transformResponse: async (response: Response) => {
const data = await response.json()
if (!data.ok) {
if (data.error === 'canvas_not_found') {
throw new Error('Canvas not found or not visible to the authenticated Slack user or bot.')
}
if (data.error === 'missing_scope') {
throw new Error(
'Missing required permissions. Please reconnect your Slack account with the canvases:read scope.'
)
}
throw new Error(data.error || 'Failed to look up canvas sections')
}
return {
success: true,
output: {
sections: data.sections ?? [],
},
}
},
outputs: {
sections: {
type: 'array',
description: 'Canvas sections matching the lookup criteria',
items: {
type: 'object',
properties: CANVAS_SECTION_OUTPUT_PROPERTIES,
},
},
},
}

View File

@@ -488,6 +488,91 @@ export const CANVAS_OUTPUT_PROPERTIES = {
title: { type: 'string', description: 'Canvas title' },
} as const satisfies Record<string, OutputProperty>
/**
* Canvas file object output properties.
* Based on Slack file objects returned by files.info and files.list for canvases.
*/
export const CANVAS_FILE_OUTPUT_PROPERTIES = {
id: { type: 'string', description: 'Unique canvas file identifier' },
created: { type: 'number', description: 'Unix timestamp when the canvas was created' },
timestamp: { type: 'number', description: 'Unix timestamp associated with the canvas' },
name: { type: 'string', description: 'Canvas file name', optional: true },
title: { type: 'string', description: 'Canvas title', optional: true },
mimetype: { type: 'string', description: 'MIME type of the canvas file', optional: true },
filetype: { type: 'string', description: 'Slack file type for the canvas', optional: true },
pretty_type: { type: 'string', description: 'Human-readable file type', optional: true },
user: { type: 'string', description: 'User ID of the canvas creator', optional: true },
editable: { type: 'boolean', description: 'Whether the canvas file is editable', optional: true },
size: { type: 'number', description: 'Canvas file size in bytes', optional: true },
mode: { type: 'string', description: 'File mode', optional: true },
is_external: {
type: 'boolean',
description: 'Whether the canvas is externally hosted',
optional: true,
},
is_public: { type: 'boolean', description: 'Whether the canvas is public', optional: true },
url_private: {
type: 'string',
description: 'Private URL for the canvas file',
optional: true,
},
url_private_download: {
type: 'string',
description: 'Private download URL for the canvas file',
optional: true,
},
permalink: { type: 'string', description: 'Permanent URL for the canvas', optional: true },
channels: {
type: 'array',
description: 'Public channel IDs where the canvas appears',
items: { type: 'string', description: 'Channel ID' },
optional: true,
},
groups: {
type: 'array',
description: 'Private channel IDs where the canvas appears',
items: { type: 'string', description: 'Channel ID' },
optional: true,
},
ims: {
type: 'array',
description: 'Direct message IDs where the canvas appears',
items: { type: 'string', description: 'Conversation ID' },
optional: true,
},
canvas_readtime: {
type: 'number',
description: 'Approximate read time for canvas content',
optional: true,
},
is_channel_space: {
type: 'boolean',
description: 'Whether this canvas is linked to a channel',
optional: true,
},
linked_channel_id: {
type: 'string',
description: 'Channel ID linked to this canvas',
optional: true,
},
canvas_creator_id: {
type: 'string',
description: 'User ID of the canvas creator',
optional: true,
},
} as const satisfies Record<string, OutputProperty>
export const CANVAS_PAGING_OUTPUT_PROPERTIES = {
count: { type: 'number', description: 'Number of items requested per page' },
total: { type: 'number', description: 'Total number of matching files' },
page: { type: 'number', description: 'Current page number' },
pages: { type: 'number', description: 'Total number of pages' },
} as const satisfies Record<string, OutputProperty>
export const CANVAS_SECTION_OUTPUT_PROPERTIES = {
id: { type: 'string', description: 'Canvas section identifier' },
} as const satisfies Record<string, OutputProperty>
/**
* Output definition for modal view objects
* Based on Slack views.open response structure
@@ -735,6 +820,29 @@ export interface SlackCreateChannelCanvasParams extends SlackBaseParams {
content?: string
}
export interface SlackGetCanvasParams extends SlackBaseParams {
canvasId: string
}
export interface SlackListCanvasesParams extends SlackBaseParams {
channel?: string
count?: number
page?: number
user?: string
tsFrom?: string
tsTo?: string
teamId?: string
}
export interface SlackLookupCanvasSectionsParams extends SlackBaseParams {
canvasId: string
criteria: Record<string, unknown> | string
}
export interface SlackDeleteCanvasParams extends SlackBaseParams {
canvasId: string
}
export interface SlackOpenViewParams extends SlackBaseParams {
triggerId: string
interactivityPointer?: string
@@ -1078,6 +1186,69 @@ export interface SlackCreateChannelCanvasResponse extends ToolResponse {
}
}
export interface SlackCanvasFile {
id: string
created: number | null
timestamp: number | null
name?: string | null
title?: string | null
mimetype?: string | null
filetype?: string | null
pretty_type?: string | null
user?: string | null
editable?: boolean | null
size?: number | null
mode?: string | null
is_external?: boolean | null
is_public?: boolean | null
url_private?: string | null
url_private_download?: string | null
permalink?: string | null
channels?: string[]
groups?: string[]
ims?: string[]
canvas_readtime?: number | null
is_channel_space?: boolean | null
linked_channel_id?: string | null
canvas_creator_id?: string | null
}
export interface SlackCanvasPaging {
count: number
total: number
page: number
pages: number
}
export interface SlackCanvasSection {
id: string
}
export interface SlackGetCanvasResponse extends ToolResponse {
output: {
canvas: SlackCanvasFile
}
}
export interface SlackListCanvasesResponse extends ToolResponse {
output: {
canvases: SlackCanvasFile[]
paging: SlackCanvasPaging
}
}
export interface SlackLookupCanvasSectionsResponse extends ToolResponse {
output: {
sections: SlackCanvasSection[]
}
}
export interface SlackDeleteCanvasResponse extends ToolResponse {
output: {
ok: boolean
}
}
export interface SlackView {
id: string
team_id?: string | null
@@ -1143,6 +1314,10 @@ export type SlackResponse =
| SlackGetUserPresenceResponse
| SlackEditCanvasResponse
| SlackCreateChannelCanvasResponse
| SlackGetCanvasResponse
| SlackListCanvasesResponse
| SlackLookupCanvasSectionsResponse
| SlackDeleteCanvasResponse
| SlackCreateConversationResponse
| SlackInviteToConversationResponse
| SlackOpenViewResponse

View File

@@ -0,0 +1,28 @@
import type { SlackCanvasFile } from '@/tools/slack/types'
export const mapCanvasFile = (file: SlackCanvasFile): SlackCanvasFile => ({
id: file.id,
created: file.created ?? null,
timestamp: file.timestamp ?? null,
name: file.name ?? null,
title: file.title ?? null,
mimetype: file.mimetype ?? null,
filetype: file.filetype ?? null,
pretty_type: file.pretty_type ?? null,
user: file.user ?? null,
editable: file.editable ?? null,
size: file.size ?? null,
mode: file.mode ?? null,
is_external: file.is_external ?? null,
is_public: file.is_public ?? null,
url_private: file.url_private ?? null,
url_private_download: file.url_private_download ?? null,
permalink: file.permalink ?? null,
channels: file.channels ?? [],
groups: file.groups ?? [],
ims: file.ims ?? [],
canvas_readtime: file.canvas_readtime ?? null,
is_channel_space: file.is_channel_space ?? null,
linked_channel_id: file.linked_channel_id ?? null,
canvas_creator_id: file.canvas_creator_id ?? null,
})