Merge pull request #394 from LLOneBot/dev

3.31.7
This commit is contained in:
idranme
2024-09-04 20:35:08 +08:00
committed by GitHub
27 changed files with 360 additions and 232 deletions

View File

@@ -4,7 +4,7 @@
"name": "LLOneBot",
"slug": "LLOneBot",
"description": "实现 OneBot 11 协议,用于 QQ 机器人开发",
"version": "3.31.6",
"version": "3.31.7",
"icon": "./icon.webp",
"authors": [
{

View File

@@ -22,7 +22,7 @@
"cors": "^2.8.5",
"cosmokit": "^1.6.2",
"express": "^4.19.2",
"fast-xml-parser": "^4.4.1",
"fast-xml-parser": "^4.5.0",
"file-type": "^19.4.1",
"fluent-ffmpeg": "^2.1.3",
"minato": "^3.5.1",
@@ -38,7 +38,7 @@
"electron": "^31.4.0",
"electron-vite": "^2.3.0",
"typescript": "^5.5.4",
"vite": "^5.4.2",
"vite": "^5.4.3",
"vite-plugin-cp": "^4.0.8"
},
"packageManager": "yarn@4.4.1"

View File

@@ -1,25 +1,8 @@
import fs from 'node:fs'
import { Config, OB11Config } from './types'
import path from 'node:path'
import { Config, OB11Config } from './types'
import { selfInfo, DATA_DIR } from './globalVars'
// 在保证老对象已有的属性不变化的情况下将新对象的属性复制到老对象
function mergeNewProperties(newObj: any, oldObj: any) {
Object.keys(newObj).forEach((key) => {
// 如果老对象不存在当前属性,则直接复制
if (!oldObj.hasOwnProperty(key)) {
oldObj[key] = newObj[key]
} else {
// 如果老对象和新对象的当前属性都是对象,则递归合并
if (typeof oldObj[key] === 'object' && typeof newObj[key] === 'object') {
mergeNewProperties(newObj[key], oldObj[key])
} else if (typeof oldObj[key] === 'object' || typeof newObj[key] === 'object') {
// 属性冲突,有一方不是对象,直接覆盖
oldObj[key] = newObj[key]
}
}
})
}
import { mergeNewProperties } from './utils/misc'
export class ConfigUtil {
private readonly configPath: string
@@ -50,7 +33,8 @@ export class ConfigUtil {
enableWsReverse: false,
messagePostFormat: 'array',
enableHttpHeart: false,
enableQOAutoQuote: false
enableQOAutoQuote: false,
listenLocalhost: false
}
const defaultConfig: Config = {
enableLLOB: true,

View File

@@ -11,6 +11,7 @@ export interface OB11Config {
messagePostFormat?: 'array' | 'string'
enableHttpHeart?: boolean
enableQOAutoQuote: boolean // 快速操作回复自动引用原消息
listenLocalhost: boolean
}
export interface CheckVersion {

View File

@@ -13,4 +13,22 @@ export function calcQQLevel(level: QQLevel) {
export function getBuildVersion(): number {
const version: string = globalThis.LiteLoader.versions.qqnt
return +version.split('-')[1]
}
/** 在保证老对象已有的属性不变化的情况下将新对象的属性复制到老对象 */
export function mergeNewProperties(newObj: any, oldObj: any) {
Object.keys(newObj).forEach((key) => {
// 如果老对象不存在当前属性,则直接复制
if (!oldObj.hasOwnProperty(key)) {
oldObj[key] = newObj[key]
} else {
// 如果老对象和新对象的当前属性都是对象,则递归合并
if (typeof oldObj[key] === 'object' && typeof newObj[key] === 'object') {
mergeNewProperties(newObj[key], oldObj[key])
} else if (typeof oldObj[key] === 'object' || typeof newObj[key] === 'object') {
// 属性冲突,有一方不是对象,直接覆盖
oldObj[key] = newObj[key]
}
}
})
}

View File

@@ -272,4 +272,40 @@ export class NTQQMsgApi extends Service {
return await invoke('nodeIKernelMsgService/getSingleMsg', [{ peer, msgSeq }, null])
}
}
async queryFirstMsgBySeq(peer: Peer, msgSeq: string) {
return await invoke('nodeIKernelMsgService/queryMsgsWithFilterEx', [{
msgId: '0',
msgTime: '0',
msgSeq,
params: {
chatInfo: peer,
filterMsgType: [],
filterSendersUid: [],
filterMsgToTime: '0',
filterMsgFromTime: '0',
isReverseOrder: true,
isIncludeCurrent: true,
pageLimit: 1,
}
}, null])
}
async queryMsgsWithFilterExBySeq(peer: Peer, msgSeq: string, filterMsgTime: string, filterSendersUid: string[]) {
return await invoke('nodeIKernelMsgService/queryMsgsWithFilterEx', [{
msgId: '0',
msgTime: '0',
msgSeq,
params: {
chatInfo: peer,
filterMsgType: [],
filterSendersUid,
filterMsgToTime: filterMsgTime,
filterMsgFromTime: filterMsgTime,
isReverseOrder: true,
isIncludeCurrent: true,
pageLimit: 1,
}
}, null])
}
}

View File

@@ -334,4 +334,14 @@ export class NTQQUserApi extends Service {
}
return selfInfo.nick
}
async setSelfStatus(status: number, extStatus: number, batteryStatus: number) {
return await invoke('nodeIKernelMsgService/setStatus', [{
statusReq: {
status,
extStatus,
batteryStatus,
}
}, null])
}
}

View File

@@ -1,5 +1,6 @@
import { RequestUtil } from '@/common/utils/request'
import { Service, Context } from 'cordis'
import { Dict } from 'cosmokit'
declare module 'cordis' {
interface Context {
@@ -49,56 +50,6 @@ interface WebApiGroupMemberRet {
extmode: number
}
export interface WebApiGroupNoticeFeed {
u: number//发送者
fid: string//fid
pubt: number//时间
msg: {
text: string
text_face: string
title: string,
pics?: {
id: string,
w: string,
h: string
}[]
}
type: number
fn: number
cn: number
vn: number
settings: {
is_show_edit_card: number
remind_ts: number
tip_window_type: number
confirm_required: number
}
read_num: number
is_read: number
is_all_confirm: number
}
export interface WebApiGroupNoticeRet {
ec: number
em: string
ltsm: number
srv_code: number
read_only: number
role: number
feeds: WebApiGroupNoticeFeed[]
group: {
group_id: number
class_ext: number
}
sta: number,
gln: number
tst: number,
ui: any
server_time: number
svrt: number
ad: number
}
interface GroupEssenceMsg {
group_code: string
msg_seq: number
@@ -124,6 +75,30 @@ export interface GroupEssenceMsgRet {
}
}
interface SetGroupNoticeParams {
groupCode: string
content: string
pinned: number
type: number
isShowEditCard: number
tipWindowType: number
confirmRequired: number
picId: string
imgWidth?: number
imgHeight?: number
}
interface SetGroupNoticeRet {
ec: number
em: string
id: number
ltsm: number
new_fid: string
read_only: number
role: number
srv_code: number
}
export class NTQQWebApi extends Service {
static inject = ['ntUserApi']
@@ -134,7 +109,7 @@ export class NTQQWebApi extends Service {
async getGroupMembers(GroupCode: string, cached: boolean = true): Promise<WebApiGroupMember[]> {
const memberData: Array<WebApiGroupMember> = new Array<WebApiGroupMember>()
const cookieObject = await this.ctx.ntUserApi.getCookies('qun.qq.com')
const cookieStr = Object.entries(cookieObject).map(([key, value]) => `${key}=${value}`).join('; ')
const cookieStr = this.cookieToString(cookieObject)
const retList: Promise<WebApiGroupMemberRet>[] = []
const params = new URLSearchParams({
st: '0',
@@ -173,49 +148,47 @@ export class NTQQWebApi extends Service {
}
genBkn(sKey: string) {
sKey = sKey || '';
let hash = 5381;
sKey = sKey || ''
let hash = 5381
for (let i = 0; i < sKey.length; i++) {
const code = sKey.charCodeAt(i);
hash = hash + (hash << 5) + code;
const code = sKey.charCodeAt(i)
hash = hash + (hash << 5) + code
}
return (hash & 0x7FFFFFFF).toString();
return (hash & 0x7FFFFFFF).toString()
}
//实现未缓存 考虑2h缓存
async getGroupHonorInfo(groupCode: string, getType: WebHonorType) {
const getDataInternal = async (Internal_groupCode: string, Internal_type: number) => {
let url = 'https://qun.qq.com/interactive/honorlist?gc=' + Internal_groupCode + '&type=' + Internal_type.toString();
let res = '';
let resJson;
let url = 'https://qun.qq.com/interactive/honorlist?gc=' + Internal_groupCode + '&type=' + Internal_type.toString()
let res = ''
let resJson
try {
res = await RequestUtil.HttpGetText(url, 'GET', '', { 'Cookie': cookieStr });
const match = res.match(/window\.__INITIAL_STATE__=(.*?);/);
res = await RequestUtil.HttpGetText(url, 'GET', '', { 'Cookie': cookieStr })
const match = res.match(/window\.__INITIAL_STATE__=(.*?);/)
if (match) {
resJson = JSON.parse(match[1].trim());
resJson = JSON.parse(match[1].trim())
}
if (Internal_type === 1) {
return resJson?.talkativeList;
return resJson?.talkativeList
} else {
return resJson?.actorList;
return resJson?.actorList
}
} catch (e) {
this.ctx.logger.error('获取当前群荣耀失败', url, e);
this.ctx.logger.error('获取当前群荣耀失败', url, e)
}
return undefined;
return undefined
}
let HonorInfo: any = { group_id: groupCode };
let HonorInfo: any = { group_id: groupCode }
const cookieObject = await this.ctx.ntUserApi.getCookies('qun.qq.com')
const cookieStr = Object.entries(cookieObject).map(([key, value]) => `${key}=${value}`).join('; ')
const cookieStr = this.cookieToString(cookieObject)
if (getType === WebHonorType.TALKACTIVE || getType === WebHonorType.ALL) {
try {
let RetInternal = await getDataInternal(groupCode, 1);
let RetInternal = await getDataInternal(groupCode, 1)
if (!RetInternal) {
throw new Error('获取龙王信息失败');
throw new Error('获取龙王信息失败')
}
HonorInfo.current_talkative = {
user_id: RetInternal[0]?.uin,
@@ -232,17 +205,17 @@ export class NTQQWebApi extends Service {
description: talkative_ele?.desc,
day_count: 0,
nickname: talkative_ele?.name
});
})
}
} catch (e) {
this.ctx.logger.error(e);
this.ctx.logger.error(e)
}
}
if (getType === WebHonorType.PERFROMER || getType === WebHonorType.ALL) {
try {
let RetInternal = await getDataInternal(groupCode, 2);
let RetInternal = await getDataInternal(groupCode, 2)
if (!RetInternal) {
throw new Error('获取群聊之火失败');
throw new Error('获取群聊之火失败')
}
HonorInfo.performer_list = [];
for (const performer_ele of RetInternal) {
@@ -251,54 +224,86 @@ export class NTQQWebApi extends Service {
nickname: performer_ele?.name,
avatar: performer_ele?.avatar,
description: performer_ele?.desc
});
})
}
} catch (e) {
this.ctx.logger.error(e);
this.ctx.logger.error(e)
}
}
if (getType === WebHonorType.PERFROMER || getType === WebHonorType.ALL) {
try {
let RetInternal = await getDataInternal(groupCode, 3);
let RetInternal = await getDataInternal(groupCode, 3)
if (!RetInternal) {
throw new Error('获取群聊炽焰失败');
throw new Error('获取群聊炽焰失败')
}
HonorInfo.legend_list = [];
HonorInfo.legend_list = []
for (const legend_ele of RetInternal) {
HonorInfo.legend_list.push({
user_id: legend_ele?.uin,
nickname: legend_ele?.name,
avatar: legend_ele?.avatar,
desc: legend_ele?.description
});
})
}
} catch (e) {
this.ctx.logger.error('获取群聊炽焰失败', e);
this.ctx.logger.error('获取群聊炽焰失败', e)
}
}
if (getType === WebHonorType.EMOTION || getType === WebHonorType.ALL) {
try {
let RetInternal = await getDataInternal(groupCode, 6);
let RetInternal = await getDataInternal(groupCode, 6)
if (!RetInternal) {
throw new Error('获取快乐源泉失败');
throw new Error('获取快乐源泉失败')
}
HonorInfo.emotion_list = [];
HonorInfo.emotion_list = []
for (const emotion_ele of RetInternal) {
HonorInfo.emotion_list.push({
user_id: emotion_ele?.uin,
nickname: emotion_ele?.name,
avatar: emotion_ele?.avatar,
desc: emotion_ele?.description
});
})
}
} catch (e) {
this.ctx.logger.error('获取快乐源泉失败', e);
this.ctx.logger.error('获取快乐源泉失败', e)
}
}
//冒尖小春笋好像已经被tx扬了
if (getType === WebHonorType.EMOTION || getType === WebHonorType.ALL) {
HonorInfo.strong_newbie_list = [];
HonorInfo.strong_newbie_list = []
}
return HonorInfo;
return HonorInfo
}
async setGroupNotice(params: SetGroupNoticeParams): Promise<SetGroupNoticeRet> {
const cookieObject = await this.ctx.ntUserApi.getCookies('qun.qq.com')
const settings = JSON.stringify({
is_show_edit_card: params.isShowEditCard,
tip_window_type: params.tipWindowType,
confirm_required: params.confirmRequired
})
return await RequestUtil.HttpGetJson<SetGroupNoticeRet>(
`https://web.qun.qq.com/cgi-bin/announce/add_qun_notice?${new URLSearchParams({
bkn: this.genBkn(cookieObject.skey),
qid: params.groupCode,
text: params.content,
pinned: params.pinned.toString(),
type: params.type.toString(),
settings: settings,
...(params.picId !== '' && {
pic: params.picId,
imgWidth: params.imgWidth?.toString(),
imgHeight: params.imgHeight?.toString(),
})
})}`,
'POST',
'',
{ 'Cookie': this.cookieToString(cookieObject) }
)
}
private cookieToString(cookieObject: Dict) {
return Object.entries(cookieObject).map(([key, value]) => `${key}=${value}`).join('; ')
}
}

View File

@@ -37,7 +37,7 @@ declare module 'cordis' {
}
class Core extends Service {
static inject = ['ntMsgApi', 'ntFileApi', 'ntFileCacheApi', 'ntFriendApi', 'ntGroupApi', 'ntUserApi', 'ntWindowApi']
static inject = ['ntMsgApi', 'ntFriendApi', 'ntGroupApi']
constructor(protected ctx: Context, public config: Core.Config) {
super(ctx, 'app', true)

View File

@@ -26,7 +26,7 @@ export const ReceiveCmdS = {
CACHE_SCAN_FINISH: 'nodeIKernelStorageCleanListener/onFinishScan',
MEDIA_UPLOAD_COMPLETE: 'nodeIKernelMsgListener/onRichMediaUploadComplete',
SKEY_UPDATE: 'onSkeyUpdate',
}
} as const
export type ReceiveCmd = string

View File

@@ -36,6 +36,7 @@ export interface Group {
memberUid: string //"u_fbf8N7aeuZEnUiJAbQ9R8Q"
}
members: GroupMember[] // 原始数据是没有这个的,为了方便自己加了这个字段
createTime: string
}
export enum GroupMemberRole {

View File

@@ -480,6 +480,8 @@ export interface RawMessage {
sourceMsgIsIncPic: boolean // 原消息是否有图片
sourceMsgText: string
replayMsgSeq: string // 源消息的msgSeq可以通过这个找到源消息的msgId
senderUidStr: string
replyMsgTime: string
}
textElement: {
atType: AtType

View File

@@ -0,0 +1,37 @@
import BaseAction from '../BaseAction'
import { ActionName } from '../types'
interface Payload {
group_id: number | string
content: string
image?: string
pinned?: number | string //扩展
confirm_required?: number | string //扩展
}
export class SendGroupNotice extends BaseAction<Payload, null> {
actionName = ActionName.GoCQHTTP_SendGroupNotice
async _handle(payload: Payload) {
const type = 1
const isShowEditCard = 0
const tipWindowType = 0
const pinned = Number(payload.pinned ?? 0)
const confirmRequired = Number(payload.confirm_required ?? 1)
const result = await this.ctx.ntWebApi.setGroupNotice({
groupCode: payload.group_id.toString(),
content: payload.content,
pinned,
type,
isShowEditCard,
tipWindowType,
confirmRequired,
picId: ''
})
if (result.ec !== 0) {
throw new Error(`设置群公告失败, 错误信息: ${result.em}`)
}
return null
}
}

View File

@@ -2,24 +2,22 @@ import fs from 'node:fs'
import BaseAction from '../BaseAction'
import { ActionName } from '../types'
import { SendElementEntities } from '@/ntqqapi/entities'
import { ChatType, SendFileElement } from '@/ntqqapi/types'
import { SendFileElement } from '@/ntqqapi/types'
import { uri2local } from '@/common/utils'
import { Peer } from '@/ntqqapi/types'
import { sendMsg } from '../../helper/createMessage'
import { sendMsg, createPeer, CreatePeerMode } from '../../helper/createMessage'
interface Payload {
user_id: number | string
group_id?: number | string
interface UploadGroupFilePayload {
group_id: number | string
file: string
name: string
folder?: string
folder_id?: string
}
export class UploadGroupFile extends BaseAction<Payload, null> {
export class UploadGroupFile extends BaseAction<UploadGroupFilePayload, null> {
actionName = ActionName.GoCQHTTP_UploadGroupFile
protected async _handle(payload: Payload): Promise<null> {
protected async _handle(payload: UploadGroupFilePayload): Promise<null> {
let file = payload.file
if (fs.existsSync(file)) {
file = `file://${file}`
@@ -29,31 +27,23 @@ export class UploadGroupFile extends BaseAction<Payload, null> {
throw new Error(downloadResult.errMsg)
}
const sendFileEle = await SendElementEntities.file(this.ctx, downloadResult.path, payload.name, payload.folder_id)
await sendMsg(this.ctx, {
chatType: ChatType.group,
peerUid: payload.group_id?.toString()!,
}, [sendFileEle], [], true)
const peer = await createPeer(this.ctx, payload, CreatePeerMode.Group)
await sendMsg(this.ctx, peer, [sendFileEle], [], true)
return null
}
}
export class UploadPrivateFile extends BaseAction<Payload, null> {
interface UploadPrivateFilePayload {
user_id: number | string
file: string
name: string
}
export class UploadPrivateFile extends BaseAction<UploadPrivateFilePayload, null> {
actionName = ActionName.GoCQHTTP_UploadPrivateFile
async getPeer(payload: Payload): Promise<Peer> {
if (payload.user_id) {
const peerUid = await this.ctx.ntUserApi.getUidByUin(payload.user_id.toString())
if (!peerUid) {
throw `私聊${payload.user_id}不存在`
}
const isBuddy = await this.ctx.ntFriendApi.isBuddy(peerUid)
return { chatType: isBuddy ? ChatType.friend : ChatType.temp, peerUid }
}
throw '缺少参数 user_id'
}
protected async _handle(payload: Payload): Promise<null> {
const peer = await this.getPeer(payload)
protected async _handle(payload: UploadPrivateFilePayload): Promise<null> {
const peer = await createPeer(this.ctx, payload, CreatePeerMode.Private)
let file = payload.file
if (fs.existsSync(file)) {
file = `file://${file}`

View File

@@ -60,6 +60,8 @@ import { CreateGroupFileFolder } from './go-cqhttp/CreateGroupFileFolder'
import { DelGroupFolder } from './go-cqhttp/DelGroupFolder'
import { GetGroupAtAllRemain } from './go-cqhttp/GetGroupAtAllRemain'
import { GetGroupRootFiles } from './go-cqhttp/GetGroupRootFiles'
import { SetOnlineStatus } from './llonebot/SetOnlineStatus'
import { SendGroupNotice } from './go-cqhttp/SendGroupNotice'
export function initActionMap(adapter: Adapter) {
const actionHandlers = [
@@ -71,6 +73,7 @@ export function initActionMap(adapter: Adapter) {
new SetQQAvatar(adapter),
new GetFriendWithCategory(adapter),
new GetEvent(adapter),
new SetOnlineStatus(adapter),
// onebot11
new SendLike(adapter),
new GetMsg(adapter),
@@ -126,7 +129,8 @@ export function initActionMap(adapter: Adapter) {
new CreateGroupFileFolder(adapter),
new DelGroupFolder(adapter),
new GetGroupAtAllRemain(adapter),
new GetGroupRootFiles(adapter)
new GetGroupRootFiles(adapter),
new SendGroupNotice(adapter)
]
const actionMap = new Map<string, BaseAction<any, any>>()
for (const action of actionHandlers) {

View File

@@ -0,0 +1,25 @@
import BaseAction from '../BaseAction'
import { ActionName } from '../types'
interface Payload {
status: number | string
ext_status: number | string
battery_status: number | string
}
export class SetOnlineStatus extends BaseAction<Payload, null> {
actionName = ActionName.SetOnlineStatus
async _handle(payload: Payload) {
const ret = await this.ctx.ntUserApi.setSelfStatus(
Number(payload.status),
Number(payload.ext_status),
Number(payload.battery_status),
)
if (ret.result !== 0) {
this.ctx.logger.error(ret)
throw new Error('设置在线状态失败')
}
return null
}
}

View File

@@ -21,45 +21,15 @@ import { CustomMusicSignPostData, IdMusicSignPostData, MusicSign, MusicSignPostD
import { Peer } from '@/ntqqapi/types/msg'
import { MessageUnique } from '@/common/utils/messageUnique'
import { selfInfo } from '@/common/globalVars'
import { convertMessage2List, createSendElements, sendMsg } from '../../helper/createMessage'
import { convertMessage2List, createSendElements, sendMsg, createPeer, CreatePeerMode } from '../../helper/createMessage'
export interface ReturnDataType {
interface ReturnData {
message_id: number
}
export enum ContextMode {
Normal = 0,
Private = 1,
Group = 2
}
export class SendMsg extends BaseAction<OB11PostSendMsg, ReturnDataType> {
export class SendMsg extends BaseAction<OB11PostSendMsg, ReturnData> {
actionName = ActionName.SendMsg
private async createContext(payload: OB11PostSendMsg, contextMode: ContextMode): Promise<Peer> {
// This function determines the type of message by the existence of user_id / group_id,
// not message_type.
// This redundant design of Ob11 here should be blamed.
if ((contextMode === ContextMode.Group || contextMode === ContextMode.Normal) && payload.group_id) {
return {
chatType: ChatType.group,
peerUid: payload.group_id.toString(),
}
}
if ((contextMode === ContextMode.Private || contextMode === ContextMode.Normal) && payload.user_id) {
const uid = await this.ctx.ntUserApi.getUidByUin(payload.user_id.toString())
if (!uid) throw new Error('无法获取用户信息')
const isBuddy = await this.ctx.ntFriendApi.isBuddy(uid)
return {
chatType: isBuddy ? ChatType.friend : ChatType.temp,
peerUid: uid,
guildId: isBuddy ? '' : payload.group_id?.toString() || ''
}
}
throw new Error('请指定 group_id 或 user_id')
}
protected async check(payload: OB11PostSendMsg): Promise<BaseCheckResult> {
const messages = convertMessage2List(payload.message)
const fmNum = this.getSpecialMsgNum(messages, OB11MessageDataType.node)
@@ -84,13 +54,13 @@ export class SendMsg extends BaseAction<OB11PostSendMsg, ReturnDataType> {
}
protected async _handle(payload: OB11PostSendMsg) {
let contextMode = ContextMode.Normal
let contextMode = CreatePeerMode.Normal
if (payload.message_type === 'group') {
contextMode = ContextMode.Group
contextMode = CreatePeerMode.Group
} else if (payload.message_type === 'private') {
contextMode = ContextMode.Private
contextMode = CreatePeerMode.Private
}
const peer = await this.createContext(payload, contextMode)
const peer = await createPeer(this.ctx, payload, contextMode)
const messages = convertMessage2List(
payload.message,
payload.auto_escape === true || payload.auto_escape === 'true',

View File

@@ -23,6 +23,7 @@ export enum ActionName {
GetFile = 'get_file',
GetFriendsWithCategory = 'get_friends_with_category',
GetEvent = 'get_event',
SetOnlineStatus = 'set_online_status',
// onebot 11
SendLike = 'send_like',
GetLoginInfo = 'get_login_info',
@@ -78,5 +79,6 @@ export enum ActionName {
GoCQHTTP_CreateGroupFileFolder = 'create_group_file_folder',
GoCQHTTP_DelGroupFolder = 'delete_group_folder',
GoCQHTTP_GetGroupAtAllRemain = 'get_group_at_all_remain',
GoCQHTTP_GetGroupRootFiles = 'get_group_root_files'
GoCQHTTP_GetGroupRootFiles = 'get_group_root_files',
GoCQHTTP_SendGroupNotice = '_send_group_notice',
}

View File

@@ -35,7 +35,7 @@ declare module 'cordis' {
}
class OneBot11Adapter extends Service {
static inject = ['ntMsgApi', 'ntFileApi', 'ntFileCacheApi', 'ntFriendApi', 'ntGroupApi', 'ntUserApi', 'ntWindowApi']
static inject = ['ntMsgApi', 'ntFileApi', 'ntFileCacheApi', 'ntFriendApi', 'ntGroupApi', 'ntUserApi', 'ntWindowApi', 'ntWebApi']
public messages: Map<string, RawMessage> = new Map()
public startTime = 0
@@ -50,7 +50,8 @@ class OneBot11Adapter extends Service {
this.ob11Http = new OB11Http(ctx, {
port: config.httpPort,
token: config.token,
actionMap
actionMap,
listenLocalhost: config.listenLocalhost
})
this.ob11HttpPost = new OB11HttpPost(ctx, {
hosts: config.httpHosts,
@@ -62,7 +63,8 @@ class OneBot11Adapter extends Service {
port: config.wsPort,
heartInterval: config.heartInterval,
token: config.token,
actionMap
actionMap,
listenLocalhost: config.listenLocalhost
})
this.ob11WebSocketReverseManager = new OB11WebSocketReverseManager(ctx, {
hosts: config.wsHosts,
@@ -110,10 +112,10 @@ class OneBot11Adapter extends Service {
for (const notify of notifies) {
try {
const notifyTime = parseInt(notify.seq) / 1000
const flag = notify.group.groupCode + '|' + notify.seq + '|' + notify.type
if (notifyTime < this.startTime) {
continue
}
const flag = notify.group.groupCode + '|' + notify.seq + '|' + notify.type
if ([GroupNotifyType.MEMBER_LEAVE_NOTIFY_ADMIN, GroupNotifyType.KICK_MEMBER_NOTIFY_ADMIN].includes(notify.type)) {
this.ctx.logger.info('有成员退出通知', notify)
const member1Uin = await this.ctx.ntUserApi.getUinByUid(notify.user1.uid)
@@ -292,7 +294,7 @@ class OneBot11Adapter extends Service {
}
}
// HTTP 端口变化,重启服务
if (config.ob11.httpPort !== old.httpPort) {
if ((config.ob11.httpPort !== old.httpPort || config.ob11.listenLocalhost !== old.listenLocalhost) && config.ob11.enableHttp) {
await this.ob11Http.stop()
this.ob11Http.start()
}
@@ -305,7 +307,7 @@ class OneBot11Adapter extends Service {
}
}
// 正向 WebSocket 端口变化,重启服务
if (config.ob11.wsPort !== old.wsPort) {
if ((config.ob11.wsPort !== old.wsPort || config.ob11.listenLocalhost !== old.listenLocalhost) && config.ob11.enableWs) {
await this.ob11WebSocket.stop()
this.ob11WebSocket.start()
llonebotError.wsServerError = ''

View File

@@ -51,8 +51,9 @@ class OB11Http {
this.expressAPP.get('/', (req: Request, res: Response) => {
res.send(`LLOneBot server 已启动`)
})
this.server = this.expressAPP.listen(this.config.port, '0.0.0.0', () => {
this.ctx.logger.info(`HTTP server started 0.0.0.0:${this.config.port}`)
const host = this.config.listenLocalhost ? '127.0.0.1' : '0.0.0.0'
this.server = this.expressAPP.listen(this.config.port, host, () => {
this.ctx.logger.info(`HTTP server started ${host}:${this.config.port}`)
})
llonebotError.httpServerError = ''
} catch (e: any) {
@@ -136,6 +137,7 @@ namespace OB11Http {
port: number
token?: string
actionMap: Map<string, BaseAction<any, any>>
listenLocalhost: boolean
}
}

View File

@@ -21,9 +21,14 @@ class OB11WebSocket {
public start() {
if (this.wsServer) return
this.ctx.logger.info(`WebSocket server started 0.0.0.0:${this.config.port}`)
const host = this.config.listenLocalhost ? '127.0.0.1' : '0.0.0.0'
this.ctx.logger.info(`WebSocket server started ${host}:${this.config.port}`)
try {
this.wsServer = new WebSocketServer({ port: this.config.port, maxPayload: 1024 * 1024 * 1024 })
this.wsServer = new WebSocketServer({
host,
port: this.config.port,
maxPayload: 1024 * 1024 * 1024
})
llonebotError.wsServerError = ''
} catch (e: any) {
llonebotError.wsServerError = '正向 WebSocket 服务启动失败, ' + e.toString()
@@ -165,6 +170,7 @@ namespace OB11WebSocket {
heartInterval: number
token?: string
actionMap: Map<string, BaseAction<any, any>>
listenLocalhost: boolean
}
}

View File

@@ -43,7 +43,7 @@ import { OB11GroupRecallNoticeEvent } from './event/notice/OB11GroupRecallNotice
import { OB11FriendPokeEvent, OB11GroupPokeEvent } from './event/notice/OB11PokeEvent'
import { OB11BaseNoticeEvent } from './event/notice/OB11BaseNoticeEvent'
import { OB11GroupEssenceEvent } from './event/notice/OB11GroupEssenceEvent'
import { omit, isNullable } from 'cosmokit'
import { omit, isNullable, pick } from 'cosmokit'
import { Context } from 'cordis'
import { selfInfo } from '@/common/globalVars'
import { pathToFileURL } from 'node:url'
@@ -154,15 +154,18 @@ export namespace OB11Entities {
guildId: ''
}
try {
const { replayMsgSeq, replyMsgTime, senderUidStr } = replyElement
const records = msg.records.find(msgRecord => msgRecord.msgId === replyElement.sourceMsgIdInRecords)
if (!records) throw new Error('找不到回复消息')
let replyMsg = (await ctx.ntMsgApi.getMsgsBySeqAndCount(peer, replyElement.replayMsgSeq, 1, true, true)).msgList[0]
if (!replyMsg || records.msgRandom !== replyMsg.msgRandom) {
replyMsg = (await ctx.ntMsgApi.getSingleMsg(peer, replyElement.replayMsgSeq)).msgList[0]
if (!records || !replyMsgTime || !senderUidStr) {
throw new Error('找不到回复消息')
}
const { msgList } = await ctx.ntMsgApi.queryMsgsWithFilterExBySeq(peer, replayMsgSeq, replyMsgTime, [senderUidStr])
const replyMsg = msgList.find(msg => msg.msgRandom === records.msgRandom)
// 284840486: 合并消息内侧 消息具体定位不到
if ((!replyMsg || records.msgRandom !== replyMsg.msgRandom) && msg.peerUin !== '284840486') {
throw new Error('回复消息消息验证失败')
if (!replyMsg && msg.peerUin !== '284840486') {
ctx.logger.info('queryMsgs', msgList.map(e => pick(e, ['msgSeq', 'msgRandom'])))
throw new Error('回复消息验证失败')
}
messageSegment = {
type: OB11MessageDataType.reply,
@@ -171,7 +174,7 @@ export namespace OB11Entities {
}
}
} catch (e: any) {
ctx.logger.error('获取不到引用的消息', replyElement.replayMsgSeq, e.stack)
ctx.logger.error('获取不到引用的消息', replyElement, e.stack)
continue
}
}
@@ -518,29 +521,27 @@ export namespace OB11Entities {
}).parse(xmlElement.content)
ctx.logger.info('收到表情回应我的消息', emojiLikeData)
try {
const senderUin = emojiLikeData.gtip.qq.jp
const msgSeq = emojiLikeData.gtip.url.msgseq
const emojiId = emojiLikeData.gtip.face.id
const replyMsgList = (await ctx.ntMsgApi.getMsgsBySeqAndCount({
const senderUin: string = emojiLikeData.gtip.qq.jp
const msgSeq: string = emojiLikeData.gtip.url.msgseq
const emojiId: string = emojiLikeData.gtip.face.id
const peer = {
chatType: ChatType.group,
guildId: '',
peerUid: msg.peerUid,
}, msgSeq, 1, true, true))?.msgList
}
const replyMsgList = (await ctx.ntMsgApi.queryFirstMsgBySeq(peer, msgSeq)).msgList
if (!replyMsgList?.length) {
return
}
const likes = [
{
emoji_id: emojiId,
count: 1,
},
]
const shortId = MessageUnique.getShortIdByMsgId(replyMsgList[0].msgId)
return new OB11GroupMsgEmojiLikeEvent(
parseInt(msg.peerUid),
parseInt(senderUin),
shortId!,
likes
[{
emoji_id: emojiId,
count: 1,
}]
)
} catch (e: any) {
ctx.logger.error('解析表情回应消息失败', e.stack)
@@ -768,6 +769,8 @@ export namespace OB11Entities {
return {
group_id: parseInt(group.groupCode),
group_name: group.groupName,
group_memo: group.remarkName,
group_create_time: +group.createTime,
member_count: group.memberCount,
max_member_count: group.maxMember,
}

View File

@@ -276,4 +276,34 @@ export async function sendMsg(
deleteAfterSentFiles.map(path => fsPromise.unlink(path))
return returnMsg
}
}
export interface CreatePeerPayload {
group_id?: string | number
user_id?: string | number
}
export enum CreatePeerMode {
Normal = 0,
Private = 1,
Group = 2
}
export async function createPeer(ctx: Context, payload: CreatePeerPayload, mode: CreatePeerMode): Promise<Peer> {
if ((mode === CreatePeerMode.Group || mode === CreatePeerMode.Normal) && payload.group_id) {
return {
chatType: ChatType.group,
peerUid: payload.group_id.toString(),
}
}
if ((mode === CreatePeerMode.Private || mode === CreatePeerMode.Normal) && payload.user_id) {
const uid = await ctx.ntUserApi.getUidByUin(payload.user_id.toString())
if (!uid) throw new Error('无法获取用户信息')
const isBuddy = await ctx.ntFriendApi.isBuddy(uid)
return {
chatType: isBuddy ? ChatType.friend : ChatType.temp,
peerUid: uid,
}
}
throw new Error('请指定 group_id 或 user_id')
}

View File

@@ -1,12 +1,12 @@
import { OB11Message, OB11MessageAt, OB11MessageData, OB11MessageDataType } from '../types'
import { OB11FriendRequestEvent } from '../event/request/OB11FriendRequest'
import { OB11GroupRequestEvent } from '../event/request/OB11GroupRequest'
import { ChatType, GroupRequestOperateTypes, Peer } from '@/ntqqapi/types'
import { convertMessage2List, createSendElements, sendMsg } from '../helper/createMessage'
import { getConfigUtil } from '@/common/config'
import { GroupRequestOperateTypes } from '@/ntqqapi/types'
import { convertMessage2List, createSendElements, sendMsg, createPeer, CreatePeerMode } from '../helper/createMessage'
import { MessageUnique } from '@/common/utils/messageUnique'
import { isNullable } from 'cosmokit'
import { Context } from 'cordis'
import { OB11Config } from '@/common/types'
interface QuickOperationPrivateMessage {
reply?: string
@@ -57,21 +57,14 @@ export async function handleQuickOperation(ctx: Context, event: QuickOperationEv
async function handleMsg(ctx: Context, msg: OB11Message, quickAction: QuickOperationPrivateMessage | QuickOperationGroupMessage) {
const reply = quickAction.reply
const ob11Config = getConfigUtil().getConfig().ob11
const peer: Peer = {
chatType: ChatType.friend,
peerUid: msg.user_id.toString(),
}
if (msg.message_type == 'private') {
peer.peerUid = (await ctx.ntUserApi.getUidByUin(msg.user_id.toString()))!
if (msg.sub_type === 'group') {
peer.chatType = ChatType.temp
}
}
else {
peer.chatType = ChatType.group
peer.peerUid = msg.group_id?.toString()!
const ob11Config: OB11Config = ctx.config
let contextMode = CreatePeerMode.Normal
if (msg.message_type === 'group') {
contextMode = CreatePeerMode.Group
} else if (msg.message_type === 'private') {
contextMode = CreatePeerMode.Private
}
const peer = await createPeer(ctx, msg, contextMode)
if (reply) {
let replyMessage: OB11MessageData[] = []
if (ob11Config.enableQOAutoQuote) {

View File

@@ -54,8 +54,10 @@ export interface OB11GroupMember {
export interface OB11Group {
group_id: number
group_name: string
member_count?: number
max_member_count?: number
group_memo: string
group_create_time: number
member_count: number
max_member_count: number
}
interface OB11Sender {

View File

@@ -166,7 +166,12 @@ async function onSettingWindowCreated(view: Element) {
SettingItem(
'快速操作回复自动引用原消息',
null,
SettingSwitch('ob11.enableQOAutoQuote', config.ob11.enableQOAutoQuote, { 'control-display-id': 'config-ob11-enableQOAutoQuote' }),
SettingSwitch('ob11.enableQOAutoQuote', config.ob11.enableQOAutoQuote),
),
SettingItem(
'HTTP、正向 WebSocket 服务仅监听 127.0.0.1',
'而不是 0.0.0.0',
SettingSwitch('ob11.listenLocalhost', config.ob11.listenLocalhost),
),
SettingItem('', null, SettingButton('保存', 'config-ob11-save', 'primary')),
]),

View File

@@ -1 +1 @@
export const version = '3.31.6'
export const version = '3.31.7'