-
Notifications
You must be signed in to change notification settings - Fork 46
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
3d6c6b4
commit 1dfde7d
Showing
12 changed files
with
1,282 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 <[email protected]>", | ||
"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" | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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<T extends SlackBot.Config = SlackBot.Config> extends Bot<T> { | ||
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<T = any>(method: Quester.Method, path: string, data = {}, headers: any = {}, zap: boolean = false): Promise<T> { | ||
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<AuthTestResponse>('POST', '/auth.test') | ||
return { | ||
userId: data.user_id, | ||
avatar: null, | ||
username: data.user, | ||
isBot: !!data.bot_id, | ||
} | ||
} | ||
|
||
async deleteMessage(channelId: string, messageId: string): Promise<void> { | ||
return this.request('POST', '/chat.delete', { channel: channelId, ts: messageId }) | ||
} | ||
|
||
async getMessage(channelId: string, messageId: string): Promise<Universal.Message> { | ||
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<Universal.Message[]> { | ||
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<Universal.User> { | ||
// users:read | ||
// @TODO guildId | ||
const { user } = await this.request<{ user: SlackUser }>('POST', '/users.info', { | ||
user: userId, | ||
}) | ||
return adaptUser(user) | ||
} | ||
|
||
async getGuildMemberList(guildId: string): Promise<Universal.GuildMember[]> { | ||
// users:read | ||
const { members } = await this.request<{ members: SlackUser[] }>('POST', '/users.list') | ||
return members.map(adaptUser) | ||
} | ||
|
||
async getChannel(channelId: string, guildId?: string): Promise<Universal.Channel> { | ||
const { channel } = await this.request<{ | ||
channel: SlackChannel | ||
}>('POST', '/conversations.info', { | ||
channel: channelId, | ||
}) | ||
return adaptChannel(channel) | ||
} | ||
|
||
async getChannelList(guildId: string): Promise<Universal.Channel[]> { | ||
const { channels } = await this.request<{ | ||
channels: SlackChannel[] | ||
}>('POST', '/conversations.list', { | ||
team_id: guildId, | ||
}) | ||
return channels.map(adaptChannel) | ||
} | ||
|
||
async getGuild(guildId: string): Promise<Universal.Guild> { | ||
const { team } = await this.request<{ team: SlackTeam }>('POST', '/team.info', { | ||
team_id: guildId, | ||
}) | ||
return adaptGuild(team) | ||
} | ||
|
||
async getGuildMember(guildId: string, userId: string): Promise<Universal.GuildMember> { | ||
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<string[]> { | ||
// "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<Config> = 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' |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,16 @@ | ||
import { Adapter, Schema } from '@satorijs/satori' | ||
import { SlackBot } from './bot' | ||
|
||
export class HttpServer extends Adapter.Server<SlackBot<SlackBot.BaseConfig & HttpServer.Config>> { | ||
|
||
} | ||
|
||
export namespace HttpServer { | ||
export interface Config { | ||
protocol: 'http' | ||
} | ||
|
||
export const Config: Schema<Config> = Schema.object({ | ||
protocol: Schema.const('http').required(), | ||
}) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 } |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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(/(?<!\u200b)[\*_~`]/g, '\u200B$&') | ||
.replace(/@everyone/g, () => '@\u200Beveryone') | ||
.replace(/@here/g, () => '@\u200Bhere') | ||
.replace(/(?<!\u200b)^>/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<SlackBot> { | ||
buffer = '' | ||
thread_ts = null | ||
elements: any[] = [] | ||
addition: Record<string, any> = {} | ||
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) | ||
} | ||
} | ||
} |
Oops, something went wrong.