Skip to content

Commit

Permalink
Merge remote-tracking branch 'origin/qqguild'
Browse files Browse the repository at this point in the history
  • Loading branch information
shigma committed Sep 26, 2023
2 parents 36cb0e2 + 3e6692c commit f8f3a19
Show file tree
Hide file tree
Showing 8 changed files with 1,189 additions and 197 deletions.
1 change: 0 additions & 1 deletion adapters/qqguild/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,6 @@
"@satorijs/satori": "^3.0.0-alpha.1"
},
"dependencies": {
"@qq-guild-sdk/core": "^2.2.2",
"qface": "^1.4.1"
}
}
139 changes: 99 additions & 40 deletions adapters/qqguild/src/bot.ts
Original file line number Diff line number Diff line change
@@ -1,67 +1,125 @@
import * as QQGuild from '@qq-guild-sdk/core'
import { Bot, Context, h, Schema } from '@satorijs/satori'
import { adaptGuild, adaptUser } from './utils'
import { Bot, Context, defineProperty, Quester, Schema, Universal } from '@satorijs/satori'
import { adaptChannel, adaptGuild, adaptUser, decodeGuildMember, decodeMessage } from './utils'
import { QQGuildMessageEncoder } from './message'
import { WsClient } from './ws'
import { Internal } from './internal'
import * as QQGuild from './types'

export class QQGuildBot extends Bot<QQGuildBot.Config> {
static MessageEncoder = QQGuildMessageEncoder

internal: QQGuild.Bot
internal: Internal
http: Quester

constructor(ctx: Context, config: QQGuildBot.Config) {
super(ctx, config)
this.platform = 'qqguild'
this.internal = new QQGuild.Bot(config as QQGuild.Bot.Options)
let endpoint = config.endpoint
if (config.sandbox) {
endpoint = endpoint.replace(/^(https?:\/\/)/, '$1sandbox.')
}
this.http = ctx.http.extend({
endpoint,
headers: {
Authorization: `Bot ${this.config.app.id}.${this.config.app.token}`,
},
})
this.internal = new Internal(this.http)
ctx.plugin(WsClient, this)
}

session(payload?: any, input?: any) {
return defineProperty(super.session(payload), 'qqguild', Object.assign(Object.create(this.internal), input))
}

async getLogin() {
this.user = adaptUser(await this.internal.me)
return { status: this.status, user: this.user }
this.user = adaptUser(await this.internal.getMe())
return this.toJSON()
}

async getGuildList() {
const guilds = await this.internal.guilds
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(adaptGuild) }
}

adaptMessage(msg: QQGuild.Message) {
const { id: messageId, author, guildId, channelId, timestamp } = msg
const session = this.session()
session.type = 'message'
session.guildId = guildId
session.messageId = messageId
session.channelId = channelId
session.timestamp = +timestamp
session.data.user = adaptUser(author)
// TODO https://github.com/satorijs/satori/blob/fbcf4665c77381ff80c8718106d2282a931d5736/packages/core/src/message.ts#L23
// satori core need set guildId is undefined when isPrivate
// this is a temporary solution
if (msg.isPrivate) {
session.guildId = undefined
session.channelId = msg.guildId
async getGuild(guildId: string) {
const guild = await this.internal.getGuild(guildId)
return adaptGuild(guild)
}

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

async getChannel(channelId: string): Promise<Universal.Channel> {
const channel = await this.internal.getChannel(channelId)
return adaptChannel(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.deleteGuildMember(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(adaptUser) }
}

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 {
session.guildId = msg.guildId
session.channelId = msg.channelId
await this.internal.deleteMessage(channelId, messageId)
}
// it's useless, but I need it
session.subtype = msg.isPrivate ? 'private' : 'group'
session.isDirect = msg.isPrivate
session.content = (msg.content ?? '')
.replace(/<@!(.+)>/, (_, $1) => h.at($1).toString())
.replace(/<#(.+)>/, (_, $1) => h.sharp($1).toString())
const { attachments = [] } = msg as { attachments?: any[] }
session.content = attachments
.filter(({ contentType }) => contentType.startsWith('image'))
.reduce((content, attachment) => content + h.image(attachment.url), session.content)
session.elements = h.parse(session.content)
return session
}
}

export namespace QQGuildBot {
type BotOptions = QQGuild.Bot.Options
type BotOptions = QQGuild.Options
type CustomBotOptions = Omit<BotOptions, 'sandbox'> & Partial<Pick<BotOptions, 'sandbox'>>
export interface Config extends Bot.Config, CustomBotOptions, WsClient.Config {
intents?: number
Expand All @@ -73,11 +131,12 @@ export namespace QQGuildBot {
id: Schema.string().description('机器人 id。').required(),
key: Schema.string().description('机器人 key。').role('secret').required(),
token: Schema.string().description('机器人令牌。').role('secret').required(),
type: Schema.union(['public', 'private'] as const).description('机器人类型。').required(),
}) as any,
sandbox: Schema.boolean().description('是否开启沙箱模式。').default(true),
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(QQGuild.Bot.Intents).description('需要订阅的机器人事件。').default(QQGuild.Bot.Intents.PUBLIC_GUILD_MESSAGES),
intents: Schema.bitset(QQGuild.Intents).description('需要订阅的机器人事件。').default(QQGuild.Intents.PUBLIC_GUILD_MESSAGES),
}),
WsClient.Config,
] as const)
Expand Down
4 changes: 2 additions & 2 deletions adapters/qqguild/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import * as QQGuild from '@qq-guild-sdk/core'
import * as QQGuild from './types'
import { QQGuildBot } from './bot'

export { QQGuild }
Expand All @@ -12,6 +12,6 @@ export default QQGuildBot

declare module '@satorijs/core' {
interface Session {
qqguild?: QQGuild.Bot
qqguild?: QQGuild.Payload
}
}
101 changes: 101 additions & 0 deletions adapters/qqguild/src/internal.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
import { Quester } from '@satorijs/satori'
import * as QQGuild from './types'

export class Internal {
constructor(private http: Quester) { }

async getMe() {
return this.http.get<QQGuild.User>('/users/@me')
}

/** https://bot.q.qq.com/wiki/develop/api/openapi/dms/post_dms.html */
async createDMS(recipient_id: string, source_guild_id: string) {
return this.http.post<QQGuild.DMS>('/users/@me/dms', {
recipient_id, source_guild_id,
})
}

async getMessage(channelId: string, messageId: string) {
const { message } = await this.http.get<{
message: QQGuild.Message
}>(`/channels/${channelId}/messages/${messageId}`)
return message
}

async getGuilds(params?: Partial<{
before: string
after: string
limit: number
}>) {
return this.http.get<QQGuild.Guild[]>('/users/@me/guilds', {
params,
})
}

async getGuild(guild_id: string) {
return this.http.get<QQGuild.Guild>(`/guilds/${guild_id}`)
}

async getChannels(guild_id: string) {
return this.http.get<QQGuild.Channel[]>(`/guilds/${guild_id}/channels`)
}

async getChannel(channel_id: string) {
return this.http.get<QQGuild.Channel>(`/channels/${channel_id}`)
}

async getGuildMembers(guild_id: string, params?: Partial<{
after: string
limit: number
}>) {
return this.http.get<QQGuild.Member[]>(`/guilds/${guild_id}/members`, { params })
}

async getGuildMember(guild_id: string, user_id: string) {
return this.http.get<QQGuild.Member>(`/guilds/${guild_id}/members/${user_id}`)
}

async deleteGuildMember(guild_id: string, user_id: string) {
return this.http.delete(`/guilds/${guild_id}/members/${user_id}`)
}

async getGuildRoles(guild_id: string) {
return this.http.get<QQGuild.Role[]>(`/guilds/${guild_id}/roles`)
}

async muteGuildMember(guild_id: string, user_id: string, duration: number) {
return this.http.patch(`/guilds/${guild_id}/members/${user_id}/mute`, {
mute_seconds: duration / 1000,
})
}

async getReactions(channel_id: string, message_id: string, type: string, id: string, params?: Partial<{
cookie: string
limit: number
}>) {
return this.http.get<{
cookie: string
is_end: boolean
users: Pick<QQGuild.User, 'id' | 'username' | 'avatar'>[]
}>(`/channels/${channel_id}/messages/${message_id}/reactions/${type}/${id}`, {
params,
})
}

async createReaction(channel_id: string, message_id: string, type: string, id: string) {
return this.http.put(`/channels/${channel_id}/messages/${message_id}/reactions/${type}/${id}`)
}

async deleteReaction(channel_id: string, message_id: string, type: string, id: string) {
return this.http.delete(`/channels/${channel_id}/messages/${message_id}/reactions/${type}/${id}`)
}

async deleteMessage(channel_id: string, message_id: string) {
return this.http.delete(`/channels/${channel_id}/messages/${message_id}`)
}

async deleteDM(guild_id: string, message_id: string) {
// guild_id 是 createDMS 之后的 id
return this.http.delete(`/dms/${guild_id}/messages/${message_id}`)
}
}
Loading

0 comments on commit f8f3a19

Please sign in to comment.