diff --git a/adapters/slack/package.json b/adapters/slack/package.json new file mode 100644 index 00000000..578f1650 --- /dev/null +++ b/adapters/slack/package.json @@ -0,0 +1,35 @@ +{ + "name": "@satorijs/adapter-slack", + "description": "Slack Adapter for Satorijs", + "version": "1.0.0", + "main": "lib/index.js", + "typings": "lib/index.d.ts", + "files": [ + "lib" + ], + "author": "LittleC ", + "license": "MIT", + "repository": { + "type": "git", + "url": "git+https://github.com/satorijs/satori.git", + "directory": "adapters/slack" + }, + "bugs": { + "url": "https://github.com/satorijs/satori/issues" + }, + "homepage": "https://koishi.chat/plugins/adapter/slack.html", + "keywords": [ + "bot", + "slack", + "adapter", + "chatbot", + "satori" + ], + "peerDependencies": { + "@satorijs/satori": "^2.4.0" + }, + "dependencies": { + "@slack/types": "^2.8.0", + "form-data": "^4.0.0" + } +} diff --git a/adapters/slack/src/bot.ts b/adapters/slack/src/bot.ts new file mode 100644 index 00000000..8845d9a0 --- /dev/null +++ b/adapters/slack/src/bot.ts @@ -0,0 +1,198 @@ +import { Bot, Context, Fragment, Quester, Schema, SendOptions, Universal } from '@satorijs/satori' +import { WsClient } from './ws' +import { HttpServer } from './http' +import { adaptChannel, adaptGuild, adaptMessage, adaptUser, AuthTestResponse } from './utils' +import { SlackMessageEncoder } from './message' +import { GenericMessageEvent, SlackChannel, SlackTeam, SlackUser } from './types' +import FormData from 'form-data' +import { Internal, Token } from './types/internal' + +export class SlackBot extends Bot { + static MessageEncoder = SlackMessageEncoder + public http: Quester + public internal: Internal + + constructor(ctx: Context, config: T) { + super(ctx, config) + this.http = ctx.http.extend(config) + + this.internal = new Internal(this, this.http) + + if (config.protocol === 'ws') { + ctx.plugin(WsClient, this) + } else { + ctx.plugin(HttpServer, this) + } + } + + async request(method: Quester.Method, path: string, data = {}, headers: any = {}, zap: boolean = false): Promise { + headers['Authorization'] = `Bearer ${zap ? this.config.token : this.config.botToken}` + if (method === 'GET') { + return (await this.http.get(path, { params: data, headers })).data + } else { + if (!headers['content-type']) { + data = data instanceof FormData ? data : JSON.stringify(data) + const type = data instanceof FormData ? 'multipart/form-data' : 'application/json; charset=utf-8' + headers['content-type'] = type + } + return (await this.http(method, path, { data, headers })) + } + } + + async getSelf() { + const data = await this.internal.authTest(Token.BOT) + return { + userId: data.user_id, + avatar: null, + username: data.user, + isBot: !!data.bot_id, + } + } + + async deleteMessage(channelId: string, messageId: string): Promise { + await this.internal.chatDelete(Token.BOT, { + channel: channelId, + ts: Number(messageId), + }) + } + + async getMessage(channelId: string, messageId: string): Promise { + const msg = await this.internal.conversationsHistory(Token.BOT, { + channel: channelId, + oldest: Number(messageId), + limit: 1, + inclusive: true, + }) + // @ts-ignore + return adaptMessage(this, msg.messages[0]) + } + + async getMessageList(channelId: string, before?: string): Promise { + const msg = await this.request<{ + messages: GenericMessageEvent[] + }>('POST', '/conversations.history', { + channel: channelId, + latest: before, + }) + return Promise.all(msg.messages.map(v => adaptMessage(this, v))) + } + + async getUser(userId: string, guildId?: string): Promise { + // users:read + // @TODO guildId + const { user } = await this.request<{ user: SlackUser }>('POST', '/users.info', { + user: userId, + }) + return adaptUser(user) + } + + async getGuildMemberList(guildId: string): Promise { + // users:read + const { members } = await this.request<{ members: SlackUser[] }>('POST', '/users.list') + return members.map(adaptUser) + } + + async getChannel(channelId: string, guildId?: string): Promise { + const { channel } = await this.request<{ + channel: SlackChannel + }>('POST', '/conversations.info', { + channel: channelId, + }) + return adaptChannel(channel) + } + + async getChannelList(guildId: string): Promise { + const { channels } = await this.request<{ + channels: SlackChannel[] + }>('POST', '/conversations.list', { + team_id: guildId, + }) + return channels.map(adaptChannel) + } + + async getGuild(guildId: string): Promise { + const { team } = await this.request<{ team: SlackTeam }>('POST', '/team.info', { + team_id: guildId, + }) + return adaptGuild(team) + } + + async getGuildMember(guildId: string, userId: string): Promise { + const { user } = await this.request<{ user: SlackUser }>('POST', '/users.info', { + user: userId, + }) + return { + ...adaptUser(user), + nickname: user.profile.display_name, + } + } + + async sendPrivateMessage(channelId: string, content: Fragment, options?: SendOptions): Promise { + // "channels:write,groups:write,mpim:write,im:write", + const { channel } = await this.internal.conversationsOpen(Token.BOT, { + users: channelId, + }) + // @ts-ignore + return this.sendMessage(channel.id, content, undefined, options) + } + + async getReactions(channelId: string, messageId: string, emoji: string): Promise { + const { message } = await this.internal.reactionsGet(Token.BOT, { + channel: channelId, + timestamp: messageId, + full: true, + }) + return message.reactions.find(v => v.name === emoji)?.users.map(v => ({ + userId: v, + })) ?? [] + } + + async createReaction(channelId: string, messageId: string, emoji: string): Promise { + // reactions.write + await this.internal.reactionsAdd(Token.BOT, { + channel: channelId, + timestamp: messageId, + name: emoji, + }) + } + + async clearReaction(channelId: string, messageId: string, emoji?: string): Promise { + const { message } = await this.internal.reactionsGet(Token.BOT, { + channel: channelId, + timestamp: messageId, + full: true, + }) + for (const reaction of message.reactions) { + if (!emoji || reaction.name === emoji) { + await this.internal.reactionsRemove(Token.BOT, { + channel: channelId, + timestamp: messageId, + name: reaction.name, + }) + } + } + } +} + +export namespace SlackBot { + export interface BaseConfig extends Bot.Config, Quester.Config { + token: string + botToken: string + } + export type Config = BaseConfig & (HttpServer.Config | WsClient.Config) + + export const Config: Schema = Schema.intersect([ + Schema.object({ + protocol: Schema.union(['http', 'ws']).description('选择要使用的协议。').required(), + token: Schema.string().description('App-Level Tokens').role('secret').required(), + botToken: Schema.string().description('OAuth Tokens(Bot Tokens)').role('secret').required(), + }), + Schema.union([ + WsClient.Config, + HttpServer.Config, + ]), + Quester.createConfig('https://slack.com/api/'), + ] as const) +} + +SlackBot.prototype.platform = 'slack' diff --git a/adapters/slack/src/http.ts b/adapters/slack/src/http.ts new file mode 100644 index 00000000..2d9ccc67 --- /dev/null +++ b/adapters/slack/src/http.ts @@ -0,0 +1,63 @@ +import { Adapter, Logger, Schema } from '@satorijs/satori' +import { SlackBot } from './bot' +import crypto from 'node:crypto' +import { EnvelopedEvent, SlackEvent, SocketEvent } from './types' +import { adaptSession } from './utils' + +export class HttpServer extends Adapter.Server { + logger = new Logger('slack') + async start(bot: SlackBot) { + // @ts-ignore + const { signing } = bot.config + const { userId } = await bot.getSelf() + bot.selfId = userId + bot.ctx.router.post('/slack', async (ctx) => { + const timestamp = ctx.request.header['x-slack-request-timestamp'].toString() + const signature = ctx.request.header['x-slack-signature'].toString() + const requestBody = ctx.request.rawBody + + const hmac = crypto.createHmac('sha256', signing) + const [version, hash] = signature.split('=') + + const fiveMinutesAgo = Math.floor(Date.now() / 1000) - 60 * 5 + if (Number(timestamp) < fiveMinutesAgo) { + return ctx.status = 403 + } + + hmac.update(`${version}:${timestamp}:${requestBody}`) + + if (hash !== hmac.digest('hex')) { + return ctx.status = 403 + } + const { type } = ctx.request.body as SocketEvent + if (type === 'url_verification') { + ctx.status = 200 + return ctx.body = { + challenge: ctx.request.body.challenge, + } + } + // https://api.slack.com/apis/connections/events-api#receiving-events + if (type === 'event_callback') { + ctx.status = 200 + ctx.body = 'ok' + const payload: EnvelopedEvent = ctx.request.body + this.logger.debug(require('util').inspect(payload, false, null, true)) + const session = await adaptSession(bot, payload) + this.logger.debug(require('util').inspect(session, false, null, true)) + if (session) bot.dispatch(session) + } + }) + } +} + +export namespace HttpServer { + export interface Config { + protocol: 'http' + signing: string + } + + export const Config: Schema = Schema.object({ + protocol: Schema.const('http').required(), + signing: Schema.string().required(), + }) +} diff --git a/adapters/slack/src/index.ts b/adapters/slack/src/index.ts new file mode 100644 index 00000000..dc504f1e --- /dev/null +++ b/adapters/slack/src/index.ts @@ -0,0 +1,9 @@ +import { SlackBot } from './bot' + +export * from './ws' +export * from './message' +export * from './utils' +export * from './http' +export * from './types' + +export { SlackBot } diff --git a/adapters/slack/src/message.ts b/adapters/slack/src/message.ts new file mode 100644 index 00000000..21165bbc --- /dev/null +++ b/adapters/slack/src/message.ts @@ -0,0 +1,117 @@ +import { h, MessageEncoder, Session } from '@satorijs/satori' +import { SlackBot } from './bot' +import FormData from 'form-data' +import { adaptMessage, adaptSentAsset } from './utils' +import { File } from './types' + +// https://api.slack.com/reference/surfaces/formatting#basics +export const escape = (val: string) => + val + .replace(/(? '@\u200Beveryone') + .replace(/@here/g, () => '@\u200Bhere') + .replace(/(?/g, '\u200A>') + // .replace(/<((?:#C|@U|!subteam\^)[0-9A-Z]{1,12})>/g, '<$1>') + // .replace(/<(\!(?:here|channel|everyone)(?:\|[0-9a-zA-Z?]*)?)>/g, '<$1>') + .replace(/<(.*?)>/g, '<$1>') + +export const unescape = (val: string) => + val + .replace(/\u200b([\*_~`])/g, '$1') + .replace(/@\u200Beveryone/g, () => '@everyone') + .replace(/@\u200Bhere/g, () => '@here') +export class SlackMessageEncoder extends MessageEncoder { + buffer = '' + thread_ts = null + elements: any[] = [] + addition: Record = {} + results: Session[] = [] + async flush() { + if (!this.buffer.length) return + const r = await this.bot.internal.chatPostMessage(this.bot.config.botToken, { + channel: this.channelId, + ...this.addition, + thread_ts: this.thread_ts, + text: this.buffer, + }) + const session = this.bot.session() + await adaptMessage(this.bot, r.message, session) + session.channelId = this.channelId + session.app.emit(session, 'send', session) + this.results.push(session) + this.buffer = '' + } + + async sendAsset(element: h) { + if (this.buffer.length) await this.flush() + const { attrs } = element + const { filename, data, mime } = await this.bot.ctx.http.file(attrs.url, attrs) + const form = new FormData() + // https://github.com/form-data/form-data/issues/468 + const value = process.env.KOISHI_ENV === 'browser' + ? new Blob([data], { type: mime }) + : Buffer.from(data) + form.append('file', value, attrs.file || filename) + form.append('channels', this.channelId) + if (this.thread_ts) form.append('thread_ts', this.thread_ts) + const sent = await this.bot.request<{ + ok: boolean + file: File + }>('POST', '/files.upload', form, form.getHeaders()) + if (sent.ok) { + const session = this.bot.session() + adaptSentAsset(sent.file, session) + session.app.emit(session, 'send', session) + this.results.push(session) + } + } + + async visit(element: h) { + const { type, attrs, children } = element + if (type === 'text') { + this.buffer += escape(attrs.content) + } else if (type === 'image' && attrs.url) { + await this.sendAsset(element) + } else if (type === 'sharp' && attrs.id) { + this.buffer += `<#${attrs.id}>` + } else if (type === 'at') { + if (attrs.id) this.buffer += `<@${attrs.id}>` + if (attrs.type === 'all') this.buffer += `` + if (attrs.type === 'here') this.buffer += `` + } else if (type === 'b' || type === 'strong') { + this.buffer += '*' + await this.render(children) + this.buffer += '*' + } else if (type === 'i' || type === 'em') { + this.buffer += '_' + await this.render(children) + this.buffer += '_' + } else if (type === 's' || type === 'del') { + this.buffer += '~' + await this.render(children) + this.buffer += '~' + } else if (type === 'code') { + this.buffer += '`' + await this.render(children) + this.buffer += '`' + } else if (type === 'a') { + this.buffer += `<${attrs.href}|` + await this.render(children) + this.buffer += `>` + } else if (type === 'quote') { + this.thread_ts = attrs.id + } else if (type === 'p') { + this.buffer += `\n` + await this.render(children) + } else if (type === 'face') { + this.buffer += `:${attrs.id}:` + } else if (type === 'author') { + this.addition = { + username: attrs.nickname, + icon_url: attrs.avatar, + } + } else if (type === 'message') { + await this.render(children) + } + } +} diff --git a/adapters/slack/src/types/admin.ts b/adapters/slack/src/types/admin.ts new file mode 100644 index 00000000..a4ae0306 --- /dev/null +++ b/adapters/slack/src/types/admin.ts @@ -0,0 +1,909 @@ +import { Internal, TokenInput } from './internal' +import { Definitions } from './definition' +Internal.define({ + '/admin.apps.approve': { + POST: { 'adminAppsApprove': true }, + }, + '/admin.apps.approved.list': { + GET: { 'adminAppsApprovedList': false }, + }, + '/admin.apps.requests.list': { + GET: { 'adminAppsRequestsList': false }, + }, + '/admin.apps.restrict': { + POST: { 'adminAppsRestrict': true }, + }, + '/admin.apps.restricted.list': { + GET: { 'adminAppsRestrictedList': false }, + }, + '/admin.conversations.archive': { + POST: { 'adminConversationsArchive': true }, + }, + '/admin.conversations.convertToPrivate': { + POST: { 'adminConversationsConvertToPrivate': true }, + }, + '/admin.conversations.create': { + POST: { 'adminConversationsCreate': true }, + }, + '/admin.conversations.delete': { + POST: { 'adminConversationsDelete': true }, + }, + '/admin.conversations.disconnectShared': { + POST: { 'adminConversationsDisconnectShared': true }, + }, + '/admin.conversations.ekm.listOriginalConnectedChannelInfo': { + GET: { 'adminConversationsEkmListOriginalConnectedChannelInfo': false }, + }, + '/admin.conversations.getConversationPrefs': { + GET: { 'adminConversationsGetConversationPrefs': true }, + }, + '/admin.conversations.getTeams': { + GET: { 'adminConversationsGetTeams': true }, + }, + '/admin.conversations.invite': { + POST: { 'adminConversationsInvite': true }, + }, + '/admin.conversations.rename': { + POST: { 'adminConversationsRename': true }, + }, + '/admin.conversations.restrictAccess.addGroup': { + POST: { 'adminConversationsRestrictAccessAddGroup': false }, + }, + '/admin.conversations.restrictAccess.listGroups': { + GET: { 'adminConversationsRestrictAccessListGroups': false }, + }, + '/admin.conversations.restrictAccess.removeGroup': { + POST: { 'adminConversationsRestrictAccessRemoveGroup': false }, + }, + '/admin.conversations.search': { + GET: { 'adminConversationsSearch': true }, + }, + '/admin.conversations.setConversationPrefs': { + POST: { 'adminConversationsSetConversationPrefs': true }, + }, + '/admin.conversations.setTeams': { + POST: { 'adminConversationsSetTeams': true }, + }, + '/admin.conversations.unarchive': { + POST: { 'adminConversationsUnarchive': true }, + }, + '/admin.emoji.add': { + POST: { 'adminEmojiAdd': false }, + }, + '/admin.emoji.addAlias': { + POST: { 'adminEmojiAddAlias': false }, + }, + '/admin.emoji.list': { + GET: { 'adminEmojiList': false }, + }, + '/admin.emoji.remove': { + POST: { 'adminEmojiRemove': false }, + }, + '/admin.emoji.rename': { + POST: { 'adminEmojiRename': false }, + }, + '/admin.inviteRequests.approve': { + POST: { 'adminInviteRequestsApprove': true }, + }, + '/admin.inviteRequests.approved.list': { + GET: { 'adminInviteRequestsApprovedList': true }, + }, + '/admin.inviteRequests.denied.list': { + GET: { 'adminInviteRequestsDeniedList': true }, + }, + '/admin.inviteRequests.deny': { + POST: { 'adminInviteRequestsDeny': true }, + }, + '/admin.inviteRequests.list': { + GET: { 'adminInviteRequestsList': true }, + }, + '/admin.teams.admins.list': { + GET: { 'adminTeamsAdminsList': false }, + }, + '/admin.teams.create': { + POST: { 'adminTeamsCreate': true }, + }, + '/admin.teams.list': { + GET: { 'adminTeamsList': true }, + }, + '/admin.teams.owners.list': { + GET: { 'adminTeamsOwnersList': false }, + }, + '/admin.teams.settings.info': { + GET: { 'adminTeamsSettingsInfo': true }, + }, + '/admin.teams.settings.setDefaultChannels': { + POST: { 'adminTeamsSettingsSetDefaultChannels': false }, + }, + '/admin.teams.settings.setDescription': { + POST: { 'adminTeamsSettingsSetDescription': true }, + }, + '/admin.teams.settings.setDiscoverability': { + POST: { 'adminTeamsSettingsSetDiscoverability': true }, + }, + '/admin.teams.settings.setIcon': { + POST: { 'adminTeamsSettingsSetIcon': false }, + }, + '/admin.teams.settings.setName': { + POST: { 'adminTeamsSettingsSetName': true }, + }, + '/admin.usergroups.addChannels': { + POST: { 'adminUsergroupsAddChannels': true }, + }, + '/admin.usergroups.addTeams': { + POST: { 'adminUsergroupsAddTeams': true }, + }, + '/admin.usergroups.listChannels': { + GET: { 'adminUsergroupsListChannels': true }, + }, + '/admin.usergroups.removeChannels': { + POST: { 'adminUsergroupsRemoveChannels': true }, + }, + '/admin.users.assign': { + POST: { 'adminUsersAssign': true }, + }, + '/admin.users.invite': { + POST: { 'adminUsersInvite': true }, + }, + '/admin.users.list': { + GET: { 'adminUsersList': true }, + }, + '/admin.users.remove': { + POST: { 'adminUsersRemove': true }, + }, + '/admin.users.session.invalidate': { + POST: { 'adminUsersSessionInvalidate': true }, + }, + '/admin.users.session.reset': { + POST: { 'adminUsersSessionReset': true }, + }, + '/admin.users.setAdmin': { + POST: { 'adminUsersSetAdmin': true }, + }, + '/admin.users.setExpiration': { + POST: { 'adminUsersSetExpiration': true }, + }, + '/admin.users.setOwner': { + POST: { 'adminUsersSetOwner': true }, + }, + '/admin.users.setRegular': { + POST: { 'adminUsersSetRegular': true }, + }, +}) + +export namespace Admin { + export namespace Params { + export interface AppsApprove { + app_id?: string + request_id?: string + team_id?: string + } + export interface AppsApprovedList { + limit?: number + cursor?: string + team_id?: string + enterprise_id?: string + } + export interface AppsRequestsList { + limit?: number + cursor?: string + team_id?: string + } + export interface AppsRestrict { + app_id?: string + request_id?: string + team_id?: string + } + export interface AppsRestrictedList { + limit?: number + cursor?: string + team_id?: string + enterprise_id?: string + } + export interface ConversationsArchive { + channel_id: string + } + export interface ConversationsConvertToPrivate { + channel_id: string + } + export interface ConversationsCreate { + name: string + description?: string + is_private: boolean + org_wide?: boolean + team_id?: string + } + export interface ConversationsDelete { + channel_id: string + } + export interface ConversationsDisconnectShared { + channel_id: string + leaving_team_ids?: string + } + export interface ConversationsEkmListOriginalConnectedChannelInfo { + channel_ids?: string + team_ids?: string + limit?: number + cursor?: string + } + export interface ConversationsGetConversationPrefs { + channel_id: string + } + export interface ConversationsGetTeams { + channel_id: string + cursor?: string + limit?: number + } + export interface ConversationsInvite { + user_ids: string + channel_id: string + } + export interface ConversationsRename { + channel_id: string + name: string + } + export interface ConversationsRestrictAccessAddGroup { + team_id?: string + group_id: string + channel_id: string + } + export interface ConversationsRestrictAccessListGroups { + channel_id: string + team_id?: string + } + export interface ConversationsRestrictAccessRemoveGroup { + team_id: string + group_id: string + channel_id: string + } + export interface ConversationsSearch { + team_ids?: string + query?: string + limit?: number + cursor?: string + search_channel_types?: string + sort?: string + sort_dir?: string + } + export interface ConversationsSetConversationPrefs { + channel_id: string + prefs: string + } + export interface ConversationsSetTeams { + channel_id: string + team_id?: string + target_team_ids?: string + org_channel?: boolean + } + export interface ConversationsUnarchive { + channel_id: string + } + export interface EmojiAdd { + name: string + url: string + } + export interface EmojiAddAlias { + name: string + alias_for: string + } + export interface EmojiList { + cursor?: string + limit?: number + } + export interface EmojiRemove { + name: string + } + export interface EmojiRename { + name: string + new_name: string + } + export interface InviteRequestsApprove { + team_id?: string + invite_request_id: string + } + export interface InviteRequestsApprovedList { + team_id?: string + cursor?: string + limit?: number + } + export interface InviteRequestsDeniedList { + team_id?: string + cursor?: string + limit?: number + } + export interface InviteRequestsDeny { + team_id?: string + invite_request_id: string + } + export interface InviteRequestsList { + team_id?: string + cursor?: string + limit?: number + } + export interface TeamsAdminsList { + limit?: number + cursor?: string + team_id: string + } + export interface TeamsCreate { + team_domain: string + team_name: string + team_description?: string + team_discoverability?: string + } + export interface TeamsList { + limit?: number + cursor?: string + } + export interface TeamsOwnersList { + team_id: string + limit?: number + cursor?: string + } + export interface TeamsSettingsInfo { + team_id: string + } + export interface TeamsSettingsSetDefaultChannels { + team_id: string + channel_ids: string + } + export interface TeamsSettingsSetDescription { + team_id: string + description: string + } + export interface TeamsSettingsSetDiscoverability { + team_id: string + discoverability: string + } + export interface TeamsSettingsSetIcon { + image_url: string + team_id: string + } + export interface TeamsSettingsSetName { + team_id: string + name: string + } + export interface UsergroupsAddChannels { + usergroup_id: string + team_id?: string + channel_ids: string + } + export interface UsergroupsAddTeams { + usergroup_id: string + team_ids: string + auto_provision?: boolean + } + export interface UsergroupsListChannels { + usergroup_id: string + team_id?: string + include_num_members?: boolean + } + export interface UsergroupsRemoveChannels { + usergroup_id: string + channel_ids: string + } + export interface UsersAssign { + team_id: string + user_id: string + is_restricted?: boolean + is_ultra_restricted?: boolean + channel_ids?: string + } + export interface UsersInvite { + team_id: string + email: string + channel_ids: string + custom_message?: string + real_name?: string + resend?: boolean + is_restricted?: boolean + is_ultra_restricted?: boolean + guest_expiration_ts?: string + } + export interface UsersList { + team_id: string + cursor?: string + limit?: number + } + export interface UsersRemove { + team_id: string + user_id: string + } + export interface UsersSessionInvalidate { + team_id: string + session_id: number + } + export interface UsersSessionReset { + user_id: string + mobile_only?: boolean + web_only?: boolean + } + export interface UsersSetAdmin { + team_id: string + user_id: string + } + export interface UsersSetExpiration { + team_id: string + user_id: string + expiration_ts: number + } + export interface UsersSetOwner { + team_id: string + user_id: string + } + export interface UsersSetRegular { + team_id: string + user_id: string + } + } +} + +declare module './internal' { + interface Internal { + + /** + * Approve an app for installation on a workspace. + * @see https://api.slack.com/methods/admin.apps.approve + */ + adminAppsApprove(token: TokenInput, params: Admin.Params.AppsApprove): Promise<{ + ok: boolean + }> + + /** + * List approved apps for an org or workspace. + * @see https://api.slack.com/methods/admin.apps.approved.list + */ + adminAppsApprovedList(token: TokenInput, params: Admin.Params.AppsApprovedList): Promise<{ + ok: boolean + }> + + /** + * List app requests for a team/workspace. + * @see https://api.slack.com/methods/admin.apps.requests.list + */ + adminAppsRequestsList(token: TokenInput, params: Admin.Params.AppsRequestsList): Promise<{ + ok: boolean + }> + + /** + * Restrict an app for installation on a workspace. + * @see https://api.slack.com/methods/admin.apps.restrict + */ + adminAppsRestrict(token: TokenInput, params: Admin.Params.AppsRestrict): Promise<{ + ok: boolean + }> + + /** + * List restricted apps for an org or workspace. + * @see https://api.slack.com/methods/admin.apps.restricted.list + */ + adminAppsRestrictedList(token: TokenInput, params: Admin.Params.AppsRestrictedList): Promise<{ + ok: boolean + }> + + /** + * Archive a public or private channel. + * @see https://api.slack.com/methods/admin.conversations.archive + */ + adminConversationsArchive(token: TokenInput, params: Admin.Params.ConversationsArchive): Promise<{ + ok: boolean + }> + + /** + * Convert a public channel to a private channel. + * @see https://api.slack.com/methods/admin.conversations.convertToPrivate + */ + adminConversationsConvertToPrivate(token: TokenInput, params: Admin.Params.ConversationsConvertToPrivate): Promise<{ + ok: boolean + }> + + /** + * Create a public or private channel-based conversation. + * @see https://api.slack.com/methods/admin.conversations.create + */ + adminConversationsCreate(token: TokenInput, params: Admin.Params.ConversationsCreate): Promise<{ + channel_id?: string + ok: boolean + }> + + /** + * Delete a public or private channel. + * @see https://api.slack.com/methods/admin.conversations.delete + */ + adminConversationsDelete(token: TokenInput, params: Admin.Params.ConversationsDelete): Promise<{ + ok: boolean + }> + + /** + * Disconnect a connected channel from one or more workspaces. + * @see https://api.slack.com/methods/admin.conversations.disconnectShared + */ + adminConversationsDisconnectShared(token: TokenInput, params: Admin.Params.ConversationsDisconnectShared): Promise<{ + ok: boolean + }> + + /** + * List all disconnected channels—i.e., channels that were once connected to other workspaces and then disconnected—and the corresponding original channel IDs for key revocation with EKM. + * @see https://api.slack.com/methods/admin.conversations.ekm.listOriginalConnectedChannelInfo + */ + adminConversationsEkmListOriginalConnectedChannelInfo(token: TokenInput, params: Admin.Params.ConversationsEkmListOriginalConnectedChannelInfo): Promise<{ + ok: boolean + }> + + /** + * Get conversation preferences for a public or private channel. + * @see https://api.slack.com/methods/admin.conversations.getConversationPrefs + */ + adminConversationsGetConversationPrefs(token: TokenInput, params: Admin.Params.ConversationsGetConversationPrefs): Promise<{ + ok: boolean + prefs?: { + can_thread: { + type: string[] + user: string[] + } + who_can_post: { + type: string[] + user: string[] + } + } + }> + + /** + * Get all the workspaces a given public or private channel is connected to within this Enterprise org. + * @see https://api.slack.com/methods/admin.conversations.getTeams + */ + adminConversationsGetTeams(token: TokenInput, params: Admin.Params.ConversationsGetTeams): Promise<{ + ok: boolean + response_metadata?: { + next_cursor: string + } + team_ids: string[] + }> + + /** + * Invite a user to a public or private channel. + * @see https://api.slack.com/methods/admin.conversations.invite + */ + adminConversationsInvite(token: TokenInput, params: Admin.Params.ConversationsInvite): Promise<{ + ok: boolean + }> + + /** + * Rename a public or private channel. + * @see https://api.slack.com/methods/admin.conversations.rename + */ + adminConversationsRename(token: TokenInput, params: Admin.Params.ConversationsRename): Promise<{ + ok: boolean + }> + + /** + * Add an allowlist of IDP groups for accessing a channel + * @see https://api.slack.com/methods/admin.conversations.restrictAccess.addGroup + */ + adminConversationsRestrictAccessAddGroup(token: TokenInput, params: Admin.Params.ConversationsRestrictAccessAddGroup): Promise<{ + ok: boolean + }> + + /** + * List all IDP Groups linked to a channel + * @see https://api.slack.com/methods/admin.conversations.restrictAccess.listGroups + */ + adminConversationsRestrictAccessListGroups(token: TokenInput, params: Admin.Params.ConversationsRestrictAccessListGroups): Promise<{ + ok: boolean + }> + + /** + * Remove a linked IDP group linked from a private channel + * @see https://api.slack.com/methods/admin.conversations.restrictAccess.removeGroup + */ + adminConversationsRestrictAccessRemoveGroup(token: TokenInput, params: Admin.Params.ConversationsRestrictAccessRemoveGroup): Promise<{ + ok: boolean + }> + + /** + * Search for public or private channels in an Enterprise organization. + * @see https://api.slack.com/methods/admin.conversations.search + */ + adminConversationsSearch(token: TokenInput, params: Admin.Params.ConversationsSearch): Promise<{ + channels: Definitions.Channel[] + next_cursor: string + }> + + /** + * Set the posting permissions for a public or private channel. + * @see https://api.slack.com/methods/admin.conversations.setConversationPrefs + */ + adminConversationsSetConversationPrefs(token: TokenInput, params: Admin.Params.ConversationsSetConversationPrefs): Promise<{ + ok: boolean + }> + + /** + * Set the workspaces in an Enterprise grid org that connect to a public or private channel. + * @see https://api.slack.com/methods/admin.conversations.setTeams + */ + adminConversationsSetTeams(token: TokenInput, params: Admin.Params.ConversationsSetTeams): Promise<{ + ok: boolean + }> + + /** + * Unarchive a public or private channel. + * @see https://api.slack.com/methods/admin.conversations.unarchive + */ + adminConversationsUnarchive(token: TokenInput, params: Admin.Params.ConversationsUnarchive): Promise<{ + ok: boolean + }> + + /** + * Add an emoji. + * @see https://api.slack.com/methods/admin.emoji.add + */ + adminEmojiAdd(token: TokenInput, params: Admin.Params.EmojiAdd): Promise<{ + ok: boolean + }> + + /** + * Add an emoji alias. + * @see https://api.slack.com/methods/admin.emoji.addAlias + */ + adminEmojiAddAlias(token: TokenInput, params: Admin.Params.EmojiAddAlias): Promise<{ + ok: boolean + }> + + /** + * List emoji for an Enterprise Grid organization. + * @see https://api.slack.com/methods/admin.emoji.list + */ + adminEmojiList(token: TokenInput, params: Admin.Params.EmojiList): Promise<{ + ok: boolean + }> + + /** + * Remove an emoji across an Enterprise Grid organization + * @see https://api.slack.com/methods/admin.emoji.remove + */ + adminEmojiRemove(token: TokenInput, params: Admin.Params.EmojiRemove): Promise<{ + ok: boolean + }> + + /** + * Rename an emoji. + * @see https://api.slack.com/methods/admin.emoji.rename + */ + adminEmojiRename(token: TokenInput, params: Admin.Params.EmojiRename): Promise<{ + ok: boolean + }> + + /** + * Approve a workspace invite request. + * @see https://api.slack.com/methods/admin.inviteRequests.approve + */ + adminInviteRequestsApprove(token: TokenInput, params: Admin.Params.InviteRequestsApprove): Promise<{ + ok: boolean + }> + + /** + * List all approved workspace invite requests. + * @see https://api.slack.com/methods/admin.inviteRequests.approved.list + */ + adminInviteRequestsApprovedList(token: TokenInput, params: Admin.Params.InviteRequestsApprovedList): Promise<{ + ok: boolean + }> + + /** + * List all denied workspace invite requests. + * @see https://api.slack.com/methods/admin.inviteRequests.denied.list + */ + adminInviteRequestsDeniedList(token: TokenInput, params: Admin.Params.InviteRequestsDeniedList): Promise<{ + ok: boolean + }> + + /** + * Deny a workspace invite request. + * @see https://api.slack.com/methods/admin.inviteRequests.deny + */ + adminInviteRequestsDeny(token: TokenInput, params: Admin.Params.InviteRequestsDeny): Promise<{ + ok: boolean + }> + + /** + * List all pending workspace invite requests. + * @see https://api.slack.com/methods/admin.inviteRequests.list + */ + adminInviteRequestsList(token: TokenInput, params: Admin.Params.InviteRequestsList): Promise<{ + ok: boolean + }> + + /** + * List all of the admins on a given workspace. + * @see https://api.slack.com/methods/admin.teams.admins.list + */ + adminTeamsAdminsList(token: TokenInput, params: Admin.Params.TeamsAdminsList): Promise<{ + ok: boolean + }> + + /** + * Create an Enterprise team. + * @see https://api.slack.com/methods/admin.teams.create + */ + adminTeamsCreate(token: TokenInput, params: Admin.Params.TeamsCreate): Promise<{ + ok: boolean + }> + + /** + * List all teams on an Enterprise organization + * @see https://api.slack.com/methods/admin.teams.list + */ + adminTeamsList(token: TokenInput, params: Admin.Params.TeamsList): Promise<{ + ok: boolean + }> + + /** + * List all of the owners on a given workspace. + * @see https://api.slack.com/methods/admin.teams.owners.list + */ + adminTeamsOwnersList(token: TokenInput, params: Admin.Params.TeamsOwnersList): Promise<{ + ok: boolean + }> + + /** + * Fetch information about settings in a workspace + * @see https://api.slack.com/methods/admin.teams.settings.info + */ + adminTeamsSettingsInfo(token: TokenInput, params: Admin.Params.TeamsSettingsInfo): Promise<{ + ok: boolean + }> + + /** + * Set the default channels of a workspace. + * @see https://api.slack.com/methods/admin.teams.settings.setDefaultChannels + */ + adminTeamsSettingsSetDefaultChannels(token: TokenInput, params: Admin.Params.TeamsSettingsSetDefaultChannels): Promise<{ + ok: boolean + }> + + /** + * Set the description of a given workspace. + * @see https://api.slack.com/methods/admin.teams.settings.setDescription + */ + adminTeamsSettingsSetDescription(token: TokenInput, params: Admin.Params.TeamsSettingsSetDescription): Promise<{ + ok: boolean + }> + + /** + * An API method that allows admins to set the discoverability of a given workspace + * @see https://api.slack.com/methods/admin.teams.settings.setDiscoverability + */ + adminTeamsSettingsSetDiscoverability(token: TokenInput, params: Admin.Params.TeamsSettingsSetDiscoverability): Promise<{ + ok: boolean + }> + + /** + * Sets the icon of a workspace. + * @see https://api.slack.com/methods/admin.teams.settings.setIcon + */ + adminTeamsSettingsSetIcon(token: TokenInput, params: Admin.Params.TeamsSettingsSetIcon): Promise<{ + ok: boolean + }> + + /** + * Set the name of a given workspace. + * @see https://api.slack.com/methods/admin.teams.settings.setName + */ + adminTeamsSettingsSetName(token: TokenInput, params: Admin.Params.TeamsSettingsSetName): Promise<{ + ok: boolean + }> + + /** + * Add one or more default channels to an IDP group. + * @see https://api.slack.com/methods/admin.usergroups.addChannels + */ + adminUsergroupsAddChannels(token: TokenInput, params: Admin.Params.UsergroupsAddChannels): Promise<{ + ok: boolean + }> + + /** + * Associate one or more default workspaces with an organization-wide IDP group. + * @see https://api.slack.com/methods/admin.usergroups.addTeams + */ + adminUsergroupsAddTeams(token: TokenInput, params: Admin.Params.UsergroupsAddTeams): Promise<{ + ok: boolean + }> + + /** + * List the channels linked to an org-level IDP group (user group). + * @see https://api.slack.com/methods/admin.usergroups.listChannels + */ + adminUsergroupsListChannels(token: TokenInput, params: Admin.Params.UsergroupsListChannels): Promise<{ + ok: boolean + }> + + /** + * Remove one or more default channels from an org-level IDP group (user group). + * @see https://api.slack.com/methods/admin.usergroups.removeChannels + */ + adminUsergroupsRemoveChannels(token: TokenInput, params: Admin.Params.UsergroupsRemoveChannels): Promise<{ + ok: boolean + }> + + /** + * Add an Enterprise user to a workspace. + * @see https://api.slack.com/methods/admin.users.assign + */ + adminUsersAssign(token: TokenInput, params: Admin.Params.UsersAssign): Promise<{ + ok: boolean + }> + + /** + * Invite a user to a workspace. + * @see https://api.slack.com/methods/admin.users.invite + */ + adminUsersInvite(token: TokenInput, params: Admin.Params.UsersInvite): Promise<{ + ok: boolean + }> + + /** + * List users on a workspace + * @see https://api.slack.com/methods/admin.users.list + */ + adminUsersList(token: TokenInput, params: Admin.Params.UsersList): Promise<{ + ok: boolean + }> + + /** + * Remove a user from a workspace. + * @see https://api.slack.com/methods/admin.users.remove + */ + adminUsersRemove(token: TokenInput, params: Admin.Params.UsersRemove): Promise<{ + ok: boolean + }> + + /** + * Invalidate a single session for a user by session_id + * @see https://api.slack.com/methods/admin.users.session.invalidate + */ + adminUsersSessionInvalidate(token: TokenInput, params: Admin.Params.UsersSessionInvalidate): Promise<{ + ok: boolean + }> + + /** + * Wipes all valid sessions on all devices for a given user + * @see https://api.slack.com/methods/admin.users.session.reset + */ + adminUsersSessionReset(token: TokenInput, params: Admin.Params.UsersSessionReset): Promise<{ + ok: boolean + }> + + /** + * Set an existing guest, regular user, or owner to be an admin user. + * @see https://api.slack.com/methods/admin.users.setAdmin + */ + adminUsersSetAdmin(token: TokenInput, params: Admin.Params.UsersSetAdmin): Promise<{ + ok: boolean + }> + + /** + * Set an expiration for a guest user + * @see https://api.slack.com/methods/admin.users.setExpiration + */ + adminUsersSetExpiration(token: TokenInput, params: Admin.Params.UsersSetExpiration): Promise<{ + ok: boolean + }> + + /** + * Set an existing guest, regular user, or admin user to be a workspace owner. + * @see https://api.slack.com/methods/admin.users.setOwner + */ + adminUsersSetOwner(token: TokenInput, params: Admin.Params.UsersSetOwner): Promise<{ + ok: boolean + }> + + /** + * Set an existing guest user, admin user, or owner to be a regular user. + * @see https://api.slack.com/methods/admin.users.setRegular + */ + adminUsersSetRegular(token: TokenInput, params: Admin.Params.UsersSetRegular): Promise<{ + ok: boolean + }> + + } +} diff --git a/adapters/slack/src/types/api.ts b/adapters/slack/src/types/api.ts new file mode 100644 index 00000000..ec90be44 --- /dev/null +++ b/adapters/slack/src/types/api.ts @@ -0,0 +1,30 @@ +import { Internal, TokenInput } from './internal' +import { Definitions } from './definition' +Internal.define({ + '/api.test': { + GET: { 'apiTest': true }, + }, +}) + +export namespace Api { + export namespace Params { + export interface Test { + error?: string + foo?: string + } + } +} + +declare module './internal' { + interface Internal { + + /** + * Checks API calling code. + * @see https://api.slack.com/methods/api.test + */ + apiTest(token: TokenInput, params: Api.Params.Test): Promise<{ + ok: boolean + }> + + } +} diff --git a/adapters/slack/src/types/apps.ts b/adapters/slack/src/types/apps.ts new file mode 100644 index 00000000..6b57b49d --- /dev/null +++ b/adapters/slack/src/types/apps.ts @@ -0,0 +1,175 @@ +import { Internal, TokenInput } from './internal' +import { Definitions } from './definition' +Internal.define({ + '/apps.event.authorizations.list': { + GET: { 'appsEventAuthorizationsList': true }, + }, + '/apps.permissions.info': { + GET: { 'appsPermissionsInfo': false }, + }, + '/apps.permissions.request': { + GET: { 'appsPermissionsRequest': false }, + }, + '/apps.permissions.resources.list': { + GET: { 'appsPermissionsResourcesList': false }, + }, + '/apps.permissions.scopes.list': { + GET: { 'appsPermissionsScopesList': false }, + }, + '/apps.permissions.users.list': { + GET: { 'appsPermissionsUsersList': false }, + }, + '/apps.permissions.users.request': { + GET: { 'appsPermissionsUsersRequest': false }, + }, + '/apps.uninstall': { + GET: { 'appsUninstall': false }, + }, +}) + +export namespace Apps { + export namespace Params { + export interface EventAuthorizationsList { + event_context: string + cursor?: string + limit?: number + } + export interface PermissionsInfo { + } + export interface PermissionsRequest { + scopes: string + trigger_id: string + } + export interface PermissionsResourcesList { + cursor?: string + limit?: number + } + export interface PermissionsScopesList { + } + export interface PermissionsUsersList { + cursor?: string + limit?: number + } + export interface PermissionsUsersRequest { + scopes: string + trigger_id: string + user: string + } + export interface Uninstall { + client_id?: string + client_secret?: string + } + } +} + +declare module './internal' { + interface Internal { + + /** + * Get a list of authorizations for the given event context. Each authorization represents an app installation that the event is visible to. + * @see https://api.slack.com/methods/apps.event.authorizations.list + */ + appsEventAuthorizationsList(token: TokenInput, params: Apps.Params.EventAuthorizationsList): Promise<{ + ok: boolean + }> + + /** + * Returns list of permissions this app has on a team. + * @see https://api.slack.com/methods/apps.permissions.info + */ + appsPermissionsInfo(token: TokenInput): Promise<{ + info: { + app_home: { + resources: Definitions.Resources + scopes: Definitions.Scopes + } + channel: { + resources: Definitions.Resources + scopes: Definitions.Scopes + } + group: { + resources: Definitions.Resources + scopes: Definitions.Scopes + } + im: { + resources: Definitions.Resources + scopes: Definitions.Scopes + } + mpim: { + resources: Definitions.Resources + scopes: Definitions.Scopes + } + team: { + resources: Definitions.Resources + scopes: Definitions.Scopes + } + } + ok: boolean + }> + + /** + * Allows an app to request additional scopes + * @see https://api.slack.com/methods/apps.permissions.request + */ + appsPermissionsRequest(token: TokenInput, params: Apps.Params.PermissionsRequest): Promise<{ + ok: boolean + }> + + /** + * Returns list of resource grants this app has on a team. + * @see https://api.slack.com/methods/apps.permissions.resources.list + */ + appsPermissionsResourcesList(token: TokenInput, params: Apps.Params.PermissionsResourcesList): Promise<{ + ok: boolean + resources: { + id: string + type: string + }[] + response_metadata?: { + next_cursor: string + } + }> + + /** + * Returns list of scopes this app has on a team. + * @see https://api.slack.com/methods/apps.permissions.scopes.list + */ + appsPermissionsScopesList(token: TokenInput): Promise<{ + ok: boolean + scopes: { + app_home: Definitions.Scopes + channel: Definitions.Scopes + group: Definitions.Scopes + im: Definitions.Scopes + mpim: Definitions.Scopes + team: Definitions.Scopes + user: Definitions.Scopes + } + }> + + /** + * Returns list of user grants and corresponding scopes this app has on a team. + * @see https://api.slack.com/methods/apps.permissions.users.list + */ + appsPermissionsUsersList(token: TokenInput, params: Apps.Params.PermissionsUsersList): Promise<{ + ok: boolean + }> + + /** + * Enables an app to trigger a permissions modal to grant an app access to a user access scope. + * @see https://api.slack.com/methods/apps.permissions.users.request + */ + appsPermissionsUsersRequest(token: TokenInput, params: Apps.Params.PermissionsUsersRequest): Promise<{ + ok: boolean + }> + + /** + * Uninstalls your app from a workspace. + * @see https://api.slack.com/methods/apps.uninstall + */ + appsUninstall(token: TokenInput, params: Apps.Params.Uninstall): Promise<{ + ok: boolean + }> + + } +} diff --git a/adapters/slack/src/types/auth.ts b/adapters/slack/src/types/auth.ts new file mode 100644 index 00000000..f651725c --- /dev/null +++ b/adapters/slack/src/types/auth.ts @@ -0,0 +1,50 @@ +import { Internal, TokenInput } from './internal' +import { Definitions } from './definition' +Internal.define({ + '/auth.revoke': { + GET: { 'authRevoke': false }, + }, + '/auth.test': { + GET: { 'authTest': true }, + }, +}) + +export namespace Auth { + export namespace Params { + export interface Revoke { + test?: boolean + } + export interface Test { + } + } +} + +declare module './internal' { + interface Internal { + + /** + * Revokes a token. + * @see https://api.slack.com/methods/auth.revoke + */ + authRevoke(token: TokenInput, params: Auth.Params.Revoke): Promise<{ + ok: boolean + revoked: boolean + }> + + /** + * Checks authentication & identity. + * @see https://api.slack.com/methods/auth.test + */ + authTest(token: TokenInput): Promise<{ + bot_id?: string + is_enterprise_install?: boolean + ok: boolean + team: string + team_id: string + url: string + user: string + user_id: string + }> + + } +} diff --git a/adapters/slack/src/types/bots.ts b/adapters/slack/src/types/bots.ts new file mode 100644 index 00000000..fc3a3c4f --- /dev/null +++ b/adapters/slack/src/types/bots.ts @@ -0,0 +1,42 @@ +import { Internal, TokenInput } from './internal' +import { Definitions } from './definition' +Internal.define({ + '/bots.info': { + GET: { 'botsInfo': false }, + }, +}) + +export namespace Bots { + export namespace Params { + export interface Info { + bot?: string + } + } +} + +declare module './internal' { + interface Internal { + + /** + * Gets information about a bot user. + * @see https://api.slack.com/methods/bots.info + */ + botsInfo(token: TokenInput, params: Bots.Params.Info): Promise<{ + bot: { + app_id: string + deleted: boolean + icons: { + image_36: string + image_48: string + image_72: string + } + id: string + name: string + updated: number + user_id: string + } + ok: boolean + }> + + } +} diff --git a/adapters/slack/src/types/calls.ts b/adapters/slack/src/types/calls.ts new file mode 100644 index 00000000..e9a0316d --- /dev/null +++ b/adapters/slack/src/types/calls.ts @@ -0,0 +1,112 @@ +import { Internal, TokenInput } from './internal' +import { Definitions } from './definition' +Internal.define({ + '/calls.add': { + POST: { 'callsAdd': true }, + }, + '/calls.end': { + POST: { 'callsEnd': true }, + }, + '/calls.info': { + GET: { 'callsInfo': true }, + }, + '/calls.participants.add': { + POST: { 'callsParticipantsAdd': true }, + }, + '/calls.participants.remove': { + POST: { 'callsParticipantsRemove': true }, + }, + '/calls.update': { + POST: { 'callsUpdate': true }, + }, +}) + +export namespace Calls { + export namespace Params { + export interface Add { + external_unique_id: string + external_display_id?: string + join_url: string + desktop_app_join_url?: string + date_start?: number + title?: string + created_by?: string + users?: string + } + export interface End { + id: string + duration?: number + } + export interface Info { + id: string + } + export interface ParticipantsAdd { + id: string + users: string + } + export interface ParticipantsRemove { + id: string + users: string + } + export interface Update { + id: string + title?: string + join_url?: string + desktop_app_join_url?: string + } + } +} + +declare module './internal' { + interface Internal { + + /** + * Registers a new Call. + * @see https://api.slack.com/methods/calls.add + */ + callsAdd(token: TokenInput, params: Calls.Params.Add): Promise<{ + ok: boolean + }> + + /** + * Ends a Call. + * @see https://api.slack.com/methods/calls.end + */ + callsEnd(token: TokenInput, params: Calls.Params.End): Promise<{ + ok: boolean + }> + + /** + * Returns information about a Call. + * @see https://api.slack.com/methods/calls.info + */ + callsInfo(token: TokenInput, params: Calls.Params.Info): Promise<{ + ok: boolean + }> + + /** + * Registers new participants added to a Call. + * @see https://api.slack.com/methods/calls.participants.add + */ + callsParticipantsAdd(token: TokenInput, params: Calls.Params.ParticipantsAdd): Promise<{ + ok: boolean + }> + + /** + * Registers participants removed from a Call. + * @see https://api.slack.com/methods/calls.participants.remove + */ + callsParticipantsRemove(token: TokenInput, params: Calls.Params.ParticipantsRemove): Promise<{ + ok: boolean + }> + + /** + * Updates information about a Call. + * @see https://api.slack.com/methods/calls.update + */ + callsUpdate(token: TokenInput, params: Calls.Params.Update): Promise<{ + ok: boolean + }> + + } +} diff --git a/adapters/slack/src/types/chat.ts b/adapters/slack/src/types/chat.ts new file mode 100644 index 00000000..efe486a9 --- /dev/null +++ b/adapters/slack/src/types/chat.ts @@ -0,0 +1,255 @@ +import { Internal, TokenInput } from './internal' +import { Definitions } from './definition' +Internal.define({ + '/chat.delete': { + POST: { 'chatDelete': true }, + }, + '/chat.deleteScheduledMessage': { + POST: { 'chatDeleteScheduledMessage': true }, + }, + '/chat.getPermalink': { + GET: { 'chatGetPermalink': false }, + }, + '/chat.meMessage': { + POST: { 'chatMeMessage': true }, + }, + '/chat.postEphemeral': { + POST: { 'chatPostEphemeral': true }, + }, + '/chat.postMessage': { + POST: { 'chatPostMessage': true }, + }, + '/chat.scheduleMessage': { + POST: { 'chatScheduleMessage': true }, + }, + '/chat.scheduledMessages.list': { + GET: { 'chatScheduledMessagesList': true }, + }, + '/chat.unfurl': { + POST: { 'chatUnfurl': true }, + }, + '/chat.update': { + POST: { 'chatUpdate': true }, + }, +}) + +export namespace Chat { + export namespace Params { + export interface Delete { + ts?: number + channel?: string + as_user?: boolean + } + export interface DeleteScheduledMessage { + as_user?: boolean + channel: string + scheduled_message_id: string + } + export interface GetPermalink { + channel: string + message_ts: string + } + export interface MeMessage { + channel?: string + text?: string + } + export interface PostEphemeral { + as_user?: boolean + attachments?: string + blocks?: string + channel: string + icon_emoji?: string + icon_url?: string + link_names?: boolean + parse?: string + text?: string + thread_ts?: string + user: string + username?: string + } + export interface PostMessage { + as_user?: string + attachments?: string + blocks?: string + channel: string + icon_emoji?: string + icon_url?: string + link_names?: boolean + mrkdwn?: boolean + parse?: string + reply_broadcast?: boolean + text?: string + thread_ts?: string + unfurl_links?: boolean + unfurl_media?: boolean + username?: string + } + export interface ScheduleMessage { + channel?: string + text?: string + post_at?: string + parse?: string + as_user?: boolean + link_names?: boolean + attachments?: string + blocks?: string + unfurl_links?: boolean + unfurl_media?: boolean + thread_ts?: number + reply_broadcast?: boolean + } + export interface ScheduledMessagesList { + channel?: string + latest?: number + oldest?: number + limit?: number + cursor?: string + } + export interface Unfurl { + channel: string + ts: string + unfurls?: string + user_auth_message?: string + user_auth_required?: boolean + user_auth_url?: string + } + export interface Update { + as_user?: string + attachments?: string + blocks?: string + channel: string + link_names?: string + parse?: string + text?: string + ts: string + } + } +} + +declare module './internal' { + interface Internal { + + /** + * Deletes a message. + * @see https://api.slack.com/methods/chat.delete + */ + chatDelete(token: TokenInput, params: Chat.Params.Delete): Promise<{ + channel: string + ok: boolean + ts: string + }> + + /** + * Deletes a pending scheduled message from the queue. + * @see https://api.slack.com/methods/chat.deleteScheduledMessage + */ + chatDeleteScheduledMessage(token: TokenInput, params: Chat.Params.DeleteScheduledMessage): Promise<{ + ok: boolean + }> + + /** + * Retrieve a permalink URL for a specific extant message + * @see https://api.slack.com/methods/chat.getPermalink + */ + chatGetPermalink(token: TokenInput, params: Chat.Params.GetPermalink): Promise<{ + channel: string + ok: boolean + permalink: string + }> + + /** + * Share a me message into a channel. + * @see https://api.slack.com/methods/chat.meMessage + */ + chatMeMessage(token: TokenInput, params: Chat.Params.MeMessage): Promise<{ + channel?: string + ok: boolean + ts?: string + }> + + /** + * Sends an ephemeral message to a user in a channel. + * @see https://api.slack.com/methods/chat.postEphemeral + */ + chatPostEphemeral(token: TokenInput, params: Chat.Params.PostEphemeral): Promise<{ + message_ts: string + ok: boolean + }> + + /** + * Sends a message to a channel. + * @see https://api.slack.com/methods/chat.postMessage + */ + chatPostMessage(token: TokenInput, params: Chat.Params.PostMessage): Promise<{ + channel: string + message: Definitions.Message + ok: boolean + ts: string + }> + + /** + * Schedules a message to be sent to a channel. + * @see https://api.slack.com/methods/chat.scheduleMessage + */ + chatScheduleMessage(token: TokenInput, params: Chat.Params.ScheduleMessage): Promise<{ + channel: string + message: { + bot_id: string + bot_profile: Definitions.BotProfile + team: string + text: string + type: string + user: string + username: string + } + ok: boolean + post_at: number + scheduled_message_id: string + }> + + /** + * Returns a list of scheduled messages. + * @see https://api.slack.com/methods/chat.scheduledMessages.list + */ + chatScheduledMessagesList(token: TokenInput, params: Chat.Params.ScheduledMessagesList): Promise<{ + ok: boolean + response_metadata: { + next_cursor: string + } + scheduled_messages: { + channel_id: string + date_created: number + id: string + post_at: number + text: string + }[] + }> + + /** + * Provide custom unfurl behavior for user-posted URLs + * @see https://api.slack.com/methods/chat.unfurl + */ + chatUnfurl(token: TokenInput, params: Chat.Params.Unfurl): Promise<{ + ok: boolean + }> + + /** + * Updates a message. + * @see https://api.slack.com/methods/chat.update + */ + chatUpdate(token: TokenInput, params: Chat.Params.Update): Promise<{ + channel: string + message: { + attachments: { + }[] + blocks: { + } + text: string + } + ok: boolean + text: string + ts: string + }> + + } +} diff --git a/adapters/slack/src/types/conversations.ts b/adapters/slack/src/types/conversations.ts new file mode 100644 index 00000000..52b7b09b --- /dev/null +++ b/adapters/slack/src/types/conversations.ts @@ -0,0 +1,343 @@ +import { Internal, TokenInput } from './internal' +import { Definitions } from './definition' +Internal.define({ + '/conversations.archive': { + POST: { 'conversationsArchive': true }, + }, + '/conversations.close': { + POST: { 'conversationsClose': true }, + }, + '/conversations.create': { + POST: { 'conversationsCreate': true }, + }, + '/conversations.history': { + GET: { 'conversationsHistory': true }, + }, + '/conversations.info': { + GET: { 'conversationsInfo': false }, + }, + '/conversations.invite': { + POST: { 'conversationsInvite': true }, + }, + '/conversations.join': { + POST: { 'conversationsJoin': true }, + }, + '/conversations.kick': { + POST: { 'conversationsKick': true }, + }, + '/conversations.leave': { + POST: { 'conversationsLeave': true }, + }, + '/conversations.list': { + GET: { 'conversationsList': false }, + }, + '/conversations.mark': { + POST: { 'conversationsMark': true }, + }, + '/conversations.members': { + GET: { 'conversationsMembers': false }, + }, + '/conversations.open': { + POST: { 'conversationsOpen': true }, + }, + '/conversations.rename': { + POST: { 'conversationsRename': true }, + }, + '/conversations.replies': { + GET: { 'conversationsReplies': false }, + }, + '/conversations.setPurpose': { + POST: { 'conversationsSetPurpose': true }, + }, + '/conversations.setTopic': { + POST: { 'conversationsSetTopic': true }, + }, + '/conversations.unarchive': { + POST: { 'conversationsUnarchive': true }, + }, +}) + +export namespace Conversations { + export namespace Params { + export interface Archive { + channel?: string + } + export interface Close { + channel?: string + } + export interface Create { + name?: string + is_private?: boolean + } + export interface History { + channel?: string + latest?: number + oldest?: number + inclusive?: boolean + limit?: number + cursor?: string + } + export interface Info { + channel?: string + include_locale?: boolean + include_num_members?: boolean + } + export interface Invite { + channel?: string + users?: string + } + export interface Join { + channel?: string + } + export interface Kick { + channel?: string + user?: string + } + export interface Leave { + channel?: string + } + export interface List { + exclude_archived?: boolean + types?: string + limit?: number + cursor?: string + } + export interface Mark { + channel?: string + ts?: number + } + export interface Members { + channel?: string + limit?: number + cursor?: string + } + export interface Open { + channel?: string + users?: string + return_im?: boolean + } + export interface Rename { + channel?: string + name?: string + } + export interface Replies { + channel?: string + ts?: number + latest?: number + oldest?: number + inclusive?: boolean + limit?: number + cursor?: string + } + export interface SetPurpose { + channel?: string + purpose?: string + } + export interface SetTopic { + channel?: string + topic?: string + } + export interface Unarchive { + channel?: string + } + } +} + +declare module './internal' { + interface Internal { + + /** + * Archives a conversation. + * @see https://api.slack.com/methods/conversations.archive + */ + conversationsArchive(token: TokenInput, params: Conversations.Params.Archive): Promise<{ + ok: boolean + }> + + /** + * Closes a direct message or multi-person direct message. + * @see https://api.slack.com/methods/conversations.close + */ + conversationsClose(token: TokenInput, params: Conversations.Params.Close): Promise<{ + already_closed?: boolean + no_op?: boolean + ok: boolean + }> + + /** + * Initiates a public or private channel-based conversation + * @see https://api.slack.com/methods/conversations.create + */ + conversationsCreate(token: TokenInput, params: Conversations.Params.Create): Promise<{ + channel: Definitions.Conversation + ok: boolean + }> + + /** + * Fetches a conversation's history of messages and events. + * @see https://api.slack.com/methods/conversations.history + */ + conversationsHistory(token: TokenInput, params: Conversations.Params.History): Promise<{ + channel_actions_count: number + channel_actions_ts: number + has_more: boolean + messages: Definitions.Message[] + ok: boolean + pin_count: number + }> + + /** + * Retrieve information about a conversation. + * @see https://api.slack.com/methods/conversations.info + */ + conversationsInfo(token: TokenInput, params: Conversations.Params.Info): Promise<{ + channel: Definitions.Conversation + ok: boolean + }> + + /** + * Invites users to a channel. + * @see https://api.slack.com/methods/conversations.invite + */ + conversationsInvite(token: TokenInput, params: Conversations.Params.Invite): Promise<{ + channel: Definitions.Conversation + ok: boolean + }> + + /** + * Joins an existing conversation. + * @see https://api.slack.com/methods/conversations.join + */ + conversationsJoin(token: TokenInput, params: Conversations.Params.Join): Promise<{ + channel: Definitions.Conversation + ok: boolean + response_metadata?: { + warnings: string[] + } + warning?: string + }> + + /** + * Removes a user from a conversation. + * @see https://api.slack.com/methods/conversations.kick + */ + conversationsKick(token: TokenInput, params: Conversations.Params.Kick): Promise<{ + ok: boolean + }> + + /** + * Leaves a conversation. + * @see https://api.slack.com/methods/conversations.leave + */ + conversationsLeave(token: TokenInput, params: Conversations.Params.Leave): Promise<{ + not_in_channel?: boolean + ok: boolean + }> + + /** + * Lists all channels in a Slack team. + * @see https://api.slack.com/methods/conversations.list + */ + conversationsList(token: TokenInput, params: Conversations.Params.List): Promise<{ + channels: Definitions.Conversation[] + ok: boolean + response_metadata?: { + next_cursor: string + } + }> + + /** + * Sets the read cursor in a channel. + * @see https://api.slack.com/methods/conversations.mark + */ + conversationsMark(token: TokenInput, params: Conversations.Params.Mark): Promise<{ + ok: boolean + }> + + /** + * Retrieve members of a conversation. + * @see https://api.slack.com/methods/conversations.members + */ + conversationsMembers(token: TokenInput, params: Conversations.Params.Members): Promise<{ + members: string[] + ok: boolean + response_metadata: { + next_cursor: string + } + }> + + /** + * Opens or resumes a direct message or multi-person direct message. + * @see https://api.slack.com/methods/conversations.open + */ + conversationsOpen(token: TokenInput, params: Conversations.Params.Open): Promise<{ + already_open?: boolean + channel: Definitions.Conversation + no_op?: boolean + ok: boolean + }> + + /** + * Renames a conversation. + * @see https://api.slack.com/methods/conversations.rename + */ + conversationsRename(token: TokenInput, params: Conversations.Params.Rename): Promise<{ + channel: Definitions.Conversation + ok: boolean + }> + + /** + * Retrieve a thread of messages posted to a conversation + * @see https://api.slack.com/methods/conversations.replies + */ + conversationsReplies(token: TokenInput, params: Conversations.Params.Replies): Promise<{ + has_more?: boolean + messages: { + last_read: string + latest_reply: string + reply_count: number + reply_users: string[] + reply_users_count: number + source_team: string + subscribed: boolean + team: string + text: string + thread_ts: string + ts: string + type: string + unread_count: number + user: string + user_profile: Definitions.UserProfileShort + user_team: string + }[] + ok: boolean + }> + + /** + * Sets the purpose for a conversation. + * @see https://api.slack.com/methods/conversations.setPurpose + */ + conversationsSetPurpose(token: TokenInput, params: Conversations.Params.SetPurpose): Promise<{ + channel: Definitions.Conversation + ok: boolean + }> + + /** + * Sets the topic for a conversation. + * @see https://api.slack.com/methods/conversations.setTopic + */ + conversationsSetTopic(token: TokenInput, params: Conversations.Params.SetTopic): Promise<{ + channel: Definitions.Conversation + ok: boolean + }> + + /** + * Reverses conversation archival. + * @see https://api.slack.com/methods/conversations.unarchive + */ + conversationsUnarchive(token: TokenInput, params: Conversations.Params.Unarchive): Promise<{ + ok: boolean + }> + + } +} diff --git a/adapters/slack/src/types/definition.ts b/adapters/slack/src/types/definition.ts new file mode 100644 index 00000000..113b879b --- /dev/null +++ b/adapters/slack/src/types/definition.ts @@ -0,0 +1,530 @@ +export namespace Definitions { + export type BotProfile = { + app_id: string + deleted: boolean + icons: { + image_36: string + image_48: string + image_72: string + } + id: string + name: string + team_id: string + updated: number + } + + export type Channel = { + accepted_user: string + created: number + creator: string + id: string + is_archived: boolean + is_channel: boolean + is_frozen: boolean + is_general: boolean + is_member: boolean + is_moved: number + is_mpim: boolean + is_non_threadable: boolean + is_org_shared: boolean + is_pending_ext_shared: boolean + is_private: boolean + is_read_only: boolean + is_shared: boolean + is_thread_only: boolean + last_read: string + latest: Definitions.Message + members: string[] + name: string + name_normalized: string + num_members: number + pending_shared: string[] + previous_names: string[] + priority: unknown + purpose: { + creator: string + last_set: number + value: string + } + topic: { + creator: string + last_set: number + value: string + } + unlinked: number + unread_count: number + unread_count_display: number + } + + export type Comment = { + comment: string + created: number + id: string + is_intro: boolean + is_starred: boolean + num_stars: number + pinned_info: { + } + pinned_to: string[] + reactions: Definitions.Reaction[] + timestamp: number + user: string + } + + export type Comments = unknown[] + + export type Conversation = { + accepted_user: string + connected_team_ids: string[] + conversation_host_id: string + created: number + creator: string + display_counts: { + display_counts: number + guest_counts: number + } + enterprise_id: string + has_pins: boolean + id: string + internal_team_ids: string[] + is_archived: boolean + is_channel: boolean + is_ext_shared: boolean + is_frozen: boolean + is_general: boolean + is_global_shared: boolean + is_group: boolean + is_im: boolean + is_member: boolean + is_moved: number + is_mpim: boolean + is_non_threadable: boolean + is_open: boolean + is_org_default: boolean + is_org_mandatory: boolean + is_org_shared: boolean + is_pending_ext_shared: boolean + is_private: boolean + is_read_only: boolean + is_shared: boolean + is_starred: boolean + is_thread_only: boolean + last_read: string + latest: Definitions.Message + members: string[] + name: string + name_normalized: string + num_members: number + parent_conversation: string + pending_connected_team_ids: string[] + pending_shared: string[] + pin_count: number + previous_names: string[] + priority: unknown + purpose: { + creator: string + last_set: number + value: string + } + shared_team_ids: string[] + shares: { + accepted_user: string + is_active: boolean + team: Definitions.Team + user: string + }[] + timezone_count: number + topic: { + creator: string + last_set: number + value: string + } + unlinked: number + unread_count: number + unread_count_display: number + use_case: string + user: string + version: number + } + + export type EnterpriseUser = { + enterprise_id: string + enterprise_name: string + id: string + is_admin: boolean + is_owner: boolean + teams: string[] + } + + export type ExternalOrgMigrations = { + current: { + date_started: number + team_id: string + }[] + date_updated: number + } + + export type File = { + channels: string[] + comments_count: number + created: number + date_delete: number + display_as_bot: boolean + editable: boolean + editor: string + external_id: string + external_type: string + external_url: string + filetype: string + groups: string[] + has_rich_preview: boolean + id: string + image_exif_rotation: number + ims: string[] + is_external: boolean + is_public: boolean + is_starred: boolean + is_tombstoned: boolean + last_editor: string + mimetype: string + mode: string + name: string + non_owner_editable: boolean + num_stars: number + original_h: number + original_w: number + permalink: string + permalink_public: string + pinned_info: { + } + pinned_to: string[] + pretty_type: string + preview: string + public_url_shared: boolean + reactions: Definitions.Reaction[] + shares: { + private: unknown + public: unknown + } + size: number + source_team: string + state: string + thumb_1024: string + thumb_1024_h: number + thumb_1024_w: number + thumb_160: string + thumb_360: string + thumb_360_h: number + thumb_360_w: number + thumb_480: string + thumb_480_h: number + thumb_480_w: number + thumb_64: string + thumb_720: string + thumb_720_h: number + thumb_720_w: number + thumb_80: string + thumb_800: string + thumb_800_h: number + thumb_800_w: number + thumb_960: string + thumb_960_h: number + thumb_960_w: number + thumb_tiny: string + timestamp: number + title: string + updated: number + url_private: string + url_private_download: string + user: string + user_team: string + username: string + } + + export type Icon = { + image_102: string + image_132: string + image_230: string + image_34: string + image_44: string + image_68: string + image_88: string + image_default: boolean + } + + export type Message = { + attachments: { + fallback: string + id: number + image_bytes: number + image_height: number + image_url: string + image_width: number + }[] + blocks: { + type: string + }[] + bot_id: string + bot_profile: Definitions.BotProfile + client_msg_id: string + comment: Definitions.Comment + display_as_bot: boolean + file: Definitions.File + files: Definitions.File[] + icons: { + emoji: string + image_64: string + } + inviter: string + is_delayed_message: boolean + is_intro: boolean + is_starred: boolean + last_read: string + latest_reply: string + name: string + old_name: string + parent_user_id: string + permalink: string + pinned_to: string[] + purpose: string + reactions: Definitions.Reaction[] + reply_count: number + reply_users: string[] + reply_users_count: number + source_team: string + subscribed: boolean + subtype: string + team: string + text: string + thread_ts: string + topic: string + ts: string + type: string + unread_count: number + upload: boolean + user: string + user_profile: Definitions.UserProfileShort + user_team: string + username: string + } + + export type Paging = { + count: number + page: number + pages: number + per_page: number + spill: number + total: number + } + + export type PrimaryOwner = { + email: string + id: string + } + + export type Reaction = { + count: number + name: string + users: string[] + } + + export type Reminder = { + complete_ts: number + creator: string + id: string + recurring: boolean + text: string + time: number + user: string + } + + export type Resources = { + excluded_ids: string[] + ids: string[] + wildcard: boolean + } + + export type ResponseMetadata = { + next_cursor: string + } + + export type Scopes = string[] + + export type Subteam = { + auto_provision: boolean + auto_type: unknown + channel_count: number + created_by: string + date_create: number + date_delete: number + date_update: number + deleted_by: unknown + description: string + enterprise_subteam_id: string + handle: string + id: string + is_external: boolean + is_subteam: boolean + is_usergroup: boolean + name: string + prefs: { + channels: string[] + groups: string[] + } + team_id: string + updated_by: string + user_count: number + users: string[] + } + + export type Team = { + archived: boolean + avatar_base_url: string + created: number + date_create: number + deleted: boolean + description: unknown + discoverable: unknown + domain: string + email_domain: string + enterprise_id: string + enterprise_name: string + external_org_migrations: Definitions.ExternalOrgMigrations + has_compliance_export: boolean + icon: Definitions.Icon + id: string + is_assigned: boolean + is_enterprise: number + is_over_storage_limit: boolean + limit_ts: number + locale: string + messages_count: number + msg_edit_window_mins: number + name: string + over_integrations_limit: boolean + over_storage_limit: boolean + pay_prod_cur: string + plan: string + primary_owner: Definitions.PrimaryOwner + sso_provider: { + label: string + name: string + type: string + } + } + + export type TeamProfileField = { + field_name: unknown + hint: string + id: string + is_hidden: boolean + label: string + options: unknown + ordering: unknown + possible_values: unknown + type: string + } + + export type TeamProfileFieldOption = { + is_custom: unknown + is_multiple_entry: unknown + is_protected: unknown + is_scim: unknown + } + + export type User = { + color: string + deleted: boolean + enterprise_user: Definitions.EnterpriseUser + has_2fa: boolean + id: string + is_admin: boolean + is_app_user: boolean + is_bot: boolean + is_external: boolean + is_forgotten: boolean + is_invited_user: boolean + is_owner: boolean + is_primary_owner: boolean + is_restricted: boolean + is_stranger: boolean + is_ultra_restricted: boolean + locale: string + name: string + presence: string + profile: Definitions.UserProfile + real_name: string + team: string + team_id: string + team_profile: { + fields: Definitions.TeamProfileField[] + } + two_factor_type: string + tz: unknown + tz_label: string + tz_offset: unknown + updated: unknown + } + + export type UserProfile = { + always_active: boolean + api_app_id: string + avatar_hash: string + bot_id: string + display_name: string + display_name_normalized: string + email: unknown + fields: unknown + first_name: unknown + guest_expiration_ts: unknown + guest_invited_by: unknown + image_1024: unknown + image_192: unknown + image_24: unknown + image_32: unknown + image_48: unknown + image_512: unknown + image_72: unknown + image_original: unknown + is_app_user: boolean + is_custom_image: boolean + is_restricted: unknown + is_ultra_restricted: unknown + last_avatar_image_hash: string + last_name: unknown + memberships_count: number + name: unknown + phone: string + pronouns: string + real_name: string + real_name_normalized: string + skype: string + status_default_emoji: string + status_default_text: string + status_default_text_canonical: unknown + status_emoji: string + status_expiration: number + status_text: string + status_text_canonical: unknown + team: string + title: string + updated: number + user_id: string + username: unknown + } + + export type UserProfileShort = { + avatar_hash: string + display_name: string + display_name_normalized: string + first_name: unknown + image_72: string + is_restricted: boolean + is_ultra_restricted: boolean + name: string + real_name: string + real_name_normalized: string + team: string + } + +} diff --git a/adapters/slack/src/types/dialog.ts b/adapters/slack/src/types/dialog.ts new file mode 100644 index 00000000..699da29e --- /dev/null +++ b/adapters/slack/src/types/dialog.ts @@ -0,0 +1,30 @@ +import { Internal, TokenInput } from './internal' +import { Definitions } from './definition' +Internal.define({ + '/dialog.open': { + GET: { 'dialogOpen': true }, + }, +}) + +export namespace Dialog { + export namespace Params { + export interface Open { + dialog: string + trigger_id: string + } + } +} + +declare module './internal' { + interface Internal { + + /** + * Open a dialog with a user + * @see https://api.slack.com/methods/dialog.open + */ + dialogOpen(token: TokenInput, params: Dialog.Params.Open): Promise<{ + ok: boolean + }> + + } +} diff --git a/adapters/slack/src/types/dnd.ts b/adapters/slack/src/types/dnd.ts new file mode 100644 index 00000000..aa9823b0 --- /dev/null +++ b/adapters/slack/src/types/dnd.ts @@ -0,0 +1,96 @@ +import { Internal, TokenInput } from './internal' +import { Definitions } from './definition' +Internal.define({ + '/dnd.endDnd': { + POST: { 'dndEndDnd': true }, + }, + '/dnd.endSnooze': { + POST: { 'dndEndSnooze': true }, + }, + '/dnd.info': { + GET: { 'dndInfo': false }, + }, + '/dnd.setSnooze': { + POST: { 'dndSetSnooze': false }, + }, + '/dnd.teamInfo': { + GET: { 'dndTeamInfo': false }, + }, +}) + +export namespace Dnd { + export namespace Params { + export interface EndDnd { + } + export interface EndSnooze { + } + export interface Info { + user?: string + } + export interface SetSnooze { + num_minutes: string + } + export interface TeamInfo { + users?: string + } + } +} + +declare module './internal' { + interface Internal { + + /** + * Ends the current user's Do Not Disturb session immediately. + * @see https://api.slack.com/methods/dnd.endDnd + */ + dndEndDnd(token: TokenInput): Promise<{ + ok: boolean + }> + + /** + * Ends the current user's snooze mode immediately. + * @see https://api.slack.com/methods/dnd.endSnooze + */ + dndEndSnooze(token: TokenInput): Promise<{ + dnd_enabled: boolean + next_dnd_end_ts: number + next_dnd_start_ts: number + ok: boolean + snooze_enabled: boolean + }> + + /** + * Retrieves a user's current Do Not Disturb status. + * @see https://api.slack.com/methods/dnd.info + */ + dndInfo(token: TokenInput, params: Dnd.Params.Info): Promise<{ + dnd_enabled: boolean + next_dnd_end_ts: number + next_dnd_start_ts: number + ok: boolean + snooze_enabled?: boolean + snooze_endtime?: number + snooze_remaining?: number + }> + + /** + * Turns on Do Not Disturb mode for the current user, or changes its duration. + * @see https://api.slack.com/methods/dnd.setSnooze + */ + dndSetSnooze(token: TokenInput, params: Dnd.Params.SetSnooze): Promise<{ + ok: boolean + snooze_enabled: boolean + snooze_endtime: number + snooze_remaining: number + }> + + /** + * Retrieves the Do Not Disturb status for up to 50 users on a team. + * @see https://api.slack.com/methods/dnd.teamInfo + */ + dndTeamInfo(token: TokenInput, params: Dnd.Params.TeamInfo): Promise<{ + ok: boolean + }> + + } +} diff --git a/adapters/slack/src/types/emoji.ts b/adapters/slack/src/types/emoji.ts new file mode 100644 index 00000000..97368cf7 --- /dev/null +++ b/adapters/slack/src/types/emoji.ts @@ -0,0 +1,28 @@ +import { Internal, TokenInput } from './internal' +import { Definitions } from './definition' +Internal.define({ + '/emoji.list': { + GET: { 'emojiList': false }, + }, +}) + +export namespace Emoji { + export namespace Params { + export interface List { + } + } +} + +declare module './internal' { + interface Internal { + + /** + * Lists custom emoji for a team. + * @see https://api.slack.com/methods/emoji.list + */ + emojiList(token: TokenInput): Promise<{ + ok: boolean + }> + + } +} diff --git a/adapters/slack/src/types/events/base-events.ts b/adapters/slack/src/types/events/base-events.ts new file mode 100644 index 00000000..6cd04b96 --- /dev/null +++ b/adapters/slack/src/types/events/base-events.ts @@ -0,0 +1,281 @@ +// https://github.com/slackapi/bolt-js/blob/main/src/types/events/base-events.ts + +import { MessageEvent as AllMessageEvents } from './message-events' +import { SlackUser } from '.' + +/** + * All known event types in Slack's Events API + * Please refer to https://api.slack.com/events for more details + * + * This is a discriminated union. The discriminant is the `type` property. + */ +export type SlackEvent = + | ChannelArchiveEvent + | ChannelCreatedEvent + | ChannelDeletedEvent + | ChannelIDChangedEvent + | ChannelLeftEvent + | ChannelRenameEvent + | EmojiChangedEvent + | FileChangeEvent + | InviteRequestedEvent + | MemberJoinedChannelEvent + | MemberLeftChannelEvent + | MessageEvent + | ReactionAddedEvent + | ReactionRemovedEvent + | UserChangeEvent + | UserProfileChangedEvent + | UserStatusChangedEvent + | HelloEvent + +export type EventTypePattern = string | RegExp + +export interface HelloEvent { + type: 'hello' +} + +export interface BasicSlackEvent { + type: Type +} + +export interface ChannelArchiveEvent { + type: 'channel_archive' + channel: string + user: string + is_moved?: number + event_ts: string +} + +export interface ChannelCreatedEvent { + type: 'channel_created' + channel: { + id: string + is_channel: boolean + name: string + name_normalized: string + created: number + creator: string // user ID + is_shared: boolean + is_org_shared: boolean + } +} + +export interface ChannelDeletedEvent { + type: 'channel_deleted' + channel: string +} + +export interface ChannelIDChangedEvent { + type: 'channel_id_changed' + old_channel_id: string + new_channel_id: string + event_ts: string +} + +export interface ChannelLeftEvent { + type: 'channel_left' + channel: string + actor_id: string + event_ts: string +} + +export interface ChannelRenameEvent { + type: 'channel_rename' + channel: { + id: string + name: string + name_normalized: string + created: number + is_channel: boolean + is_mpim: boolean + } + event_ts: string +} + +// NOTE: this should probably be broken into its two subtypes +export interface EmojiChangedEvent { + type: 'emoji_changed' + subtype: 'add' | 'remove' | 'rename' + names?: string[] // only for remove + name?: string // only for add + value?: string // only for add + old_name?: string + new_name?: string + event_ts: string +} + +export interface FileChangeEvent { + type: 'file_change' + file_id: string + file: { + id: string + } +} + +export interface InviteRequestedEvent { + type: 'invite_requested' + invite_request: { + id: string + email: string + date_created: number + requester_ids: string[] + channel_ids: string[] + invite_type: 'restricted' | 'ultra_restricted' | 'full_member' + real_name: string + date_expire: number + request_reason: string + team: { + id: string + name: string + domain: string + } + } +} + +export interface MemberJoinedChannelEvent { + type: 'member_joined_channel' + user: string + channel: string + channel_type: string + team: string + inviter?: string + event_ts: string +} + +export interface MemberLeftChannelEvent { + type: 'member_left_channel' + user: string + channel: string + channel_type: string + team: string + event_ts: string +} + +export type MessageEvent = AllMessageEvents + +export interface ReactionMessageItem { + type: 'message' + channel: string + ts: string +} + +export interface ReactionAddedEvent { + type: 'reaction_added' + user: string + reaction: string + item_user: string + item: ReactionMessageItem + event_ts: string +} + +export interface ReactionRemovedEvent { + type: 'reaction_removed' + user: string + reaction: string + item_user: string + item: ReactionMessageItem + event_ts: string +} + +export interface StatusEmojiDisplayInfo { + emoji_name?: string + display_alias?: string + display_url?: string +} + +export interface UserChangeEvent { + type: 'user_change' + user: SlackUser + cache_ts: number + event_ts: string +} + +export interface UserProfileChangedEvent { + type: 'user_profile_changed' + user: { + id: string + team_id: string + name: string + deleted: boolean + color: string + real_name: string + tz: string + tz_label: string + tz_offset: number + profile: { + title: string + phone: string + skype: string + real_name: string + real_name_normalized: string + display_name: string + display_name_normalized: string + status_text: string + status_text_canonical: string + status_emoji: string + status_emoji_display_info: StatusEmojiDisplayInfo[] + status_expiration: number + avatar_hash: string + huddle_state: string + huddle_state_expiration_ts: number + first_name: string + last_name: string + email?: string + image_original?: string + is_custom_image?: boolean + image_24: string + image_32: string + image_48: string + image_72: string + image_192: string + image_512: string + image_1024?: string + team: string + fields: + | { + [key: string]: { + value: string + alt: string + } + } + | [] + | null + } + is_admin: boolean + is_owner: boolean + is_primary_owner: boolean + is_restricted: boolean + is_ultra_restricted: boolean + is_bot: boolean + is_stranger?: boolean + updated: number + is_email_confirmed: boolean + is_app_user: boolean + is_invited_user?: boolean + has_2fa?: boolean + locale: string + presence?: string + enterprise_user?: { + id: string + enterprise_id: string + enterprise_name: string + is_admin: boolean + is_owner: boolean + teams: string[] + } + two_factor_type?: string + has_files?: boolean + is_workflow_bot?: boolean + who_can_share_contact_card: string + } + cache_ts: number + event_ts: string +} + +export interface UserStatusChangedEvent { + type: 'user_status_changed' + user: SlackUser + cache_ts: number + event_ts: string +} diff --git a/adapters/slack/src/types/events/index.ts b/adapters/slack/src/types/events/index.ts new file mode 100644 index 00000000..e1f1976b --- /dev/null +++ b/adapters/slack/src/types/events/index.ts @@ -0,0 +1,125 @@ +// https://github.com/slackapi/bolt-js/blob/main/src/types/events/index.ts + +import { BasicSlackEvent, UserProfileChangedEvent } from './base-events' +import { Block } from '@slack/types' + +export type StringIndexed = Record + +export * from './base-events' +export { + GenericMessageEvent, + BotMessageEvent, + ChannelJoinMessageEvent, + ChannelLeaveMessageEvent, + ChannelNameMessageEvent, + FileShareMessageEvent, + MeMessageEvent, + MessageChangedEvent, + MessageDeletedEvent, + File, +} from './message-events' + +export type SocketEvent = HelloEvent | EventsApiEvent | UrlVerificationEvent | EnvelopedEvent + +export interface HelloEvent { + type: 'hello' +} + +export interface EventsApiEvent { + type: 'events_api' + envelope_id: string + payload: EnvelopedEvent +} + +export interface UrlVerificationEvent { + type: 'url_verification' + token: string + challenge: string +} + +/** + * A Slack Events API event wrapped in the standard envelope. + * + * This describes the entire JSON-encoded body of a request from Slack's Events API. + */ +export interface EnvelopedEvent extends StringIndexed { + token: string + team_id: string + enterprise_id?: string + api_app_id: string + event: Event + type: 'event_callback' + event_id: string + event_time: number + // TODO: the two properties below are being deprecated on Feb 24, 2021 + authed_users?: string[] + authed_teams?: string[] + is_ext_shared_channel?: boolean + authorizations?: Authorization[] +} + +interface Authorization { + enterprise_id: string | null + team_id: string | null + user_id: string + is_bot: boolean + is_enterprise_install?: boolean +} + +export interface RichTextBlock extends Block { + type: 'rich_text' + elements: RichTextElement[] +} + +/** https://api.slack.com/changelog/2019-09-what-they-see-is-what-you-get-and-more-and-less#the_riches */ +export type RichTextElement = RichTextElement.TextSection | RichTextElement.TextList + +export type RichText = RichText.User | RichText.Text | RichText.Emoji | RichText.Link | RichText.Broadcast + +export namespace RichTextElement { + export interface TextSection { + type: 'rich_text_section' + elements: RichText[] + } + + export interface TextList { + type: 'rich_text_list' + style: 'bullet' | 'ordered' + indent: number + border: 0 + elements: TextSection[] + } +} + +export namespace RichText { + export interface User { + type: 'user' + user_id: string + } + export interface Text { + text: string + type: 'text' + style?: { + bold?: boolean + italic?: boolean + strike?: boolean + code?: boolean + } + } + export interface Emoji { + type: 'emoji' + unicode: string + name: string + } + export interface Link { + url: string + type: 'link' + text: string + } + export interface Broadcast { + type: 'broadcast' + range: 'here' | 'channel' | 'everyone' + } +} + +export type SlackUser = UserProfileChangedEvent['user'] diff --git a/adapters/slack/src/types/events/message-events.ts b/adapters/slack/src/types/events/message-events.ts new file mode 100644 index 00000000..80bdc27b --- /dev/null +++ b/adapters/slack/src/types/events/message-events.ts @@ -0,0 +1,253 @@ +// https://github.com/slackapi/bolt-js/blob/main/src/types/events/message-events.ts + +import { Block, KnownBlock, MessageAttachment } from '@slack/types' + +export type MessageEvent = + | GenericMessageEvent + | BotMessageEvent + | ChannelJoinMessageEvent + | ChannelLeaveMessageEvent + | ChannelNameMessageEvent + | FileShareMessageEvent + | MeMessageEvent + | MessageChangedEvent + | MessageDeletedEvent + +export interface GenericMessageEvent { + type: 'message' + subtype: undefined + event_ts: string + team?: string + channel: string + user: string + bot_id?: string + app_id?: string + username?: string + bot_profile?: BotProfile + text?: string + ts: string + thread_ts?: string + channel_type: channelTypes + attachments?: MessageAttachment[] + blocks?: (KnownBlock | Block)[] + files?: File[] + edited?: { + user: string + ts: string + } + client_msg_id?: string + parent_user_id?: string + + // TODO: optional types that maybe should flow into other subtypes? + is_starred?: boolean + pinned_to?: string[] + reactions?: { + name: string + count: number + users: string[] + }[] +} + +export interface BotMessageEvent { + type: 'message' + subtype: 'bot_message' + event_ts: string + channel: string + channel_type: channelTypes + ts: string + text: string + bot_id: string + username?: string + icons?: { + [size: string]: string + } + + // copied from MessageEvent + // TODO: is a user really optional? likely for things like IncomingWebhook authored messages + user?: string + attachments?: MessageAttachment[] + blocks?: (KnownBlock | Block)[] + edited?: { + user: string + ts: string + } + thread_ts?: string +} + +export interface ChannelJoinMessageEvent { + type: 'message' + subtype: 'channel_join' + team: string + user: string + inviter: string + channel: string + channel_type: channelTypes + text: string + ts: string + event_ts: string +} + +export interface ChannelLeaveMessageEvent { + type: 'message' + subtype: 'channel_leave' + team: string + user: string + channel: string + channel_type: channelTypes + text: string + ts: string + event_ts: string +} + +export interface ChannelNameMessageEvent { + type: 'message' + subtype: 'channel_name' + team: string + user: string + name: string + old_name: string + channel: string + channel_type: channelTypes + text: string + ts: string + event_ts: string +} + +export interface FileShareMessageEvent { + type: 'message' + subtype: 'file_share' + text: string + attachments?: MessageAttachment[] + blocks?: (KnownBlock | Block)[] + files?: File[] + upload?: boolean + display_as_bot?: boolean + x_files?: string[] + user: string + parent_user_id?: string + ts: string + thread_ts?: string + channel: string + channel_type: channelTypes + event_ts: string +} + +export interface MeMessageEvent { + type: 'message' + subtype: 'me_message' + event_ts: string + channel: string + channel_type: channelTypes + user: string + text: string + ts: string +} + +export interface MessageChangedEvent { + type: 'message' + subtype: 'message_changed' + event_ts: string + hidden: true + channel: string + channel_type: channelTypes + ts: string + message: GenericMessageEvent + previous_message: GenericMessageEvent +} + +export interface MessageDeletedEvent { + type: 'message' + subtype: 'message_deleted' + event_ts: string + hidden: true + channel: string + channel_type: channelTypes + ts: string + deleted_ts: string + // previous_message: MessageEvent + previous_message: GenericMessageEvent +} + +export type channelTypes = 'channel' | 'group' | 'im' | 'mpim' | 'app_home' + +interface BotProfile { + id: string + name: string + app_id: string + team_id: string + icons: { + [size: string]: string + } + updated: number + deleted: boolean +} + +export interface File { + id: string + created: number + name: string | null + title: string | null + mimetype: string + filetype: string + pretty_type: string + user?: string + editable: boolean + size: number + mode: 'hosted' | 'external' | 'snippet' | 'post' + is_external: boolean + external_type: string | null + is_public: boolean + public_url_shared: boolean + display_as_bot: boolean + username: string | null + + // Authentication required + url_private?: string + url_private_download?: string + + // Thumbnails (authentication still required) + thumb_64?: string + thumb_80?: string + thumb_160?: string + thumb_360?: string + thumb_360_w?: number + thumb_360_h?: number + thumb_360_gif?: string + thumb_480?: string + thumb_720?: string + thumb_960?: string + thumb_1024?: string + permalink: string + permalink_public?: string + edit_link?: string + image_exif_rotation?: number + original_w?: number + original_h?: number + deanimate_gif?: string + + // Posts + preview?: string + preview_highlight?: string + lines?: string + lines_more?: string + preview_is_truncated?: boolean + has_rich_preview?: boolean + + shares?: { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + [key: string]: any + } + channels: string[] | null + groups: string[] | null + users?: string[] + pinned_to?: string[] + reactions?: { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + [key: string]: any + }[] + is_starred?: boolean + num_stars?: number + + initial_comment?: string + comments_count?: string +} diff --git a/adapters/slack/src/types/files.ts b/adapters/slack/src/types/files.ts new file mode 100644 index 00000000..20927aed --- /dev/null +++ b/adapters/slack/src/types/files.ts @@ -0,0 +1,246 @@ +import { Internal, TokenInput } from './internal' +import { Definitions } from './definition' +Internal.define({ + '/files.comments.delete': { + POST: { 'filesCommentsDelete': true }, + }, + '/files.delete': { + POST: { 'filesDelete': true }, + }, + '/files.info': { + GET: { 'filesInfo': false }, + }, + '/files.list': { + GET: { 'filesList': false }, + }, + '/files.remote.add': { + POST: { 'filesRemoteAdd': false }, + }, + '/files.remote.info': { + GET: { 'filesRemoteInfo': false }, + }, + '/files.remote.list': { + GET: { 'filesRemoteList': false }, + }, + '/files.remote.remove': { + POST: { 'filesRemoteRemove': false }, + }, + '/files.remote.share': { + GET: { 'filesRemoteShare': false }, + }, + '/files.remote.update': { + POST: { 'filesRemoteUpdate': false }, + }, + '/files.revokePublicURL': { + POST: { 'filesRevokePublicURL': true }, + }, + '/files.sharedPublicURL': { + POST: { 'filesSharedPublicURL': true }, + }, + '/files.upload': { + POST: { 'filesUpload': false }, + }, +}) + +export namespace Files { + export namespace Params { + export interface CommentsDelete { + file?: string + id?: string + } + export interface Delete { + file?: string + } + export interface Info { + file?: string + count?: string + page?: string + limit?: number + cursor?: string + } + export interface List { + user?: string + channel?: string + ts_from?: number + ts_to?: number + types?: string + count?: string + page?: string + show_files_hidden_by_limit?: boolean + } + export interface RemoteAdd { + external_id?: string + title?: string + filetype?: string + external_url?: string + preview_image?: string + indexable_file_contents?: string + } + export interface RemoteInfo { + file?: string + external_id?: string + } + export interface RemoteList { + channel?: string + ts_from?: number + ts_to?: number + limit?: number + cursor?: string + } + export interface RemoteRemove { + file?: string + external_id?: string + } + export interface RemoteShare { + file?: string + external_id?: string + channels?: string + } + export interface RemoteUpdate { + file?: string + external_id?: string + title?: string + filetype?: string + external_url?: string + preview_image?: string + indexable_file_contents?: string + } + export interface RevokePublicURL { + file?: string + } + export interface SharedPublicURL { + file?: string + } + export interface Upload { + file?: string + content?: string + filetype?: string + filename?: string + title?: string + initial_comment?: string + channels?: string + thread_ts?: number + } + } +} + +declare module './internal' { + interface Internal { + + /** + * Deletes an existing comment on a file. + * @see https://api.slack.com/methods/files.comments.delete + */ + filesCommentsDelete(token: TokenInput, params: Files.Params.CommentsDelete): Promise<{ + ok: boolean + }> + + /** + * Deletes a file. + * @see https://api.slack.com/methods/files.delete + */ + filesDelete(token: TokenInput, params: Files.Params.Delete): Promise<{ + ok: boolean + }> + + /** + * Gets information about a file. + * @see https://api.slack.com/methods/files.info + */ + filesInfo(token: TokenInput, params: Files.Params.Info): Promise<{ + comments: Definitions.Comments + content_html?: unknown + editor?: string + file: Definitions.File + ok: boolean + paging?: Definitions.Paging + response_metadata?: Definitions.ResponseMetadata + }> + + /** + * List for a team, in a channel, or from a user with applied filters. + * @see https://api.slack.com/methods/files.list + */ + filesList(token: TokenInput, params: Files.Params.List): Promise<{ + files: Definitions.File[] + ok: boolean + paging: Definitions.Paging + }> + + /** + * Adds a file from a remote service + * @see https://api.slack.com/methods/files.remote.add + */ + filesRemoteAdd(token: TokenInput, params: Files.Params.RemoteAdd): Promise<{ + ok: boolean + }> + + /** + * Retrieve information about a remote file added to Slack + * @see https://api.slack.com/methods/files.remote.info + */ + filesRemoteInfo(token: TokenInput, params: Files.Params.RemoteInfo): Promise<{ + ok: boolean + }> + + /** + * Retrieve information about a remote file added to Slack + * @see https://api.slack.com/methods/files.remote.list + */ + filesRemoteList(token: TokenInput, params: Files.Params.RemoteList): Promise<{ + ok: boolean + }> + + /** + * Remove a remote file. + * @see https://api.slack.com/methods/files.remote.remove + */ + filesRemoteRemove(token: TokenInput, params: Files.Params.RemoteRemove): Promise<{ + ok: boolean + }> + + /** + * Share a remote file into a channel. + * @see https://api.slack.com/methods/files.remote.share + */ + filesRemoteShare(token: TokenInput, params: Files.Params.RemoteShare): Promise<{ + ok: boolean + }> + + /** + * Updates an existing remote file. + * @see https://api.slack.com/methods/files.remote.update + */ + filesRemoteUpdate(token: TokenInput, params: Files.Params.RemoteUpdate): Promise<{ + ok: boolean + }> + + /** + * Revokes public/external sharing access for a file + * @see https://api.slack.com/methods/files.revokePublicURL + */ + filesRevokePublicURL(token: TokenInput, params: Files.Params.RevokePublicURL): Promise<{ + file: Definitions.File + ok: boolean + }> + + /** + * Enables a file for public/external sharing. + * @see https://api.slack.com/methods/files.sharedPublicURL + */ + filesSharedPublicURL(token: TokenInput, params: Files.Params.SharedPublicURL): Promise<{ + file: Definitions.File + ok: boolean + }> + + /** + * Uploads or creates a file. + * @see https://api.slack.com/methods/files.upload + */ + filesUpload(token: TokenInput, params: Files.Params.Upload): Promise<{ + file: Definitions.File + ok: boolean + }> + + } +} diff --git a/adapters/slack/src/types/index.ts b/adapters/slack/src/types/index.ts new file mode 100644 index 00000000..a6a4623b --- /dev/null +++ b/adapters/slack/src/types/index.ts @@ -0,0 +1,84 @@ +export interface SlackChannel { + id: string + name: string + is_channel: boolean + is_group: boolean + is_im: boolean + created: number + creator: string + is_archived: boolean + is_general: boolean + unlinked: number + name_normalized: string + is_read_only: boolean + is_shared: boolean + parent_conversation: null + is_ext_shared: boolean + is_org_shared: boolean + pending_shared: any[] + is_pending_ext_shared: boolean + is_member: boolean + is_private: boolean + is_mpim: boolean + last_read: string + topic: { + value: string + creator: string + last_set: number + } + purpose: { + value: string + creator: string + last_set: number + } + previous_names: string[] + locale: string +} + +export interface SlackTeam { + id: string + name: string + domain: string + email_domain: string + icon: { + image_34: string + image_44: string + image_68: string + image_88: string + image_102: string + image_132: string + image_default: boolean + } + enterprise_id: string + enterprise_name: string +} + +export * from './events' +export * from './internal' +export * from './definition' + +export * from './admin' +export * from './api' +export * from './apps' +export * from './auth' +export * from './bots' +export * from './calls' +export * from './chat' +export * from './conversations' +export * from './dialog' +export * from './dnd' +export * from './emoji' +export * from './files' +export * from './migration' +export * from './oauth' +export * from './pins' +export * from './reactions' +export * from './reminders' +export * from './rtm' +export * from './search' +export * from './stars' +export * from './team' +export * from './usergroups' +export * from './users' +export * from './views' +export * from './workflows' diff --git a/adapters/slack/src/types/internal.ts b/adapters/slack/src/types/internal.ts new file mode 100644 index 00000000..1171e55b --- /dev/null +++ b/adapters/slack/src/types/internal.ts @@ -0,0 +1,57 @@ +import { Dict, makeArray, Quester } from '@satorijs/satori' +import { SlackBot } from '../bot' + +// https://api.slack.com/web#methods_supporting_json + +type SupportPostJSON = boolean + +export enum Token { + BOT = 0, + APP = 1, +} + +export type TokenInput = string | Token + +// https://api.slack.com/web#basics +export class Internal { + constructor(private bot: SlackBot, private http: Quester) { } + + // route: content-type + static define(routes: Dict>>>) { + for (const path in routes) { + for (const key in routes[path]) { + const method = key as Quester.Method + for (const name of Object.keys(routes[path][method])) { + Internal.prototype[name] = async function (this: Internal, ...args: any[]) { + const config: Quester.AxiosRequestConfig = { + headers: {}, + } + let token = '' + if (typeof args[0] === 'string') { + token = args[0] + } else { + token = args[0] === Token.BOT ? this.bot.config.botToken : this.bot.config.token + } + config.headers.Authorization = `Bearer ${token}` + const supportJson = routes[path][method][name] + if (method === 'GET') { + config.params = args[1] + } else if (supportJson && !(args[1] instanceof FormData)) { + config.headers['content-type'] = 'application/json; charset=utf-8' + config.data = JSON.stringify(args[1]) + } else { + config.headers['content-type'] = 'application/x-www-form-urlencoded' + config.data = args[1] + } + try { + return await this.http(method, path, config) + } catch (error) { + if (!Quester.isAxiosError(error) || !error.response) throw error + throw new Error(`[${error.response.status}] ${JSON.stringify(error.response.data)}`) + } + } + } + } + } + } +} diff --git a/adapters/slack/src/types/migration.ts b/adapters/slack/src/types/migration.ts new file mode 100644 index 00000000..87d13ff7 --- /dev/null +++ b/adapters/slack/src/types/migration.ts @@ -0,0 +1,36 @@ +import { Internal, TokenInput } from './internal' +import { Definitions } from './definition' +Internal.define({ + '/migration.exchange': { + GET: { 'migrationExchange': false }, + }, +}) + +export namespace Migration { + export namespace Params { + export interface Exchange { + users: string + team_id?: string + to_old?: boolean + } + } +} + +declare module './internal' { + interface Internal { + + /** + * For Enterprise Grid workspaces, map local user IDs to global user IDs + * @see https://api.slack.com/methods/migration.exchange + */ + migrationExchange(token: TokenInput, params: Migration.Params.Exchange): Promise<{ + enterprise_id: string + invalid_user_ids?: string[] + ok: boolean + team_id: string + user_id_map?: { + } + }> + + } +} diff --git a/adapters/slack/src/types/oauth.ts b/adapters/slack/src/types/oauth.ts new file mode 100644 index 00000000..b693fbb0 --- /dev/null +++ b/adapters/slack/src/types/oauth.ts @@ -0,0 +1,68 @@ +import { Internal, TokenInput } from './internal' +import { Definitions } from './definition' +Internal.define({ + '/oauth.access': { + GET: { 'oauthAccess': false }, + }, + '/oauth.token': { + GET: { 'oauthToken': false }, + }, + '/oauth.v2.access': { + GET: { 'oauthV2Access': false }, + }, +}) + +export namespace Oauth { + export namespace Params { + export interface Access { + client_id?: string + client_secret?: string + code?: string + redirect_uri?: string + single_channel?: boolean + } + export interface Token { + client_id?: string + client_secret?: string + code?: string + redirect_uri?: string + single_channel?: boolean + } + export interface V2Access { + client_id?: string + client_secret?: string + code: string + redirect_uri?: string + } + } +} + +declare module './internal' { + interface Internal { + + /** + * Exchanges a temporary OAuth verifier code for an access token. + * @see https://api.slack.com/methods/oauth.access + */ + oauthAccess(token: TokenInput, params: Oauth.Params.Access): Promise<{ + ok: boolean + }> + + /** + * Exchanges a temporary OAuth verifier code for a workspace token. + * @see https://api.slack.com/methods/oauth.token + */ + oauthToken(token: TokenInput, params: Oauth.Params.Token): Promise<{ + ok: boolean + }> + + /** + * Exchanges a temporary OAuth verifier code for an access token. + * @see https://api.slack.com/methods/oauth.v2.access + */ + oauthV2Access(token: TokenInput, params: Oauth.Params.V2Access): Promise<{ + ok: boolean + }> + + } +} diff --git a/adapters/slack/src/types/pins.ts b/adapters/slack/src/types/pins.ts new file mode 100644 index 00000000..d51e8868 --- /dev/null +++ b/adapters/slack/src/types/pins.ts @@ -0,0 +1,57 @@ +import { Internal, TokenInput } from './internal' +import { Definitions } from './definition' +Internal.define({ + '/pins.add': { + POST: { 'pinsAdd': true }, + }, + '/pins.list': { + GET: { 'pinsList': false }, + }, + '/pins.remove': { + POST: { 'pinsRemove': true }, + }, +}) + +export namespace Pins { + export namespace Params { + export interface Add { + channel: string + timestamp?: string + } + export interface List { + channel: string + } + export interface Remove { + channel: string + timestamp?: string + } + } +} + +declare module './internal' { + interface Internal { + + /** + * Pins an item to a channel. + * @see https://api.slack.com/methods/pins.add + */ + pinsAdd(token: TokenInput, params: Pins.Params.Add): Promise<{ + ok: boolean + }> + + /** + * Lists items pinned to a channel. + * @see https://api.slack.com/methods/pins.list + */ + pinsList(token: TokenInput, params: Pins.Params.List): Promise + + /** + * Un-pins an item from a channel. + * @see https://api.slack.com/methods/pins.remove + */ + pinsRemove(token: TokenInput, params: Pins.Params.Remove): Promise<{ + ok: boolean + }> + + } +} diff --git a/adapters/slack/src/types/reactions.ts b/adapters/slack/src/types/reactions.ts new file mode 100644 index 00000000..f74d3d1d --- /dev/null +++ b/adapters/slack/src/types/reactions.ts @@ -0,0 +1,96 @@ +import { Internal, TokenInput } from './internal' +import { Definitions } from './definition' +Internal.define({ + '/reactions.add': { + POST: { 'reactionsAdd': true }, + }, + '/reactions.get': { + GET: { 'reactionsGet': false }, + }, + '/reactions.list': { + GET: { 'reactionsList': false }, + }, + '/reactions.remove': { + POST: { 'reactionsRemove': true }, + }, +}) + +export namespace Reactions { + export namespace Params { + export interface Add { + channel: string + name: string + timestamp: string + } + export interface Get { + channel?: string + file?: string + file_comment?: string + full?: boolean + timestamp?: string + } + export interface List { + user?: string + full?: boolean + count?: number + page?: number + cursor?: string + limit?: number + } + export interface Remove { + name: string + file?: string + file_comment?: string + channel?: string + timestamp?: string + } + } +} + +declare module './internal' { + interface Internal { + + /** + * Adds a reaction to an item. + * @see https://api.slack.com/methods/reactions.add + */ + reactionsAdd(token: TokenInput, params: Reactions.Params.Add): Promise<{ + ok: boolean + }> + + /** + * Gets reactions for an item. + * @see https://api.slack.com/methods/reactions.get + */ + reactionsGet(token: TokenInput, params: Reactions.Params.Get): Promise<{ + channel: string + message: Definitions.Message + ok: boolean + type: string + }> + + /** + * Lists reactions made by a user. + * @see https://api.slack.com/methods/reactions.list + */ + reactionsList(token: TokenInput, params: Reactions.Params.List): Promise<{ + items: { + channel: string + message: Definitions.Message + type: string + }[] + ok: boolean + paging?: Definitions.Paging + response_metadata?: Definitions.ResponseMetadata + }> + + /** + * Removes a reaction from an item. + * @see https://api.slack.com/methods/reactions.remove + */ + reactionsRemove(token: TokenInput, params: Reactions.Params.Remove): Promise<{ + ok: boolean + }> + + } +} diff --git a/adapters/slack/src/types/reminders.ts b/adapters/slack/src/types/reminders.ts new file mode 100644 index 00000000..0e0cae0c --- /dev/null +++ b/adapters/slack/src/types/reminders.ts @@ -0,0 +1,89 @@ +import { Internal, TokenInput } from './internal' +import { Definitions } from './definition' +Internal.define({ + '/reminders.add': { + POST: { 'remindersAdd': true }, + }, + '/reminders.complete': { + POST: { 'remindersComplete': true }, + }, + '/reminders.delete': { + POST: { 'remindersDelete': true }, + }, + '/reminders.info': { + GET: { 'remindersInfo': false }, + }, + '/reminders.list': { + GET: { 'remindersList': false }, + }, +}) + +export namespace Reminders { + export namespace Params { + export interface Add { + text: string + time: string + user?: string + } + export interface Complete { + reminder?: string + } + export interface Delete { + reminder?: string + } + export interface Info { + reminder?: string + } + export interface List { + } + } +} + +declare module './internal' { + interface Internal { + + /** + * Creates a reminder. + * @see https://api.slack.com/methods/reminders.add + */ + remindersAdd(token: TokenInput, params: Reminders.Params.Add): Promise<{ + ok: boolean + reminder: Definitions.Reminder + }> + + /** + * Marks a reminder as complete. + * @see https://api.slack.com/methods/reminders.complete + */ + remindersComplete(token: TokenInput, params: Reminders.Params.Complete): Promise<{ + ok: boolean + }> + + /** + * Deletes a reminder. + * @see https://api.slack.com/methods/reminders.delete + */ + remindersDelete(token: TokenInput, params: Reminders.Params.Delete): Promise<{ + ok: boolean + }> + + /** + * Gets information about a reminder. + * @see https://api.slack.com/methods/reminders.info + */ + remindersInfo(token: TokenInput, params: Reminders.Params.Info): Promise<{ + ok: boolean + reminder: Definitions.Reminder + }> + + /** + * Lists all reminders created by or for a given user. + * @see https://api.slack.com/methods/reminders.list + */ + remindersList(token: TokenInput): Promise<{ + ok: boolean + reminders: Definitions.Reminder[] + }> + + } +} diff --git a/adapters/slack/src/types/rtm.ts b/adapters/slack/src/types/rtm.ts new file mode 100644 index 00000000..7e86399d --- /dev/null +++ b/adapters/slack/src/types/rtm.ts @@ -0,0 +1,40 @@ +import { Internal, TokenInput } from './internal' +import { Definitions } from './definition' +Internal.define({ + '/rtm.connect': { + GET: { 'rtmConnect': false }, + }, +}) + +export namespace Rtm { + export namespace Params { + export interface Connect { + batch_presence_aware?: boolean + presence_sub?: boolean + } + } +} + +declare module './internal' { + interface Internal { + + /** + * Starts a Real Time Messaging session. + * @see https://api.slack.com/methods/rtm.connect + */ + rtmConnect(token: TokenInput, params: Rtm.Params.Connect): Promise<{ + ok: boolean + self: { + id: string + name: string + } + team: { + domain: string + id: string + name: string + } + url: string + }> + + } +} diff --git a/adapters/slack/src/types/search.ts b/adapters/slack/src/types/search.ts new file mode 100644 index 00000000..6d0848bc --- /dev/null +++ b/adapters/slack/src/types/search.ts @@ -0,0 +1,34 @@ +import { Internal, TokenInput } from './internal' +import { Definitions } from './definition' +Internal.define({ + '/search.messages': { + GET: { 'searchMessages': false }, + }, +}) + +export namespace Search { + export namespace Params { + export interface Messages { + count?: number + highlight?: boolean + page?: number + query: string + sort?: string + sort_dir?: string + } + } +} + +declare module './internal' { + interface Internal { + + /** + * Searches for messages matching a query. + * @see https://api.slack.com/methods/search.messages + */ + searchMessages(token: TokenInput, params: Search.Params.Messages): Promise<{ + ok: boolean + }> + + } +} diff --git a/adapters/slack/src/types/stars.ts b/adapters/slack/src/types/stars.ts new file mode 100644 index 00000000..29e4412c --- /dev/null +++ b/adapters/slack/src/types/stars.ts @@ -0,0 +1,73 @@ +import { Internal, TokenInput } from './internal' +import { Definitions } from './definition' +Internal.define({ + '/stars.add': { + POST: { 'starsAdd': true }, + }, + '/stars.list': { + GET: { 'starsList': false }, + }, + '/stars.remove': { + POST: { 'starsRemove': true }, + }, +}) + +export namespace Stars { + export namespace Params { + export interface Add { + channel?: string + file?: string + file_comment?: string + timestamp?: string + } + export interface List { + count?: string + page?: string + cursor?: string + limit?: number + } + export interface Remove { + channel?: string + file?: string + file_comment?: string + timestamp?: string + } + } +} + +declare module './internal' { + interface Internal { + + /** + * Adds a star to an item. + * @see https://api.slack.com/methods/stars.add + */ + starsAdd(token: TokenInput, params: Stars.Params.Add): Promise<{ + ok: boolean + }> + + /** + * Lists stars for a user. + * @see https://api.slack.com/methods/stars.list + */ + starsList(token: TokenInput, params: Stars.Params.List): Promise<{ + items: { + channel: string + date_create: number + message: Definitions.Message + type: string + }[] + ok: boolean + paging?: Definitions.Paging + }> + + /** + * Removes a star from an item. + * @see https://api.slack.com/methods/stars.remove + */ + starsRemove(token: TokenInput, params: Stars.Params.Remove): Promise<{ + ok: boolean + }> + + } +} diff --git a/adapters/slack/src/types/team.ts b/adapters/slack/src/types/team.ts new file mode 100644 index 00000000..f304459f --- /dev/null +++ b/adapters/slack/src/types/team.ts @@ -0,0 +1,123 @@ +import { Internal, TokenInput } from './internal' +import { Definitions } from './definition' +Internal.define({ + '/team.accessLogs': { + GET: { 'teamAccessLogs': false }, + }, + '/team.billableInfo': { + GET: { 'teamBillableInfo': false }, + }, + '/team.info': { + GET: { 'teamInfo': false }, + }, + '/team.integrationLogs': { + GET: { 'teamIntegrationLogs': false }, + }, + '/team.profile.get': { + GET: { 'teamProfileGet': false }, + }, +}) + +export namespace Team { + export namespace Params { + export interface AccessLogs { + before?: string + count?: string + page?: string + } + export interface BillableInfo { + user?: string + } + export interface Info { + team?: string + } + export interface IntegrationLogs { + app_id?: string + change_type?: string + count?: string + page?: string + service_id?: string + user?: string + } + export interface ProfileGet { + visibility?: string + } + } +} + +declare module './internal' { + interface Internal { + + /** + * Gets the access logs for the current team. + * @see https://api.slack.com/methods/team.accessLogs + */ + teamAccessLogs(token: TokenInput, params: Team.Params.AccessLogs): Promise<{ + logins: { + count: number + country: unknown + date_first: number + date_last: number + ip: unknown + isp: unknown + region: unknown + user_agent: string + user_id: string + username: string + }[] + ok: boolean + paging: Definitions.Paging + }> + + /** + * Gets billable users information for the current team. + * @see https://api.slack.com/methods/team.billableInfo + */ + teamBillableInfo(token: TokenInput, params: Team.Params.BillableInfo): Promise<{ + ok: boolean + }> + + /** + * Gets information about the current team. + * @see https://api.slack.com/methods/team.info + */ + teamInfo(token: TokenInput, params: Team.Params.Info): Promise<{ + ok: boolean + team: Definitions.Team + }> + + /** + * Gets the integration logs for the current team. + * @see https://api.slack.com/methods/team.integrationLogs + */ + teamIntegrationLogs(token: TokenInput, params: Team.Params.IntegrationLogs): Promise<{ + logs: { + admin_app_id: string + app_id: string + app_type: string + change_type: string + channel: string + date: string + scope: string + service_id: string + service_type: string + user_id: string + user_name: string + }[] + ok: boolean + paging: Definitions.Paging + }> + + /** + * Retrieve a team's profile. + * @see https://api.slack.com/methods/team.profile.get + */ + teamProfileGet(token: TokenInput, params: Team.Params.ProfileGet): Promise<{ + ok: boolean + profile: { + fields: Definitions.TeamProfileField[] + } + }> + + } +} diff --git a/adapters/slack/src/types/usergroups.ts b/adapters/slack/src/types/usergroups.ts new file mode 100644 index 00000000..c53dfeca --- /dev/null +++ b/adapters/slack/src/types/usergroups.ts @@ -0,0 +1,136 @@ +import { Internal, TokenInput } from './internal' +import { Definitions } from './definition' +Internal.define({ + '/usergroups.create': { + POST: { 'usergroupsCreate': true }, + }, + '/usergroups.disable': { + POST: { 'usergroupsDisable': true }, + }, + '/usergroups.enable': { + POST: { 'usergroupsEnable': true }, + }, + '/usergroups.list': { + GET: { 'usergroupsList': false }, + }, + '/usergroups.update': { + POST: { 'usergroupsUpdate': true }, + }, + '/usergroups.users.list': { + GET: { 'usergroupsUsersList': false }, + }, + '/usergroups.users.update': { + POST: { 'usergroupsUsersUpdate': true }, + }, +}) + +export namespace Usergroups { + export namespace Params { + export interface Create { + channels?: string + description?: string + handle?: string + include_count?: boolean + name: string + } + export interface Disable { + include_count?: boolean + usergroup: string + } + export interface Enable { + include_count?: boolean + usergroup: string + } + export interface List { + include_users?: boolean + include_count?: boolean + include_disabled?: boolean + } + export interface Update { + handle?: string + description?: string + channels?: string + include_count?: boolean + usergroup: string + name?: string + } + export interface UsersList { + include_disabled?: boolean + usergroup: string + } + export interface UsersUpdate { + include_count?: boolean + usergroup: string + users: string + } + } +} + +declare module './internal' { + interface Internal { + + /** + * Create a User Group + * @see https://api.slack.com/methods/usergroups.create + */ + usergroupsCreate(token: TokenInput, params: Usergroups.Params.Create): Promise<{ + ok: boolean + usergroup: Definitions.Subteam + }> + + /** + * Disable an existing User Group + * @see https://api.slack.com/methods/usergroups.disable + */ + usergroupsDisable(token: TokenInput, params: Usergroups.Params.Disable): Promise<{ + ok: boolean + usergroup: Definitions.Subteam + }> + + /** + * Enable a User Group + * @see https://api.slack.com/methods/usergroups.enable + */ + usergroupsEnable(token: TokenInput, params: Usergroups.Params.Enable): Promise<{ + ok: boolean + usergroup: Definitions.Subteam + }> + + /** + * List all User Groups for a team + * @see https://api.slack.com/methods/usergroups.list + */ + usergroupsList(token: TokenInput, params: Usergroups.Params.List): Promise<{ + ok: boolean + usergroups: Definitions.Subteam[] + }> + + /** + * Update an existing User Group + * @see https://api.slack.com/methods/usergroups.update + */ + usergroupsUpdate(token: TokenInput, params: Usergroups.Params.Update): Promise<{ + ok: boolean + usergroup: Definitions.Subteam + }> + + /** + * List all users in a User Group + * @see https://api.slack.com/methods/usergroups.users.list + */ + usergroupsUsersList(token: TokenInput, params: Usergroups.Params.UsersList): Promise<{ + ok: boolean + users: string[] + }> + + /** + * Update the list of users for a User Group + * @see https://api.slack.com/methods/usergroups.users.update + */ + usergroupsUsersUpdate(token: TokenInput, params: Usergroups.Params.UsersUpdate): Promise<{ + ok: boolean + usergroup: Definitions.Subteam + }> + + } +} diff --git a/adapters/slack/src/types/users.ts b/adapters/slack/src/types/users.ts new file mode 100644 index 00000000..97a8d0d0 --- /dev/null +++ b/adapters/slack/src/types/users.ts @@ -0,0 +1,222 @@ +import { Internal, TokenInput } from './internal' +import { Definitions } from './definition' +Internal.define({ + '/users.conversations': { + GET: { 'usersConversations': false }, + }, + '/users.deletePhoto': { + POST: { 'usersDeletePhoto': false }, + }, + '/users.getPresence': { + GET: { 'usersGetPresence': false }, + }, + '/users.identity': { + GET: { 'usersIdentity': false }, + }, + '/users.info': { + GET: { 'usersInfo': false }, + }, + '/users.list': { + GET: { 'usersList': false }, + }, + '/users.lookupByEmail': { + GET: { 'usersLookupByEmail': false }, + }, + '/users.profile.get': { + GET: { 'usersProfileGet': false }, + }, + '/users.profile.set': { + POST: { 'usersProfileSet': true }, + }, + '/users.setActive': { + POST: { 'usersSetActive': true }, + }, + '/users.setPhoto': { + POST: { 'usersSetPhoto': false }, + }, + '/users.setPresence': { + POST: { 'usersSetPresence': true }, + }, +}) + +export namespace Users { + export namespace Params { + export interface Conversations { + user?: string + types?: string + exclude_archived?: boolean + limit?: number + cursor?: string + } + export interface DeletePhoto { + } + export interface GetPresence { + user?: string + } + export interface Identity { + } + export interface Info { + include_locale?: boolean + user?: string + } + export interface List { + limit?: number + cursor?: string + include_locale?: boolean + } + export interface LookupByEmail { + email: string + } + export interface ProfileGet { + include_labels?: boolean + user?: string + } + export interface ProfileSet { + name?: string + profile?: string + user?: string + value?: string + } + export interface SetActive { + } + export interface SetPhoto { + crop_w?: string + crop_x?: string + crop_y?: string + image?: string + } + export interface SetPresence { + presence: string + } + } +} + +declare module './internal' { + interface Internal { + + /** + * List conversations the calling user may access. + * @see https://api.slack.com/methods/users.conversations + */ + usersConversations(token: TokenInput, params: Users.Params.Conversations): Promise<{ + channels: Definitions.Conversation[] + ok: boolean + response_metadata?: { + next_cursor: string + } + }> + + /** + * Delete the user profile photo + * @see https://api.slack.com/methods/users.deletePhoto + */ + usersDeletePhoto(token: TokenInput): Promise<{ + ok: boolean + }> + + /** + * Gets user presence information. + * @see https://api.slack.com/methods/users.getPresence + */ + usersGetPresence(token: TokenInput, params: Users.Params.GetPresence): Promise<{ + auto_away?: boolean + connection_count?: number + last_activity?: number + manual_away?: boolean + ok: boolean + online?: boolean + presence: string + }> + + /** + * Get a user's identity. + * @see https://api.slack.com/methods/users.identity + */ + usersIdentity(token: TokenInput): Promise + + /** + * Gets information about a user. + * @see https://api.slack.com/methods/users.info + */ + usersInfo(token: TokenInput, params: Users.Params.Info): Promise<{ + ok: boolean + user: Definitions.User + }> + + /** + * Lists all users in a Slack team. + * @see https://api.slack.com/methods/users.list + */ + usersList(token: TokenInput, params: Users.Params.List): Promise<{ + cache_ts: number + members: Definitions.User[] + ok: boolean + response_metadata?: Definitions.ResponseMetadata + }> + + /** + * Find a user with an email address. + * @see https://api.slack.com/methods/users.lookupByEmail + */ + usersLookupByEmail(token: TokenInput, params: Users.Params.LookupByEmail): Promise<{ + ok: boolean + user: Definitions.User + }> + + /** + * Retrieves a user's profile information. + * @see https://api.slack.com/methods/users.profile.get + */ + usersProfileGet(token: TokenInput, params: Users.Params.ProfileGet): Promise<{ + ok: boolean + profile: Definitions.UserProfile + }> + + /** + * Set the profile information for a user. + * @see https://api.slack.com/methods/users.profile.set + */ + usersProfileSet(token: TokenInput, params: Users.Params.ProfileSet): Promise<{ + email_pending?: string + ok: boolean + profile: Definitions.UserProfile + username: string + }> + + /** + * Marked a user as active. Deprecated and non-functional. + * @see https://api.slack.com/methods/users.setActive + */ + usersSetActive(token: TokenInput): Promise<{ + ok: boolean + }> + + /** + * Set the user profile photo + * @see https://api.slack.com/methods/users.setPhoto + */ + usersSetPhoto(token: TokenInput, params: Users.Params.SetPhoto): Promise<{ + ok: boolean + profile: { + avatar_hash: string + image_1024: string + image_192: string + image_24: string + image_32: string + image_48: string + image_512: string + image_72: string + image_original: string + } + }> + + /** + * Manually sets user presence. + * @see https://api.slack.com/methods/users.setPresence + */ + usersSetPresence(token: TokenInput, params: Users.Params.SetPresence): Promise<{ + ok: boolean + }> + + } +} diff --git a/adapters/slack/src/types/views.ts b/adapters/slack/src/types/views.ts new file mode 100644 index 00000000..b15a22b3 --- /dev/null +++ b/adapters/slack/src/types/views.ts @@ -0,0 +1,78 @@ +import { Internal, TokenInput } from './internal' +import { Definitions } from './definition' +Internal.define({ + '/views.open': { + GET: { 'viewsOpen': true }, + }, + '/views.publish': { + GET: { 'viewsPublish': true }, + }, + '/views.push': { + GET: { 'viewsPush': true }, + }, + '/views.update': { + GET: { 'viewsUpdate': true }, + }, +}) + +export namespace Views { + export namespace Params { + export interface Open { + trigger_id: string + view: string + } + export interface Publish { + user_id: string + view: string + hash?: string + } + export interface Push { + trigger_id: string + view: string + } + export interface Update { + view_id?: string + external_id?: string + view?: string + hash?: string + } + } +} + +declare module './internal' { + interface Internal { + + /** + * Open a view for a user. + * @see https://api.slack.com/methods/views.open + */ + viewsOpen(token: TokenInput, params: Views.Params.Open): Promise<{ + ok: boolean + }> + + /** + * Publish a static view for a User. + * @see https://api.slack.com/methods/views.publish + */ + viewsPublish(token: TokenInput, params: Views.Params.Publish): Promise<{ + ok: boolean + }> + + /** + * Push a view onto the stack of a root view. + * @see https://api.slack.com/methods/views.push + */ + viewsPush(token: TokenInput, params: Views.Params.Push): Promise<{ + ok: boolean + }> + + /** + * Update an existing view. + * @see https://api.slack.com/methods/views.update + */ + viewsUpdate(token: TokenInput, params: Views.Params.Update): Promise<{ + ok: boolean + }> + + } +} diff --git a/adapters/slack/src/types/workflows.ts b/adapters/slack/src/types/workflows.ts new file mode 100644 index 00000000..b353d389 --- /dev/null +++ b/adapters/slack/src/types/workflows.ts @@ -0,0 +1,63 @@ +import { Internal, TokenInput } from './internal' +import { Definitions } from './definition' +Internal.define({ + '/workflows.stepCompleted': { + GET: { 'workflowsStepCompleted': true }, + }, + '/workflows.stepFailed': { + GET: { 'workflowsStepFailed': true }, + }, + '/workflows.updateStep': { + GET: { 'workflowsUpdateStep': true }, + }, +}) + +export namespace Workflows { + export namespace Params { + export interface StepCompleted { + workflow_step_execute_id: string + outputs?: string + } + export interface StepFailed { + workflow_step_execute_id: string + error: string + } + export interface UpdateStep { + workflow_step_edit_id: string + inputs?: string + outputs?: string + step_name?: string + step_image_url?: string + } + } +} + +declare module './internal' { + interface Internal { + + /** + * Indicate that an app's step in a workflow completed execution. + * @see https://api.slack.com/methods/workflows.stepCompleted + */ + workflowsStepCompleted(token: TokenInput, params: Workflows.Params.StepCompleted): Promise<{ + ok: boolean + }> + + /** + * Indicate that an app's step in a workflow failed to execute. + * @see https://api.slack.com/methods/workflows.stepFailed + */ + workflowsStepFailed(token: TokenInput, params: Workflows.Params.StepFailed): Promise<{ + ok: boolean + }> + + /** + * Update the configuration for a workflow extension step. + * @see https://api.slack.com/methods/workflows.updateStep + */ + workflowsUpdateStep(token: TokenInput, params: Workflows.Params.UpdateStep): Promise<{ + ok: boolean + }> + + } +} diff --git a/adapters/slack/src/utils.ts b/adapters/slack/src/utils.ts new file mode 100644 index 00000000..77dc7120 --- /dev/null +++ b/adapters/slack/src/utils.ts @@ -0,0 +1,232 @@ +import { Element, h, Session, Universal } from '@satorijs/satori' +import { SlackBot } from './bot' +import { BasicSlackEvent, EnvelopedEvent, GenericMessageEvent, MessageChangedEvent, MessageDeletedEvent, MessageEvent, ReactionAddedEvent, ReactionRemovedEvent, RichText, RichTextBlock, SlackEvent, SlackUser } from './types/events' +import { KnownBlock } from '@slack/types' +import { Definitions, File, SlackChannel, SlackTeam } from './types' +import { unescape } from './message' + +type NewKnownBlock = KnownBlock | RichTextBlock + +function adaptRichText(elements: RichText[]) { + const result: Element[] = [] + for (const text of elements) { + if (text.type === 'text') { + let item = h.text(unescape(text.text)) + if (text.style?.bold) item = h('b', {}, item) + if (text.style?.italic) item = h('i', {}, item) + if (text.style?.strike) item = h('del', {}, item) + if (text.style?.code) item = h('code', {}, item) + result.push(item) + } else if (text.type === 'link') { + result.push(h('a', { href: text.url }, text.text)) + } else if (text.type === 'emoji') { + result.push(h.text(String.fromCodePoint(...text.unicode.split('-').map(v => parseInt(v, 16))))) + } else if (text.type === 'user') { + result.push(h.at(text.user_id)) + } else if (text.type === 'broadcast') { + result.push(h('at', { type: text.range })) + } + } + return result +} + +function adaptMarkdown(markdown: string) { + let list = markdown.split(/(<(?:.*?)>)/g) + list = list.map(v => v.split(/(:(?:[a-zA-Z0-9_]+):)/g)).flat() // face + const result: Element[] = [] + for (const item of list) { + if (!item) continue + const match = item.match(/<(.*?)>/) + if (match) { + if (match[0].startsWith('@U')) result.push(h.at(match[0].slice(2))) + if (match[0].startsWith('#C')) result.push(h.sharp(match[0].slice(2))) + } else if (item.startsWith(':') && item.endsWith(':')) { + result.push(h('face', { id: item.slice(1, -1) })) + } else { + result.push(h.text(item)) + } + } + return result +} + +function adaptMessageBlocks(blocks: NewKnownBlock[]) { + let result: Element[] = [] + for (const block of blocks) { + if (block.type === 'rich_text') { + for (const element of block.elements) { + if (element.type === 'rich_text_section') { + result = result.concat(adaptRichText(element.elements)) + } else if (element.type === 'rich_text_list') { + result.push(h(element.style === 'bullet' ? 'ul' : 'ol', {}, + element.elements.map(v => h('li', {}, adaptRichText(v.elements)), + ))) + } + } + } else if (block.type === 'section') { + result = result.concat(adaptMarkdown(block.text.text)) + } + } + return result +} + +const adaptAuthor = (evt: Partial): Universal.Author => ({ + userId: evt.user || evt.bot_id as string, + // username: evt.username +}) + +const adaptBotProfile = (evt: Partial): Universal.Author => ({ + userId: evt.bot_profile.app_id, + username: evt.bot_profile.name, + isBot: true, + avatar: evt.bot_profile.icons.image_72, +}) + +export async function adaptMessage(bot: SlackBot, evt: Partial, session: Partial = {}) { + session.messageId = evt.ts + session.timestamp = Math.floor(Number(evt.ts) * 1000) + session.author = evt.bot_profile ? adaptBotProfile(evt) : adaptAuthor(evt) + session.userId = session.author.userId + if (evt.team) session.guildId = evt.team + + let elements = [] + // if a message(parent message) was a thread, it has thread_ts property too + if (evt.thread_ts && evt.thread_ts !== evt.ts) { + const quoted = await bot.getMessage(session.channelId, evt.thread_ts) + session.quote = quoted + session.quote.channelId = session.channelId + } + + // if (evt.thread_ts) elements.push(h.quote(evt.thread_ts)) + elements = [...elements, ...adaptMessageBlocks(evt.blocks as unknown as NewKnownBlock[])] + for (const file of evt.files ?? []) { + if (file.mimetype.startsWith('video/')) { + elements.push(h.video(file.url_private, { id: file.id })) + } else if (file.mimetype.startsWith('audio/')) { + elements.push(h.video(file.url_private, { id: file.id })) + } else if (file.mimetype.startsWith('image/')) { + elements.push(h.image(file.url_private, { id: file.id })) + } else { + elements.push(h.file(file.url_private, { id: file.id })) + } + } + let forward = null + for (const attachment of evt.attachments ?? []) { + // @ts-ignore + if (attachment.is_msg_unfurl) { + forward = attachment.ts + } + } + session.elements = forward ? [h('message', { forward: true, id: forward }, elements)] : elements + session.content = session.elements.join('') + return session as Universal.Message +} + +export function adaptMessageDeleted(bot: SlackBot, evt: MessageDeletedEvent, session: Partial = {}) { + session.isDirect = evt.channel_type === 'im' + session.channelId = evt.channel + session.guildId = evt.previous_message.team + session.type = 'message-deleted' + session.messageId = evt.previous_message.ts + session.timestamp = Math.floor(Number(evt.previous_message.ts) * 1000) + + adaptMessage(bot, evt.previous_message, session) +} + +export function adaptSentAsset(file: File, session: Partial = {}) { + session.messageId = file.shares.public[Object.keys(file.shares.public)[0]][0].ts + session.timestamp = file.created * 1000 + session.elements = [h.image(file.url_private, { id: file.id })] + session.content = session.elements.join('') + session.channelId = file.channels[0] + // session.guildId = file.shares.public[Object.keys(file.shares.public)[0]][0].ts + session.type = 'message' + session.author = { + userId: file.user, + } + session.userId = session.author.userId + return session as Universal.Message +} + +function setupReaction(session: Partial, data: EnvelopedEvent | EnvelopedEvent) { + session.guildId = data.team_id + session.channelId = data.event.item.channel + session.messageId = data.event.item.ts + session.timestamp = Math.floor(Number(data.event.item.ts) * 1000) + session.userId = data.event.user + session.content = data.event.reaction +} + +export async function adaptSession(bot: SlackBot, payload: EnvelopedEvent) { + const session = bot.session() + // https://api.slack.com/events + if (payload.event.type === 'message') { + const input = payload.event as GenericMessageEvent + // @ts-ignore + if (input.user === bot.selfId) return + if (!input.subtype) { + session.type = 'message' + session.isDirect = input.channel_type === 'im' + session.channelId = input.channel + await adaptMessage(bot, input as unknown as GenericMessageEvent, session) + } + else if (input.subtype === 'message_deleted') adaptMessageDeleted(bot, input as unknown as MessageDeletedEvent, session) + else if (input.subtype === 'message_changed') { + const evt = input as unknown as MessageChangedEvent + if (evt.message.subtype === 'thread_broadcast') return + session.type = 'message-updated' + // @ts-ignore + session.guildId = payload.team_id + session.isDirect = input.channel_type === 'im' + session.channelId = input.channel + await adaptMessage(bot, evt.message, session) + } else { + return + } + } else if (payload.event.type === 'channel_left') { + session.type = 'channel-removed' + session.channelId = payload.event.channel + session.timestamp = Math.floor(Number(payload.event.event_ts) * 1000) + session.guildId = payload.team_id + } else if (payload.event.type === 'reaction_added') { + session.type = 'reaction-added' + setupReaction(session, payload as any) + } else if (payload.event.type === 'reaction_removed') { + session.type = 'reaction-deleted' + setupReaction(session, payload as any) + } else { + return + } + return session +} + +export interface AuthTestResponse { + ok: boolean + url: string + team: string + user: string + team_id: string + user_id: string + bot_id?: string + is_enterprise_install: boolean +} + +export function adaptUser(data: SlackUser): Universal.User { + return { + userId: data.id, + avatar: data.profile.image_512 ?? data.profile.image_192 ?? data.profile.image_72 ?? data.profile.image_48 ?? data.profile.image_32 ?? data.profile.image_24, + username: data.real_name, + isBot: data.is_bot, + } +} + +export function adaptChannel(data: SlackChannel): Universal.Channel { + return { + channelId: data.id, + channelName: data.name, + } +} + +export const adaptGuild = (data: SlackTeam): Universal.Guild => ({ + guildId: data.id, + guildName: data.name, +}) diff --git a/adapters/slack/src/ws.ts b/adapters/slack/src/ws.ts new file mode 100644 index 00000000..fe454af2 --- /dev/null +++ b/adapters/slack/src/ws.ts @@ -0,0 +1,58 @@ +import { Adapter, Logger, Schema } from '@satorijs/satori' +import { SlackBot } from './bot' +import { adaptSession } from './utils' +import { BasicSlackEvent, EnvelopedEvent, MessageEvent, SocketEvent } from './types/events' + +const logger = new Logger('slack') + +export class WsClient extends Adapter.WsClient { + async prepare(bot: SlackBot) { + const { userId } = await bot.getSelf() + bot.selfId = userId + const data = await bot.request('POST', '/apps.connections.open', {}, {}, true) + const { url } = data + logger.debug('ws url: %s', url) + return bot.ctx.http.ws(url) + } + + async accept(bot: SlackBot) { + bot.socket.addEventListener('message', async ({ data }) => { + const parsed: SocketEvent = JSON.parse(data.toString()) + logger.debug(require('util').inspect(parsed, false, null, true)) + const { type } = parsed + if (type === 'hello') { + // @ts-ignore + // this.bot.selfId = parsed.connection_info.app_id + return this.bot.online() + } + if (type === 'events_api') { + const { envelope_id } = parsed + const payload: EnvelopedEvent = parsed.payload + bot.socket.send(JSON.stringify({ envelope_id })) + const session = await adaptSession(bot, payload) + + if (session) { + bot.dispatch(session) + logger.debug(require('util').inspect(session, false, null, true)) + } + } + }) + + bot.socket.addEventListener('close', () => { + + }) + } +} + +export namespace WsClient { + export interface Config extends Adapter.WsClient.Config { + protocol: 'ws' + } + + export const Config: Schema = Schema.intersect([ + Schema.object({ + protocol: Schema.const('ws').required(process.env.KOISHI_ENV !== 'browser'), + }), + Adapter.WsClient.Config, + ]) +} diff --git a/adapters/slack/tsconfig.json b/adapters/slack/tsconfig.json new file mode 100644 index 00000000..a14e38f4 --- /dev/null +++ b/adapters/slack/tsconfig.json @@ -0,0 +1,10 @@ +{ + "extends": "../../tsconfig.base", + "compilerOptions": { + "outDir": "lib", + "rootDir": "src", + }, + "include": [ + "src", + ], +}