From ee7c018327ba6033e5dc5e0d0dda11a1876ffa1d Mon Sep 17 00:00:00 2001 From: Shigma Date: Thu, 23 Jan 2025 15:19:40 +0800 Subject: [PATCH] feat(satori): sync login without user + platform --- .eslintrc.yml | 4 +++ adapters/satori/src/bot.ts | 45 +++++++++++++++++------ adapters/satori/src/ws.ts | 65 ++++++++++++++++------------------ adapters/satori/tsconfig.json | 2 ++ packages/core/src/adapter.ts | 1 + packages/core/src/bot.ts | 21 +++++------ packages/protocol/src/index.ts | 2 +- 7 files changed, 84 insertions(+), 56 deletions(-) diff --git a/.eslintrc.yml b/.eslintrc.yml index 8e6181fc..1aa1412d 100644 --- a/.eslintrc.yml +++ b/.eslintrc.yml @@ -13,3 +13,7 @@ extends: rules: '@typescript-eslint/naming-convention': off + '@typescript-eslint/no-unused-vars': + - error + - args: none + ignoreRestSiblings: true diff --git a/adapters/satori/src/bot.ts b/adapters/satori/src/bot.ts index c66ff567..db4e6e9e 100644 --- a/adapters/satori/src/bot.ts +++ b/adapters/satori/src/bot.ts @@ -1,6 +1,7 @@ -import { Bot, camelCase, Context, h, HTTP, JsonForm, snakeCase, Universal } from '@satorijs/core' +import { Bot, camelCase, Context, h, HTTP, JsonForm, omit, pick, snakeCase, Universal } from '@satorijs/core' +import { SatoriAdapter } from './ws' -function createInternal(bot: SatoriBot, prefix = '') { +function createInternal(bot: SatoriBot, prefix = '') { return new Proxy(() => {}, { apply(target, thisArg, args) { const key = prefix.slice(1) @@ -11,7 +12,7 @@ function createInternal(bot: SatoriBot, prefix = '') { if (pagination) { request.headers.set('Satori-Pagination', 'true') } - const response = await bot.http('/v1/' + bot.getInternalUrl(`/_api/${key}`, {}, true), { + const response = await bot.request('/v1/' + bot.getInternalUrl(`/_api/${key}`, {}, true), { method: 'POST', headers: Object.fromEntries(request.headers.entries()), data: request.body, @@ -28,14 +29,15 @@ function createInternal(bot: SatoriBot, prefix = '') { } } - let pagination: { data: any[]; next?: any } | undefined + type Pagination = { data: any[]; next?: any } + let pagination: Pagination | undefined result.next = async function () { - pagination ??= await impl(true) + pagination ??= await impl(true) as Pagination if (!pagination.data) throw new Error('Invalid pagination response') if (pagination.data.length) return { done: false, value: pagination.data.shift() } if (!pagination.next) return { done: true, value: undefined } args = pagination.next - pagination = await impl(true) + pagination = await impl(true) as Pagination return this.next() } result[Symbol.asyncIterator] = function () { @@ -57,15 +59,18 @@ function createInternal(bot: SatoriBot, prefix = '') { } export class SatoriBot extends Bot { - public http: HTTP + declare adapter: SatoriAdapter + public internal = createInternal(this) + public upstream: Pick constructor(ctx: C, config: Universal.Login) { super(ctx, config, 'satori') - Object.assign(this, config) + Object.assign(this, omit(config, ['sn', 'adapter'])) + this.upstream = pick(config, ['sn', 'adapter']) this.defineInternalRoute('/*path', async ({ method, params, query, headers, body }) => { - const response = await this.http(`/v1/${this.getInternalUrl('/' + params.path, query, true)}`, { + const response = await this.request(`/v1/${this.getInternalUrl('/' + params.path, query, true)}`, { method, headers, data: method === 'GET' || method === 'HEAD' ? null : body, @@ -79,6 +84,23 @@ export class SatoriBot extends Bot extends Adapter.WsClientBase> { +export class SatoriAdapter = SatoriBot> extends Adapter.WsClientBase { static schema = true as any static reusable = true static inject = ['http'] @@ -45,34 +45,30 @@ export class SatoriAdapter extends Adapter.WsClient return this.http.ws('/v1/events') } - getBot(platform: string, selfId: string, login: Universal.Login) { - // Do not dispatch event from outside adapters. - let bot = this.bots.find(bot => bot.selfId === selfId && bot.platform === platform) + getBot(login: Universal.Login, action?: 'created' | 'updated' | 'removed') { + // FIXME Do not dispatch event from outside adapters. + let bot = this.bots.find(bot => bot.upstream.sn === login.sn) if (bot) { - if (login) bot.update(login) - return this.bots.includes(bot) ? bot : undefined - } - - if (!login) { - this.logger.error('cannot find bot for', platform, selfId) + if (action === 'created') { + this.logger.warn('bot already exists when login created, sn = %s, adapter = %s', login.sn, login.adapter) + } else if (action === 'updated') { + bot.update(login) + } else if (action === 'removed') { + bot.dispose() + } + return bot + } else if (!action) { + this.logger.warn('bot not found when non-login event received, sn = %s, adapter = %s', action, login.sn, login.adapter) return } - bot = new SatoriBot(this.ctx, login) - this.bots.push(bot) + + bot = new SatoriBot(this.ctx, login) as B bot.adapter = this - bot.http = this.http.extend({ - headers: { - 'Satori-Platform': platform, - 'Satori-User-ID': selfId, - 'X-Platform': platform, - 'X-Self-ID': selfId, - }, - }) - bot.status = login.status + this.bots.push(bot) } - accept() { - this.socket.send(JSON.stringify({ + accept(socket: WebSocket) { + socket.send(JSON.stringify({ op: Universal.Opcode.IDENTIFY, body: { token: this.config.token, @@ -80,14 +76,16 @@ export class SatoriAdapter extends Adapter.WsClient }, })) + clearInterval(this.timeout) this.timeout = setInterval(() => { - this.socket.send(JSON.stringify({ + if (socket !== this.socket) return + socket.send(JSON.stringify({ op: Universal.Opcode.PING, body: {}, })) }, Time.second * 10) - this.socket.addEventListener('message', async ({ data }) => { + socket.addEventListener('message', async ({ data }) => { let parsed: Universal.ServerPayload data = data.toString() try { @@ -99,8 +97,9 @@ export class SatoriAdapter extends Adapter.WsClient if (parsed.op === Universal.Opcode.READY) { this.logger.debug('ready') for (const login of parsed.body.logins) { - this.getBot(login.platform, login.user.id, login) + this.getBot(login) } + this._metaDispose?.() this._metaDispose = this.ctx.satori.proxyUrls.add(...parsed.body.proxyUrls ?? []) } @@ -110,17 +109,13 @@ export class SatoriAdapter extends Adapter.WsClient } if (parsed.op === Universal.Opcode.EVENT) { - const { sn, type, login, selfId = login?.user.id, platform = login?.platform } = parsed.body + // Satori protocol ensures that login.user and login.platform are always present ? + const { sn, type, login } = parsed.body this.sequence = sn // `login-*` events will be dispatched by the bot, // so there is no need to create sessions manually. - const bot = this.getBot(platform, selfId, type === 'login-added' && login) + const bot = this.getBot(login, type.startsWith('login-') ? type.slice(6) as any : undefined) if (!bot) return - if (type === 'login-updated') { - return bot.update(login) - } else if (type === 'login-removed') { - return bot.dispose() - } const session = bot.session(parsed.body) if (typeof parsed.body.message?.content === 'string') { session.content = parsed.body.message.content @@ -130,13 +125,13 @@ export class SatoriAdapter extends Adapter.WsClient } bot.dispatch(session) // temporary solution for `send` event - if (type === 'message-created' && session.userId === selfId) { + if (type === 'message-created' && session.userId === login.user?.id) { session.app.emit(session, 'send', session) } } }) - this.socket.addEventListener('close', () => { + socket.addEventListener('close', () => { clearInterval(this.timeout) this._metaDispose?.() }) diff --git a/adapters/satori/tsconfig.json b/adapters/satori/tsconfig.json index 74ac2c8d..62b85635 100644 --- a/adapters/satori/tsconfig.json +++ b/adapters/satori/tsconfig.json @@ -3,6 +3,8 @@ "compilerOptions": { "outDir": "lib", "rootDir": "src", + "strict": true, + "noImplicitAny": false, }, "include": [ "src", diff --git a/packages/core/src/adapter.ts b/packages/core/src/adapter.ts index 4104daa2..923525e0 100644 --- a/packages/core/src/adapter.ts +++ b/packages/core/src/adapter.ts @@ -6,6 +6,7 @@ import { Bot } from './bot' export abstract class Adapter = Bot> { static schema = false as const + public name?: string public bots: B[] = [] constructor(protected ctx: C) {} diff --git a/packages/core/src/bot.ts b/packages/core/src/bot.ts index 1fdd469d..0e31da08 100644 --- a/packages/core/src/bot.ts +++ b/packages/core/src/bot.ts @@ -31,12 +31,11 @@ export abstract class Bot { } public sn: number - public user = {} as User - public isBot = true - public hidden = false - public platform!: string + public user?: User + public platform?: string public features: string[] - public adapter?: Adapter + public hidden = false + public adapter!: Adapter public error: any public callbacks: Dict = {} public logger!: Logger @@ -77,6 +76,10 @@ export abstract class Bot { }) } + get adapterName() { + return this.adapter.name + } + getInternalUrl(path: string, init?: ConstructorParameters[0], slash?: boolean) { let search = new URLSearchParams(init).toString() if (search) search = '?' + search @@ -90,7 +93,7 @@ export abstract class Bot { update(login: Login) { // make sure `status` is the last property to be assigned // so that `login-updated` event can be dispatched after all properties are updated - const { status, ...rest } = login + const { sn, status, ...rest } = login Object.assign(this, rest) this.status = status } @@ -246,10 +249,8 @@ export abstract class Bot { toJSON(): Login { return clone({ - ...pick(this, ['sn', 'platform', 'selfId', 'status', 'hidden', 'features']), - // make sure `user.id` is present - user: this.user.id ? this.user : undefined, - adapter: this.platform, + ...pick(this, ['sn', 'user', 'platform', 'selfId', 'status', 'hidden', 'features']), + adapter: this.adapterName, }) } diff --git a/packages/protocol/src/index.ts b/packages/protocol/src/index.ts index 33e6a227..a2d56379 100644 --- a/packages/protocol/src/index.ts +++ b/packages/protocol/src/index.ts @@ -295,7 +295,7 @@ export interface GuildMember { export interface Login { sn: number - adapter: string + adapter?: string user?: User platform?: string /** @deprecated use `login.user.id` instead */