@koishijs/core
Version:
Core Features for Koishi
252 lines (220 loc) • 8.59 kB
text/typescript
import * as utils from '@koishijs/utils'
import { Dict, MaybeArray } from 'cosmokit'
import { Driver, FlatKeys, FlatPick, Update } from 'minato'
import * as minato from 'minato'
import { Fragment, Universal } from '@satorijs/core'
import { Context } from './context'
declare module './context' {
interface Context {
[minato.Types]: Types
[minato.Tables]: Tables
[Context.Database]: Context.Database<this>
broadcast(content: Fragment, forced?: boolean): Promise<string[]>
broadcast(channels: readonly string[], content: Fragment, forced?: boolean): Promise<string[]>
}
namespace Context {
// https://github.com/typescript-eslint/typescript-eslint/issues/6720
// eslint-disable-next-line @typescript-eslint/no-unused-vars
interface Database<C extends Context = Context> {
getUser<K extends FlatKeys<User>>(platform: string, pid: string, modifier?: Driver.Cursor<K>): Promise<FlatPick<User, K>>
setUser(platform: string, pid: string, data: Update<User>): Promise<void>
createUser(platform: string, pid: string, data: Partial<User>): Promise<User>
getChannel<K extends FlatKeys<Channel>>(platform: string, id: string, modifier?: Driver.Cursor<K>): Promise<FlatPick<Channel, K | 'id' | 'platform'>>
getChannel<K extends FlatKeys<Channel>>(platform: string, ids: string[], modifier?: Driver.Cursor<K>): Promise<FlatPick<Channel, K>[]>
getAssignedChannels<K extends Channel.Field>(fields?: K[], selfIdMap?: Dict<string[]>): Promise<Pick<Channel, K>[]>
setChannel(platform: string, id: string, data: Update<Channel>): Promise<void>
createChannel(platform: string, id: string, data: Partial<Channel>): Promise<Channel>
}
}
}
export interface Types extends minato.Types {}
export interface Tables extends minato.Tables {
user: User
binding: Binding
channel: Channel
}
export interface User {
id: number
name: string
/** @deprecated */
flag: number
authority: number
locales: string[]
permissions: string[]
createdAt: Date
}
export namespace User {
export enum Flag {
ignore = 1,
}
export type Field = keyof User
export type Observed<K extends Field = Field> = utils.Observed<Pick<User, K>, Promise<void>>
}
export interface Binding {
aid: number
bid: number
pid: string
platform: string
}
export interface Channel {
id: string
platform: string
/** @deprecated */
flag: number
assignee: string
guildId: string
locales: string[]
permissions: string[]
createdAt: Date
}
export namespace Channel {
export enum Flag {
ignore = 1,
silent = 4,
}
export type Field = keyof Channel
export type Observed<K extends Field = Field> = utils.Observed<Pick<Channel, K>, Promise<void>>
}
interface KoishiDatabase extends minato.Database<Tables, Types, Context> {}
class KoishiDatabase {
constructor(public ctx: Context) {
ctx.mixin(this, {
getUser: 'database.getUser',
setUser: 'database.setUser',
createUser: 'database.createUser',
getChannel: 'database.getChannel',
getAssignedChannels: 'database.getAssignedChannels',
setChannel: 'database.setChannel',
createChannel: 'database.createChannel',
broadcast: 'database.broadcast',
})
ctx.mixin('database', ['broadcast'] as never[])
ctx.model.extend('user', {
id: 'unsigned(8)',
name: { type: 'string', length: 255 },
flag: 'unsigned(8)',
authority: 'unsigned(4)',
locales: 'list(255)',
permissions: 'list',
createdAt: 'timestamp',
}, {
autoInc: true,
})
ctx.model.extend('binding', {
aid: 'unsigned(8)',
bid: 'unsigned(8)',
pid: 'string(255)',
platform: 'string(255)',
}, {
primary: ['pid', 'platform'],
})
ctx.model.extend('channel', {
id: 'string(255)',
platform: 'string(255)',
flag: 'unsigned(8)',
assignee: 'string(255)',
guildId: 'string(255)',
locales: 'list(255)',
permissions: 'list',
createdAt: 'timestamp',
}, {
primary: ['id', 'platform'],
})
ctx.on('login-added', ({ platform }) => {
if (platform in ctx.model.tables.user.fields) return
ctx.model.migrate('user', { [platform]: 'string(255)' }, async (db) => {
const users = await db.get('user', { [platform]: { $exists: true } }, ['id', platform as never])
await db.upsert('binding', users.filter(u => u[platform]).map((user) => ({
aid: user.id,
bid: user.id,
pid: user[platform],
platform,
})))
})
})
}
async getUser<K extends FlatKeys<User>>(platform: string, pid: string, modifier?: Driver.Cursor<K>): Promise<FlatPick<User, K>> {
const [binding] = await this.get('binding', { platform, pid }, ['aid'])
if (!binding) return
const [user] = await this.get('user', { id: binding.aid }, modifier)
return user
}
async setUser(platform: string, pid: string, data: Update<User>) {
const [binding] = await this.get('binding', { platform, pid }, ['aid'])
if (!binding) throw new Error('user not found')
return this.set('user', binding.aid, data)
}
async createUser(platform: string, pid: string, data: Partial<User>) {
const user = await this.create('user', data)
await this.create('binding', { aid: user.id, bid: user.id, pid, platform })
return user
}
getChannel<K extends FlatKeys<Channel>>(platform: string, id: string, modifier?: Driver.Cursor<K>): Promise<FlatPick<Channel, K | 'id' | 'platform'>>
getChannel<K extends FlatKeys<Channel>>(platform: string, ids: string[], modifier?: Driver.Cursor<K>): Promise<FlatPick<Channel, K>[]>
async getChannel(platform: string, id: MaybeArray<string>, modifier?: any) {
const data = await this.get('channel', { platform, id }, modifier)
if (Array.isArray(id)) return data
if (data[0]) Object.assign(data[0], { platform, id })
return data[0]
}
getSelfIds(platforms?: string[]): Dict<string[]> {
const selfIdMap: Dict<string[]> = Object.create(null)
for (const bot of this.ctx.bots) {
if (platforms && !platforms.includes(bot.platform)) continue
(selfIdMap[bot.platform] ||= []).push(bot.selfId)
}
return selfIdMap
}
getAssignedChannels<K extends Channel.Field>(fields?: K[], selfIdMap?: Dict<string[]>): Promise<Pick<Channel, K>[]>
async getAssignedChannels(fields?: Channel.Field[], selfIdMap: Dict<string[]> = this.getSelfIds()) {
return this.get('channel', {
$or: Object.entries(selfIdMap).map(([platform, assignee]) => ({ platform, assignee })),
}, fields)
}
setChannel(platform: string, id: string, data: Update<Channel>) {
return this.set('channel', { platform, id }, data)
}
createChannel(platform: string, id: string, data: Partial<Channel>) {
return this.create('channel', { platform, id, ...data })
}
async broadcast(...args: [Fragment, boolean?] | [readonly string[], Fragment, boolean?]) {
let channels: string[], platforms: string[]
if (Array.isArray(args[0])) {
channels = args.shift() as any
platforms = channels.map(c => c.split(':')[0])
}
const [content, forced] = args as [Fragment, boolean]
if (!content) return []
const selfIdMap = this.getSelfIds(platforms)
const data = await this.getAssignedChannels(['id', 'assignee', 'flag', 'platform', 'guildId', 'locales'], selfIdMap)
const assignMap: Dict<Dict<Pick<Channel, 'id' | 'guildId' | 'locales'>[]>> = {}
for (const channel of data) {
const { platform, id, assignee, flag } = channel
if (channels) {
const index = channels?.indexOf(`${platform}:${id}`)
if (index < 0) continue
channels.splice(index, 1)
}
if (!forced && (flag & Channel.Flag.silent)) continue
((assignMap[platform] ||= {})[assignee] ||= []).push(channel)
}
if (channels?.length) {
this.ctx.logger('app').warn('broadcast', 'channel not found: ', channels.join(', '))
}
return (await Promise.all(this.ctx.bots.map((bot) => {
const targets = assignMap[bot.platform]?.[bot.selfId]
if (!targets) return Promise.resolve([])
const sessions = targets.map(({ id, guildId, locales }) => {
const session = bot.session({
type: 'message',
channel: { id, type: Universal.Channel.Type.TEXT },
guild: { id: guildId },
})
session.locales = locales
return session
})
return bot.broadcast(sessions, content)
}))).flat(1)
}
}
export default KoishiDatabase