diff --git a/adapters/satori/.npmignore b/adapters/satori/.npmignore new file mode 100644 index 00000000..7e5fcbc1 --- /dev/null +++ b/adapters/satori/.npmignore @@ -0,0 +1,2 @@ +.DS_Store +tsconfig.tsbuildinfo diff --git a/adapters/satori/package.json b/adapters/satori/package.json new file mode 100644 index 00000000..ce33f3cf --- /dev/null +++ b/adapters/satori/package.json @@ -0,0 +1,31 @@ +{ + "name": "@satorijs/adapter-satori", + "description": "Satori Adapter for Satorijs", + "version": "0.1.0", + "main": "lib/index.js", + "typings": "lib/index.d.ts", + "files": [ + "lib" + ], + "author": "Shigma ", + "license": "MIT", + "repository": { + "type": "git", + "url": "git+https://github.com/satorijs/satori.git", + "directory": "adapters/satori" + }, + "bugs": { + "url": "https://github.com/satorijs/satori/issues" + }, + "homepage": "https://koishi.chat/plugins/adapter/satori.html", + "keywords": [ + "bot", + "protocol", + "client", + "chatbot", + "satori" + ], + "peerDependencies": { + "@satorijs/satori": "^3.0.0-alpha.1" + } +} diff --git a/adapters/satori/readme.md b/adapters/satori/readme.md new file mode 100644 index 00000000..51d23e2e --- /dev/null +++ b/adapters/satori/readme.md @@ -0,0 +1,5 @@ +# [@satorijs/adapter-satori](https://koishi.chat/plugins/adapter/satori.html) + +Satori adapter for [Satori](https://github.com/satorijs/satori). + +- [Documentation](https://koishi.chat/plugins/adapter/satori.html) diff --git a/adapters/satori/src/bot.ts b/adapters/satori/src/bot.ts new file mode 100644 index 00000000..a314b60c --- /dev/null +++ b/adapters/satori/src/bot.ts @@ -0,0 +1,48 @@ +import { Bot, camelize, Context, Quester, Schema, Universal } from '@satorijs/satori' +import { WsClient } from './ws' + +export function camelizeKeys(source: T): T { + if (!source || typeof source !== 'object') return source + if (Array.isArray(source)) return source.map(camelizeKeys) as any + return Object.fromEntries(Object.entries(source).map(([k, v]) => [camelize(k), camelizeKeys(v)])) as any +} + +export class SatoriBot extends Bot { + public http: Quester + + constructor(ctx: Context, config: SatoriBot.Config) { + super(ctx, config) + this.platform = 'discord' + this.http = ctx.http.extend(config) + // TODO: Internal + // this.internal = new Internal(this.http) + ctx.plugin(WsClient, this) + } +} + +for (const [key, method] of Object.entries(Universal.Methods)) { + SatoriBot.prototype[key] = function (this: SatoriBot, ...args: any[]) { + const payload = {} + for (const key of method.fields) { + payload[key] = args.shift() + } + this.http.post('/' + key, payload) + } +} + +export namespace SatoriBot { + export interface Config extends Bot.Config, WsClient.Config { + slash?: boolean + endpoint: string + } + + export const Config: Schema = Schema.intersect([ + Schema.object({ + endpoint: Schema.string().description('API endpoint.').required(), + }), + Schema.object({ + slash: Schema.boolean().description('是否启用斜线指令。').default(true), + }).description('功能设置'), + WsClient.Config, + ]) +} diff --git a/adapters/satori/src/index.ts b/adapters/satori/src/index.ts new file mode 100644 index 00000000..dbbc8dfb --- /dev/null +++ b/adapters/satori/src/index.ts @@ -0,0 +1,6 @@ +import { SatoriBot } from './bot' + +export * from './bot' +export * from './ws' + +export default SatoriBot diff --git a/adapters/satori/src/ws.ts b/adapters/satori/src/ws.ts new file mode 100644 index 00000000..45577d44 --- /dev/null +++ b/adapters/satori/src/ws.ts @@ -0,0 +1,90 @@ +import { Adapter, Logger, Schema, Time, Universal } from '@satorijs/satori' +import { camelizeKeys, SatoriBot } from './bot' + +const logger = new Logger('discord') + +type Message = Message.Heartbeat | Message.Dispatch | Message.Ready + +namespace Message { + export interface Base { + op: string + seq?: number + } + + export interface Heartbeat extends Base { + op: 'heartbeat' + } + + export interface Ready extends Base { + op: 'ready' + data: { + user: Universal.User + } + } + + export interface Dispatch extends Base { + op: 'dispatch' + data: any + } +} + +export class WsClient extends Adapter.WsClient { + _seq = 0 + _ses?: string + _ping: NodeJS.Timeout + + async prepare() { + const { url } = await this.bot.internal.getGatewayBot() + return this.bot.http.ws(url + '/?v=10&encoding=json') + } + + accept() { + this.bot.socket.send(JSON.stringify({ + op: 'identify', + d: { + seq: this._ses, + }, + })) + + this._ping = setInterval(() => { + this.bot.socket.send(JSON.stringify({ + op: 'heartbeat', + })) + }, Time.second * 10) + + this.bot.socket.addEventListener('message', async ({ data }) => { + let parsed: Message + try { + parsed = JSON.parse(data.toString()) + } catch (error) { + return logger.warn('cannot parse message', data) + } + if (parsed.seq) { + this._seq = parsed.seq + } + + if (parsed.op === 'ready') { + logger.debug('ready') + Object.assign(this.bot, camelizeKeys(parsed.data.user)) + return this.bot.online() + } + + if (parsed.op === 'dispatch') { + const session = this.bot.session(camelizeKeys(parsed.data)) + this.bot.dispatch(session) + } + }) + + this.bot.socket.addEventListener('close', () => { + clearInterval(this._ping) + }) + } +} + +export namespace WsClient { + export interface Config extends Adapter.WsClient.Config {} + + export const Config: Schema = Schema.intersect([ + Adapter.WsClient.Config, + ] as const) +} diff --git a/adapters/satori/tsconfig.json b/adapters/satori/tsconfig.json new file mode 100644 index 00000000..74ac2c8d --- /dev/null +++ b/adapters/satori/tsconfig.json @@ -0,0 +1,10 @@ +{ + "extends": "../../tsconfig.base", + "compilerOptions": { + "outDir": "lib", + "rootDir": "src", + }, + "include": [ + "src", + ], +} \ No newline at end of file