Skip to content

Commit

Permalink
feat(satori): sync login without user + platform
Browse files Browse the repository at this point in the history
  • Loading branch information
shigma committed Jan 23, 2025
1 parent 5b21e61 commit ee7c018
Show file tree
Hide file tree
Showing 7 changed files with 84 additions and 56 deletions.
4 changes: 4 additions & 0 deletions .eslintrc.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,3 +13,7 @@ extends:

rules:
'@typescript-eslint/naming-convention': off
'@typescript-eslint/no-unused-vars':
- error
- args: none
ignoreRestSiblings: true
45 changes: 35 additions & 10 deletions adapters/satori/src/bot.ts
Original file line number Diff line number Diff line change
@@ -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<C extends Context = Context>(bot: SatoriBot<C>, prefix = '') {
return new Proxy(() => {}, {
apply(target, thisArg, args) {
const key = prefix.slice(1)
Expand All @@ -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,
Expand All @@ -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 () {
Expand All @@ -57,15 +59,18 @@ function createInternal(bot: SatoriBot, prefix = '') {
}

export class SatoriBot<C extends Context = Context> extends Bot<C, Universal.Login> {
public http: HTTP
declare adapter: SatoriAdapter<C, this>

public internal = createInternal(this)
public upstream: Pick<Universal.Login, 'sn' | 'adapter'>

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,
Expand All @@ -79,6 +84,23 @@ export class SatoriBot<C extends Context = Context> extends Bot<C, Universal.Log
}
})
}

get adapterName() {
return this.upstream.adapter
}

request(url: string, config: HTTP.RequestConfig) {
return this.adapter.http(url, {
...config,
headers: {
...config.headers,
'Satori-Platform': this.platform,
'Satori-User-ID': this.user?.id,
'X-Platform': this.platform,
'X-Self-ID': this.user?.id,
},
})
}
}

for (const [key, method] of Object.entries(Universal.Methods)) {
Expand Down Expand Up @@ -109,7 +131,10 @@ for (const [key, method] of Object.entries(Universal.Methods)) {
}
}
this.logger.debug('[request]', key, payload)
const result = await this.http.post('/v1/' + key, payload)
const result = await this.request('/v1/' + key, {
method: 'POST',
data: payload,
})
return Universal.transformKey(result, camelCase)
}
}
65 changes: 30 additions & 35 deletions adapters/satori/src/ws.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { Adapter, camelize, Context, HTTP, Logger, Schema, Time, Universal } from '@satorijs/core'
import { SatoriBot } from './bot'

export class SatoriAdapter<C extends Context = Context> extends Adapter.WsClientBase<C, SatoriBot<C>> {
export class SatoriAdapter<C extends Context = Context, B extends SatoriBot<C> = SatoriBot<C>> extends Adapter.WsClientBase<C, B> {
static schema = true as any
static reusable = true
static inject = ['http']
Expand Down Expand Up @@ -45,49 +45,47 @@ export class SatoriAdapter<C extends Context = Context> 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,
sn: this.sequence,
},
}))

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 {
Expand All @@ -99,8 +97,9 @@ export class SatoriAdapter<C extends Context = Context> 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 ?? [])
}

Expand All @@ -110,17 +109,13 @@ export class SatoriAdapter<C extends Context = Context> 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
Expand All @@ -130,13 +125,13 @@ export class SatoriAdapter<C extends Context = Context> 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?.()
})
Expand Down
2 changes: 2 additions & 0 deletions adapters/satori/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
"compilerOptions": {
"outDir": "lib",
"rootDir": "src",
"strict": true,
"noImplicitAny": false,
},
"include": [
"src",
Expand Down
1 change: 1 addition & 0 deletions packages/core/src/adapter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { Bot } from './bot'
export abstract class Adapter<C extends Context = Context, B extends Bot<C> = Bot<C>> {
static schema = false as const

public name?: string
public bots: B[] = []

constructor(protected ctx: C) {}
Expand Down
21 changes: 11 additions & 10 deletions packages/core/src/bot.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,12 +31,11 @@ export abstract class Bot<C extends Context = Context, T = any> {
}

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<C, this>
public hidden = false
public adapter!: Adapter<C, this>
public error: any
public callbacks: Dict<Function> = {}
public logger!: Logger
Expand Down Expand Up @@ -77,6 +76,10 @@ export abstract class Bot<C extends Context = Context, T = any> {
})
}

get adapterName() {
return this.adapter.name
}

getInternalUrl(path: string, init?: ConstructorParameters<typeof URLSearchParams>[0], slash?: boolean) {
let search = new URLSearchParams(init).toString()
if (search) search = '?' + search
Expand All @@ -90,7 +93,7 @@ export abstract class Bot<C extends Context = Context, T = any> {
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
}
Expand Down Expand Up @@ -246,10 +249,8 @@ export abstract class Bot<C extends Context = Context, T = any> {

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,
})
}

Expand Down
2 changes: 1 addition & 1 deletion packages/protocol/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 */
Expand Down

0 comments on commit ee7c018

Please sign in to comment.