From 1dfde7dfb2e2384cb0d06a5e7901b3e3a861ce47 Mon Sep 17 00:00:00 2001 From: LittleC Date: Wed, 5 Jul 2023 20:19:21 +0800 Subject: [PATCH 01/20] feat(slack): add adapter-slack --- adapters/slack/package.json | 35 +++ adapters/slack/src/bot.ts | 157 ++++++++++ adapters/slack/src/http.ts | 16 + adapters/slack/src/index.ts | 9 + adapters/slack/src/message.ts | 115 +++++++ .../slack/src/types/events/base-events.ts | 281 ++++++++++++++++++ adapters/slack/src/types/events/index.ts | 119 ++++++++ .../slack/src/types/events/message-events.ts | 251 ++++++++++++++++ adapters/slack/src/types/index.ts | 56 ++++ adapters/slack/src/utils.ts | 178 +++++++++++ adapters/slack/src/ws.ts | 55 ++++ adapters/slack/tsconfig.json | 10 + 12 files changed, 1282 insertions(+) create mode 100644 adapters/slack/package.json create mode 100644 adapters/slack/src/bot.ts create mode 100644 adapters/slack/src/http.ts create mode 100644 adapters/slack/src/index.ts create mode 100644 adapters/slack/src/message.ts create mode 100644 adapters/slack/src/types/events/base-events.ts create mode 100644 adapters/slack/src/types/events/index.ts create mode 100644 adapters/slack/src/types/events/message-events.ts create mode 100644 adapters/slack/src/types/index.ts create mode 100644 adapters/slack/src/utils.ts create mode 100644 adapters/slack/src/ws.ts create mode 100644 adapters/slack/tsconfig.json 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..c78cf647 --- /dev/null +++ b/adapters/slack/src/bot.ts @@ -0,0 +1,157 @@ +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' + +export class SlackBot extends Bot { + static MessageEncoder = SlackMessageEncoder + public http: Quester + + constructor(ctx: Context, config: T) { + super(ctx, config) + this.http = ctx.http.extend({ + headers: { + // 'Authorization': `Bearer ${config.token}`, + }, + }).extend(config) + + if (config.protocol === 'ws') { + ctx.plugin(WsClient, 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 { + 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.request('POST', '/auth.test') + return { + userId: data.user_id, + avatar: null, + username: data.user, + isBot: !!data.bot_id, + } + } + + async deleteMessage(channelId: string, messageId: string): Promise { + return this.request('POST', '/chat.delete', { channel: channelId, ts: messageId }) + } + + async getMessage(channelId: string, messageId: string): Promise { + const msg = await this.request<{ + messages: GenericMessageEvent[] + }>('POST', '/conversations.history', { + channel: channelId, + latest: messageId, + limit: 1, + }) + 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 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.request<{ + channel: { + id: string + } + }>('POST', '/conversations.open', { + users: channelId, + }) + return this.sendMessage(channel.id, content, undefined, options) + } +} + +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..2a296549 --- /dev/null +++ b/adapters/slack/src/http.ts @@ -0,0 +1,16 @@ +import { Adapter, Schema } from '@satorijs/satori' +import { SlackBot } from './bot' + +export class HttpServer extends Adapter.Server> { + +} + +export namespace HttpServer { + export interface Config { + protocol: 'http' + } + + export const Config: Schema = Schema.object({ + protocol: Schema.const('http').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..199eb737 --- /dev/null +++ b/adapters/slack/src/message.ts @@ -0,0 +1,115 @@ +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 sanitize = (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 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.request('POST', '/chat.postMessage', { + channel: this.channelId, + ...this.addition, + thread_ts: this.thread_ts, + 'blocks': [ + { + 'type': 'section', + 'text': { + 'type': 'mrkdwn', + 'text': this.buffer, + }, + }, + ], + }) + const session = this.bot.session() + adaptMessage(this.bot, r.message, session) + 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 += sanitize(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}>` + } 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 === '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/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..852bfd30 --- /dev/null +++ b/adapters/slack/src/types/events/index.ts @@ -0,0 +1,119 @@ +// 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 + +export interface HelloEvent { + type: 'hello' +} + +export interface EventsApiEvent { + type: 'events_api' + envelope_id: string + payload: EnvelopedEvent +} + +/** + * 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..57b895c9 --- /dev/null +++ b/adapters/slack/src/types/events/message-events.ts @@ -0,0 +1,251 @@ +// 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 + 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/index.ts b/adapters/slack/src/types/index.ts new file mode 100644 index 00000000..2f2cbcff --- /dev/null +++ b/adapters/slack/src/types/index.ts @@ -0,0 +1,56 @@ +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' diff --git a/adapters/slack/src/utils.ts b/adapters/slack/src/utils.ts new file mode 100644 index 00000000..0b1507e6 --- /dev/null +++ b/adapters/slack/src/utils.ts @@ -0,0 +1,178 @@ +import { Element, h, Session, Universal } from '@satorijs/satori' +import { SlackBot } from './bot' +import { GenericMessageEvent, MessageChangedEvent, MessageDeletedEvent, MessageEvent, RichText, RichTextBlock, SlackUser } from './types/events' +import { KnownBlock } from '@slack/types' +import { File, SlackChannel, SlackTeam } from './types' + +type NewKnownBlock = KnownBlock | RichTextBlock + +function adaptRichText(elements: RichText[]) { + const result: Element[] = [] + for (const text of elements) { + if (text.type === 'text') { + let item = h.text(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 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)), + ))) + } + } + } + } + return result +} + +const adaptAuthor = (evt: GenericMessageEvent): Universal.Author => ({ + userId: evt.user, +}) + +const adaptBotProfile = (evt: GenericMessageEvent): Universal.Author => ({ + userId: evt.bot_profile.app_id, + username: evt.bot_profile.name, + isBot: true, + avatar: evt.bot_profile.icons.image_72, +}) + +export function prepareMessage(session: Partial, evt: MessageEvent) { + session.subtype = evt.channel_type === 'channel' ? 'group' : 'private' + session.channelId = evt.channel +} + +export function adaptMessage(bot: SlackBot, evt: GenericMessageEvent, session: Partial = {}) { + session.messageId = evt.ts + session.timestamp = ~~(Number(evt.ts) * 1000) + session.author = evt.bot_profile ? adaptBotProfile(evt) : adaptAuthor(evt) + session.userId = session.author.userId + session.guildId = evt.team + + let elements = [] + 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.subtype = evt.channel_type === 'channel' ? 'group' : 'private' + session.channelId = evt.channel + session.guildId = evt.previous_message.team + session.type = 'message-deleted' + session.messageId = evt.previous_message.ts + session.timestamp = ~~(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 +} + +export async function adaptSession(bot: SlackBot, input: MessageEvent) { + const session = bot.session() + if (input.type === 'message') { + // @ts-ignore + if (input.app_id === bot.selfId) return + if (!input.subtype) { + session.type = 'message' + prepareMessage(session, input) + adaptMessage(bot, input as unknown as GenericMessageEvent, session) + } + if (input.subtype === 'message_deleted') adaptMessageDeleted(bot, input as unknown as MessageDeletedEvent, session) + if (input.subtype === 'message_changed') { + const evt = input as unknown as MessageChangedEvent + session.type = 'message-updated' + // @ts-ignore + session.guildId = evt.message.user_team + prepareMessage(session, input) + adaptMessage(bot, evt.message, session) + } + 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..14cf5061 --- /dev/null +++ b/adapters/slack/src/ws.ts @@ -0,0 +1,55 @@ +import { Adapter, Logger, Schema } from '@satorijs/satori' +import { SlackBot } from './bot' +import { adaptSession } from './utils' +import { EnvelopedEvent, MessageEvent, SocketEvent } from './types/events' + +const logger = new Logger('slack') + +export class WsClient extends Adapter.WsClient { + async prepare(bot: SlackBot) { + 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, payload } = parsed as unknown as EnvelopedEvent + bot.socket.send(JSON.stringify({ envelope_id })) + const session = await adaptSession(bot, payload.event) + + 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", + ], +} From ab44236236695ceb8946a8adf21a8d6ae1f44406 Mon Sep 17 00:00:00 2001 From: LittleC Date: Wed, 5 Jul 2023 22:14:17 +0800 Subject: [PATCH 02/20] chore(slack): message --- adapters/slack/src/bot.ts | 3 +- adapters/slack/src/message.ts | 23 +++++------ .../slack/src/types/events/message-events.ts | 2 + adapters/slack/src/utils.ts | 38 +++++++++++++++---- adapters/slack/src/ws.ts | 7 ++-- 5 files changed, 51 insertions(+), 22 deletions(-) diff --git a/adapters/slack/src/bot.ts b/adapters/slack/src/bot.ts index c78cf647..6b206eb7 100644 --- a/adapters/slack/src/bot.ts +++ b/adapters/slack/src/bot.ts @@ -54,8 +54,9 @@ export class SlackBot extends Bot('POST', '/conversations.history', { channel: channelId, - latest: messageId, + oldest: messageId, limit: 1, + inclusive: true }) return adaptMessage(this, msg.messages[0]) } diff --git a/adapters/slack/src/message.ts b/adapters/slack/src/message.ts index 199eb737..75c02503 100644 --- a/adapters/slack/src/message.ts +++ b/adapters/slack/src/message.ts @@ -5,7 +5,7 @@ import { adaptMessage, adaptSentAsset } from './utils' import { File } from './types' // https://api.slack.com/reference/surfaces/formatting#basics -export const sanitize = (val: string) => +export const escape = (val: string) => val .replace(/(? '@\u200Beveryone') @@ -15,6 +15,11 @@ export const sanitize = (val: string) => // .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 @@ -27,15 +32,7 @@ export class SlackMessageEncoder extends MessageEncoder { channel: this.channelId, ...this.addition, thread_ts: this.thread_ts, - 'blocks': [ - { - 'type': 'section', - 'text': { - 'type': 'mrkdwn', - 'text': this.buffer, - }, - }, - ], + text: this.buffer }) const session = this.bot.session() adaptMessage(this.bot, r.message, session) @@ -71,13 +68,15 @@ export class SlackMessageEncoder extends MessageEncoder { async visit(element: h) { const { type, attrs, children } = element if (type === 'text') { - this.buffer += sanitize(attrs.content) + 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) @@ -103,6 +102,8 @@ export class SlackMessageEncoder extends MessageEncoder { } 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, diff --git a/adapters/slack/src/types/events/message-events.ts b/adapters/slack/src/types/events/message-events.ts index 57b895c9..80bdc27b 100644 --- a/adapters/slack/src/types/events/message-events.ts +++ b/adapters/slack/src/types/events/message-events.ts @@ -21,6 +21,8 @@ export interface GenericMessageEvent { channel: string user: string bot_id?: string + app_id?: string + username?: string bot_profile?: BotProfile text?: string ts: string diff --git a/adapters/slack/src/utils.ts b/adapters/slack/src/utils.ts index 0b1507e6..c5b51845 100644 --- a/adapters/slack/src/utils.ts +++ b/adapters/slack/src/utils.ts @@ -1,8 +1,9 @@ import { Element, h, Session, Universal } from '@satorijs/satori' import { SlackBot } from './bot' -import { GenericMessageEvent, MessageChangedEvent, MessageDeletedEvent, MessageEvent, RichText, RichTextBlock, SlackUser } from './types/events' +import { BasicSlackEvent, EnvelopedEvent, GenericMessageEvent, MessageChangedEvent, MessageDeletedEvent, MessageEvent, RichText, RichTextBlock, SlackUser } from './types/events' import { KnownBlock } from '@slack/types' import { File, SlackChannel, SlackTeam } from './types' +import { unescape } from './message' type NewKnownBlock = KnownBlock | RichTextBlock @@ -10,7 +11,7 @@ function adaptRichText(elements: RichText[]) { const result: Element[] = [] for (const text of elements) { if (text.type === 'text') { - let item = h.text(text.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) @@ -29,6 +30,25 @@ function adaptRichText(elements: RichText[]) { return result } +function adaptMarkdown(markdown: string) { + let list = markdown.split(/(<(?:.*?)>)/g) + list = list.map(v => v.split(/(:(?:[a-zA-Z0-9_]+):)/g)).flat() // face + let 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) { @@ -42,13 +62,16 @@ function adaptMessageBlocks(blocks: NewKnownBlock[]) { ))) } } + } else if (block.type === 'section') { + result = result.concat(adaptMarkdown(block.text.text)) } } return result } const adaptAuthor = (evt: GenericMessageEvent): Universal.Author => ({ - userId: evt.user, + userId: evt.user || evt.app_id, + // username: evt.username }) const adaptBotProfile = (evt: GenericMessageEvent): Universal.Author => ({ @@ -68,7 +91,7 @@ export function adaptMessage(bot: SlackBot, evt: GenericMessageEvent, session: P session.timestamp = ~~(Number(evt.ts) * 1000) session.author = evt.bot_profile ? adaptBotProfile(evt) : adaptAuthor(evt) session.userId = session.author.userId - session.guildId = evt.team + if (evt.team) session.guildId = evt.team let elements = [] if (evt.thread_ts) elements.push(h.quote(evt.thread_ts)) @@ -122,9 +145,10 @@ export function adaptSentAsset(file: File, session: Partial = {}) { return session as Universal.Message } -export async function adaptSession(bot: SlackBot, input: MessageEvent) { +export async function adaptSession(bot: SlackBot, payload: EnvelopedEvent) { const session = bot.session() - if (input.type === 'message') { + if (payload.event.type === 'message') { + const input = payload.event as GenericMessageEvent // @ts-ignore if (input.app_id === bot.selfId) return if (!input.subtype) { @@ -137,7 +161,7 @@ export async function adaptSession(bot: SlackBot, input: MessageEvent) { const evt = input as unknown as MessageChangedEvent session.type = 'message-updated' // @ts-ignore - session.guildId = evt.message.user_team + session.guildId = payload.team_id prepareMessage(session, input) adaptMessage(bot, evt.message, session) } diff --git a/adapters/slack/src/ws.ts b/adapters/slack/src/ws.ts index 14cf5061..5ebd678b 100644 --- a/adapters/slack/src/ws.ts +++ b/adapters/slack/src/ws.ts @@ -1,7 +1,7 @@ import { Adapter, Logger, Schema } from '@satorijs/satori' import { SlackBot } from './bot' import { adaptSession } from './utils' -import { EnvelopedEvent, MessageEvent, SocketEvent } from './types/events' +import { BasicSlackEvent, EnvelopedEvent, MessageEvent, SocketEvent } from './types/events' const logger = new Logger('slack') @@ -24,9 +24,10 @@ export class WsClient extends Adapter.WsClient { return this.bot.online() } if (type === 'events_api') { - const { envelope_id, payload } = parsed as unknown as EnvelopedEvent + const { envelope_id} = parsed + const payload: EnvelopedEvent = parsed.payload bot.socket.send(JSON.stringify({ envelope_id })) - const session = await adaptSession(bot, payload.event) + const session = await adaptSession(bot, payload) if (session) { bot.dispatch(session) From 0935b98783e408049c94a4ffb6aad3108eeb2d73 Mon Sep 17 00:00:00 2001 From: LittleC Date: Sat, 8 Jul 2023 17:55:14 +0800 Subject: [PATCH 03/20] feat(slack): http mode, `session.quote`, reactions --- adapters/slack/package.json | 3 +- adapters/slack/src/bot.ts | 46 ++++++++++++++-- adapters/slack/src/http.ts | 51 +++++++++++++++++- adapters/slack/src/message.ts | 8 +-- adapters/slack/src/types/events/index.ts | 8 ++- adapters/slack/src/utils.ts | 67 ++++++++++++++++-------- adapters/slack/src/ws.ts | 6 ++- 7 files changed, 154 insertions(+), 35 deletions(-) diff --git a/adapters/slack/package.json b/adapters/slack/package.json index 578f1650..129f433f 100644 --- a/adapters/slack/package.json +++ b/adapters/slack/package.json @@ -30,6 +30,7 @@ }, "dependencies": { "@slack/types": "^2.8.0", - "form-data": "^4.0.0" + "form-data": "^4.0.0", + "seratch-slack-types": "^0.8.0" } } diff --git a/adapters/slack/src/bot.ts b/adapters/slack/src/bot.ts index 6b206eb7..79809eca 100644 --- a/adapters/slack/src/bot.ts +++ b/adapters/slack/src/bot.ts @@ -5,6 +5,7 @@ import { adaptChannel, adaptGuild, adaptMessage, adaptUser, AuthTestResponse } f import { SlackMessageEncoder } from './message' import { GenericMessageEvent, SlackChannel, SlackTeam, SlackUser } from './types' import FormData from 'form-data' +import * as WebApi from 'seratch-slack-types/web-api' export class SlackBot extends Bot { static MessageEncoder = SlackMessageEncoder @@ -20,6 +21,8 @@ export class SlackBot extends Bot extends Bot extends Bot extends Bot { + const { message } = await this.request('POST', '/reactions.get', `channel=${channelId}×tamp=${messageId}&emoji=${emoji}`, { + 'content-type': 'application/x-www-form-urlencoded', + }) + return message.reactions.find(v => v.name === emoji)?.users.map(v => ({ + userId: v, + })) ?? [] + } + + async createReaction(channelId: string, messageId: string, emoji: string): Promise { + // reactions.write + return this.request('POST', '/reactions.add', { + channel: channelId, + timestamp: messageId, + name: emoji, + }) + } + + async clearReaction(channelId: string, messageId: string, emoji?: string): Promise { + const { message } = await this.request('POST', '/reactions.get', `channel=${channelId}×tamp=${messageId}&full=true`, { + 'content-type': 'application/x-www-form-urlencoded', + }) + for (const reaction of message.reactions) { + if (!emoji || reaction.name === emoji) { + await this.request('POST', '/reactions.remove', { + channel: channelId, + timestamp: messageId, + name: reaction.name, + }) + } + } + } } export namespace SlackBot { diff --git a/adapters/slack/src/http.ts b/adapters/slack/src/http.ts index 2a296549..2d9ccc67 100644 --- a/adapters/slack/src/http.ts +++ b/adapters/slack/src/http.ts @@ -1,16 +1,63 @@ -import { Adapter, Schema } from '@satorijs/satori' +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> { +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/message.ts b/adapters/slack/src/message.ts index 75c02503..a0b2eec4 100644 --- a/adapters/slack/src/message.ts +++ b/adapters/slack/src/message.ts @@ -32,10 +32,10 @@ export class SlackMessageEncoder extends MessageEncoder { channel: this.channelId, ...this.addition, thread_ts: this.thread_ts, - text: this.buffer + text: this.buffer, }) const session = this.bot.session() - adaptMessage(this.bot, r.message, session) + await adaptMessage(this.bot, r.message, session) session.app.emit(session, 'send', session) this.results.push(session) this.buffer = '' @@ -75,8 +75,8 @@ export class SlackMessageEncoder extends MessageEncoder { 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 += `` + if (attrs.type === 'all') this.buffer += `` + if (attrs.type === 'here') this.buffer += `` } else if (type === 'b' || type === 'strong') { this.buffer += '*' await this.render(children) diff --git a/adapters/slack/src/types/events/index.ts b/adapters/slack/src/types/events/index.ts index 852bfd30..e1f1976b 100644 --- a/adapters/slack/src/types/events/index.ts +++ b/adapters/slack/src/types/events/index.ts @@ -19,7 +19,7 @@ export { File, } from './message-events' -export type SocketEvent = HelloEvent | EventsApiEvent +export type SocketEvent = HelloEvent | EventsApiEvent | UrlVerificationEvent | EnvelopedEvent export interface HelloEvent { type: 'hello' @@ -31,6 +31,12 @@ export interface EventsApiEvent { payload: EnvelopedEvent } +export interface UrlVerificationEvent { + type: 'url_verification' + token: string + challenge: string +} + /** * A Slack Events API event wrapped in the standard envelope. * diff --git a/adapters/slack/src/utils.ts b/adapters/slack/src/utils.ts index c5b51845..e889c9f6 100644 --- a/adapters/slack/src/utils.ts +++ b/adapters/slack/src/utils.ts @@ -1,6 +1,6 @@ import { Element, h, Session, Universal } from '@satorijs/satori' import { SlackBot } from './bot' -import { BasicSlackEvent, EnvelopedEvent, GenericMessageEvent, MessageChangedEvent, MessageDeletedEvent, MessageEvent, RichText, RichTextBlock, SlackUser } from './types/events' +import { BasicSlackEvent, EnvelopedEvent, GenericMessageEvent, MessageChangedEvent, MessageDeletedEvent, MessageEvent, ReactionAddedEvent, ReactionRemovedEvent, RichText, RichTextBlock, SlackEvent, SlackUser } from './types/events' import { KnownBlock } from '@slack/types' import { File, SlackChannel, SlackTeam } from './types' import { unescape } from './message' @@ -33,14 +33,14 @@ function adaptRichText(elements: RichText[]) { function adaptMarkdown(markdown: string) { let list = markdown.split(/(<(?:.*?)>)/g) list = list.map(v => v.split(/(:(?:[a-zA-Z0-9_]+):)/g)).flat() // face - let result: Element[] = [] + 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(":")) { + 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)) @@ -81,20 +81,24 @@ const adaptBotProfile = (evt: GenericMessageEvent): Universal.Author => ({ avatar: evt.bot_profile.icons.image_72, }) -export function prepareMessage(session: Partial, evt: MessageEvent) { - session.subtype = evt.channel_type === 'channel' ? 'group' : 'private' +export async function adaptMessage(bot: SlackBot, evt: GenericMessageEvent, session: Partial = {}) { + session.isDirect = evt.channel_type === 'im' session.channelId = evt.channel -} - -export function adaptMessage(bot: SlackBot, evt: GenericMessageEvent, session: Partial = {}) { session.messageId = evt.ts - session.timestamp = ~~(Number(evt.ts) * 1000) + 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 (evt.thread_ts) elements.push(h.quote(evt.thread_ts)) + // 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/')) { @@ -120,12 +124,12 @@ export function adaptMessage(bot: SlackBot, evt: GenericMessageEvent, session: P } export function adaptMessageDeleted(bot: SlackBot, evt: MessageDeletedEvent, session: Partial = {}) { - session.subtype = evt.channel_type === 'channel' ? 'group' : 'private' + 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 = ~~(Number(evt.previous_message.ts) * 1000) + session.timestamp = Math.floor(Number(evt.previous_message.ts) * 1000) adaptMessage(bot, evt.previous_message, session) } @@ -145,16 +149,25 @@ export function adaptSentAsset(file: File, session: Partial = {}) { return session as Universal.Message } -export async function adaptSession(bot: SlackBot, payload: EnvelopedEvent) { +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.app_id === bot.selfId) return + if (input.user === bot.selfId) return if (!input.subtype) { session.type = 'message' - prepareMessage(session, input) - adaptMessage(bot, input as unknown as GenericMessageEvent, session) + await adaptMessage(bot, input as unknown as GenericMessageEvent, session) } if (input.subtype === 'message_deleted') adaptMessageDeleted(bot, input as unknown as MessageDeletedEvent, session) if (input.subtype === 'message_changed') { @@ -162,11 +175,23 @@ export async function adaptSession(bot: SlackBot, payload: EnvelopedEvent { 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) @@ -20,11 +22,11 @@ export class WsClient extends Adapter.WsClient { const { type } = parsed if (type === 'hello') { // @ts-ignore - this.bot.selfId = parsed.connection_info.app_id + // this.bot.selfId = parsed.connection_info.app_id return this.bot.online() } if (type === 'events_api') { - const { envelope_id} = parsed + const { envelope_id } = parsed const payload: EnvelopedEvent = parsed.payload bot.socket.send(JSON.stringify({ envelope_id })) const session = await adaptSession(bot, payload) From 2077591b65d9f518e3064740248214af151b7040 Mon Sep 17 00:00:00 2001 From: LittleC Date: Thu, 13 Jul 2023 21:40:26 +0800 Subject: [PATCH 04/20] feat(slack): generated internal api --- adapters/slack/src/bot.ts | 49 +- adapters/slack/src/message.ts | 3 +- adapters/slack/src/types/admin.ts | 909 ++++++++++++++++++++++ adapters/slack/src/types/api.ts | 30 + adapters/slack/src/types/apps.ts | 175 +++++ adapters/slack/src/types/auth.ts | 50 ++ adapters/slack/src/types/bots.ts | 42 + adapters/slack/src/types/calls.ts | 112 +++ adapters/slack/src/types/chat.ts | 255 ++++++ adapters/slack/src/types/conversations.ts | 343 ++++++++ adapters/slack/src/types/definition.ts | 530 +++++++++++++ adapters/slack/src/types/dialog.ts | 30 + adapters/slack/src/types/dnd.ts | 96 +++ adapters/slack/src/types/emoji.ts | 28 + adapters/slack/src/types/files.ts | 246 ++++++ adapters/slack/src/types/index.ts | 28 + adapters/slack/src/types/internal.ts | 57 ++ adapters/slack/src/types/migration.ts | 36 + adapters/slack/src/types/oauth.ts | 68 ++ adapters/slack/src/types/pins.ts | 57 ++ adapters/slack/src/types/reactions.ts | 96 +++ adapters/slack/src/types/reminders.ts | 89 +++ adapters/slack/src/types/rtm.ts | 40 + adapters/slack/src/types/search.ts | 34 + adapters/slack/src/types/stars.ts | 73 ++ adapters/slack/src/types/team.ts | 123 +++ adapters/slack/src/types/usergroups.ts | 136 ++++ adapters/slack/src/types/users.ts | 222 ++++++ adapters/slack/src/types/views.ts | 78 ++ adapters/slack/src/types/workflows.ts | 63 ++ adapters/slack/src/utils.ts | 16 +- 31 files changed, 4083 insertions(+), 31 deletions(-) create mode 100644 adapters/slack/src/types/admin.ts create mode 100644 adapters/slack/src/types/api.ts create mode 100644 adapters/slack/src/types/apps.ts create mode 100644 adapters/slack/src/types/auth.ts create mode 100644 adapters/slack/src/types/bots.ts create mode 100644 adapters/slack/src/types/calls.ts create mode 100644 adapters/slack/src/types/chat.ts create mode 100644 adapters/slack/src/types/conversations.ts create mode 100644 adapters/slack/src/types/definition.ts create mode 100644 adapters/slack/src/types/dialog.ts create mode 100644 adapters/slack/src/types/dnd.ts create mode 100644 adapters/slack/src/types/emoji.ts create mode 100644 adapters/slack/src/types/files.ts create mode 100644 adapters/slack/src/types/internal.ts create mode 100644 adapters/slack/src/types/migration.ts create mode 100644 adapters/slack/src/types/oauth.ts create mode 100644 adapters/slack/src/types/pins.ts create mode 100644 adapters/slack/src/types/reactions.ts create mode 100644 adapters/slack/src/types/reminders.ts create mode 100644 adapters/slack/src/types/rtm.ts create mode 100644 adapters/slack/src/types/search.ts create mode 100644 adapters/slack/src/types/stars.ts create mode 100644 adapters/slack/src/types/team.ts create mode 100644 adapters/slack/src/types/usergroups.ts create mode 100644 adapters/slack/src/types/users.ts create mode 100644 adapters/slack/src/types/views.ts create mode 100644 adapters/slack/src/types/workflows.ts diff --git a/adapters/slack/src/bot.ts b/adapters/slack/src/bot.ts index 79809eca..5be04326 100644 --- a/adapters/slack/src/bot.ts +++ b/adapters/slack/src/bot.ts @@ -6,18 +6,18 @@ import { SlackMessageEncoder } from './message' import { GenericMessageEvent, SlackChannel, SlackTeam, SlackUser } from './types' import FormData from 'form-data' import * as WebApi from 'seratch-slack-types/web-api' +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({ - headers: { - // 'Authorization': `Bearer ${config.token}`, - }, - }).extend(config) + this.http = ctx.http.extend(config) + + this.internal = new Internal(this, this.http) if (config.protocol === 'ws') { ctx.plugin(WsClient, this) @@ -41,7 +41,7 @@ export class SlackBot extends Bot('POST', '/auth.test') + const data = await this.internal.authTest(Token.BOT) return { userId: data.user_id, avatar: null, @@ -51,18 +51,20 @@ export class SlackBot extends Bot { - return this.request('POST', '/chat.delete', { channel: channelId, ts: messageId }) + await this.internal.chatDelete(Token.BOT, { + channel: channelId, + ts: Number(messageId), + }) } async getMessage(channelId: string, messageId: string): Promise { - const msg = await this.request<{ - messages: GenericMessageEvent[] - }>('POST', '/conversations.history', { + const msg = await this.internal.conversationsHistory(Token.BOT, { channel: channelId, - oldest: messageId, + oldest: Number(messageId), limit: 1, inclusive: true, }) + // @ts-ignore return adaptMessage(this, msg.messages[0]) } @@ -73,7 +75,7 @@ export class SlackBot extends Bot adaptMessage(this, v)) + return Promise.all(msg.messages.map(v => adaptMessage(this, v))) } async getUser(userId: string, guildId?: string): Promise { @@ -128,19 +130,18 @@ export class SlackBot extends Bot { // "channels:write,groups:write,mpim:write,im:write", - const { channel } = await this.request<{ - channel: { - id: string - } - }>('POST', '/conversations.open', { + 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.request('POST', '/reactions.get', `channel=${channelId}×tamp=${messageId}&emoji=${emoji}`, { - 'content-type': 'application/x-www-form-urlencoded', + 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, @@ -149,7 +150,7 @@ export class SlackBot extends Bot { // reactions.write - return this.request('POST', '/reactions.add', { + await this.internal.reactionsAdd(Token.BOT, { channel: channelId, timestamp: messageId, name: emoji, @@ -157,12 +158,14 @@ export class SlackBot extends Bot { - const { message } = await this.request('POST', '/reactions.get', `channel=${channelId}×tamp=${messageId}&full=true`, { - 'content-type': 'application/x-www-form-urlencoded', + 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.request('POST', '/reactions.remove', { + await this.internal.reactionsRemove(Token.BOT, { channel: channelId, timestamp: messageId, name: reaction.name, diff --git a/adapters/slack/src/message.ts b/adapters/slack/src/message.ts index a0b2eec4..21165bbc 100644 --- a/adapters/slack/src/message.ts +++ b/adapters/slack/src/message.ts @@ -28,7 +28,7 @@ export class SlackMessageEncoder extends MessageEncoder { results: Session[] = [] async flush() { if (!this.buffer.length) return - const r = await this.bot.request('POST', '/chat.postMessage', { + const r = await this.bot.internal.chatPostMessage(this.bot.config.botToken, { channel: this.channelId, ...this.addition, thread_ts: this.thread_ts, @@ -36,6 +36,7 @@ export class SlackMessageEncoder extends MessageEncoder { }) 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 = '' 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/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 index 2f2cbcff..a6a4623b 100644 --- a/adapters/slack/src/types/index.ts +++ b/adapters/slack/src/types/index.ts @@ -54,3 +54,31 @@ export interface SlackTeam { } 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 index e889c9f6..fc5b1538 100644 --- a/adapters/slack/src/utils.ts +++ b/adapters/slack/src/utils.ts @@ -2,7 +2,7 @@ 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 { File, SlackChannel, SlackTeam } from './types' +import { Definitions, File, SlackChannel, SlackTeam } from './types' import { unescape } from './message' type NewKnownBlock = KnownBlock | RichTextBlock @@ -69,21 +69,19 @@ function adaptMessageBlocks(blocks: NewKnownBlock[]) { return result } -const adaptAuthor = (evt: GenericMessageEvent): Universal.Author => ({ - userId: evt.user || evt.app_id, +const adaptAuthor = (evt: Partial): Universal.Author => ({ + userId: evt.user || evt.bot_id as string, // username: evt.username }) -const adaptBotProfile = (evt: GenericMessageEvent): Universal.Author => ({ +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: GenericMessageEvent, session: Partial = {}) { - session.isDirect = evt.channel_type === 'im' - session.channelId = evt.channel +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) @@ -167,6 +165,8 @@ export async function adaptSession(bot: SlackBot, payload: EnvelopedEvent Date: Fri, 14 Jul 2023 01:26:18 +0800 Subject: [PATCH 05/20] fix(slack): message event --- adapters/slack/package.json | 3 +-- adapters/slack/src/bot.ts | 1 - adapters/slack/src/utils.ts | 7 +++++-- 3 files changed, 6 insertions(+), 5 deletions(-) diff --git a/adapters/slack/package.json b/adapters/slack/package.json index 129f433f..578f1650 100644 --- a/adapters/slack/package.json +++ b/adapters/slack/package.json @@ -30,7 +30,6 @@ }, "dependencies": { "@slack/types": "^2.8.0", - "form-data": "^4.0.0", - "seratch-slack-types": "^0.8.0" + "form-data": "^4.0.0" } } diff --git a/adapters/slack/src/bot.ts b/adapters/slack/src/bot.ts index 5be04326..8845d9a0 100644 --- a/adapters/slack/src/bot.ts +++ b/adapters/slack/src/bot.ts @@ -5,7 +5,6 @@ import { adaptChannel, adaptGuild, adaptMessage, adaptUser, AuthTestResponse } f import { SlackMessageEncoder } from './message' import { GenericMessageEvent, SlackChannel, SlackTeam, SlackUser } from './types' import FormData from 'form-data' -import * as WebApi from 'seratch-slack-types/web-api' import { Internal, Token } from './types/internal' export class SlackBot extends Bot { diff --git a/adapters/slack/src/utils.ts b/adapters/slack/src/utils.ts index fc5b1538..77dc7120 100644 --- a/adapters/slack/src/utils.ts +++ b/adapters/slack/src/utils.ts @@ -169,15 +169,18 @@ export async function adaptSession(bot: SlackBot, payload: EnvelopedEvent Date: Tue, 18 Jul 2023 15:50:12 +0800 Subject: [PATCH 06/20] feat(whatsapp): add adapter-whatsapp --- adapters/whatsapp/package.json | 33 ++++++++++ adapters/whatsapp/src/bot.ts | 46 ++++++++++++++ adapters/whatsapp/src/http.ts | 52 ++++++++++++++++ adapters/whatsapp/src/index.ts | 6 ++ adapters/whatsapp/src/message.ts | 104 +++++++++++++++++++++++++++++++ adapters/whatsapp/src/types.ts | 68 ++++++++++++++++++++ adapters/whatsapp/src/utils.ts | 2 + adapters/whatsapp/tsconfig.json | 10 +++ 8 files changed, 321 insertions(+) create mode 100644 adapters/whatsapp/package.json create mode 100644 adapters/whatsapp/src/bot.ts create mode 100644 adapters/whatsapp/src/http.ts create mode 100644 adapters/whatsapp/src/index.ts create mode 100644 adapters/whatsapp/src/message.ts create mode 100644 adapters/whatsapp/src/types.ts create mode 100644 adapters/whatsapp/src/utils.ts create mode 100644 adapters/whatsapp/tsconfig.json diff --git a/adapters/whatsapp/package.json b/adapters/whatsapp/package.json new file mode 100644 index 00000000..87e183e8 --- /dev/null +++ b/adapters/whatsapp/package.json @@ -0,0 +1,33 @@ +{ + "name": "@satorijs/adapter-whatsapp", + "description": "WhatsApp 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/whatsapp" + }, + "bugs": { + "url": "https://github.com/satorijs/satori/issues" + }, + "homepage": "https://koishi.chat/plugins/adapter/whatsapp.html", + "keywords": [ + "bot", + "whatsapp", + "adapter", + "chatbot", + "satori" + ], + "peerDependencies": { + "@satorijs/satori": "^2.4.0" + }, + "dependencies": {}, + "devDependencies": {} +} diff --git a/adapters/whatsapp/src/bot.ts b/adapters/whatsapp/src/bot.ts new file mode 100644 index 00000000..f42d265f --- /dev/null +++ b/adapters/whatsapp/src/bot.ts @@ -0,0 +1,46 @@ +import { Bot, Context, Logger, Quester, Schema, Universal } from '@satorijs/satori' +import { WhatsAppMessageEncoder } from './message' +import { HttpServer } from './http' + +export class WhatsAppBot extends Bot { + static MessageEncoder = WhatsAppMessageEncoder + public http: Quester + + constructor(ctx: Context, config: WhatsAppBot.Config) { + super(ctx, config) + ctx.plugin(HttpServer, this) + this.http = ctx.http.extend({ + ...config, + headers: { + Authorization: `Bearer ${config.systemToken}`, + }, + }) + } + + async initialize() { + const { data } = await this.http('GET', `/v17.0/${this.config.id}/phone_numbers`) + if (data.length) { + console.log(data[0]) + this.selfId = data[0].id + this.username = data[0].display_phone_number + } + } +} + +export namespace WhatsAppBot { + export interface Config extends Bot.Config, Quester.Config { + systemToken: string + verifyToken: string + id: string + } + export const Config: Schema = Schema.intersect([ + Schema.object({ + systemToken: Schema.string(), + verifyToken: Schema.string().required(), + id: Schema.string().description('WhatsApp Business Account ID').required(), + }), + Quester.createConfig('https://graph.facebook.com'), + ] as const) +} + +WhatsAppBot.prototype.platform = 'whatsapp' diff --git a/adapters/whatsapp/src/http.ts b/adapters/whatsapp/src/http.ts new file mode 100644 index 00000000..e186b431 --- /dev/null +++ b/adapters/whatsapp/src/http.ts @@ -0,0 +1,52 @@ +import { Adapter, Context, Logger } from '@satorijs/satori' +import { WhatsAppBot } from './bot' +import { WebhookBody } from './types' + +export class HttpServer extends Adapter.Server { + logger = new Logger('whatsapp') + constructor(ctx: Context, bot: WhatsAppBot) { + super() + } + + async start(bot: WhatsAppBot) { + // @TODO selfId + // https://developers.facebook.com/docs/graph-api/webhooks/getting-started + await bot.initialize() + bot.ctx.router.post('/whatsapp', async (ctx) => { + const parsed = ctx.request.body as WebhookBody + this.logger.debug(require('util').inspect(parsed, false, null, true)) + ctx.body = 'ok' + ctx.status = 200 + for (const entry of parsed.entry) { + for (const change of entry.changes) { + if (change.field === 'messages' && change.value.messages?.length) { + const session = bot.session() + session.type = 'message' + session.isDirect = true + session.content = change.value.messages[0].text.body + session.channelId = change.value.messages[0].from + session.guildId = change.value.messages[0].from + session.messageId = change.value.messages[0].id + session.author = { + userId: change.value.messages[0].from, + username: change.value.contacts[0].profile.name, + } + session.userId = change.value.messages[0].from + session.timestamp = parseInt(change.value.messages[0].timestamp) * 1000 + + bot.dispatch(session) + } + } + } + }) + bot.ctx.router.get('/whatsapp', async (ctx) => { + this.logger.debug(require('util').inspect(ctx.query, false, null, true)) + const verifyToken = ctx.query['hub.verify_token'] + const challenge = ctx.query['hub.challenge'] + if (verifyToken !== bot.config.verifyToken) return ctx.status = 403 + ctx.body = challenge + ctx.status = 200 + }) + bot.online() + } +} diff --git a/adapters/whatsapp/src/index.ts b/adapters/whatsapp/src/index.ts new file mode 100644 index 00000000..1a1a4feb --- /dev/null +++ b/adapters/whatsapp/src/index.ts @@ -0,0 +1,6 @@ +import { WhatsAppBot } from './bot' + +export * from './http' +export * from './bot' + +export default WhatsAppBot diff --git a/adapters/whatsapp/src/message.ts b/adapters/whatsapp/src/message.ts new file mode 100644 index 00000000..fb1f5501 --- /dev/null +++ b/adapters/whatsapp/src/message.ts @@ -0,0 +1,104 @@ +import { Dict, h, Logger, MessageEncoder } from '@satorijs/satori' +import { WhatsAppBot } from './bot' +import FormData from 'form-data' +import { SendMessage } from './types' + +const SUPPORTED_MEDIA = 'audio/aac, audio/mp4, audio/mpeg, audio/amr, audio/ogg, audio/opus, application/vnd.ms-powerpoint, application/msword, application/vnd.openxmlformats-officedocument.wordprocessingml.document, application/vnd.openxmlformats-officedocument.presentationml.presentation, application/vnd.openxmlformats-officedocument.spreadsheetml.sheet, application/pdf, text/plain, application/vnd.ms-excel, image/jpeg, image/png, image/webp, video/mp4, video/3gpp'.split(', ') + +export class WhatsAppMessageEncoder extends MessageEncoder { + private buffer = '' + quoteId: string = null + logger: Logger + prepare(): Promise { + this.logger = this.bot.ctx.logger('whatsapp') + return + } + + async flush(): Promise { + await this.flushTextMessage() + } + + async flushTextMessage() { + await this.sendMessage('text', { body: this.buffer }) + this.buffer = '' + } + + async sendMessage(type: T, data: Dict) { + if (type === 'text' && !this.buffer.length) return + // https://developers.facebook.com/docs/whatsapp/api/messages/text + const { messages } = await this.bot.http.post<{ + messages: { id: string }[] + }>(`/${this.bot.selfId}/messages`, { + 'messaging_product': 'whatsapp', + to: this.channelId, + recipient_type: 'individual', + type, + [type]: data, + ...(this.quoteId ? { + context: { + message_id: this.quoteId, + }, + } : {}), + }) + + for (const msg of messages) { + const session = this.bot.session() + session.type = 'message' + session.messageId = msg.id + // @TODO session body + session.app.emit(session, 'send', session) + this.results.push(session) + } + } + + // https://developers.facebook.com/docs/whatsapp/cloud-api/reference/media#upload-media + async uploadMedia(attrs: Dict) { + const { filename, data, mime } = await this.bot.ctx.http.file(attrs.url, attrs) + + if (!SUPPORTED_MEDIA.includes(mime)) { + this.logger.warn(`Unsupported media type: ${mime}`) + return + } + + const form = new FormData() + const value = process.env.KOISHI_ENV === 'browser' + ? new Blob([data], { type: mime }) + : Buffer.from(data) + form.append('file', value, attrs.file || filename) + form.append('type', mime) + form.append('messaging_product', 'whatsapp') + + const r = await this.bot.http.post<{ + id: string + }>(`/${this.bot.selfId}/media`, form, { + headers: form.getHeaders(), + }) + return r.id + } + + async visit(element: h): Promise { + const { type, attrs, children } = element + if (type === 'text') { + this.buffer += attrs.content + } else if (( + type === 'image' || type === 'audio' || type === 'video' + ) && attrs.url) { + // https://developers.facebook.com/docs/whatsapp/cloud-api/reference/media#supported-media-types + const id = await this.uploadMedia(attrs) + if (!id) return + await this.flushTextMessage() + await this.sendMessage(type, { id }) + } else if (type === 'file') { + const id = await this.uploadMedia(attrs) + if (!id) return + await this.flushTextMessage() + await this.sendMessage('document', { id }) + } else if (type === 'message') { + await this.flush() + await this.render(children) + await this.flush() + } else if (type === 'quote') { + this.quoteId = attrs.id + } + } +} diff --git a/adapters/whatsapp/src/types.ts b/adapters/whatsapp/src/types.ts new file mode 100644 index 00000000..0d6947e7 --- /dev/null +++ b/adapters/whatsapp/src/types.ts @@ -0,0 +1,68 @@ +export interface WebhookBody { + object: string + entry: Entry[] +} + +export interface Entry { + id: string + time: number + uid: string + changes: Change[] +} + +export interface Change { + field: 'messages' + value: MessageValue +} + +export interface MessageValue { + messaging_product: string + metadata: { + display_phone_number: string + phone_number_id: string + } + contacts: { + profile: { + name: string + } + wa_id: string + }[] + messages: { + from: string + id: string + timestamp: string + type: string + text: { + body: string + } + }[] +} + +export interface SendMessageBase { + messaging_product: 'whatsapp' + recipient_type: 'individual' + to: string +} + +export type SendMessage = SendTextMessage | SendMediaMessage<'image'> | SendMediaMessage<'audio'> | SendMediaMessage<'video'> | SendMediaMessage<'document'> + +export interface SendTextMessage extends SendMessageBase { + type: 'text' + text: { + body: string + } +} +export type MediaType = 'image' | 'audio' | 'video' | 'document' + +type MediaDetail = { id: string; link: string } + +interface Media { + image?: MediaDetail + audio?: MediaDetail + video?: MediaDetail + document?: MediaDetail +} + +export interface SendMediaMessage extends SendMessageBase, Media { + type: T +} diff --git a/adapters/whatsapp/src/utils.ts b/adapters/whatsapp/src/utils.ts new file mode 100644 index 00000000..5700c6b8 --- /dev/null +++ b/adapters/whatsapp/src/utils.ts @@ -0,0 +1,2 @@ +import { h } from '@satorijs/satori' +import { WhatsAppBot } from './bot' diff --git a/adapters/whatsapp/tsconfig.json b/adapters/whatsapp/tsconfig.json new file mode 100644 index 00000000..74ac2c8d --- /dev/null +++ b/adapters/whatsapp/tsconfig.json @@ -0,0 +1,10 @@ +{ + "extends": "../../tsconfig.base", + "compilerOptions": { + "outDir": "lib", + "rootDir": "src", + }, + "include": [ + "src", + ], +} \ No newline at end of file From b64615f8e92463d7a419878bb5df7aa50fa45750 Mon Sep 17 00:00:00 2001 From: LittleC Date: Tue, 18 Jul 2023 17:38:23 +0800 Subject: [PATCH 07/20] feat(whatsapp): media --- adapters/whatsapp/src/bot.ts | 28 ++++++++++++++-- adapters/whatsapp/src/http.ts | 50 +++++++++++++++++----------- adapters/whatsapp/src/message.ts | 6 ++-- adapters/whatsapp/src/types.ts | 54 ++++++++++++++++++++++++------ adapters/whatsapp/src/utils.ts | 57 +++++++++++++++++++++++++++++++- 5 files changed, 159 insertions(+), 36 deletions(-) diff --git a/adapters/whatsapp/src/bot.ts b/adapters/whatsapp/src/bot.ts index f42d265f..deb8d1fa 100644 --- a/adapters/whatsapp/src/bot.ts +++ b/adapters/whatsapp/src/bot.ts @@ -18,13 +18,35 @@ export class WhatsAppBot extends Bot { } async initialize() { - const { data } = await this.http('GET', `/v17.0/${this.config.id}/phone_numbers`) + const { data } = await this.http<{ + data: { + verified_name: string + code_verification_status: string + display_phone_number: string + quality_rating: string + id: string + }[] + }>('GET', `/v17.0/${this.config.id}/phone_numbers`) + this.ctx.logger('whatsapp').debug(require('util').inspect(data, false, null, true)) if (data.length) { - console.log(data[0]) this.selfId = data[0].id - this.username = data[0].display_phone_number + this.username = data[0].verified_name } } + + async createReaction(channelId: string, messageId: string, emoji: string): Promise { + // https://developers.facebook.com/docs/whatsapp/cloud-api/guides/send-messages#reaction-messages + await this.http.post(`/${this.selfId}/messages`, { + messaging_product: 'whatsapp', + to: channelId, + recipient_type: 'individual', + type: 'reaction', + reaction: { + message_id: messageId, + emoji, + }, + }) + } } export namespace WhatsAppBot { diff --git a/adapters/whatsapp/src/http.ts b/adapters/whatsapp/src/http.ts index e186b431..b6e620e0 100644 --- a/adapters/whatsapp/src/http.ts +++ b/adapters/whatsapp/src/http.ts @@ -1,6 +1,8 @@ import { Adapter, Context, Logger } from '@satorijs/satori' import { WhatsAppBot } from './bot' import { WebhookBody } from './types' +import { decodeMessage } from './utils' +import internal from 'stream' export class HttpServer extends Adapter.Server { logger = new Logger('whatsapp') @@ -9,34 +11,19 @@ export class HttpServer extends Adapter.Server { } async start(bot: WhatsAppBot) { - // @TODO selfId // https://developers.facebook.com/docs/graph-api/webhooks/getting-started + // https://developers.facebook.com/docs/graph-api/webhooks/getting-started/webhooks-for-whatsapp/ await bot.initialize() bot.ctx.router.post('/whatsapp', async (ctx) => { const parsed = ctx.request.body as WebhookBody this.logger.debug(require('util').inspect(parsed, false, null, true)) ctx.body = 'ok' ctx.status = 200 + if (parsed.object !== 'whatsapp_business_account') return for (const entry of parsed.entry) { - for (const change of entry.changes) { - if (change.field === 'messages' && change.value.messages?.length) { - const session = bot.session() - session.type = 'message' - session.isDirect = true - session.content = change.value.messages[0].text.body - session.channelId = change.value.messages[0].from - session.guildId = change.value.messages[0].from - session.messageId = change.value.messages[0].id - session.author = { - userId: change.value.messages[0].from, - username: change.value.contacts[0].profile.name, - } - session.userId = change.value.messages[0].from - session.timestamp = parseInt(change.value.messages[0].timestamp) * 1000 - - bot.dispatch(session) - } - } + const session = await decodeMessage(bot, entry) + if (session.length) session.forEach(bot.dispatch.bind(bot)) + this.logger.debug(require('util').inspect(session, false, null, true)) } }) bot.ctx.router.get('/whatsapp', async (ctx) => { @@ -47,6 +34,29 @@ export class HttpServer extends Adapter.Server { ctx.body = challenge ctx.status = 200 }) + bot.ctx.router.get('/whatsapp/assets/:self_id/:media_id', async (ctx) => { + const mediaId = ctx.params.media_id + const selfId = ctx.params.self_id + const localBot = this.bots.find((bot) => bot.selfId === selfId) + if (!localBot) return ctx.status = 404 + + const fetched = await localBot.http.get<{ + url: string + }>('/' + mediaId) + this.logger.debug(fetched.url) + const resp = await localBot.ctx.http.axios({ + url: fetched.url, + method: 'GET', + responseType: 'stream', + headers: { + Authorization: `Bearer ${localBot.config.systemToken}`, + }, + }) + ctx.type = resp.headers['content-type'] + ctx.set('cache-control', resp.headers['cache-control']) + ctx.response.body = resp.data + ctx.status = 200 + }) bot.online() } } diff --git a/adapters/whatsapp/src/message.ts b/adapters/whatsapp/src/message.ts index fb1f5501..69e20785 100644 --- a/adapters/whatsapp/src/message.ts +++ b/adapters/whatsapp/src/message.ts @@ -11,7 +11,6 @@ export class WhatsAppMessageEncoder extends MessageEncoder { logger: Logger prepare(): Promise { this.logger = this.bot.ctx.logger('whatsapp') - return } async flush(): Promise { @@ -29,7 +28,7 @@ export class WhatsAppMessageEncoder extends MessageEncoder { const { messages } = await this.bot.http.post<{ messages: { id: string }[] }>(`/${this.bot.selfId}/messages`, { - 'messaging_product': 'whatsapp', + messaging_product: 'whatsapp', to: this.channelId, recipient_type: 'individual', type, @@ -93,6 +92,9 @@ export class WhatsAppMessageEncoder extends MessageEncoder { if (!id) return await this.flushTextMessage() await this.sendMessage('document', { id }) + } else if (type === 'face' && attrs.id) { + await this.flushTextMessage() + await this.sendMessage('sticker', { id: attrs.id }) } else if (type === 'message') { await this.flush() await this.render(children) diff --git a/adapters/whatsapp/src/types.ts b/adapters/whatsapp/src/types.ts index 0d6947e7..1376bfa6 100644 --- a/adapters/whatsapp/src/types.ts +++ b/adapters/whatsapp/src/types.ts @@ -27,24 +27,57 @@ export interface MessageValue { } wa_id: string }[] - messages: { + messages: MessageBody[] +} + +export interface MessageBodyBase { + from: string + id: string + timestamp: string + context?: { from: string id: string - timestamp: string - type: string - text: { - body: string - } - }[] + } +} + +export interface ReceivedMedia { + filename?: string + caption?: string + mime_type: string + sha256: string + id: string + animated?: boolean +} + +export interface MessageBodyText extends MessageBodyBase { + type: 'text' + text: { + body: string + } } +export interface MessageBodyMedia extends MessageBodyBase { + type: 'image' | 'audio' | 'video' | 'document' + image?: ReceivedMedia + audio?: ReceivedMedia + video?: ReceivedMedia + document?: ReceivedMedia +} + +export interface MessageBodySticker extends MessageBodyBase { + type: 'sticker' + sticker?: ReceivedMedia +} + +export type MessageBody = MessageBodyText | MessageBodyMedia | MessageBodySticker + export interface SendMessageBase { messaging_product: 'whatsapp' recipient_type: 'individual' to: string } -export type SendMessage = SendTextMessage | SendMediaMessage<'image'> | SendMediaMessage<'audio'> | SendMediaMessage<'video'> | SendMediaMessage<'document'> +export type SendMessage = SendTextMessage | SendMediaMessage<'image'> | SendMediaMessage<'audio'> | SendMediaMessage<'video'> | SendMediaMessage<'document'> | SendMediaMessage<'sticker'> export interface SendTextMessage extends SendMessageBase { type: 'text' @@ -52,15 +85,16 @@ export interface SendTextMessage extends SendMessageBase { body: string } } -export type MediaType = 'image' | 'audio' | 'video' | 'document' +export type MediaType = 'image' | 'audio' | 'video' | 'document' | 'sticker' -type MediaDetail = { id: string; link: string } +type MediaDetail = { id?: string; link?: string } interface Media { image?: MediaDetail audio?: MediaDetail video?: MediaDetail document?: MediaDetail + sticker?: MediaDetail } export interface SendMediaMessage extends SendMessageBase, Media { diff --git a/adapters/whatsapp/src/utils.ts b/adapters/whatsapp/src/utils.ts index 5700c6b8..30101544 100644 --- a/adapters/whatsapp/src/utils.ts +++ b/adapters/whatsapp/src/utils.ts @@ -1,2 +1,57 @@ -import { h } from '@satorijs/satori' +import { h, Session } from '@satorijs/satori' import { WhatsAppBot } from './bot' +import { Entry } from './types' + +export async function decodeMessage(bot: WhatsAppBot, entry: Entry) { + const result: Session[] = [] + for (const change of entry.changes) { + if (change.field === 'messages' && change.value.messages?.length) { + const session = bot.session() + session.type = 'message' + session.isDirect = true + const message = change.value.messages[0] + session.channelId = message.from + session.guildId = message.from + session.messageId = message.id + session.author = { + userId: message.from, + username: change.value.contacts[0].profile.name, + } + session.userId = message.from + session.timestamp = parseInt(message.timestamp) * 1000 + + if (message.context) { + session.quote = { + messageId: message.context.id, + channelId: message.context.from, + userId: message.context.from, + content: '', + } + } + + if (message.type === 'text') { + session.elements = [h.text(message.text.body)] + } else if (['video', 'audio', 'image', 'document'].includes(message.type)) { + const elements = [] + let type = message.type as string + if (message.type === 'document') type = 'file' + const resource = message[message.type] + if (resource.caption) elements.push(h.text(message[message.type].caption)) + elements.push(h[type](`${bot.ctx.root.config.selfUrl}/whatsapp/assets/${bot.selfId}/${resource.id}`)) + session.elements = elements + } else if (message.type === 'sticker') { + session.elements = [h('face', { + id: /* (message.sticker.animated ? 'a:' : '') + */message.sticker.id, + platform: 'whatsapp', + }, [ + h.image(`${bot.ctx.root.config.selfUrl}/whatsapp/assets/${bot.selfId}/${message.sticker.id}`), + ])] + } else { + continue + } + session.content = session.elements.join('') + result.push(session) + } + } + return result +} From bc5fce748289dc06c0bcc29459f310dbeeeeaa0e Mon Sep 17 00:00:00 2001 From: LittleC Date: Tue, 18 Jul 2023 19:52:02 +0800 Subject: [PATCH 08/20] feat(whatsapp): support multiple bots --- adapters/whatsapp/src/bot.ts | 32 +++++--------------- adapters/whatsapp/src/http.ts | 25 ++++++++++++--- adapters/whatsapp/src/index.ts | 52 +++++++++++++++++++++++++++++++- adapters/whatsapp/src/message.ts | 45 +++++++++++++++++++-------- adapters/whatsapp/src/types.ts | 8 ++++- 5 files changed, 117 insertions(+), 45 deletions(-) diff --git a/adapters/whatsapp/src/bot.ts b/adapters/whatsapp/src/bot.ts index deb8d1fa..f2de6ca3 100644 --- a/adapters/whatsapp/src/bot.ts +++ b/adapters/whatsapp/src/bot.ts @@ -1,6 +1,6 @@ -import { Bot, Context, Logger, Quester, Schema, Universal } from '@satorijs/satori' +import { Bot, Context, Quester, Schema } from '@satorijs/satori' import { WhatsAppMessageEncoder } from './message' -import { HttpServer } from './http' +import { WhatsAppBusiness } from '.' export class WhatsAppBot extends Bot { static MessageEncoder = WhatsAppMessageEncoder @@ -8,7 +8,6 @@ export class WhatsAppBot extends Bot { constructor(ctx: Context, config: WhatsAppBot.Config) { super(ctx, config) - ctx.plugin(HttpServer, this) this.http = ctx.http.extend({ ...config, headers: { @@ -18,20 +17,7 @@ export class WhatsAppBot extends Bot { } async initialize() { - const { data } = await this.http<{ - data: { - verified_name: string - code_verification_status: string - display_phone_number: string - quality_rating: string - id: string - }[] - }>('GET', `/v17.0/${this.config.id}/phone_numbers`) - this.ctx.logger('whatsapp').debug(require('util').inspect(data, false, null, true)) - if (data.length) { - this.selfId = data[0].id - this.username = data[0].verified_name - } + this.selfId = this.config.phoneNumber } async createReaction(channelId: string, messageId: string, emoji: string): Promise { @@ -50,18 +36,14 @@ export class WhatsAppBot extends Bot { } export namespace WhatsAppBot { - export interface Config extends Bot.Config, Quester.Config { - systemToken: string - verifyToken: string - id: string + export interface Config extends WhatsAppBusiness.Config, Bot.Config { + phoneNumber: string } export const Config: Schema = Schema.intersect([ Schema.object({ - systemToken: Schema.string(), - verifyToken: Schema.string().required(), - id: Schema.string().description('WhatsApp Business Account ID').required(), + phoneNumber: Schema.string().description('手机号').required(), }), - Quester.createConfig('https://graph.facebook.com'), + WhatsAppBusiness.Config, ] as const) } diff --git a/adapters/whatsapp/src/http.ts b/adapters/whatsapp/src/http.ts index b6e620e0..f7ef61d6 100644 --- a/adapters/whatsapp/src/http.ts +++ b/adapters/whatsapp/src/http.ts @@ -3,26 +3,41 @@ import { WhatsAppBot } from './bot' import { WebhookBody } from './types' import { decodeMessage } from './utils' import internal from 'stream' +import crypto from 'crypto' export class HttpServer extends Adapter.Server { logger = new Logger('whatsapp') - constructor(ctx: Context, bot: WhatsAppBot) { - super() + + fork(ctx: Context, bot: WhatsAppBot) { + super.fork(ctx, bot) + return bot.initialize() } async start(bot: WhatsAppBot) { // https://developers.facebook.com/docs/graph-api/webhooks/getting-started // https://developers.facebook.com/docs/graph-api/webhooks/getting-started/webhooks-for-whatsapp/ - await bot.initialize() bot.ctx.router.post('/whatsapp', async (ctx) => { + const receivedSignature = ctx.get('X-Hub-Signature-256').split('sha256=')[1] + + const payload = JSON.stringify(ctx.request.body) + + const generatedSignature = crypto + .createHmac('sha256', bot.config.secret) + .update(payload) + .digest('hex') + if (receivedSignature !== generatedSignature) return ctx.status = 403 + const parsed = ctx.request.body as WebhookBody this.logger.debug(require('util').inspect(parsed, false, null, true)) ctx.body = 'ok' ctx.status = 200 if (parsed.object !== 'whatsapp_business_account') return for (const entry of parsed.entry) { - const session = await decodeMessage(bot, entry) - if (session.length) session.forEach(bot.dispatch.bind(bot)) + const phone_number_id = entry.changes[0].value.metadata.phone_number_id + const localBot = this.bots.find((bot) => bot.selfId === phone_number_id) + const session = await decodeMessage(localBot, entry) + if (session.length) session.forEach(localBot.dispatch.bind(localBot)) + this.logger.debug('handling bot: %s', localBot.sid) this.logger.debug(require('util').inspect(session, false, null, true)) } }) diff --git a/adapters/whatsapp/src/index.ts b/adapters/whatsapp/src/index.ts index 1a1a4feb..9706cfff 100644 --- a/adapters/whatsapp/src/index.ts +++ b/adapters/whatsapp/src/index.ts @@ -1,6 +1,56 @@ +import { Bot, Context, Quester, Schema } from '@satorijs/satori' import { WhatsAppBot } from './bot' +import { HttpServer } from './http' export * from './http' export * from './bot' +export * from './types' +export * from './utils' +export * from './message' -export default WhatsAppBot +export async function WhatsAppBusiness(ctx: Context, config: WhatsAppBusiness.Config) { + const http: Quester = ctx.http.extend({ + ...config, + headers: { + Authorization: `Bearer ${config.systemToken}`, + }, + }) + const { data } = await http<{ + data: { + verified_name: string + code_verification_status: string + display_phone_number: string + quality_rating: string + id: string + }[] + }>('GET', `/${config.id}/phone_numbers`) + ctx.logger('whatsapp').debug(require('util').inspect(data, false, null, true)) + const httpServer = new HttpServer() + for (const item of data) { + const bot = new WhatsAppBot(ctx, { + ...config, + phoneNumber: item.id, + }) + httpServer.fork(ctx, bot) + } +} + +export namespace WhatsAppBusiness { + export interface Config extends Quester.Config { + systemToken: string + verifyToken: string + id: string + secret: string + } + export const Config: Schema = Schema.intersect([ + Schema.object({ + secret: Schema.string().role('secret').description('App Secret').required(), + systemToken: Schema.string().role('secret').description('System User Token').required(), + verifyToken: Schema.string().required(), + id: Schema.string().description('WhatsApp Business Account ID').required(), + }), + Quester.createConfig('https://graph.facebook.com'), + ] as const) +} + +export default WhatsAppBusiness diff --git a/adapters/whatsapp/src/message.ts b/adapters/whatsapp/src/message.ts index 69e20785..ff477860 100644 --- a/adapters/whatsapp/src/message.ts +++ b/adapters/whatsapp/src/message.ts @@ -1,4 +1,4 @@ -import { Dict, h, Logger, MessageEncoder } from '@satorijs/satori' +import { Dict, h, MessageEncoder } from '@satorijs/satori' import { WhatsAppBot } from './bot' import FormData from 'form-data' import { SendMessage } from './types' @@ -8,22 +8,19 @@ const SUPPORTED_MEDIA = 'audio/aac, audio/mp4, audio/mpeg, audio/amr, audio/ogg, export class WhatsAppMessageEncoder extends MessageEncoder { private buffer = '' quoteId: string = null - logger: Logger - prepare(): Promise { - this.logger = this.bot.ctx.logger('whatsapp') - } async flush(): Promise { await this.flushTextMessage() } async flushTextMessage() { - await this.sendMessage('text', { body: this.buffer }) + await this.sendMessage('text', { body: this.buffer, preview_url: this.options.linkPreview }) this.buffer = '' } async sendMessage(type: T, data: Dict) { if (type === 'text' && !this.buffer.length) return + if (type !== 'text' && this.buffer.length) await this.flushTextMessage() // https://developers.facebook.com/docs/whatsapp/api/messages/text const { messages } = await this.bot.http.post<{ messages: { id: string }[] @@ -44,7 +41,15 @@ export class WhatsAppMessageEncoder extends MessageEncoder { const session = this.bot.session() session.type = 'message' session.messageId = msg.id - // @TODO session body + session.channelId = this.channelId + session.guildId = this.channelId + session.isDirect = true + session.userId = this.bot.selfId + session.author = { + userId: this.bot.selfId, + username: this.bot.username, + } + session.timestamp = Date.now() session.app.emit(session, 'send', session) this.results.push(session) } @@ -55,7 +60,7 @@ export class WhatsAppMessageEncoder extends MessageEncoder { const { filename, data, mime } = await this.bot.ctx.http.file(attrs.url, attrs) if (!SUPPORTED_MEDIA.includes(mime)) { - this.logger.warn(`Unsupported media type: ${mime}`) + this.bot.ctx.logger('whatsapp').warn(`Unsupported media type: ${mime}`) return } @@ -85,22 +90,36 @@ export class WhatsAppMessageEncoder extends MessageEncoder { // https://developers.facebook.com/docs/whatsapp/cloud-api/reference/media#supported-media-types const id = await this.uploadMedia(attrs) if (!id) return - await this.flushTextMessage() await this.sendMessage(type, { id }) } else if (type === 'file') { const id = await this.uploadMedia(attrs) if (!id) return - await this.flushTextMessage() await this.sendMessage('document', { id }) - } else if (type === 'face' && attrs.id) { - await this.flushTextMessage() - await this.sendMessage('sticker', { id: attrs.id }) + } else if (type === 'face') { + if (attrs.platform && attrs.platform !== this.bot.platform) { + return this.render(children) + } else { + await this.sendMessage('sticker', { id: attrs.id }) + } + } else if (type === 'p') { + await this.render(children) + this.buffer += '\n' + } else if (type === 'a') { + await this.render(children) + this.buffer += ` (${attrs.href}) ` + } else if (type === 'at') { + if (attrs.id) { + this.buffer += `@${attrs.id}` + } } else if (type === 'message') { await this.flush() await this.render(children) await this.flush() + this.quoteId = null } else if (type === 'quote') { this.quoteId = attrs.id + } else { + await this.render(children) } } } diff --git a/adapters/whatsapp/src/types.ts b/adapters/whatsapp/src/types.ts index 1376bfa6..9ea7f283 100644 --- a/adapters/whatsapp/src/types.ts +++ b/adapters/whatsapp/src/types.ts @@ -77,7 +77,13 @@ export interface SendMessageBase { to: string } -export type SendMessage = SendTextMessage | SendMediaMessage<'image'> | SendMediaMessage<'audio'> | SendMediaMessage<'video'> | SendMediaMessage<'document'> | SendMediaMessage<'sticker'> +export type SendMessage = + SendTextMessage | + SendMediaMessage<'image'> | + SendMediaMessage<'audio'> | + SendMediaMessage<'video'> | + SendMediaMessage<'document'> | + SendMediaMessage<'sticker'> export interface SendTextMessage extends SendMessageBase { type: 'text' From af2baa445afe6a11a203a7efe30269a880441ba0 Mon Sep 17 00:00:00 2001 From: _LittleC_ Date: Mon, 7 Aug 2023 17:40:30 +0800 Subject: [PATCH 09/20] fix(core): support xml body (#143) --- packages/satori/src/router.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/satori/src/router.ts b/packages/satori/src/router.ts index 6a8ecac6..2ba2f21a 100644 --- a/packages/satori/src/router.ts +++ b/packages/satori/src/router.ts @@ -62,7 +62,9 @@ export class Router extends KoaRouter { // create server const koa = new Koa() - koa.use(require('koa-bodyparser')()) + koa.use(require('koa-bodyparser')({ + enableTypes: ['json', 'form', 'xml'], + })) koa.use(this.routes()) koa.use(this.allowedMethods()) From 62f08ed5d7f986215976e007d1b2ec99ffa53819 Mon Sep 17 00:00:00 2001 From: Shigma Date: Tue, 8 Aug 2023 02:13:19 +0800 Subject: [PATCH 10/20] fix(core): fix bot.platform maybe undefined --- adapters/dingtalk/src/bot.ts | 6 +++--- adapters/discord/src/bot.ts | 3 +-- adapters/kook/src/bot.ts | 4 +--- adapters/line/src/bot.ts | 7 ++++--- adapters/line/src/http.ts | 17 +++++++++-------- adapters/mail/src/bot.ts | 9 ++++++--- adapters/matrix/src/bot.ts | 7 ++++--- adapters/onebot/src/bot/index.ts | 3 +-- adapters/onebot/src/http.ts | 2 +- adapters/qqguild/src/bot.ts | 5 ++--- adapters/telegram/src/bot.ts | 5 ++--- packages/core/src/bot.ts | 1 + 12 files changed, 35 insertions(+), 34 deletions(-) diff --git a/adapters/dingtalk/src/bot.ts b/adapters/dingtalk/src/bot.ts index 15c1d9d9..8d73a5d0 100644 --- a/adapters/dingtalk/src/bot.ts +++ b/adapters/dingtalk/src/bot.ts @@ -11,10 +11,12 @@ export class DingtalkBot extends Bot { static MessageEncoder = DingtalkMessageEncoder public oldHttp: Quester public http: Quester - refreshTokenTimer: NodeJS.Timeout public internal: Internal + refreshTokenTimer: NodeJS.Timeout + constructor(ctx: Context, config: DingtalkBot.Config) { super(ctx, config) + this.platform = 'dingtalk' this.http = ctx.http.extend(config.api) this.oldHttp = ctx.http.extend(config.oldApi) this.internal = new Internal(this) @@ -106,5 +108,3 @@ export namespace DingtalkBot { WsClient.Config, ]) } - -DingtalkBot.prototype.platform = 'dingtalk' diff --git a/adapters/discord/src/bot.ts b/adapters/discord/src/bot.ts index 091ef395..553c684c 100644 --- a/adapters/discord/src/bot.ts +++ b/adapters/discord/src/bot.ts @@ -21,6 +21,7 @@ export class DiscordBot extends Bot { constructor(ctx: Context, config: DiscordBot.Config) { super(ctx, config) + this.platform = 'discord' this.http = ctx.http.extend({ ...config, headers: { @@ -257,5 +258,3 @@ export namespace DiscordBot { Quester.createConfig('https://discord.com/api/v10'), ]) } - -DiscordBot.prototype.platform = 'discord' diff --git a/adapters/kook/src/bot.ts b/adapters/kook/src/bot.ts index cc114e42..3b3ac903 100644 --- a/adapters/kook/src/bot.ts +++ b/adapters/kook/src/bot.ts @@ -14,6 +14,7 @@ export class KookBot extends Bot { constructor(ctx: Context, config: T) { super(ctx, config) + this.platform = 'kook' this.http = ctx.http.extend({ headers: { 'Authorization': `Bot ${config.token}`, @@ -192,6 +193,3 @@ export namespace KookBot { Quester.createConfig('https://www.kookapp.cn/api/v3'), ] as const) } - -// for backward compatibility -KookBot.prototype.platform = 'kook' diff --git a/adapters/line/src/bot.ts b/adapters/line/src/bot.ts index 1bbd13e7..1e101f28 100644 --- a/adapters/line/src/bot.ts +++ b/adapters/line/src/bot.ts @@ -10,13 +10,14 @@ export class LineBot extends Bot { public http: Quester public contentHttp: Quester public internal: Internal + constructor(ctx: Context, config: LineBot.Config) { super(ctx, config) if (!ctx.root.config.selfUrl) { logger.warn('selfUrl is not set, some features may not work') } - ctx.plugin(HttpServer, this) + this.platform = 'line' this.http = ctx.http.extend({ ...config.api, headers: { @@ -30,6 +31,8 @@ export class LineBot extends Bot { }, }) this.internal = new Internal(this.http) + + ctx.plugin(HttpServer, this) } async initialize(callback: (bot: this) => Promise) { @@ -115,5 +118,3 @@ export namespace LineBot { }), ]) } - -LineBot.prototype.platform = 'line' diff --git a/adapters/line/src/http.ts b/adapters/line/src/http.ts index ba3129a4..fe27c35d 100644 --- a/adapters/line/src/http.ts +++ b/adapters/line/src/http.ts @@ -7,6 +7,7 @@ import internal from 'stream' export class HttpServer extends Adapter.Server { logger = new Logger('line') + constructor(ctx: Context, bot: LineBot) { super() } @@ -16,16 +17,16 @@ export class HttpServer extends Adapter.Server { const sign = ctx.headers['x-line-signature']?.toString() const parsed = ctx.request.body as WebhookRequestBody const { destination } = parsed - const localBot = this.bots.find(bot => bot.selfId === destination) - if (!localBot) return ctx.status = 403 - const hash = crypto.createHmac('SHA256', localBot?.config?.secret).update(ctx.request.rawBody || '').digest('base64') + const bot = this.bots.find(bot => bot.selfId === destination) + if (!bot) return ctx.status = 403 + const hash = crypto.createHmac('SHA256', bot?.config?.secret).update(ctx.request.rawBody || '').digest('base64') if (hash !== sign) { return ctx.status = 403 } this.logger.debug(require('util').inspect(parsed, false, null, true)) for (const event of parsed.events) { - const sessions = await adaptSessions(localBot, event) - if (sessions.length) sessions.forEach(localBot.dispatch.bind(localBot)) + const sessions = await adaptSessions(bot, event) + if (sessions.length) sessions.forEach(bot.dispatch.bind(bot)) this.logger.debug(require('util').inspect(sessions, false, null, true)) } ctx.status = 200 @@ -34,9 +35,9 @@ export class HttpServer extends Adapter.Server { bot.ctx.router.get('/line/assets/:self_id/:message_id', async (ctx) => { const messageId = ctx.params.message_id const selfId = ctx.params.self_id - const localBot = this.bots.find((bot) => bot.selfId === selfId) - if (!localBot) return ctx.status = 404 - const resp = await localBot.contentHttp.axios(`/v2/bot/message/${messageId}/content`, { + const bot = this.bots.find((bot) => bot.selfId === selfId) + if (!bot) return ctx.status = 404 + const resp = await bot.contentHttp.axios(`/v2/bot/message/${messageId}/content`, { method: 'GET', responseType: 'stream', }) diff --git a/adapters/mail/src/bot.ts b/adapters/mail/src/bot.ts index 0d7ac338..fdd61efc 100644 --- a/adapters/mail/src/bot.ts +++ b/adapters/mail/src/bot.ts @@ -1,4 +1,4 @@ -import { Bot, Logger, Schema } from '@satorijs/satori' +import { Bot, Context, Logger, Schema } from '@satorijs/satori' import { ParsedMail } from 'mailparser' import { IMAP, SMTP } from './mail' import { MailMessageEncoder } from './message' @@ -12,6 +12,11 @@ export class MailBot extends Bot { imap: IMAP smtp: SMTP + constructor(ctx: Context, config: MailBot.Config) { + super(ctx, config) + this.platform = 'mail' + } + async start() { this.username = this.config.username await super.start() @@ -113,5 +118,3 @@ export namespace MailBot { ]), }) } - -MailBot.prototype.platform = 'mail' diff --git a/adapters/matrix/src/bot.ts b/adapters/matrix/src/bot.ts index e69ad6b7..1427b017 100644 --- a/adapters/matrix/src/bot.ts +++ b/adapters/matrix/src/bot.ts @@ -6,14 +6,17 @@ import { adaptMessage, dispatchSession } from './utils' export class MatrixBot extends Bot { static MessageEncoder = MatrixMessageEncoder + http: Quester id: string endpoint: string rooms: string[] = [] - declare internal: Matrix.Internal + internal: Matrix.Internal + constructor(ctx: Context, config: MatrixBot.Config) { super(ctx, config) this.id = config.id + this.platform = 'matrix' this.selfId = `@${this.id}:${this.config.host}` this.userId = this.selfId this.endpoint = (config.endpoint || `https://${config.host}`) + '/_matrix' @@ -196,5 +199,3 @@ export namespace MatrixBot { ...omit(Quester.Config.dict, ['endpoint']), }) } - -MatrixBot.prototype.platform = 'matrix' diff --git a/adapters/onebot/src/bot/index.ts b/adapters/onebot/src/bot/index.ts index ec330c5b..60e7f238 100644 --- a/adapters/onebot/src/bot/index.ts +++ b/adapters/onebot/src/bot/index.ts @@ -15,6 +15,7 @@ export class OneBotBot extends Ba constructor(ctx: Context, config: T) { super(ctx, config) + this.platform = 'onebot' this.internal = new OneBot.Internal() this.avatar = `http://q.qlogo.cn/headimg_dl?dst_uin=${config.selfId}&spec=640` @@ -96,8 +97,6 @@ export class OneBotBot extends Ba } } -OneBotBot.prototype.platform = 'onebot' - export namespace OneBotBot { export interface QQGuildConfig extends Bot.Config {} diff --git a/adapters/onebot/src/http.ts b/adapters/onebot/src/http.ts index 775b6082..01876bd6 100644 --- a/adapters/onebot/src/http.ts +++ b/adapters/onebot/src/http.ts @@ -6,7 +6,7 @@ import { createHmac } from 'crypto' const logger = new Logger('onebot') export class HttpServer extends Adapter.Server { - public bots: OneBotBot[] + declare bots: OneBotBot[] async fork(ctx: Context, bot: OneBotBot) { const config = bot.config diff --git a/adapters/qqguild/src/bot.ts b/adapters/qqguild/src/bot.ts index 6364e6c6..b8829b2b 100644 --- a/adapters/qqguild/src/bot.ts +++ b/adapters/qqguild/src/bot.ts @@ -11,6 +11,7 @@ export class QQGuildBot extends Bot { constructor(ctx: Context, config: QQGuildBot.Config) { super(ctx, config) + this.platform = 'qqguild' this.internal = new QQGuild.Bot(config as QQGuild.Bot.Options) ctx.plugin(WsClient, this) } @@ -75,7 +76,7 @@ export namespace QQGuildBot { id: Schema.string().description('机器人 id。').required(), key: Schema.string().description('机器人 key。').role('secret').required(), token: Schema.string().description('机器人令牌。').role('secret').required(), - }), + }) as any, sandbox: Schema.boolean().description('是否开启沙箱模式。').default(true), endpoint: Schema.string().role('link').description('要连接的服务器地址。').default('https://api.sgroup.qq.com/'), authType: Schema.union(['bot', 'bearer'] as const).description('采用的验证方式。').default('bot'), @@ -84,5 +85,3 @@ export namespace QQGuildBot { WsClient.Config, ] as const) } - -QQGuildBot.prototype.platform = 'qqguild' diff --git a/adapters/telegram/src/bot.ts b/adapters/telegram/src/bot.ts index 585951d8..b79e9a23 100644 --- a/adapters/telegram/src/bot.ts +++ b/adapters/telegram/src/bot.ts @@ -37,6 +37,7 @@ export class TelegramBot exte constructor(ctx: Context, config: T) { super(ctx, config) + this.platform = 'telegram' this.selfId = config.token.split(':')[0] this.local = config.files.local this.http = this.ctx.http.extend({ @@ -270,7 +271,7 @@ export class TelegramBot exte } let { mime, data } = await this.$getFile(filePath) if (mime === 'application/octet-stream') { - mime = await FileType.fromBuffer(data)?.mime + mime = (await FileType.fromBuffer(data))?.mime } const base64 = `data:${mime};base64,` + arrayBufferToBase64(data) return { url: base64 } @@ -290,8 +291,6 @@ export class TelegramBot exte } } -TelegramBot.prototype.platform = 'telegram' - export namespace TelegramBot { export interface BaseConfig extends Bot.Config, Quester.Config { protocol: 'server' | 'polling' diff --git a/packages/core/src/bot.ts b/packages/core/src/bot.ts index ee478105..c3513a6a 100644 --- a/packages/core/src/bot.ts +++ b/packages/core/src/bot.ts @@ -34,6 +34,7 @@ export abstract class Bot { this.selfId = config.selfId } + this.internal = null this.context = ctx ctx.bots.push(this) this.context.emit('bot-added', this) From d91885e4577330bc2f77a370d57fd9286c52c639 Mon Sep 17 00:00:00 2001 From: Shigma Date: Tue, 8 Aug 2023 02:16:57 +0800 Subject: [PATCH 11/20] chore: bump versions --- adapters/dingtalk/package.json | 4 ++-- adapters/discord/package.json | 4 ++-- adapters/kook/package.json | 4 ++-- adapters/lark/package.json | 4 ++-- adapters/lark/src/bot.ts | 1 + adapters/line/package.json | 4 ++-- adapters/mail/package.json | 4 ++-- adapters/matrix/package.json | 4 ++-- adapters/onebot/package.json | 4 ++-- adapters/qqguild/package.json | 4 ++-- adapters/telegram/package.json | 4 ++-- packages/axios/package.json | 2 +- packages/core/package.json | 4 ++-- packages/satori/package.json | 4 ++-- 14 files changed, 26 insertions(+), 25 deletions(-) diff --git a/adapters/dingtalk/package.json b/adapters/dingtalk/package.json index f2ba123b..9f8ed0fb 100644 --- a/adapters/dingtalk/package.json +++ b/adapters/dingtalk/package.json @@ -1,7 +1,7 @@ { "name": "@satorijs/adapter-dingtalk", "description": "Dingtalk Adapter for Satorijs", - "version": "1.0.0", + "version": "1.0.1", "main": "lib/index.js", "typings": "lib/index.d.ts", "files": [ @@ -26,6 +26,6 @@ "satori" ], "peerDependencies": { - "@satorijs/satori": "^2.5.3" + "@satorijs/satori": "^2.6.1" } } diff --git a/adapters/discord/package.json b/adapters/discord/package.json index 88a82217..500f9a11 100644 --- a/adapters/discord/package.json +++ b/adapters/discord/package.json @@ -1,7 +1,7 @@ { "name": "@satorijs/adapter-discord", "description": "Discord Adapter for Satorijs", - "version": "3.8.4", + "version": "3.8.5", "main": "lib/index.js", "typings": "lib/index.d.ts", "files": [ @@ -29,7 +29,7 @@ "satori" ], "peerDependencies": { - "@satorijs/satori": "^2.6.0" + "@satorijs/satori": "^2.6.1" }, "dependencies": { "form-data": "^4.0.0" diff --git a/adapters/kook/package.json b/adapters/kook/package.json index 08c5461f..87e687d1 100644 --- a/adapters/kook/package.json +++ b/adapters/kook/package.json @@ -1,7 +1,7 @@ { "name": "@satorijs/adapter-kook", "description": "Kook (Kaiheila) Adapter for Satorijs", - "version": "3.10.1", + "version": "3.10.2", "main": "lib/index.js", "typings": "lib/index.d.ts", "files": [ @@ -26,7 +26,7 @@ "satori" ], "peerDependencies": { - "@satorijs/satori": "^2.6.0" + "@satorijs/satori": "^2.6.1" }, "dependencies": { "form-data": "^4.0.0" diff --git a/adapters/lark/package.json b/adapters/lark/package.json index 62ef8fdf..c1643fa1 100644 --- a/adapters/lark/package.json +++ b/adapters/lark/package.json @@ -1,7 +1,7 @@ { "name": "@satorijs/adapter-lark", "description": "Lark / Feishu Adapter for Satorijs", - "version": "2.1.2", + "version": "2.1.3", "main": "lib/index.js", "typings": "lib/index.d.ts", "files": [ @@ -30,7 +30,7 @@ "satori" ], "peerDependencies": { - "@satorijs/satori": "^2.6.0" + "@satorijs/satori": "^2.6.1" }, "dependencies": { "form-data": "^4.0.0" diff --git a/adapters/lark/src/bot.ts b/adapters/lark/src/bot.ts index 991dc956..cec0e665 100644 --- a/adapters/lark/src/bot.ts +++ b/adapters/lark/src/bot.ts @@ -23,6 +23,7 @@ export class LarkBot extends Bot { logger.warn('selfUrl is not set, some features may not work') } + this.platform = 'lark' this.selfId = config.appId this.http = ctx.http.extend({ diff --git a/adapters/line/package.json b/adapters/line/package.json index 97d2ed33..3d6a1916 100644 --- a/adapters/line/package.json +++ b/adapters/line/package.json @@ -1,7 +1,7 @@ { "name": "@satorijs/adapter-line", "description": "Line Adapter for Satorijs", - "version": "1.1.0", + "version": "1.1.1", "main": "lib/index.js", "typings": "lib/index.d.ts", "files": [ @@ -26,6 +26,6 @@ "satori" ], "peerDependencies": { - "@satorijs/satori": "^2.6.0" + "@satorijs/satori": "^2.6.1" } } diff --git a/adapters/mail/package.json b/adapters/mail/package.json index 8674b288..adc4a1b7 100644 --- a/adapters/mail/package.json +++ b/adapters/mail/package.json @@ -1,7 +1,7 @@ { "name": "@satorijs/adapter-mail", "description": "Mail Adapter for Satorijs", - "version": "1.1.1", + "version": "1.1.2", "main": "lib/index.js", "typings": "lib/index.d.ts", "files": [ @@ -25,7 +25,7 @@ "satori" ], "peerDependencies": { - "@satorijs/satori": "^2.6.0" + "@satorijs/satori": "^2.6.1" }, "dependencies": { "@types/mailparser": "^3.4.0", diff --git a/adapters/matrix/package.json b/adapters/matrix/package.json index 13926535..2c5bdd11 100644 --- a/adapters/matrix/package.json +++ b/adapters/matrix/package.json @@ -1,7 +1,7 @@ { "name": "@satorijs/adapter-matrix", "description": "Matrix Adapter for Satorijs", - "version": "3.1.2", + "version": "3.2.1", "main": "lib/index.js", "typings": "lib/index.d.ts", "files": [ @@ -24,7 +24,7 @@ "satori" ], "peerDependencies": { - "@satorijs/satori": "^2.6.0" + "@satorijs/satori": "^2.6.1" }, "dependencies": { "image-size": "^1.0.2", diff --git a/adapters/onebot/package.json b/adapters/onebot/package.json index 8a058c4b..b3d6b3f4 100644 --- a/adapters/onebot/package.json +++ b/adapters/onebot/package.json @@ -1,7 +1,7 @@ { "name": "@satorijs/adapter-onebot", "description": "OneBot Adapter for Satorijs", - "version": "5.7.3", + "version": "5.7.4", "main": "lib/index.js", "typings": "lib/index.d.ts", "files": [ @@ -25,7 +25,7 @@ "satori" ], "peerDependencies": { - "@satorijs/satori": "^2.6.0" + "@satorijs/satori": "^2.6.1" }, "dependencies": { "qface": "^1.4.1" diff --git a/adapters/qqguild/package.json b/adapters/qqguild/package.json index 9c10b7df..73b85ee5 100644 --- a/adapters/qqguild/package.json +++ b/adapters/qqguild/package.json @@ -1,7 +1,7 @@ { "name": "@satorijs/adapter-qqguild", "description": "QQ Guild Adapter for Satorijs", - "version": "3.5.4", + "version": "3.5.5", "main": "lib/index.js", "typings": "lib/index.d.ts", "files": [ @@ -26,7 +26,7 @@ "satori" ], "peerDependencies": { - "@satorijs/satori": "^2.6.0" + "@satorijs/satori": "^2.6.1" }, "dependencies": { "@qq-guild-sdk/core": "^2.2.2", diff --git a/adapters/telegram/package.json b/adapters/telegram/package.json index 5a6d4a4e..892f9a42 100644 --- a/adapters/telegram/package.json +++ b/adapters/telegram/package.json @@ -1,7 +1,7 @@ { "name": "@satorijs/adapter-telegram", "description": "Telegram Adapter for Satorijs", - "version": "3.7.13", + "version": "3.7.15", "main": "lib/index.js", "typings": "lib/index.d.ts", "files": [ @@ -30,7 +30,7 @@ "satori" ], "peerDependencies": { - "@satorijs/satori": "^2.6.0" + "@satorijs/satori": "^2.6.1" }, "dependencies": { "file-type": "^16.5.4", diff --git a/packages/axios/package.json b/packages/axios/package.json index 76cd6927..200f35d1 100644 --- a/packages/axios/package.json +++ b/packages/axios/package.json @@ -33,7 +33,7 @@ "service" ], "peerDependencies": { - "cordis": "^2.8.6" + "cordis": "^2.8.7" }, "devDependencies": { "@types/mime-db": "^1.43.1", diff --git a/packages/core/package.json b/packages/core/package.json index 34a1e53f..ffe046dc 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -1,7 +1,7 @@ { "name": "@satorijs/core", "description": "Core components of Satorijs", - "version": "2.6.0", + "version": "2.6.1", "main": "lib/index.cjs", "module": "lib/index.mjs", "typings": "lib/index.d.ts", @@ -35,7 +35,7 @@ }, "dependencies": { "@satorijs/element": "^2.4.2", - "cordis": "^2.8.6", + "cordis": "^2.8.7", "cordis-axios": "^3.1.4", "cosmokit": "^1.4.4", "ws": "^8.13.0", diff --git a/packages/satori/package.json b/packages/satori/package.json index 0afc521c..ad5828d8 100644 --- a/packages/satori/package.json +++ b/packages/satori/package.json @@ -1,7 +1,7 @@ { "name": "@satorijs/satori", "description": "Core components of Satorijs", - "version": "2.6.0", + "version": "2.6.1", "main": "lib/index.js", "typings": "lib/index.d.ts", "files": [ @@ -29,7 +29,7 @@ }, "dependencies": { "@koa/router": "^10.1.1", - "@satorijs/core": "2.6.0", + "@satorijs/core": "2.6.1", "@types/koa": "*", "@types/koa__router": "*", "@types/ws": "^8.5.5", From 6e3e0373731be408eb605ed984470bcea95e829f Mon Sep 17 00:00:00 2001 From: Shigma Date: Fri, 11 Aug 2023 15:51:31 +0800 Subject: [PATCH 12/20] chore(slack): fix typings and styles --- adapters/dingtalk/src/api/.eslintrc.yml | 2 ++ adapters/dingtalk/src/api/contact.ts | 2 +- adapters/matrix/src/bot.ts | 11 ++++----- adapters/slack/src/bot.ts | 6 ++--- adapters/slack/src/message.ts | 3 ++- adapters/slack/src/types/admin.ts | 9 ++++---- adapters/slack/src/types/api.ts | 6 ++--- adapters/slack/src/types/apps.ts | 4 ++-- adapters/slack/src/types/auth.ts | 5 ++-- adapters/slack/src/types/bots.ts | 6 ++--- adapters/slack/src/types/calls.ts | 6 ++--- adapters/slack/src/types/chat.ts | 4 ++-- adapters/slack/src/types/conversations.ts | 1 - adapters/slack/src/types/dialog.ts | 6 ++--- adapters/slack/src/types/dnd.ts | 6 ++--- adapters/slack/src/types/emoji.ts | 6 ++--- adapters/slack/src/types/events/index.ts | 4 ++-- .../slack/src/types/events/message-events.ts | 2 +- adapters/slack/src/types/files.ts | 1 - adapters/slack/src/types/internal.ts | 2 +- adapters/slack/src/types/migration.ts | 6 ++--- adapters/slack/src/types/oauth.ts | 6 ++--- adapters/slack/src/types/pins.ts | 6 ++--- adapters/slack/src/types/reactions.ts | 1 - adapters/slack/src/types/reminders.ts | 1 - adapters/slack/src/types/rtm.ts | 6 ++--- adapters/slack/src/types/search.ts | 6 ++--- adapters/slack/src/types/stars.ts | 1 - adapters/slack/src/types/team.ts | 1 - adapters/slack/src/types/usergroups.ts | 1 - adapters/slack/src/types/users.ts | 1 - adapters/slack/src/types/views.ts | 6 ++--- adapters/slack/src/types/workflows.ts | 6 ++--- adapters/slack/src/utils.ts | 23 +++++++++++-------- adapters/slack/src/ws.ts | 12 ++++------ 35 files changed, 70 insertions(+), 105 deletions(-) create mode 100644 adapters/dingtalk/src/api/.eslintrc.yml diff --git a/adapters/dingtalk/src/api/.eslintrc.yml b/adapters/dingtalk/src/api/.eslintrc.yml new file mode 100644 index 00000000..772595db --- /dev/null +++ b/adapters/dingtalk/src/api/.eslintrc.yml @@ -0,0 +1,2 @@ +rules: + max-len: off diff --git a/adapters/dingtalk/src/api/contact.ts b/adapters/dingtalk/src/api/contact.ts index 73205ca1..13e34c62 100644 --- a/adapters/dingtalk/src/api/contact.ts +++ b/adapters/dingtalk/src/api/contact.ts @@ -18,7 +18,7 @@ export interface GetOrgAuthInfoResponse { } export interface BatchApproveUnionApplyParams { - /** 申请的合作伙伴组织CorpId,参考[基础概念-CorpId](https://open.dingtalk.com/document/org/basic-concepts)。 */ + /** 申请的合作伙伴组织 CorpId,参考[基础概念-CorpId](https://open.dingtalk.com/document/org/basic-concepts)。 */ branchCorpId?: string /** 合作伙伴组织在上下游组织内的名称。 */ unionRootName?: string diff --git a/adapters/matrix/src/bot.ts b/adapters/matrix/src/bot.ts index 1427b017..544ba3f0 100644 --- a/adapters/matrix/src/bot.ts +++ b/adapters/matrix/src/bot.ts @@ -17,8 +17,7 @@ export class MatrixBot extends Bot { super(ctx, config) this.id = config.id this.platform = 'matrix' - this.selfId = `@${this.id}:${this.config.host}` - this.userId = this.selfId + this.userId = `@${this.id}:${this.config.host}` this.endpoint = (config.endpoint || `https://${config.host}`) + '/_matrix' this.internal = new Matrix.Internal(this) ctx.plugin(HttpAdapter, this) @@ -189,13 +188,11 @@ export namespace MatrixBot { export const Config: Schema = Schema.object({ name: Schema.string().description('机器人的名称,如果设置了将会在启动时为机器人更改。'), avatar: Schema.string().description('机器人的头像地址,如果设置了将会在启动时为机器人更改。'), - // eslint-disable-next-line - id: Schema.string().description('机器人的 ID。机器人最后的用户名将会是 @${id}:${host}。').required(), - host: Schema.string().description('Matrix homeserver 域名。').required(), + id: Schema.string().description('机器人的 ID。机器人最后的用户名将会是 `@{id}:{host}`。').required(), + host: Schema.string().description('Matrix Homeserver 域名。').required(), hsToken: Schema.string().description('hs_token').role('secret').required(), asToken: Schema.string().description('as_token').role('secret').required(), - // eslint-disable-next-line - endpoint: Schema.string().description('Matrix homeserver 地址。默认为 https://${host}。'), + endpoint: Schema.string().description('Matrix Homeserver 地址。默认为 `https://{host}`。'), ...omit(Quester.Config.dict, ['endpoint']), }) } diff --git a/adapters/slack/src/bot.ts b/adapters/slack/src/bot.ts index 8845d9a0..12e49855 100644 --- a/adapters/slack/src/bot.ts +++ b/adapters/slack/src/bot.ts @@ -1,7 +1,7 @@ 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 { adaptChannel, adaptGuild, adaptMessage, adaptUser } from './utils' import { SlackMessageEncoder } from './message' import { GenericMessageEvent, SlackChannel, SlackTeam, SlackUser } from './types' import FormData from 'form-data' @@ -14,8 +14,8 @@ export class SlackBot extends Bot .replace(/\u200b([\*_~`])/g, '$1') .replace(/@\u200Beveryone/g, () => '@everyone') .replace(/@\u200Bhere/g, () => '@here') + export class SlackMessageEncoder extends MessageEncoder { buffer = '' thread_ts = null @@ -35,7 +36,7 @@ export class SlackMessageEncoder extends MessageEncoder { text: this.buffer, }) const session = this.bot.session() - await adaptMessage(this.bot, r.message, session) + await adaptMessage(this.bot, r.message as any, session) session.channelId = this.channelId session.app.emit(session, 'send', session) this.results.push(session) diff --git a/adapters/slack/src/types/admin.ts b/adapters/slack/src/types/admin.ts index a4ae0306..b9724134 100644 --- a/adapters/slack/src/types/admin.ts +++ b/adapters/slack/src/types/admin.ts @@ -1,5 +1,6 @@ -import { Internal, TokenInput } from './internal' +import { Internal } from './internal' import { Definitions } from './definition' + Internal.define({ '/admin.apps.approve': { POST: { 'adminAppsApprove': true }, @@ -440,7 +441,6 @@ export namespace Admin { declare module './internal' { interface Internal { - /** * Approve an app for installation on a workspace. * @see https://api.slack.com/methods/admin.apps.approve @@ -523,7 +523,9 @@ declare module './internal' { }> /** - * 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. + * 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<{ @@ -904,6 +906,5 @@ declare module './internal' { 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 index ec90be44..82024bb9 100644 --- a/adapters/slack/src/types/api.ts +++ b/adapters/slack/src/types/api.ts @@ -1,5 +1,5 @@ -import { Internal, TokenInput } from './internal' -import { Definitions } from './definition' +import { Internal } from './internal' + Internal.define({ '/api.test': { GET: { 'apiTest': true }, @@ -17,7 +17,6 @@ export namespace Api { declare module './internal' { interface Internal { - /** * Checks API calling code. * @see https://api.slack.com/methods/api.test @@ -25,6 +24,5 @@ declare module './internal' { 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 index 6b57b49d..13f819f6 100644 --- a/adapters/slack/src/types/apps.ts +++ b/adapters/slack/src/types/apps.ts @@ -1,5 +1,6 @@ -import { Internal, TokenInput } from './internal' +import { Internal } from './internal' import { Definitions } from './definition' + Internal.define({ '/apps.event.authorizations.list': { GET: { 'appsEventAuthorizationsList': true }, @@ -170,6 +171,5 @@ declare module './internal' { 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 index f651725c..1b6d0ac9 100644 --- a/adapters/slack/src/types/auth.ts +++ b/adapters/slack/src/types/auth.ts @@ -1,5 +1,5 @@ -import { Internal, TokenInput } from './internal' -import { Definitions } from './definition' +import { Internal } from './internal' + Internal.define({ '/auth.revoke': { GET: { 'authRevoke': false }, @@ -45,6 +45,5 @@ declare module './internal' { user: string user_id: string }> - } } diff --git a/adapters/slack/src/types/bots.ts b/adapters/slack/src/types/bots.ts index fc3a3c4f..9f0c7fb4 100644 --- a/adapters/slack/src/types/bots.ts +++ b/adapters/slack/src/types/bots.ts @@ -1,5 +1,5 @@ -import { Internal, TokenInput } from './internal' -import { Definitions } from './definition' +import { Internal } from './internal' + Internal.define({ '/bots.info': { GET: { 'botsInfo': false }, @@ -16,7 +16,6 @@ export namespace Bots { declare module './internal' { interface Internal { - /** * Gets information about a bot user. * @see https://api.slack.com/methods/bots.info @@ -37,6 +36,5 @@ declare module './internal' { } ok: boolean }> - } } diff --git a/adapters/slack/src/types/calls.ts b/adapters/slack/src/types/calls.ts index e9a0316d..40fd0b39 100644 --- a/adapters/slack/src/types/calls.ts +++ b/adapters/slack/src/types/calls.ts @@ -1,5 +1,5 @@ -import { Internal, TokenInput } from './internal' -import { Definitions } from './definition' +import { Internal } from './internal' + Internal.define({ '/calls.add': { POST: { 'callsAdd': true }, @@ -59,7 +59,6 @@ export namespace Calls { declare module './internal' { interface Internal { - /** * Registers a new Call. * @see https://api.slack.com/methods/calls.add @@ -107,6 +106,5 @@ declare module './internal' { 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 index efe486a9..5c52d9a6 100644 --- a/adapters/slack/src/types/chat.ts +++ b/adapters/slack/src/types/chat.ts @@ -1,5 +1,6 @@ -import { Internal, TokenInput } from './internal' +import { Internal } from './internal' import { Definitions } from './definition' + Internal.define({ '/chat.delete': { POST: { 'chatDelete': true }, @@ -250,6 +251,5 @@ declare module './internal' { text: string ts: string }> - } } diff --git a/adapters/slack/src/types/conversations.ts b/adapters/slack/src/types/conversations.ts index 52b7b09b..ee1bc80d 100644 --- a/adapters/slack/src/types/conversations.ts +++ b/adapters/slack/src/types/conversations.ts @@ -338,6 +338,5 @@ declare module './internal' { conversationsUnarchive(token: TokenInput, params: Conversations.Params.Unarchive): Promise<{ ok: boolean }> - } } diff --git a/adapters/slack/src/types/dialog.ts b/adapters/slack/src/types/dialog.ts index 699da29e..6221bff5 100644 --- a/adapters/slack/src/types/dialog.ts +++ b/adapters/slack/src/types/dialog.ts @@ -1,5 +1,5 @@ -import { Internal, TokenInput } from './internal' -import { Definitions } from './definition' +import { Internal } from './internal' + Internal.define({ '/dialog.open': { GET: { 'dialogOpen': true }, @@ -17,7 +17,6 @@ export namespace Dialog { declare module './internal' { interface Internal { - /** * Open a dialog with a user * @see https://api.slack.com/methods/dialog.open @@ -25,6 +24,5 @@ declare module './internal' { 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 index aa9823b0..05d97783 100644 --- a/adapters/slack/src/types/dnd.ts +++ b/adapters/slack/src/types/dnd.ts @@ -1,5 +1,5 @@ -import { Internal, TokenInput } from './internal' -import { Definitions } from './definition' +import { Internal } from './internal' + Internal.define({ '/dnd.endDnd': { POST: { 'dndEndDnd': true }, @@ -38,7 +38,6 @@ export namespace Dnd { declare module './internal' { interface Internal { - /** * Ends the current user's Do Not Disturb session immediately. * @see https://api.slack.com/methods/dnd.endDnd @@ -91,6 +90,5 @@ declare module './internal' { 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 index 97368cf7..7692d216 100644 --- a/adapters/slack/src/types/emoji.ts +++ b/adapters/slack/src/types/emoji.ts @@ -1,5 +1,5 @@ -import { Internal, TokenInput } from './internal' -import { Definitions } from './definition' +import { Internal } from './internal' + Internal.define({ '/emoji.list': { GET: { 'emojiList': false }, @@ -15,7 +15,6 @@ export namespace Emoji { declare module './internal' { interface Internal { - /** * Lists custom emoji for a team. * @see https://api.slack.com/methods/emoji.list @@ -23,6 +22,5 @@ declare module './internal' { emojiList(token: TokenInput): Promise<{ ok: boolean }> - } } diff --git a/adapters/slack/src/types/events/index.ts b/adapters/slack/src/types/events/index.ts index e1f1976b..5716d1a8 100644 --- a/adapters/slack/src/types/events/index.ts +++ b/adapters/slack/src/types/events/index.ts @@ -1,6 +1,6 @@ // https://github.com/slackapi/bolt-js/blob/main/src/types/events/index.ts -import { BasicSlackEvent, UserProfileChangedEvent } from './base-events' +import { BasicSlackEvent, SlackEvent, UserProfileChangedEvent } from './base-events' import { Block } from '@slack/types' export type StringIndexed = Record @@ -42,7 +42,7 @@ export interface UrlVerificationEvent { * * This describes the entire JSON-encoded body of a request from Slack's Events API. */ -export interface EnvelopedEvent extends StringIndexed { +export interface EnvelopedEvent extends StringIndexed { token: string team_id: string enterprise_id?: string diff --git a/adapters/slack/src/types/events/message-events.ts b/adapters/slack/src/types/events/message-events.ts index 80bdc27b..ec05d87b 100644 --- a/adapters/slack/src/types/events/message-events.ts +++ b/adapters/slack/src/types/events/message-events.ts @@ -3,7 +3,7 @@ import { Block, KnownBlock, MessageAttachment } from '@slack/types' export type MessageEvent = - | GenericMessageEvent + // | GenericMessageEvent | BotMessageEvent | ChannelJoinMessageEvent | ChannelLeaveMessageEvent diff --git a/adapters/slack/src/types/files.ts b/adapters/slack/src/types/files.ts index 20927aed..13c562bb 100644 --- a/adapters/slack/src/types/files.ts +++ b/adapters/slack/src/types/files.ts @@ -241,6 +241,5 @@ declare module './internal' { file: Definitions.File ok: boolean }> - } } diff --git a/adapters/slack/src/types/internal.ts b/adapters/slack/src/types/internal.ts index 1171e55b..132735fd 100644 --- a/adapters/slack/src/types/internal.ts +++ b/adapters/slack/src/types/internal.ts @@ -1,4 +1,4 @@ -import { Dict, makeArray, Quester } from '@satorijs/satori' +import { Dict, Quester } from '@satorijs/satori' import { SlackBot } from '../bot' // https://api.slack.com/web#methods_supporting_json diff --git a/adapters/slack/src/types/migration.ts b/adapters/slack/src/types/migration.ts index 87d13ff7..908cfd24 100644 --- a/adapters/slack/src/types/migration.ts +++ b/adapters/slack/src/types/migration.ts @@ -1,5 +1,5 @@ -import { Internal, TokenInput } from './internal' -import { Definitions } from './definition' +import { Internal } from './internal' + Internal.define({ '/migration.exchange': { GET: { 'migrationExchange': false }, @@ -18,7 +18,6 @@ export namespace Migration { 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 @@ -31,6 +30,5 @@ declare module './internal' { user_id_map?: { } }> - } } diff --git a/adapters/slack/src/types/oauth.ts b/adapters/slack/src/types/oauth.ts index b693fbb0..f02792f6 100644 --- a/adapters/slack/src/types/oauth.ts +++ b/adapters/slack/src/types/oauth.ts @@ -1,5 +1,5 @@ -import { Internal, TokenInput } from './internal' -import { Definitions } from './definition' +import { Internal } from './internal' + Internal.define({ '/oauth.access': { GET: { 'oauthAccess': false }, @@ -39,7 +39,6 @@ export namespace Oauth { declare module './internal' { interface Internal { - /** * Exchanges a temporary OAuth verifier code for an access token. * @see https://api.slack.com/methods/oauth.access @@ -63,6 +62,5 @@ declare module './internal' { 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 index d51e8868..28bd2ae3 100644 --- a/adapters/slack/src/types/pins.ts +++ b/adapters/slack/src/types/pins.ts @@ -1,5 +1,5 @@ -import { Internal, TokenInput } from './internal' -import { Definitions } from './definition' +import { Internal } from './internal' + Internal.define({ '/pins.add': { POST: { 'pinsAdd': true }, @@ -30,7 +30,6 @@ export namespace Pins { declare module './internal' { interface Internal { - /** * Pins an item to a channel. * @see https://api.slack.com/methods/pins.add @@ -52,6 +51,5 @@ declare module './internal' { 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 index f74d3d1d..4ec59e0e 100644 --- a/adapters/slack/src/types/reactions.ts +++ b/adapters/slack/src/types/reactions.ts @@ -91,6 +91,5 @@ declare module './internal' { 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 index 0e0cae0c..17e0e1af 100644 --- a/adapters/slack/src/types/reminders.ts +++ b/adapters/slack/src/types/reminders.ts @@ -84,6 +84,5 @@ declare module './internal' { ok: boolean reminders: Definitions.Reminder[] }> - } } diff --git a/adapters/slack/src/types/rtm.ts b/adapters/slack/src/types/rtm.ts index 7e86399d..9c28e4c6 100644 --- a/adapters/slack/src/types/rtm.ts +++ b/adapters/slack/src/types/rtm.ts @@ -1,5 +1,5 @@ -import { Internal, TokenInput } from './internal' -import { Definitions } from './definition' +import { Internal } from './internal' + Internal.define({ '/rtm.connect': { GET: { 'rtmConnect': false }, @@ -17,7 +17,6 @@ export namespace Rtm { declare module './internal' { interface Internal { - /** * Starts a Real Time Messaging session. * @see https://api.slack.com/methods/rtm.connect @@ -35,6 +34,5 @@ declare module './internal' { } url: string }> - } } diff --git a/adapters/slack/src/types/search.ts b/adapters/slack/src/types/search.ts index 6d0848bc..7e145fba 100644 --- a/adapters/slack/src/types/search.ts +++ b/adapters/slack/src/types/search.ts @@ -1,5 +1,5 @@ -import { Internal, TokenInput } from './internal' -import { Definitions } from './definition' +import { Internal } from './internal' + Internal.define({ '/search.messages': { GET: { 'searchMessages': false }, @@ -21,7 +21,6 @@ export namespace Search { declare module './internal' { interface Internal { - /** * Searches for messages matching a query. * @see https://api.slack.com/methods/search.messages @@ -29,6 +28,5 @@ declare module './internal' { 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 index 29e4412c..785b0f9f 100644 --- a/adapters/slack/src/types/stars.ts +++ b/adapters/slack/src/types/stars.ts @@ -68,6 +68,5 @@ declare module './internal' { 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 index f304459f..64d1c976 100644 --- a/adapters/slack/src/types/team.ts +++ b/adapters/slack/src/types/team.ts @@ -118,6 +118,5 @@ declare module './internal' { fields: Definitions.TeamProfileField[] } }> - } } diff --git a/adapters/slack/src/types/usergroups.ts b/adapters/slack/src/types/usergroups.ts index c53dfeca..17d562b4 100644 --- a/adapters/slack/src/types/usergroups.ts +++ b/adapters/slack/src/types/usergroups.ts @@ -131,6 +131,5 @@ declare module './internal' { ok: boolean usergroup: Definitions.Subteam }> - } } diff --git a/adapters/slack/src/types/users.ts b/adapters/slack/src/types/users.ts index 97a8d0d0..6d0805a0 100644 --- a/adapters/slack/src/types/users.ts +++ b/adapters/slack/src/types/users.ts @@ -217,6 +217,5 @@ declare module './internal' { 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 index b15a22b3..cfe65104 100644 --- a/adapters/slack/src/types/views.ts +++ b/adapters/slack/src/types/views.ts @@ -1,5 +1,5 @@ -import { Internal, TokenInput } from './internal' -import { Definitions } from './definition' +import { Internal } from './internal' + Internal.define({ '/views.open': { GET: { 'viewsOpen': true }, @@ -41,7 +41,6 @@ export namespace Views { declare module './internal' { interface Internal { - /** * Open a view for a user. * @see https://api.slack.com/methods/views.open @@ -73,6 +72,5 @@ declare module './internal' { 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 index b353d389..9bb58d5c 100644 --- a/adapters/slack/src/types/workflows.ts +++ b/adapters/slack/src/types/workflows.ts @@ -1,5 +1,5 @@ -import { Internal, TokenInput } from './internal' -import { Definitions } from './definition' +import { Internal } from './internal' + Internal.define({ '/workflows.stepCompleted': { GET: { 'workflowsStepCompleted': true }, @@ -34,7 +34,6 @@ export namespace Workflows { declare module './internal' { interface Internal { - /** * Indicate that an app's step in a workflow completed execution. * @see https://api.slack.com/methods/workflows.stepCompleted @@ -58,6 +57,5 @@ declare module './internal' { workflowsUpdateStep(token: TokenInput, params: Workflows.Params.UpdateStep): Promise<{ ok: boolean }> - } } diff --git a/adapters/slack/src/utils.ts b/adapters/slack/src/utils.ts index 77dc7120..68138fb8 100644 --- a/adapters/slack/src/utils.ts +++ b/adapters/slack/src/utils.ts @@ -1,8 +1,8 @@ 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 { EnvelopedEvent, GenericMessageEvent, MessageChangedEvent, MessageDeletedEvent, ReactionAddedEvent, ReactionRemovedEvent, RichText, RichTextBlock, SlackEvent, SlackUser } from './types/events' import { KnownBlock } from '@slack/types' -import { Definitions, File, SlackChannel, SlackTeam } from './types' +import { File, SlackChannel, SlackTeam } from './types' import { unescape } from './message' type NewKnownBlock = KnownBlock | RichTextBlock @@ -160,18 +160,18 @@ export async function adaptSession(bot: SlackBot, payload: EnvelopedEvent { async prepare(bot: SlackBot) { - const { userId } = await bot.getSelf() - bot.selfId = userId + const user = await bot.getSelf() + Object.assign(bot, user) const data = await bot.request('POST', '/apps.connections.open', {}, {}, true) const { url } = data logger.debug('ws url: %s', url) @@ -27,7 +27,7 @@ export class WsClient extends Adapter.WsClient { } if (type === 'events_api') { const { envelope_id } = parsed - const payload: EnvelopedEvent = parsed.payload + const payload = parsed.payload bot.socket.send(JSON.stringify({ envelope_id })) const session = await adaptSession(bot, payload) @@ -37,10 +37,6 @@ export class WsClient extends Adapter.WsClient { } } }) - - bot.socket.addEventListener('close', () => { - - }) } } From 4142bdaf9fdcaf43f2336f4b71d12cfddfe52413 Mon Sep 17 00:00:00 2001 From: Shigma Date: Fri, 11 Aug 2023 22:51:16 +0800 Subject: [PATCH 13/20] feat(whatsapp): support arbitrary bots adapter --- adapters/whatsapp/src/adapter.ts | 113 ++++++++++++++++++++++++++++++ adapters/whatsapp/src/bot.ts | 47 +++---------- adapters/whatsapp/src/http.ts | 77 -------------------- adapters/whatsapp/src/index.ts | 55 ++------------- adapters/whatsapp/src/internal.ts | 32 +++++++++ adapters/whatsapp/src/message.ts | 22 +++++- 6 files changed, 178 insertions(+), 168 deletions(-) create mode 100644 adapters/whatsapp/src/adapter.ts delete mode 100644 adapters/whatsapp/src/http.ts create mode 100644 adapters/whatsapp/src/internal.ts diff --git a/adapters/whatsapp/src/adapter.ts b/adapters/whatsapp/src/adapter.ts new file mode 100644 index 00000000..6aa7278b --- /dev/null +++ b/adapters/whatsapp/src/adapter.ts @@ -0,0 +1,113 @@ +import { Adapter, Context, Logger, Quester, Schema } from '@satorijs/satori' +import { Internal } from './internal' +import { WhatsAppBot } from './bot' +import { WebhookBody } from './types' +import { decodeMessage } from './utils' +import internal from 'stream' +import crypto from 'crypto' + +export class WhatsAppAdapter extends Adapter { + static reusable = true + + public bots: WhatsAppBot[] = [] + public logger = new Logger('whatsapp') + + constructor(private ctx: Context, public config: WhatsAppAdapter.Config) { + super() + + const http = ctx.http.extend({ + ...config, + headers: { + Authorization: `Bearer ${config.systemToken}`, + }, + }) + const internal = new Internal(http) + + ctx.on('ready', async () => { + const data = await internal.getPhoneNumbers(config.id) + for (const item of data) { + const bot = new WhatsAppBot(ctx, { + selfId: item.id, + }) + bot.adapter = this + bot.internal = internal + this.bots.push(bot) + bot.online() + } + }) + + // https://developers.facebook.com/docs/graph-api/webhooks/getting-started + // https://developers.facebook.com/docs/graph-api/webhooks/getting-started/webhooks-for-whatsapp/ + ctx.router.post('/whatsapp', async (ctx) => { + const receivedSignature = ctx.get('X-Hub-Signature-256').split('sha256=')[1] + + const payload = JSON.stringify(ctx.request.body) + + const generatedSignature = crypto + .createHmac('sha256', this.config.secret) + .update(payload) + .digest('hex') + if (receivedSignature !== generatedSignature) return ctx.status = 403 + + const parsed = ctx.request.body as WebhookBody + this.logger.debug(require('util').inspect(parsed, false, null, true)) + ctx.body = 'ok' + ctx.status = 200 + if (parsed.object !== 'whatsapp_business_account') return + for (const entry of parsed.entry) { + const phone_number_id = entry.changes[0].value.metadata.phone_number_id + const bot = this.bots.find((bot) => bot.selfId === phone_number_id) + const session = await decodeMessage(bot, entry) + if (session.length) session.forEach(bot.dispatch.bind(bot)) + this.logger.debug('handling bot: %s', bot.sid) + this.logger.debug(require('util').inspect(session, false, null, true)) + } + }) + + ctx.router.get('/whatsapp', async (ctx) => { + this.logger.debug(require('util').inspect(ctx.query, false, null, true)) + const verifyToken = ctx.query['hub.verify_token'] + const challenge = ctx.query['hub.challenge'] + if (verifyToken !== this.config.verifyToken) return ctx.status = 403 + ctx.body = challenge + ctx.status = 200 + }) + + ctx.router.get('/whatsapp/assets/:self_id/:media_id', async (ctx) => { + const mediaId = ctx.params.media_id + const selfId = ctx.params.self_id + const bot = this.bots.find((bot) => bot.selfId === selfId) + if (!bot) return ctx.status = 404 + + const fetched = await bot.http.get<{ url: string }>('/' + mediaId) + this.logger.debug(fetched.url) + const resp = await bot.ctx.http.axios({ + url: fetched.url, + method: 'GET', + responseType: 'stream', + }) + ctx.type = resp.headers['content-type'] + ctx.set('cache-control', resp.headers['cache-control']) + ctx.response.body = resp.data + ctx.status = 200 + }) + } +} + +export namespace WhatsAppAdapter { + export interface Config extends Quester.Config { + systemToken: string + verifyToken: string + id: string + secret: string + } + export const Config: Schema = Schema.intersect([ + Schema.object({ + secret: Schema.string().role('secret').description('App Secret').required(), + systemToken: Schema.string().role('secret').description('System User Token').required(), + verifyToken: Schema.string().required(), + id: Schema.string().description('WhatsApp Business Account ID').required(), + }), + Quester.createConfig('https://graph.facebook.com'), + ] as const) +} diff --git a/adapters/whatsapp/src/bot.ts b/adapters/whatsapp/src/bot.ts index f2de6ca3..70bf4b10 100644 --- a/adapters/whatsapp/src/bot.ts +++ b/adapters/whatsapp/src/bot.ts @@ -1,50 +1,19 @@ -import { Bot, Context, Quester, Schema } from '@satorijs/satori' +import { Bot, Context, Quester } from '@satorijs/satori' import { WhatsAppMessageEncoder } from './message' -import { WhatsAppBusiness } from '.' +import { Internal } from './internal' -export class WhatsAppBot extends Bot { +export class WhatsAppBot extends Bot { static MessageEncoder = WhatsAppMessageEncoder + + public internal: Internal public http: Quester - constructor(ctx: Context, config: WhatsAppBot.Config) { + constructor(ctx: Context, config: Bot.Config) { super(ctx, config) - this.http = ctx.http.extend({ - ...config, - headers: { - Authorization: `Bearer ${config.systemToken}`, - }, - }) - } - - async initialize() { - this.selfId = this.config.phoneNumber + this.platform = 'whatsapp' } async createReaction(channelId: string, messageId: string, emoji: string): Promise { - // https://developers.facebook.com/docs/whatsapp/cloud-api/guides/send-messages#reaction-messages - await this.http.post(`/${this.selfId}/messages`, { - messaging_product: 'whatsapp', - to: channelId, - recipient_type: 'individual', - type: 'reaction', - reaction: { - message_id: messageId, - emoji, - }, - }) + await this.internal.messageReaction(this.selfId, channelId, messageId, emoji) } } - -export namespace WhatsAppBot { - export interface Config extends WhatsAppBusiness.Config, Bot.Config { - phoneNumber: string - } - export const Config: Schema = Schema.intersect([ - Schema.object({ - phoneNumber: Schema.string().description('手机号').required(), - }), - WhatsAppBusiness.Config, - ] as const) -} - -WhatsAppBot.prototype.platform = 'whatsapp' diff --git a/adapters/whatsapp/src/http.ts b/adapters/whatsapp/src/http.ts deleted file mode 100644 index f7ef61d6..00000000 --- a/adapters/whatsapp/src/http.ts +++ /dev/null @@ -1,77 +0,0 @@ -import { Adapter, Context, Logger } from '@satorijs/satori' -import { WhatsAppBot } from './bot' -import { WebhookBody } from './types' -import { decodeMessage } from './utils' -import internal from 'stream' -import crypto from 'crypto' - -export class HttpServer extends Adapter.Server { - logger = new Logger('whatsapp') - - fork(ctx: Context, bot: WhatsAppBot) { - super.fork(ctx, bot) - return bot.initialize() - } - - async start(bot: WhatsAppBot) { - // https://developers.facebook.com/docs/graph-api/webhooks/getting-started - // https://developers.facebook.com/docs/graph-api/webhooks/getting-started/webhooks-for-whatsapp/ - bot.ctx.router.post('/whatsapp', async (ctx) => { - const receivedSignature = ctx.get('X-Hub-Signature-256').split('sha256=')[1] - - const payload = JSON.stringify(ctx.request.body) - - const generatedSignature = crypto - .createHmac('sha256', bot.config.secret) - .update(payload) - .digest('hex') - if (receivedSignature !== generatedSignature) return ctx.status = 403 - - const parsed = ctx.request.body as WebhookBody - this.logger.debug(require('util').inspect(parsed, false, null, true)) - ctx.body = 'ok' - ctx.status = 200 - if (parsed.object !== 'whatsapp_business_account') return - for (const entry of parsed.entry) { - const phone_number_id = entry.changes[0].value.metadata.phone_number_id - const localBot = this.bots.find((bot) => bot.selfId === phone_number_id) - const session = await decodeMessage(localBot, entry) - if (session.length) session.forEach(localBot.dispatch.bind(localBot)) - this.logger.debug('handling bot: %s', localBot.sid) - this.logger.debug(require('util').inspect(session, false, null, true)) - } - }) - bot.ctx.router.get('/whatsapp', async (ctx) => { - this.logger.debug(require('util').inspect(ctx.query, false, null, true)) - const verifyToken = ctx.query['hub.verify_token'] - const challenge = ctx.query['hub.challenge'] - if (verifyToken !== bot.config.verifyToken) return ctx.status = 403 - ctx.body = challenge - ctx.status = 200 - }) - bot.ctx.router.get('/whatsapp/assets/:self_id/:media_id', async (ctx) => { - const mediaId = ctx.params.media_id - const selfId = ctx.params.self_id - const localBot = this.bots.find((bot) => bot.selfId === selfId) - if (!localBot) return ctx.status = 404 - - const fetched = await localBot.http.get<{ - url: string - }>('/' + mediaId) - this.logger.debug(fetched.url) - const resp = await localBot.ctx.http.axios({ - url: fetched.url, - method: 'GET', - responseType: 'stream', - headers: { - Authorization: `Bearer ${localBot.config.systemToken}`, - }, - }) - ctx.type = resp.headers['content-type'] - ctx.set('cache-control', resp.headers['cache-control']) - ctx.response.body = resp.data - ctx.status = 200 - }) - bot.online() - } -} diff --git a/adapters/whatsapp/src/index.ts b/adapters/whatsapp/src/index.ts index 9706cfff..66ec0d11 100644 --- a/adapters/whatsapp/src/index.ts +++ b/adapters/whatsapp/src/index.ts @@ -1,56 +1,9 @@ -import { Bot, Context, Quester, Schema } from '@satorijs/satori' -import { WhatsAppBot } from './bot' -import { HttpServer } from './http' +import { WhatsAppAdapter } from './adapter' -export * from './http' +export default WhatsAppAdapter + +export * from './adapter' export * from './bot' export * from './types' export * from './utils' export * from './message' - -export async function WhatsAppBusiness(ctx: Context, config: WhatsAppBusiness.Config) { - const http: Quester = ctx.http.extend({ - ...config, - headers: { - Authorization: `Bearer ${config.systemToken}`, - }, - }) - const { data } = await http<{ - data: { - verified_name: string - code_verification_status: string - display_phone_number: string - quality_rating: string - id: string - }[] - }>('GET', `/${config.id}/phone_numbers`) - ctx.logger('whatsapp').debug(require('util').inspect(data, false, null, true)) - const httpServer = new HttpServer() - for (const item of data) { - const bot = new WhatsAppBot(ctx, { - ...config, - phoneNumber: item.id, - }) - httpServer.fork(ctx, bot) - } -} - -export namespace WhatsAppBusiness { - export interface Config extends Quester.Config { - systemToken: string - verifyToken: string - id: string - secret: string - } - export const Config: Schema = Schema.intersect([ - Schema.object({ - secret: Schema.string().role('secret').description('App Secret').required(), - systemToken: Schema.string().role('secret').description('System User Token').required(), - verifyToken: Schema.string().required(), - id: Schema.string().description('WhatsApp Business Account ID').required(), - }), - Quester.createConfig('https://graph.facebook.com'), - ] as const) -} - -export default WhatsAppBusiness diff --git a/adapters/whatsapp/src/internal.ts b/adapters/whatsapp/src/internal.ts new file mode 100644 index 00000000..7386f150 --- /dev/null +++ b/adapters/whatsapp/src/internal.ts @@ -0,0 +1,32 @@ +import { Quester } from '@satorijs/satori' + +interface PhoneNumber { + verified_name: string + code_verification_status: string + display_phone_number: string + quality_rating: string + id: string +} + +export class Internal { + constructor(public http: Quester) {} + + async getPhoneNumbers(id: string) { + const { data } = await this.http.get<{ data: PhoneNumber[] }>(`/${id}/phone_numbers`) + return data + } + + async messageReaction(selfId: string, channelId: string, messageId: string, emoji: string) { + // https://developers.facebook.com/docs/whatsapp/cloud-api/guides/send-messages#reaction-messages + await this.http.post(`/${selfId}/messages`, { + messaging_product: 'whatsapp', + to: channelId, + recipient_type: 'individual', + type: 'reaction', + reaction: { + message_id: messageId, + emoji, + }, + }) + } +} diff --git a/adapters/whatsapp/src/message.ts b/adapters/whatsapp/src/message.ts index ff477860..33950d33 100644 --- a/adapters/whatsapp/src/message.ts +++ b/adapters/whatsapp/src/message.ts @@ -3,7 +3,27 @@ import { WhatsAppBot } from './bot' import FormData from 'form-data' import { SendMessage } from './types' -const SUPPORTED_MEDIA = 'audio/aac, audio/mp4, audio/mpeg, audio/amr, audio/ogg, audio/opus, application/vnd.ms-powerpoint, application/msword, application/vnd.openxmlformats-officedocument.wordprocessingml.document, application/vnd.openxmlformats-officedocument.presentationml.presentation, application/vnd.openxmlformats-officedocument.spreadsheetml.sheet, application/pdf, text/plain, application/vnd.ms-excel, image/jpeg, image/png, image/webp, video/mp4, video/3gpp'.split(', ') +const SUPPORTED_MEDIA = [ + 'audio/aac', + 'audio/mp4', + 'audio/mpeg', + 'audio/amr', + 'audio/ogg', + 'audio/opus', + 'application/vnd.ms-powerpoint', + 'application/msword', + 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', + 'application/vnd.openxmlformats-officedocument.presentationml.presentation', + 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', + 'application/pdf', + 'text/plain', + 'application/vnd.ms-excel', + 'image/jpeg', + 'image/png', + 'image/webp', + 'video/mp4', + 'video/3gpp', +] export class WhatsAppMessageEncoder extends MessageEncoder { private buffer = '' From a21f965488fbf1f1d3766be87e331fcf37a0966c Mon Sep 17 00:00:00 2001 From: Shigma Date: Fri, 11 Aug 2023 23:02:08 +0800 Subject: [PATCH 14/20] chore: update readme --- README.md | 17 ++++++++++------- adapters/dingtalk/readme.md | 5 +++++ adapters/discord/readme.md | 5 +++++ adapters/kook/readme.md | 5 +++++ adapters/lark/readme.md | 5 +++++ adapters/line/readme.md | 5 +++++ adapters/mail/readme.md | 5 +++++ adapters/matrix/readme.md | 5 +++++ adapters/onebot/readme.md | 5 +++++ adapters/qqguild/readme.md | 5 +++++ adapters/slack/.npmignore | 2 ++ adapters/slack/readme.md | 5 +++++ adapters/telegram/readme.md | 5 +++++ adapters/whatsapp/.npmignore | 2 ++ adapters/whatsapp/readme.md | 5 +++++ 15 files changed, 74 insertions(+), 7 deletions(-) create mode 100644 adapters/dingtalk/readme.md create mode 100644 adapters/discord/readme.md create mode 100644 adapters/kook/readme.md create mode 100644 adapters/lark/readme.md create mode 100644 adapters/line/readme.md create mode 100644 adapters/mail/readme.md create mode 100644 adapters/matrix/readme.md create mode 100644 adapters/onebot/readme.md create mode 100644 adapters/qqguild/readme.md create mode 100644 adapters/slack/.npmignore create mode 100644 adapters/slack/readme.md create mode 100644 adapters/telegram/readme.md create mode 100644 adapters/whatsapp/.npmignore create mode 100644 adapters/whatsapp/readme.md diff --git a/README.md b/README.md index a2c0a0f6..d7de0ee3 100644 --- a/README.md +++ b/README.md @@ -12,17 +12,20 @@ - [ ] @satorijs/database - [ ] @satorijs/server - Ecosystem + - [x] DingTalk - [x] Discord - - [x] Telegram - - [x] OneBot (v11) - - [x] QQ Guild - [x] KOOK (开黑啦) - [x] Lark (飞书) - [x] Line - - [ ] Whatsapp - - [ ] Slack - - [ ] Dingding - - [ ] Wecom + - [x] Mail + - [x] Matrix + - [x] OneBot (v11,可用于 QQ) + - [x] QQ Guild + - [x] Slack + - [x] Telegram + - [x] WhatsApp + - [ ] 企业微信 + - [ ] 微信公众号 ## Examples diff --git a/adapters/dingtalk/readme.md b/adapters/dingtalk/readme.md new file mode 100644 index 00000000..3eb1b64e --- /dev/null +++ b/adapters/dingtalk/readme.md @@ -0,0 +1,5 @@ +# [@satorijs/adapter-dingtalk](https://koishi.chat/plugins/adapter/dingtalk.html) + +DingTalk (钉钉) adapter for [Satori](https://github.com/satorijs/satori). + +- [Documentation](https://koishi.chat/plugins/adapter/dingtalk.html) diff --git a/adapters/discord/readme.md b/adapters/discord/readme.md new file mode 100644 index 00000000..d6b61a92 --- /dev/null +++ b/adapters/discord/readme.md @@ -0,0 +1,5 @@ +# [@satorijs/adapter-discord](https://koishi.chat/plugins/adapter/discord.html) + +Discord adapter for [Satori](https://github.com/satorijs/satori). + +- [Documentation](https://koishi.chat/plugins/adapter/discord.html) diff --git a/adapters/kook/readme.md b/adapters/kook/readme.md new file mode 100644 index 00000000..b76bad6b --- /dev/null +++ b/adapters/kook/readme.md @@ -0,0 +1,5 @@ +# [@satorijs/adapter-kook](https://koishi.chat/plugins/adapter/kook.html) + +KOOK (开黑啦) adapter for [Satori](https://github.com/satorijs/satori). + +- [Documentation](https://koishi.chat/plugins/adapter/kook.html) diff --git a/adapters/lark/readme.md b/adapters/lark/readme.md new file mode 100644 index 00000000..5fc16006 --- /dev/null +++ b/adapters/lark/readme.md @@ -0,0 +1,5 @@ +# [@satorijs/adapter-lark](https://koishi.chat/plugins/adapter/lark.html) + +Lark (飞书) adapter for [Satori](https://github.com/satorijs/satori). + +- [Documentation](https://koishi.chat/plugins/adapter/lark.html) diff --git a/adapters/line/readme.md b/adapters/line/readme.md new file mode 100644 index 00000000..470d85dd --- /dev/null +++ b/adapters/line/readme.md @@ -0,0 +1,5 @@ +# [@satorijs/adapter-line](https://koishi.chat/plugins/adapter/line.html) + +LINE adapter for [Satori](https://github.com/satorijs/satori). + +- [Documentation](https://koishi.chat/plugins/adapter/line.html) diff --git a/adapters/mail/readme.md b/adapters/mail/readme.md new file mode 100644 index 00000000..280f481a --- /dev/null +++ b/adapters/mail/readme.md @@ -0,0 +1,5 @@ +# [@satorijs/adapter-mail](https://koishi.chat/plugins/adapter/mail.html) + +Mail adapter for [Satori](https://github.com/satorijs/satori). + +- [Documentation](https://koishi.chat/plugins/adapter/mail.html) diff --git a/adapters/matrix/readme.md b/adapters/matrix/readme.md new file mode 100644 index 00000000..4e95ebed --- /dev/null +++ b/adapters/matrix/readme.md @@ -0,0 +1,5 @@ +# [@satorijs/adapter-matrix](https://koishi.chat/plugins/adapter/matrix.html) + +Matrix adapter for [Satori](https://github.com/satorijs/satori). + +- [Documentation](https://koishi.chat/plugins/adapter/matrix.html) diff --git a/adapters/onebot/readme.md b/adapters/onebot/readme.md new file mode 100644 index 00000000..92b4f2e2 --- /dev/null +++ b/adapters/onebot/readme.md @@ -0,0 +1,5 @@ +# [@satorijs/adapter-onebot](https://koishi.chat/plugins/adapter/onebot.html) + +OneBot adapter for [Satori](https://github.com/satorijs/satori). + +- [Documentation](https://koishi.chat/plugins/adapter/onebot.html) diff --git a/adapters/qqguild/readme.md b/adapters/qqguild/readme.md new file mode 100644 index 00000000..a9068b2b --- /dev/null +++ b/adapters/qqguild/readme.md @@ -0,0 +1,5 @@ +# [@satorijs/adapter-qqguild](https://koishi.chat/plugins/adapter/qqguild.html) + +QQ Guild (QQ 频道) adapter for [Satori](https://github.com/satorijs/satori). + +- [Documentation](https://koishi.chat/plugins/adapter/qqguild.html) diff --git a/adapters/slack/.npmignore b/adapters/slack/.npmignore new file mode 100644 index 00000000..7e5fcbc1 --- /dev/null +++ b/adapters/slack/.npmignore @@ -0,0 +1,2 @@ +.DS_Store +tsconfig.tsbuildinfo diff --git a/adapters/slack/readme.md b/adapters/slack/readme.md new file mode 100644 index 00000000..0d16444d --- /dev/null +++ b/adapters/slack/readme.md @@ -0,0 +1,5 @@ +# [@satorijs/adapter-slack](https://koishi.chat/plugins/adapter/slack.html) + +Slack adapter for [Satori](https://github.com/satorijs/satori). + +- [Documentation](https://koishi.chat/plugins/adapter/slack.html) diff --git a/adapters/telegram/readme.md b/adapters/telegram/readme.md new file mode 100644 index 00000000..2e1c64ac --- /dev/null +++ b/adapters/telegram/readme.md @@ -0,0 +1,5 @@ +# [@satorijs/adapter-telegram](https://koishi.chat/plugins/adapter/telegram.html) + +Telegram adapter for [Satori](https://github.com/satorijs/satori). + +- [Documentation](https://koishi.chat/plugins/adapter/telegram.html) diff --git a/adapters/whatsapp/.npmignore b/adapters/whatsapp/.npmignore new file mode 100644 index 00000000..7e5fcbc1 --- /dev/null +++ b/adapters/whatsapp/.npmignore @@ -0,0 +1,2 @@ +.DS_Store +tsconfig.tsbuildinfo diff --git a/adapters/whatsapp/readme.md b/adapters/whatsapp/readme.md new file mode 100644 index 00000000..ede43613 --- /dev/null +++ b/adapters/whatsapp/readme.md @@ -0,0 +1,5 @@ +# [@satorijs/adapter-whatsapp](https://koishi.chat/plugins/adapter/whatsapp.html) + +WhatsApp adapter for [Satori](https://github.com/satorijs/satori). + +- [Documentation](https://koishi.chat/plugins/adapter/whatsapp.html) From f3d3bdd5083e7cf4dcbd0c16de2045526705428f Mon Sep 17 00:00:00 2001 From: Shigma Date: Sat, 12 Aug 2023 01:32:50 +0800 Subject: [PATCH 15/20] fix(core): allow schema for base Adapter --- README.md | 4 ++-- adapters/whatsapp/package.json | 2 +- adapters/whatsapp/src/adapter.ts | 6 +++--- packages/core/src/adapter.ts | 4 ++-- 4 files changed, 8 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index d7de0ee3..f1947c8a 100644 --- a/README.md +++ b/README.md @@ -24,8 +24,8 @@ - [x] Slack - [x] Telegram - [x] WhatsApp - - [ ] 企业微信 - - [ ] 微信公众号 + - [ ] WeCom (企业微信) + - [ ] Wechat Official (微信公众号) ## Examples diff --git a/adapters/whatsapp/package.json b/adapters/whatsapp/package.json index 87e183e8..d6ccfc2c 100644 --- a/adapters/whatsapp/package.json +++ b/adapters/whatsapp/package.json @@ -1,7 +1,7 @@ { "name": "@satorijs/adapter-whatsapp", "description": "WhatsApp Adapter for Satorijs", - "version": "1.0.0", + "version": "1.0.2", "main": "lib/index.js", "typings": "lib/index.d.ts", "files": [ diff --git a/adapters/whatsapp/src/adapter.ts b/adapters/whatsapp/src/adapter.ts index 6aa7278b..cda20aa8 100644 --- a/adapters/whatsapp/src/adapter.ts +++ b/adapters/whatsapp/src/adapter.ts @@ -16,11 +16,10 @@ export class WhatsAppAdapter extends Adapter { super() const http = ctx.http.extend({ - ...config, headers: { Authorization: `Bearer ${config.systemToken}`, }, - }) + }).extend(config) const internal = new Internal(http) ctx.on('ready', async () => { @@ -101,11 +100,12 @@ export namespace WhatsAppAdapter { id: string secret: string } + export const Config: Schema = Schema.intersect([ Schema.object({ secret: Schema.string().role('secret').description('App Secret').required(), systemToken: Schema.string().role('secret').description('System User Token').required(), - verifyToken: Schema.string().required(), + verifyToken: Schema.string().role('secret').description('Verify Token').required(), id: Schema.string().description('WhatsApp Business Account ID').required(), }), Quester.createConfig('https://graph.facebook.com'), diff --git a/packages/core/src/adapter.ts b/packages/core/src/adapter.ts index dae409ab..473b86ec 100644 --- a/packages/core/src/adapter.ts +++ b/packages/core/src/adapter.ts @@ -8,14 +8,13 @@ import WebSocket from 'ws' const logger = new Logger('adapter') export abstract class Adapter { - static schema = false - async start(bot: T) {} async stop(bot: T) {} } export namespace Adapter { export abstract class Client extends Adapter { + static schema = false static reusable = true constructor(protected ctx: Context, protected bot: T) { @@ -25,6 +24,7 @@ export namespace Adapter { } export abstract class Server extends Adapter { + static schema = false public bots: T[] = [] fork(ctx: Context, bot: T) { From 45b5755f526b1accba80ec83ab1ab3da5341b859 Mon Sep 17 00:00:00 2001 From: LittleC Date: Sat, 12 Aug 2023 14:50:16 +0800 Subject: [PATCH 16/20] fix(whatsapp): can not receive specific messages --- adapters/whatsapp/src/adapter.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/adapters/whatsapp/src/adapter.ts b/adapters/whatsapp/src/adapter.ts index cda20aa8..e6a0e832 100644 --- a/adapters/whatsapp/src/adapter.ts +++ b/adapters/whatsapp/src/adapter.ts @@ -40,7 +40,7 @@ export class WhatsAppAdapter extends Adapter { ctx.router.post('/whatsapp', async (ctx) => { const receivedSignature = ctx.get('X-Hub-Signature-256').split('sha256=')[1] - const payload = JSON.stringify(ctx.request.body) + const payload = ctx.request.rawBody const generatedSignature = crypto .createHmac('sha256', this.config.secret) From 28414e94ca3fbd55e346a7ccfed8c14ea0dcee6e Mon Sep 17 00:00:00 2001 From: LittleC Date: Sat, 12 Aug 2023 14:51:28 +0800 Subject: [PATCH 17/20] feat(whatsapp): location message --- adapters/whatsapp/src/types.ts | 10 +++++++++- adapters/whatsapp/src/utils.ts | 5 +++++ 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/adapters/whatsapp/src/types.ts b/adapters/whatsapp/src/types.ts index 9ea7f283..b9438724 100644 --- a/adapters/whatsapp/src/types.ts +++ b/adapters/whatsapp/src/types.ts @@ -69,7 +69,15 @@ export interface MessageBodySticker extends MessageBodyBase { sticker?: ReceivedMedia } -export type MessageBody = MessageBodyText | MessageBodyMedia | MessageBodySticker +export interface MessageBodyLocation extends MessageBodyBase { + type: 'location' + location: { + latitude: number + longitude: number + } +} + +export type MessageBody = MessageBodyText | MessageBodyMedia | MessageBodySticker | MessageBodyLocation export interface SendMessageBase { messaging_product: 'whatsapp' diff --git a/adapters/whatsapp/src/utils.ts b/adapters/whatsapp/src/utils.ts index 30101544..ce47d397 100644 --- a/adapters/whatsapp/src/utils.ts +++ b/adapters/whatsapp/src/utils.ts @@ -46,6 +46,11 @@ export async function decodeMessage(bot: WhatsAppBot, entry: Entry) { }, [ h.image(`${bot.ctx.root.config.selfUrl}/whatsapp/assets/${bot.selfId}/${message.sticker.id}`), ])] + } else if (message.type === 'location') { + session.elements = [h('whatsapp:location', { + latitude: message.location.latitude, + longitude: message.location.longitude, + })] } else { continue } From fc1cb99506aa0821171491b96fa09d5940addeba Mon Sep 17 00:00:00 2001 From: LittleC Date: Sat, 12 Aug 2023 14:51:43 +0800 Subject: [PATCH 18/20] feat(whatsapp): internal api --- adapters/whatsapp/src/adapter.ts | 2 +- adapters/whatsapp/src/internal.ts | 24 +++++++++++++++++++++++- adapters/whatsapp/src/message.ts | 13 +++---------- 3 files changed, 27 insertions(+), 12 deletions(-) diff --git a/adapters/whatsapp/src/adapter.ts b/adapters/whatsapp/src/adapter.ts index e6a0e832..881ee57b 100644 --- a/adapters/whatsapp/src/adapter.ts +++ b/adapters/whatsapp/src/adapter.ts @@ -78,7 +78,7 @@ export class WhatsAppAdapter extends Adapter { const bot = this.bots.find((bot) => bot.selfId === selfId) if (!bot) return ctx.status = 404 - const fetched = await bot.http.get<{ url: string }>('/' + mediaId) + const fetched = await bot.internal.getMedia(mediaId) this.logger.debug(fetched.url) const resp = await bot.ctx.http.axios({ url: fetched.url, diff --git a/adapters/whatsapp/src/internal.ts b/adapters/whatsapp/src/internal.ts index 7386f150..d6d5bc06 100644 --- a/adapters/whatsapp/src/internal.ts +++ b/adapters/whatsapp/src/internal.ts @@ -1,4 +1,6 @@ import { Quester } from '@satorijs/satori' +import { SendMessage } from './types' +import FormData from 'form-data' interface PhoneNumber { verified_name: string @@ -9,9 +11,10 @@ interface PhoneNumber { } export class Internal { - constructor(public http: Quester) {} + constructor(public http: Quester) { } async getPhoneNumbers(id: string) { + // https://developers.facebook.com/docs/whatsapp/business-management-api/manage-phone-numbers#all-phone-numbers const { data } = await this.http.get<{ data: PhoneNumber[] }>(`/${id}/phone_numbers`) return data } @@ -29,4 +32,23 @@ export class Internal { }, }) } + + async sendMessage(selfId: string, data: SendMessage) { + const response = await this.http.post<{ + messages: { id: string }[] + }>(`/${selfId}/messages`, data) + return response + } + + getMedia(mediaId: string) { + return this.http.get<{ url: string }>('/' + mediaId) + } + + uploadMedia(selfId: string, form: FormData) { + return this.http.post<{ + id: string + }>(`/${selfId}/media`, form, { + headers: form.getHeaders(), + }) + } } diff --git a/adapters/whatsapp/src/message.ts b/adapters/whatsapp/src/message.ts index 33950d33..43ea6113 100644 --- a/adapters/whatsapp/src/message.ts +++ b/adapters/whatsapp/src/message.ts @@ -42,9 +42,7 @@ export class WhatsAppMessageEncoder extends MessageEncoder { if (type === 'text' && !this.buffer.length) return if (type !== 'text' && this.buffer.length) await this.flushTextMessage() // https://developers.facebook.com/docs/whatsapp/api/messages/text - const { messages } = await this.bot.http.post<{ - messages: { id: string }[] - }>(`/${this.bot.selfId}/messages`, { + const { messages } = await this.bot.internal.sendMessage(this.bot.selfId, { messaging_product: 'whatsapp', to: this.channelId, recipient_type: 'individual', @@ -55,8 +53,7 @@ export class WhatsAppMessageEncoder extends MessageEncoder { message_id: this.quoteId, }, } : {}), - }) - + } as SendMessage) for (const msg of messages) { const session = this.bot.session() session.type = 'message' @@ -92,11 +89,7 @@ export class WhatsAppMessageEncoder extends MessageEncoder { form.append('type', mime) form.append('messaging_product', 'whatsapp') - const r = await this.bot.http.post<{ - id: string - }>(`/${this.bot.selfId}/media`, form, { - headers: form.getHeaders(), - }) + const r = await this.bot.internal.uploadMedia(this.bot.selfId, form) return r.id } From 1d7f04c1109570f3ed009bc20b1a2f5133fd3899 Mon Sep 17 00:00:00 2001 From: Anillc Date: Sat, 12 Aug 2023 22:21:04 +0800 Subject: [PATCH 19/20] fix(matrix): same guild and channel, fix koishijs/koishi#1158 (#147) --- adapters/matrix/src/bot.ts | 29 ++++------------------------- adapters/matrix/src/utils.ts | 3 +++ 2 files changed, 7 insertions(+), 25 deletions(-) diff --git a/adapters/matrix/src/bot.ts b/adapters/matrix/src/bot.ts index 544ba3f0..def995b7 100644 --- a/adapters/matrix/src/bot.ts +++ b/adapters/matrix/src/bot.ts @@ -89,9 +89,8 @@ export class MatrixBot extends Bot { async deleteFriend(): Promise { } async getGuild(guildId: string): Promise { - const events = await this.internal.getState(guildId) - const guildName = (events.find(event => event.type === 'm.room.name')?.content as Matrix.M_ROOM_NAME)?.name - return { guildId, guildName } + const { channelName } = await this.getChannel(guildId) + return { guildId, guildName: channelName } } async getChannel(channelId: string): Promise { @@ -101,31 +100,11 @@ export class MatrixBot extends Bot { } async getGuildList(): Promise { - const sync = await this.syncRooms() - const joined = sync?.rooms?.join - if (!joined) return [] - const result: Universal.Guild[] = [] - for (const roomId of Object.keys(joined)) { - const state = await this.internal.getState(roomId) - const create = state.find(state => state.type === 'm.room.create') - const name = state.find(state => state.type === 'm.room.name')?.content as Matrix.M_ROOM_NAME - if (!create) continue - if (create.content['type'] !== 'm.space') continue - result.push({ - guildId: roomId, - guildName: name?.name, - }) - } - return result + return await Promise.all(this.rooms.map(roomId => this.getGuild(roomId))) } async getChannelList(guildId: string): Promise { - const state = await this.internal.getState(guildId) - const children = state - .filter(event => event.type === 'm.space.child') - .map(event => event.state_key) - .filter(roomId => this.rooms.includes(roomId)) - return await Promise.all(children.map(this.getChannel.bind(this))) + return await Promise.all(this.rooms.map(roomId => this.getChannel(roomId))) } async getGuildMemberList(guildId: string): Promise { diff --git a/adapters/matrix/src/utils.ts b/adapters/matrix/src/utils.ts index 76024d0f..9302f9c6 100644 --- a/adapters/matrix/src/utils.ts +++ b/adapters/matrix/src/utils.ts @@ -15,9 +15,11 @@ export async function adaptMessage(bot: MatrixBot, event: Matrix.ClientEvent, re result.subtype = 'group' result.messageId = event.event_id result.channelId = event.room_id + result.guildId = event.room_id result.userId = event.sender result.timestamp = event.origin_server_ts result.author = adaptAuthor(bot, event) + result.isDirect = false const content = event.content as Matrix.M_ROOM_MESSAGE const reply = content['m.relates_to']?.['m.in_reply_to'] if (reply) { @@ -68,6 +70,7 @@ export async function adaptSession(bot: MatrixBot, event: Matrix.ClientEvent): P session.messageId = event.event_id session.timestamp = event.origin_server_ts session.author = adaptAuthor(bot, event) + session.isDirect = false switch (event.type) { case 'm.room.redaction': { session.type = 'message-deleted' From 76eaa4ce548fc6cb0132d1823c75718812853aeb Mon Sep 17 00:00:00 2001 From: _LittleC_ <26459759+XxLittleCxX@users.noreply.github.com> Date: Fri, 18 Aug 2023 11:40:34 +0800 Subject: [PATCH 20/20] feat(telegram): add `getUser` (#149) --- adapters/telegram/src/bot.ts | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/adapters/telegram/src/bot.ts b/adapters/telegram/src/bot.ts index b79e9a23..08a455cc 100644 --- a/adapters/telegram/src/bot.ts +++ b/adapters/telegram/src/bot.ts @@ -289,6 +289,16 @@ export class TelegramBot exte user.avatar = `${endpoint}/${file.file_path}` } } + + async getUser(userId: string, guildId?: string) { + const data = await this.internal.getChat({ chat_id: userId }) + if (!data.photo?.big_file_id && !data.photo?.small_file_id) return adaptUser(data) + const { url } = await this.$getFileFromId(data.photo?.big_file_id || data.photo?.small_file_id) + return { + ...adaptUser(data), + avatar: url, + } + } } export namespace TelegramBot {