Skip to content

Commit

Permalink
feat(qq): basic support for qq
Browse files Browse the repository at this point in the history
  • Loading branch information
XxLittleCxX committed Oct 1, 2023
1 parent 4c746a5 commit 0d68815
Show file tree
Hide file tree
Showing 9 changed files with 533 additions and 193 deletions.
177 changes: 0 additions & 177 deletions adapters/qq/src/bot.ts

This file was deleted.

112 changes: 112 additions & 0 deletions adapters/qq/src/bot/guild.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
import { Bot, Context, Quester, Universal } from '@satorijs/satori'
import { QQBot } from '.'
import { decodeChannel, decodeGuild, decodeGuildMember, decodeMessage, decodeUser } from '../utils'
import { GuildInternal } from '../internal/guild'
import { QQGuildMessageEncoder } from '../message'

export namespace QQGuildBot {
export interface Config {
parent: QQBot
}
}

export class QQGuildBot extends Bot {
declare parent: QQBot
hidden = true
public internal: GuildInternal
public http: Quester
static MessageEncoder = QQGuildMessageEncoder

constructor(ctx: Context, config: QQGuildBot.Config) {
super(ctx, config)
this.parent = config.parent
this.parent.guildBot = this
this.platform = 'qq'
this.internal = new GuildInternal(() => config.parent.guildHttp)
this.http = config.parent.guildHttp
}

async getUser(userId: string, guildId?: string): Promise<Universal.User> {
const { user } = await this.getGuildMember(guildId, userId)
return user
}

async getGuildList(next?: string) {
const guilds = await this.internal.getGuilds()
return { data: guilds.map(decodeGuild) }
}

async getGuild(guildId: string) {
const guild = await this.internal.getGuild(guildId)
return decodeGuild(guild)
}

async getChannelList(guildId: string, next?: string): Promise<Universal.List<Universal.Channel>> {
const channels = await this.internal.getChannels(guildId)
return { data: channels.map(decodeChannel) }
}

async getChannel(channelId: string): Promise<Universal.Channel> {
const channel = await this.internal.getChannel(channelId)
return decodeChannel(channel)
}

async getGuildMemberList(guildId: string, next?: string): Promise<Universal.List<Universal.GuildMember>> {
const members = await this.internal.getGuildMembers(guildId, {
limit: 400,
after: next,
})
return { data: members.map(decodeGuildMember), next: members[members.length - 1].user.id }
}

async getGuildMember(guildId: string, userId: string): Promise<Universal.GuildMember> {
const member = await this.internal.getGuildMember(guildId, userId)
return decodeGuildMember(member)
}

async kickGuildMember(guildId: string, userId: string) {
await this.internal.removeGuildMember(guildId, userId)
}

async muteGuildMember(guildId: string, userId: string, duration: number) {
await this.internal.muteGuildMember(guildId, userId, duration)
}

async getReactionList(channelId: string, messageId: string, emoji: string, next?: string): Promise<Universal.List<Universal.User>> {
const [type, id] = emoji.split(':')
const { users, cookie } = await this.internal.getReactions(channelId, messageId, type, id, {
limit: 50,
cookie: next,
})
return { next: cookie, data: users.map(decodeUser) }
}

async createReaction(channelId: string, messageId: string, emoji: string) {
const [type, id] = emoji.split(':')
await this.internal.createReaction(channelId, messageId, type, id)
}

async deleteReaction(channelId: string, messageId: string, emoji: string) {
const [type, id] = emoji.split(':')
await this.internal.deleteReaction(channelId, messageId, type, id)
}

async getMessage(channelId: string, messageId: string): Promise<Universal.Message> {
const r = await this.internal.getMessage(channelId, messageId)
return decodeMessage(this, r)
}

async deleteMessage(channelId: string, messageId: string) {
if (channelId.includes('_')) {
// direct message
const [guildId] = channelId.split('_')
await this.internal.deleteDM(guildId, messageId)
} else {
await this.internal.deleteMessage(channelId, messageId)
}
}

async getLogin(): Promise<Universal.Login> {
return this.parent.getLogin()
}
}
107 changes: 107 additions & 0 deletions adapters/qq/src/bot/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
import { Bot, Context, Quester, Schema } from '@satorijs/satori'
import { WsClient } from '../ws'
import * as QQ from '../types'
import { QQGuildBot } from './guild'
import { GroupInternal } from '../internal/group'
import { QQMessageEncoder } from '../message'

interface GetAppAccessTokenResult {
access_token: string
expires_in: number
}

export class QQBot extends Bot<QQBot.Config> {
static MessageEncoder = QQMessageEncoder
public guildBot: QQGuildBot

internal: GroupInternal
groupHttp: Quester
guildHttp: Quester

private _token: string
private _timer: NodeJS.Timeout

constructor(ctx: Context, config: QQBot.Config) {
super(ctx, config)
this.platform = 'qq'
let endpoint = config.endpoint
if (config.sandbox) {
endpoint = endpoint.replace(/^(https?:\/\/)/, '$1sandbox.')
}
this.guildHttp = ctx.http.extend({
endpoint,
headers: {
'Authorization': `Bot ${this.config.id}.${this.config.token}`,
},
})
this.getAccessToken()
this.initialize()
this.internal = new GroupInternal(() => this.groupHttp)
ctx.plugin(WsClient, this)
}

initialize() {
this.ctx.plugin(QQGuildBot, {
parent: this,
})
}

async stop() {
clearTimeout(this._timer)
if (this.guildBot) {
delete this.ctx.bots[this.guildBot.sid]
}
await super.stop()
}

async _ensureAccessToken() {
const result = await this.ctx.http.post<GetAppAccessTokenResult>('https://bots.qq.com/app/getAppAccessToken', {
appId: this.config.id,
clientSecret: this.config.secret,
})
this._token = result.access_token
this.groupHttp = this.ctx.http.extend({
endpoint: this.config.endpoint,
headers: {
'Authorization': `QQBot ${this._token}`,
'X-Union-Appid': this.config.id,
},
})
// 在上一个 access_token 接近过期的 60 秒内
// 重新请求可以获取到一个新的 access_token
this._timer = setTimeout(() => {
this._ensureAccessToken()
}, (result.expires_in - 40) * 1000)
}

async getAccessToken() {
if (!this._token) {
await this._ensureAccessToken()
}
return this._token
}

async getLogin() {
return this.toJSON()
}
}

export namespace QQBot {
export interface Config extends QQ.Options, WsClient.Config {
intents?: number
}

export const Config: Schema<Config> = Schema.intersect([
Schema.object({
id: Schema.string().description('机器人 id。').required(),
secret: Schema.string().description('机器人密钥。').role('secret'),
token: Schema.string().description('机器人令牌。').role('secret'),
type: Schema.union(['public', 'private'] as const).description('机器人类型。').required(),
sandbox: Schema.boolean().description('是否开启沙箱模式。').default(false),
endpoint: Schema.string().role('link').description('要连接的服务器地址。').default('https://api.sgroup.qq.com/'),
authType: Schema.union(['bot', 'bearer'] as const).description('采用的验证方式。').default('bot'),
intents: Schema.bitset(QQ.Intents).description('需要订阅的机器人事件。'),
}),
WsClient.Config,
] as const)
}
Loading

0 comments on commit 0d68815

Please sign in to comment.