Compare commits

...

47 Commits

Author SHA1 Message Date
idranme
e204bb0957 Merge pull request #395 from LLOneBot/dev
3.31.8
2024-09-05 15:00:57 +08:00
idranme
ed546ace3d chore: v3.31.8 2024-09-05 15:00:05 +08:00
idranme
3c79cffa42 optimize 2024-09-05 14:58:52 +08:00
idranme
acce444dee optimize 2024-09-05 02:26:42 +08:00
idranme
f359e3ea9d fix 2024-09-05 02:20:23 +08:00
idranme
fe99da985f Merge pull request #394 from LLOneBot/dev
3.31.7
2024-09-04 20:35:08 +08:00
idranme
58d5de572c chore: v3.31.7 2024-09-04 20:32:44 +08:00
idranme
b2088824cc feat 2024-09-04 17:15:41 +08:00
idranme
fffa664400 fix: reply message segment 2024-09-04 16:18:48 +08:00
idranme
02e5222f92 feat: SendGroupNotice 2024-09-04 15:42:10 +08:00
idranme
18d253edf6 fix: GroupMsgEmojiLikeEvent 2024-09-04 13:13:49 +08:00
idranme
da8b5e2429 chore 2024-09-04 13:12:39 +08:00
idranme
502be69bc5 feat: SetOnlineStatus 2024-09-04 01:23:25 +08:00
idranme
273d4133eb refactor 2024-09-04 00:44:41 +08:00
idranme
44bfc5aab9 optimize 2024-09-03 21:46:26 +08:00
idranme
050c9d9b54 fix 2024-09-03 21:43:18 +08:00
idranme
7904f45c20 Merge pull request #392 from LLOneBot/dev
3.31.6
2024-09-03 18:38:07 +08:00
idranme
1afdad1452 chore: v3.31.6 2024-09-03 18:34:30 +08:00
idranme
cd930c43b6 feat: GetGroupRootFiles 2024-09-03 15:14:05 +08:00
idranme
b7efbdf239 fix: ws 2024-09-03 13:16:25 +08:00
idranme
56706f3838 chore 2024-09-03 01:24:21 +08:00
idranme
387c9dcb52 refactor 2024-09-03 01:04:16 +08:00
idranme
a7bb55b31c chore 2024-09-02 19:53:18 +08:00
idranme
fbf09e1db4 chore 2024-09-02 19:48:17 +08:00
idranme
9b98f8f33d optimize 2024-09-02 19:30:23 +08:00
idranme
727f399de6 fix: GetGroupMsgHistory 2024-09-02 19:24:27 +08:00
idranme
e03b82fb44 optimize: ci 2024-09-02 18:28:21 +08:00
idranme
ba413b9581 Merge pull request #390 from LLOneBot/dev
3.31.5
2024-09-02 16:42:35 +08:00
idranme
abcec99ce0 chore: v3.31.5 2024-09-02 16:39:36 +08:00
idranme
a7da7ab598 optimize 2024-09-02 01:58:31 +08:00
idranme
5cc8a2b96e fix 2024-09-02 01:46:08 +08:00
idranme
f0d8c851d4 optimize 2024-09-02 01:24:15 +08:00
idranme
828b20e0e8 optimize 2024-09-02 01:05:58 +08:00
idranme
3570349fcd optimize 2024-09-02 00:42:35 +08:00
idranme
ad74854e42 fix 2024-09-01 20:28:12 +08:00
idranme
15e7afed62 Merge pull request #385 from LLOneBot/dev
3.31.4
2024-09-01 18:50:38 +08:00
idranme
bf71328650 chore: v3.31.4 2024-09-01 18:50:09 +08:00
idranme
b3299ba1e3 chore 2024-09-01 15:39:37 +08:00
idranme
d36ea93e63 refactor 2024-09-01 15:26:34 +08:00
idranme
0bd3f8f1a2 feat 2024-09-01 15:26:11 +08:00
idranme
4bf79e021e Merge pull request #383 from LLOneBot/dev
3.31.3
2024-09-01 00:36:41 +08:00
idranme
2dac109e58 chore: v3.31.3 2024-09-01 00:34:08 +08:00
idranme
2637a5da6d chore 2024-08-31 22:59:42 +08:00
idranme
f8b2be246f optimize 2024-08-31 22:55:26 +08:00
idranme
44921e85ad chore 2024-08-31 19:46:35 +08:00
idranme
388e016365 optimize 2024-08-31 19:41:48 +08:00
idranme
a2056a43f3 fix 2024-08-31 01:29:44 +08:00
67 changed files with 1012 additions and 638 deletions

View File

@@ -1,4 +1,4 @@
name: 'publish'
name: Publish
on:
push:
tags:
@@ -8,31 +8,36 @@ jobs:
build-and-publish:
runs-on: ubuntu-latest
steps:
- name: checkout
- name: Checkout
uses: actions/checkout@v4
- name: setup node
- name: Setup node
uses: actions/setup-node@v4
with:
node-version: 20
- name: install dependenies
- name: Install dependenies
run: |
export ELECTRON_SKIP_BINARY_DOWNLOAD=1
npm install
- name: build
- name: Build
run: npm run build
- name: zip
- name: Compress
run: |
sudo apt install zip -y
cd ./dist/
zip -r ../LLOneBot.zip ./*
- name: publish
- name: Extract version from tag
id: get-version
run: echo "VERSION=${GITHUB_REF#refs/tags/v}" >> "$GITHUB_OUTPUT"
- name: Release
uses: ncipollo/release-action@v1
with:
artifacts: 'LLOneBot.zip'
draft: true
token: ${{ secrets.RELEASE_TOKEN }}
name: LLOneBot v${{ steps.get-version.outputs.VERSION }}

View File

@@ -15,10 +15,6 @@ TG 群:<https://t.me/+nLZEnpne-pQ1OWFl>
<img src="./doc/image/setting.png" width="400px" alt="设置界面"/>
## HTTP 调用示例
<img src="./doc/image/example.jpg" width="500px" alt="HTTP调用示例"/>
## 支持的 API
<https://llonebot.github.io/zh-CN/develop/api>
@@ -31,10 +27,10 @@ TG 群:<https://t.me/+nLZEnpne-pQ1OWFl>
- [NapCatQQ](https://github.com/NapNeko/NapCatQQ)
- [LiteLoaderQQNT](https://liteloaderqqnt.github.io/guide/install.html)
- [chronocat](https://github.com/chrononeko/chronocat)
- [Chronocat](https://github.com/chrononeko/chronocat)
- [koishi-plugin-adapter-onebot](https://github.com/koishijs/koishi-plugin-adapter-onebot)
- [silk-wasm](https://github.com/idranme/silk-wasm)
## 友链
- [Lagrange.Core](https://github.com/LagrangeDev/Lagrange.Core) 一款用C#实现的NTQQ纯协议跨平台QQ机器人框架
- [Lagrange.Core](https://github.com/LagrangeDev/Lagrange.Core): An Implementation of NTQQ Protocol

View File

@@ -1,6 +1,7 @@
import cp from 'vite-plugin-cp'
import path from 'node:path'
import './scripts/gen-manifest'
import type { ElectronViteConfig } from 'electron-vite'
const external = [
'silk-wasm',
@@ -12,7 +13,7 @@ function genCpModule(module: string) {
return { src: `./node_modules/${module}`, dest: `dist/node_modules/${module}`, flatten: false }
}
let config = {
const config: ElectronViteConfig = {
main: {
build: {
outDir: 'dist/main',
@@ -39,9 +40,6 @@ let config = {
...external.map(genCpModule),
{ src: './manifest.json', dest: 'dist' },
{ src: './icon.webp', dest: 'dist' },
// { src: './src/ntqqapi/native/crychic/crychic-win32-x64.node', dest: 'dist/main/' },
// { src: './src/ntqqapi/native/moehook/MoeHoo-win32-x64.node', dest: 'dist/main/' },
// { src: './src/ntqqapi/native/moehook/MoeHoo-linux-x64.node', dest: 'dist/main/' },
],
}),
],

View File

@@ -4,7 +4,7 @@
"name": "LLOneBot",
"slug": "LLOneBot",
"description": "实现 OneBot 11 协议,用于 QQ 机器人开发",
"version": "3.31.2",
"version": "3.31.8",
"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
@@ -38,7 +21,7 @@ export class ConfigUtil {
}
reloadConfig(): Config {
let ob11Default: OB11Config = {
const ob11Default: OB11Config = {
httpPort: 3000,
httpHosts: [],
httpSecret: '',
@@ -50,9 +33,10 @@ export class ConfigUtil {
enableWsReverse: false,
messagePostFormat: 'array',
enableHttpHeart: false,
enableQOAutoQuote: false
enableQOAutoQuote: false,
listenLocalhost: false
}
let defaultConfig: Config = {
const defaultConfig: Config = {
enableLLOB: true,
ob11: ob11Default,
heartInterval: 60000,
@@ -83,7 +67,6 @@ export class ConfigUtil {
this.checkOldConfig(jsonData.ob11, jsonData, 'httpPort', 'http')
this.checkOldConfig(jsonData.ob11, jsonData, 'httpHosts', 'hosts')
this.checkOldConfig(jsonData.ob11, jsonData, 'wsPort', 'wsPort')
// console.log("get config", jsonData);
this.config = jsonData
return this.config
}
@@ -95,15 +78,15 @@ export class ConfigUtil {
}
private checkOldConfig(
currentConfig: Config | OB11Config,
oldConfig: Config | OB11Config,
currentKey: string,
oldKey: string,
currentConfig: OB11Config,
oldConfig: Config,
currentKey: 'httpPort' | 'httpHosts' | 'wsPort',
oldKey: 'http' | 'hosts' | 'wsPort',
) {
// 迁移旧的配置到新配置,避免用户重新填写配置
const oldValue = oldConfig[oldKey]
if (oldValue) {
currentConfig[currentKey] = oldValue
currentConfig[currentKey] = oldValue as any
delete oldConfig[oldKey]
}
}

View File

@@ -11,6 +11,7 @@ export interface OB11Config {
messagePostFormat?: 'array' | 'string'
enableHttpHeart?: boolean
enableQOAutoQuote: boolean // 快速操作回复自动引用原消息
listenLocalhost: boolean
}
export interface CheckVersion {
@@ -34,6 +35,12 @@ export interface Config {
ignoreBeforeLoginMsg?: boolean
/** 单位为秒 */
msgCacheExpire?: number
/** @deprecated */
http?: string
/** @deprecated */
hosts?: string[]
/** @deprecated */
wsPort?: string
}
export interface LLOneBotError {

View File

@@ -3,7 +3,6 @@ import ffmpeg from 'fluent-ffmpeg'
import fsPromise from 'node:fs/promises'
import { decode, encode, getDuration, getWavFileInfo, isWav, isSilk, EncodeResult } from 'silk-wasm'
import { TEMP_DIR } from '../globalVars'
import { getConfigUtil } from '../config'
import { randomUUID } from 'node:crypto'
import { Readable } from 'node:stream'
import { Context } from 'cordis'
@@ -38,7 +37,7 @@ function convert(ctx: Context, input: Input, options: FFmpegOptions, outputPath?
if (options.output) {
command = command.outputOptions(options.output)
}
const ffmpegPath = getConfigUtil().getConfig().ffmpeg
const ffmpegPath: string | undefined = ctx.config.ffmpeg
if (ffmpegPath) {
command = command.setFfmpegPath(ffmpegPath)
}

View File

@@ -82,7 +82,7 @@ class MessageUniqueWrapper {
return ret.map((t) => t?.MsgId).filter((t) => t !== undefined)
}
createMsg(peer: Peer, msgId: string): number | undefined {
createMsg(peer: Peer, msgId: string): number {
const key = `${msgId}|${peer.chatType}|${peer.peerUid}`
const hash = createHash('md5').update(key).digest()
//设置第一个bit为0 保证shortId为正数

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

File diff suppressed because one or more lines are too long

7
src/global.d.ts vendored
View File

@@ -2,9 +2,8 @@ import type { LLOneBot } from './preload'
import { Dict } from 'cosmokit'
declare global {
interface Window {
llonebot: LLOneBot
LiteLoader: Dict
}
var llonebot: LLOneBot
var LiteLoader: Dict
var authData: Dict | undefined
var navigation: Dict | undefined
}

View File

@@ -175,6 +175,9 @@ function onLoad() {
debug: config.debug!,
reportSelfMessage: config.reportSelfMessage!,
msgCacheExpire: config.msgCacheExpire!,
musicSignUrl: config.musicSignUrl,
enableLocalFile2Url: config.enableLocalFile2Url!,
ffmpeg: config.ffmpeg,
})
ctx.start()
ipcMain.on(CHANNEL_SET_CONFIG_CONFIRMED, (event, config: LLOBConfig) => {

View File

@@ -187,7 +187,7 @@ export class NTQQFileApi extends Service {
filePath = data[1].filePath
} else {
const data = await invoke<{ notifyInfo: OnRichMediaDownloadCompleteParams }>(
NTMethod.DOWNLOAD_MEDIA,
'nodeIKernelMsgService/downloadRichMedia',
[
{
getReq: {

View File

@@ -4,7 +4,6 @@ import { invoke, NTMethod, NTClass } from '../ntcall'
import { getSession } from '@/ntqqapi/wrapper'
import { BuddyListReqType, NodeIKernelProfileService } from '../services'
import { NTEventDispatch } from '@/common/utils/eventTask'
import { LimitedHashTable } from '@/common/utils/table'
import { pick } from 'cosmokit'
import { Service, Context } from 'cordis'
@@ -101,8 +100,9 @@ export class NTQQFriendApi extends Service {
}
}
async getBuddyIdMap(refresh = false): Promise<LimitedHashTable<string, string>> {
const retMap: LimitedHashTable<string, string> = new LimitedHashTable<string, string>(5000)
/** uid => uin */
async getBuddyIdMap(refresh = false): Promise<Map<string, string>> {
const retMap: Map<string, string> = new Map()
const session = getSession()
if (session) {
const uids: string[] = []
@@ -112,9 +112,12 @@ export class NTQQFriendApi extends Service {
const data = await NTEventDispatch.CallNoListenerEvent<NodeIKernelProfileService['getCoreAndBaseInfo']>(
'NodeIKernelProfileService/getCoreAndBaseInfo', 5000, 'nodeStore', uids
)
data.forEach((value, key) => {
retMap.set(value.uin!, value.uid!)
})
for (const [, item] of data) {
if (retMap.size > 5000) {
break
}
retMap.set(item.uid!, item.uin!)
}
} else {
const data = await invoke<{
buddyCategory: CategoryFriend[]
@@ -129,7 +132,10 @@ export class NTQQFriendApi extends Service {
}
)
for (const item of Object.values(data.userSimpleInfos)) {
retMap.set(item.uin!, item.uid!)
if (retMap.size > 5000) {
break
}
retMap.set(item.uid!, item.uin!)
}
}
return retMap

View File

@@ -1,11 +1,11 @@
import { ReceiveCmdS } from '../hook'
import { Group, GroupMember, GroupMemberRole, GroupNotifies, GroupRequestOperateTypes, GroupNotify } from '../types'
import { Group, GroupMember, GroupMemberRole, GroupNotifies, GroupRequestOperateTypes, GroupNotify, GetFileListParam } from '../types'
import { invoke, NTClass, NTMethod } from '../ntcall'
import { GeneralCallResult } from '../services'
import { NTQQWindows } from './window'
import { getSession } from '../wrapper'
import { NTEventDispatch } from '@/common/utils/eventTask'
import { NodeIKernelGroupListener } from '../listeners'
import { NodeIKernelGroupListener, OnGroupFileInfoUpdateParams } from '../listeners'
import { NodeIKernelGroupService } from '../services'
import { Service, Context } from 'cordis'
import { isNumeric } from '@/common/utils/misc'
@@ -19,7 +19,7 @@ declare module 'cordis' {
export class NTQQGroupApi extends Service {
static inject = ['ntWindowApi']
private groupMembers: Map<string, Map<string, GroupMember>> = new Map<string, Map<string, GroupMember>>()
public groupMembers: Map<string, Map<string, GroupMember>> = new Map<string, Map<string, GroupMember>>()
constructor(protected ctx: Context) {
super(ctx, 'ntGroupApi', true)
@@ -132,7 +132,7 @@ export class NTQQGroupApi extends Service {
} else {
invoke(ReceiveCmdS.GROUP_NOTIFY, [], { classNameIsRegister: true })
return (await invoke<GroupNotifies>(
NTMethod.GET_GROUP_NOTICE,
'nodeIKernelGroupService/getSingleScreenNotifies',
[{ doubt: false, startSeq: '', number: num }, null],
{
@@ -246,7 +246,7 @@ export class NTQQGroupApi extends Service {
}
}
async getGroupAtAllRemainCount(groupCode: string) {
async getGroupRemainAtTimes(groupCode: string) {
return await invoke<
GeneralCallResult & {
atInfo: {
@@ -301,4 +301,24 @@ export class NTQQGroupApi extends Service {
async deleteGroupFile(groupId: string, fileIdList: string[]) {
return await invoke('nodeIKernelRichMediaService/deleteGroupFile', [{ groupId, busIdList: [102], fileIdList }, null])
}
async getGroupFileList(groupId: string, fileListForm: GetFileListParam) {
invoke('nodeIKernelMsgListener/onGroupFileInfoUpdate', [], { classNameIsRegister: true })
const data = await invoke<{ fileInfo: OnGroupFileInfoUpdateParams }>(
'nodeIKernelRichMediaService/getGroupFileList',
[
{
groupId,
fileListForm
},
null,
],
{
cbCmd: 'nodeIKernelMsgListener/onGroupFileInfoUpdate',
afterFirstCmd: false,
cmdCB: (payload, result) => payload.fileInfo.reqId === result
}
)
return data.fileInfo.item
}
}

View File

@@ -68,6 +68,10 @@ export class NTQQMsgApi extends Service {
return await invoke<GeneralCallResult>(NTMethod.ACTIVE_CHAT_HISTORY, [{ peer, cnt: 20 }, null])
}
async getAioFirstViewLatestMsgs(peer: Peer, cnt: number) {
return await invoke('nodeIKernelMsgService/getAioFirstViewLatestMsgs', [{ peer, cnt }, null])
}
async getMsgsByMsgId(peer: Peer | undefined, msgIds: string[] | undefined) {
if (!peer) throw new Error('peer is not allowed')
if (!msgIds) throw new Error('msgIds is not allowed')
@@ -153,12 +157,7 @@ export class NTQQMsgApi extends Service {
)
msgList = data.msgList
}
const retMsg = msgList.find(msgRecord => {
if (msgRecord.guildId === msgId) {
return true
}
})
return retMsg!
return msgList.find(msgRecord => msgRecord.guildId === msgId)
}
async forwardMsg(srcPeer: Peer, destPeer: Peer, msgIds: string[]) {
@@ -265,22 +264,6 @@ export class NTQQMsgApi extends Service {
}
}
/** 27187 TODO */
async getLastestMsgByUids(peer: Peer, count = 20, isReverseOrder = false) {
const session = getSession()
const ret = await session?.getMsgService().queryMsgsWithFilterEx('0', '0', '0', {
chatInfo: peer,
filterMsgType: [],
filterSendersUid: [],
filterMsgToTime: '0',
filterMsgFromTime: '0',
isReverseOrder: isReverseOrder, //此参数有点离谱 注意不是本次查询的排序 而是全部消历史信息的排序 默认false 从新消息拉取到旧消息
isIncludeCurrent: true,
pageLimit: count,
})
return ret
}
async getSingleMsg(peer: Peer, msgSeq: string) {
const session = getSession()
if (session) {
@@ -289,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

@@ -3,7 +3,7 @@ import { User, UserDetailInfoByUin, UserDetailInfoByUinV2, UserDetailInfoListene
import { getBuildVersion } from '@/common/utils'
import { getSession } from '@/ntqqapi/wrapper'
import { RequestUtil } from '@/common/utils/request'
import { NodeIKernelProfileService, UserDetailSource, ProfileBizType, forceFetchClientKeyRetType } from '../services'
import { NodeIKernelProfileService, UserDetailSource, ProfileBizType } from '../services'
import { NodeIKernelProfileListener } from '../listeners'
import { NTEventDispatch } from '@/common/utils/eventTask'
import { Time } from 'cosmokit'
@@ -17,7 +17,7 @@ declare module 'cordis' {
}
export class NTQQUserApi extends Service {
static inject = ['ntFriendApi']
static inject = ['ntFriendApi', 'ntGroupApi']
constructor(protected ctx: Context) {
super(ctx, 'ntUserApi', true)
@@ -25,7 +25,7 @@ export class NTQQUserApi extends Service {
async setQQAvatar(path: string) {
return await invoke(
NTMethod.SET_QQ_AVATAR,
'nodeIKernelProfileService/setHeader',
[
{ path },
null,
@@ -144,7 +144,7 @@ export class NTQQUserApi extends Service {
}
const uin = selfInfo.uin
const requestUrl = 'https://ssl.ptlogin2.qq.com/jump?ptlang=1033&clientuin=' + uin + '&clientkey=' + clientKeyData.clientKey + '&u1=https%3A%2F%2F' + domain + '%2F' + uin + '%2Finfocenter&keyindex=19%27'
const cookies: { [key: string]: string; } = await RequestUtil.HttpsGetCookies(requestUrl)
const cookies: { [key: string]: string } = await RequestUtil.HttpsGetCookies(requestUrl)
return cookies
}
@@ -187,16 +187,31 @@ export class NTQQUserApi extends Service {
}
}
async getUidByUinV1(Uin: string) {
async getUidByUinV1(uin: string) {
const session = getSession()
// 通用转换开始尝试
let uid = (await session?.getUixConvertService().getUid([Uin]))?.uidInfo.get(Uin)
let uid = (await session?.getUixConvertService().getUid([uin]))?.uidInfo.get(uin)
if (!uid) {
let unveifyUid = (await this.getUserDetailInfoByUin(Uin)).info.uid;//从QQ Native 特殊转换 方法三
if (unveifyUid.indexOf('*') == -1) {
for (const membersList of this.ctx.ntGroupApi.groupMembers.values()) { //从群友列表转
for (const member of membersList.values()) {
if (member.uin === uin) {
uid = member.uid
break
}
}
if (uid) break
}
}
if (!uid) {
const unveifyUid = (await this.getUserDetailInfoByUin(uin)).info.uid //特殊转换
if (unveifyUid.indexOf('*') === -1) {
uid = unveifyUid
}
}
if (!uid) {
const friends = await this.ctx.ntFriendApi.getFriends() //从好友列表转
uid = friends.find(item => item.uin === uin)?.uid
}
return uid
}
@@ -210,22 +225,22 @@ export class NTQQUserApi extends Service {
uid = (await session.getUixConvertService().getUid([uin])).uidInfo.get(uin)
if (uid) return uid
} else {
let uid = (await invoke('nodeIKernelGroupService/getUidByUins', [{ uin: [uin] }, null])).uids.get(uin)
let uid = (await invoke('nodeIKernelGroupService/getUidByUins', [{ uin: [uin] }])).uids.get(uin)
if (uid) return uid
uid = (await invoke('nodeIKernelProfileService/getUidByUin', [{ callFrom: 'FriendsServiceImpl', uin: [uin] }, null])).get(uin)
uid = (await invoke('nodeIKernelProfileService/getUidByUin', [{ callFrom: 'FriendsServiceImpl', uin: [uin] }])).get(uin)
if (uid) return uid
uid = (await invoke('nodeIKernelUixConvertService/getUid', [{ uin: [uin] }, null])).uidInfo.get(uin)
uid = (await invoke('nodeIKernelUixConvertService/getUid', [{ uins: [uin] }])).uidInfo.get(uin)
if (uid) return uid
}
const unveifyUid = (await this.getUserDetailInfoByUinV2(uin)).detail.uid //从QQ Native 特殊转换
if (unveifyUid.indexOf('*') == -1) return unveifyUid
}
async getUidByUin(Uin: string) {
async getUidByUin(uin: string) {
if (getBuildVersion() >= 26702) {
return await this.getUidByUinV2(Uin)
return this.getUidByUinV2(uin)
}
return await this.getUidByUinV1(Uin)
return this.getUidByUinV1(uin)
}
async getUserDetailInfoByUinV2(uin: string) {
@@ -247,25 +262,25 @@ export class NTQQUserApi extends Service {
}
}
async getUserDetailInfoByUin(Uin: string) {
async getUserDetailInfoByUin(uin: string) {
return NTEventDispatch.CallNoListenerEvent
<(Uin: string) => Promise<UserDetailInfoByUin>>(
'NodeIKernelProfileService/getUserDetailInfoByUin',
5000,
Uin
uin
)
}
async getUinByUidV1(Uid: string) {
async getUinByUidV1(uid: string) {
const ret = await NTEventDispatch.CallNoListenerEvent
<(Uin: string[]) => Promise<{ uinInfo: Map<string, string> }>>(
'NodeIKernelUixConvertService/getUin',
5000,
[Uid]
[uid]
)
let uin = ret.uinInfo.get(Uid)
let uin = ret.uinInfo.get(uid)
if (!uin) {
uin = (await this.getUserDetailInfo(Uid)).uin //从QQ Native 转换
uin = (await this.getUserDetailInfo(uid)).uin //从QQ Native 转换
}
return uin
}
@@ -280,24 +295,24 @@ export class NTQQUserApi extends Service {
uin = (await session.getUixConvertService().getUin([uid])).uinInfo.get(uid)
if (uin) return uin
} else {
let uin = (await invoke('nodeIKernelGroupService/getUinByUids', [{ uid: [uid] }, null])).uins.get(uid)
let uin = (await invoke('nodeIKernelGroupService/getUinByUids', [{ uid: [uid] }])).uins.get(uid)
if (uin) return uin
uin = (await invoke('nodeIKernelProfileService/getUinByUid', [{ callFrom: 'FriendsServiceImpl', uid: [uid] }, null])).get(uid)
uin = (await invoke('nodeIKernelProfileService/getUinByUid', [{ callFrom: 'FriendsServiceImpl', uid: [uid] }])).get(uid)
if (uin) return uin
uin = (await invoke('nodeIKernelUixConvertService/getUin', [{ uid: [uid] }, null])).uinInfo.get(uid)
uin = (await invoke('nodeIKernelUixConvertService/getUin', [{ uids: [uid] }])).uinInfo.get(uid)
if (uin) return uin
}
let uin = (await this.ctx.ntFriendApi.getBuddyIdMap(true)).getKey(uid)
let uin = (await this.ctx.ntFriendApi.getBuddyIdMap(true)).get(uid)
if (uin) return uin
uin = (await this.getUserDetailInfo(uid)).uin //从QQ Native 转换
return uin
}
async getUinByUid(Uid: string) {
async getUinByUid(uid: string) {
if (getBuildVersion() >= 26702) {
return (await this.getUinByUidV2(Uid))!
return this.getUinByUidV2(uid)
}
return await this.getUinByUidV1(Uid)
return this.getUinByUidV1(uid)
}
async forceFetchClientKey() {
@@ -319,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)
@@ -52,6 +52,9 @@ class Core extends Service {
MessageUnique.init(selfInfo.uin)
this.registerListener()
this.ctx.logger.info(`LLOneBot/${version}`)
this.ctx.on('llonebot/config-updated', input => {
Object.assign(this.config, input)
})
}
private registerListener() {

View File

@@ -22,7 +22,7 @@ import { encodeSilk } from '../common/utils/audio'
import { Context } from 'cordis'
import { isNullable } from 'cosmokit'
export const mFaceCache = new Map<string, string>() // emojiId -> faceName
//export const mFaceCache = new Map<string, string>() // emojiId -> faceName
export namespace SendElementEntities {
export function text(content: string): SendTextElement {
@@ -147,7 +147,7 @@ export namespace SendElementEntities {
filePath,
}
try {
videoInfo = await getVideoInfo(path)
videoInfo = await getVideoInfo(ctx, path)
ctx.logger.info('视频信息', videoInfo)
} catch (e) {
ctx.logger.info('获取视频信息失败', e)
@@ -295,7 +295,7 @@ export namespace SendElementEntities {
}
}
export function mface(emojiPackageId: number, emojiId: string, key: string, faceName: string): SendMarketFaceElement {
export function mface(emojiPackageId: number, emojiId: string, key: string, summary?: string): SendMarketFaceElement {
return {
elementType: ElementType.MFACE,
marketFaceElement: {
@@ -304,14 +304,13 @@ export namespace SendElementEntities {
emojiPackageId,
emojiId,
key,
faceName: faceName || mFaceCache.get(emojiId) || '[商城表情]',
faceName: summary || '[商城表情]',
},
}
}
export function dice(resultId: number | null): SendFaceElement {
export function dice(resultId?: string | number): SendFaceElement {
// 实际测试并不能控制结果
// 随机1到6
if (isNullable(resultId)) resultId = Math.floor(Math.random() * 6) + 1
return {
@@ -325,7 +324,7 @@ export namespace SendElementEntities {
stickerId: '33',
sourceType: 1,
stickerType: 2,
resultId: resultId?.toString(),
resultId: resultId.toString(),
surpriseId: '',
// "randomType": 1,
},
@@ -333,7 +332,7 @@ export namespace SendElementEntities {
}
// 猜拳(石头剪刀布)表情
export function rps(resultId: number | null): SendFaceElement {
export function rps(resultId?: string | number): SendFaceElement {
// 实际测试并不能控制结果
if (isNullable(resultId)) resultId = Math.floor(Math.random() * 3) + 1
return {
@@ -347,7 +346,7 @@ export namespace SendElementEntities {
stickerId: '34',
sourceType: 1,
stickerType: 2,
resultId: resultId?.toString(),
resultId: resultId.toString(),
surpriseId: '',
// "randomType": 1,
},

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

@@ -23,15 +23,55 @@ export interface OnRichMediaDownloadCompleteParams {
userUsedSpacePerDay: unknown | null
}
export interface onGroupFileInfoUpdateParamType {
export interface OnGroupFileInfoUpdateParams {
retCode: number
retMsg: string
clientWording: string
isEnd: boolean
item: Array<any>
allFileCount: string
nextIndex: string
reqId: string
item: {
peerId: string
type: number
folderInfo?: {
folderId: string
parentFolderId: string
folderName: string
createTime: number
modifyTime: number
createUin: string
creatorName: string
totalFileCount: number
modifyUin: string
modifyName: string
usedSpace: string
}
fileInfo?: {
fileModelId: string
fileId: string
fileName: string
fileSize: string
busId: number
uploadedSize: string
uploadTime: number
deadTime: number
modifyTime: number
downloadTimes: number
sha: string
sha3: string
md5: string
uploaderLocalPath: string
uploaderName: string
uploaderUin: string
parentFolderId: string
localPath: string
transStatus: number
transType: number
elementId: string
isFolder: boolean
}
}[]
allFileCount: number
nextIndex: number
reqId: number
}
// {
@@ -82,7 +122,7 @@ export interface IKernelMsgListener {
onGroupFileInfoAdd(groupItem: unknown): void
onGroupFileInfoUpdate(groupFileListResult: onGroupFileInfoUpdateParamType): void
onGroupFileInfoUpdate(groupFileListResult: OnGroupFileInfoUpdateParams): void
onGroupGuildUpdate(groupGuildNotifyInfo: unknown): void
@@ -295,7 +335,7 @@ export class MsgListener implements IKernelMsgListener {
}
onGroupFileInfoUpdate(groupFileListResult: onGroupFileInfoUpdateParamType) {
onGroupFileInfoUpdate(groupFileListResult: OnGroupFileInfoUpdateParams) {
}

View File

@@ -32,73 +32,48 @@ export enum NTClass {
}
export enum NTMethod {
RECENT_CONTACT = 'nodeIKernelRecentContactService/fetchAndSubscribeABatchOfRecentContact',
ACTIVE_CHAT_PREVIEW = 'nodeIKernelMsgService/getAioFirstViewLatestMsgsAndAddActiveChat', // 激活聊天窗口,有时候必须这样才能收到消息, 并返回最新预览消息
ACTIVE_CHAT_HISTORY = 'nodeIKernelMsgService/getMsgsIncludeSelfAndAddActiveChat', // 激活聊天窗口,有时候必须这样才能收到消息, 并返回历史消息
HISTORY_MSG = 'nodeIKernelMsgService/getMsgsIncludeSelf',
GET_MULTI_MSG = 'nodeIKernelMsgService/getMultiMsg',
DELETE_ACTIVE_CHAT = 'nodeIKernelMsgService/deleteActiveChatByUid',
ENTER_OR_EXIT_AIO = 'nodeIKernelMsgService/enterOrExitAio',
MEDIA_FILE_PATH = 'nodeIKernelMsgService/getRichMediaFilePathForGuild',
RECALL_MSG = 'nodeIKernelMsgService/recallMsg',
EMOJI_LIKE = 'nodeIKernelMsgService/setMsgEmojiLikes',
FORWARD_MSG = 'nodeIKernelMsgService/forwardMsgWithComment',
LIKE_FRIEND = 'nodeIKernelProfileLikeService/setBuddyProfileLike',
SELF_INFO = 'fetchAuthData',
FRIENDS = 'nodeIKernelBuddyService/getBuddyList',
GROUPS = 'nodeIKernelGroupService/getGroupList',
GROUP_MEMBER_SCENE = 'nodeIKernelGroupService/createMemberListScene',
GROUP_MEMBERS = 'nodeIKernelGroupService/getNextMemberList',
GROUP_MEMBERS_INFO = 'nodeIKernelGroupService/getMemberInfo',
USER_INFO = 'nodeIKernelProfileService/getUserSimpleInfo',
USER_DETAIL_INFO = 'nodeIKernelProfileService/getUserDetailInfo',
USER_DETAIL_INFO_WITH_BIZ_INFO = 'nodeIKernelProfileService/getUserDetailInfoWithBizInfo',
FILE_TYPE = 'getFileType',
FILE_MD5 = 'getFileMd5',
FILE_COPY = 'copyFile',
IMAGE_SIZE = 'getImageSizeFromPath',
FILE_SIZE = 'getFileSize',
MEDIA_FILE_PATH = 'nodeIKernelMsgService/getRichMediaFilePathForGuild',
CACHE_PATH_HOT_UPDATE = 'getHotUpdateCachePath',
CACHE_PATH_DESKTOP_TEMP = 'getDesktopTmpPath',
CACHE_PATH_SESSION = 'getCleanableAppSessionPathList',
OPEN_EXTRA_WINDOW = 'openExternalWindow',
RECALL_MSG = 'nodeIKernelMsgService/recallMsg',
SEND_MSG = 'nodeIKernelMsgService/sendMsg',
EMOJI_LIKE = 'nodeIKernelMsgService/setMsgEmojiLikes',
DOWNLOAD_MEDIA = 'nodeIKernelMsgService/downloadRichMedia',
FORWARD_MSG = 'nodeIKernelMsgService/forwardMsgWithComment',
MULTI_FORWARD_MSG = 'nodeIKernelMsgService/multiForwardMsgWithComment', // 合并转发
GET_GROUP_NOTICE = 'nodeIKernelGroupService/getSingleScreenNotifies',
GROUP_MEMBER_SCENE = 'nodeIKernelGroupService/createMemberListScene',
GROUP_MEMBERS = 'nodeIKernelGroupService/getNextMemberList',
HANDLE_GROUP_REQUEST = 'nodeIKernelGroupService/operateSysNotify',
QUIT_GROUP = 'nodeIKernelGroupService/quitGroup',
GROUP_AT_ALL_REMAIN_COUNT = 'nodeIKernelGroupService/getGroupRemainAtTimes',
HANDLE_FRIEND_REQUEST = 'nodeIKernelBuddyService/approvalFriendRequest',
KICK_MEMBER = 'nodeIKernelGroupService/kickMember',
MUTE_MEMBER = 'nodeIKernelGroupService/setMemberShutUp',
MUTE_GROUP = 'nodeIKernelGroupService/setGroupShutUp',
SET_MEMBER_CARD = 'nodeIKernelGroupService/modifyMemberCardName',
SET_MEMBER_ROLE = 'nodeIKernelGroupService/modifyMemberRole',
PUBLISH_GROUP_BULLETIN = 'nodeIKernelGroupService/publishGroupBulletinBulletin',
SET_GROUP_NAME = 'nodeIKernelGroupService/modifyGroupName',
SET_GROUP_TITLE = 'nodeIKernelGroupService/modifyMemberSpecialTitle',
ACTIVATE_MEMBER_LIST_CHANGE = 'nodeIKernelGroupListener/onMemberListChange',
ACTIVATE_MEMBER_INFO_CHANGE = 'nodeIKernelGroupListener/onMemberInfoChange',
GET_MSG_BOX_INFO = 'nodeIKernelMsgService/getABatchOfContactMsgBoxInfo',
GET_GROUP_ALL_INFO = 'nodeIKernelGroupService/getGroupAllInfo',
HANDLE_FRIEND_REQUEST = 'nodeIKernelBuddyService/approvalFriendRequest',
CACHE_SET_SILENCE = 'nodeIKernelStorageCleanService/setSilentScan',
CACHE_ADD_SCANNED_PATH = 'nodeIKernelStorageCleanService/addCacheScanedPaths',
CACHE_PATH_HOT_UPDATE = 'getHotUpdateCachePath',
CACHE_PATH_DESKTOP_TEMP = 'getDesktopTmpPath',
CACHE_PATH_SESSION = 'getCleanableAppSessionPathList',
CACHE_SCAN = 'nodeIKernelStorageCleanService/scanCache',
CACHE_CLEAR = 'nodeIKernelStorageCleanService/clearCacheDataByKeys',
CACHE_CHAT_GET = 'nodeIKernelStorageCleanService/getChatCacheInfo',
CACHE_FILE_GET = 'nodeIKernelStorageCleanService/getFileCacheInfo',
CACHE_CHAT_CLEAR = 'nodeIKernelStorageCleanService/clearChatCacheInfo',
OPEN_EXTRA_WINDOW = 'openExternalWindow',
SET_QQ_AVATAR = 'nodeIKernelProfileService/setHeader',
}
export enum NTChannel {
@@ -127,43 +102,43 @@ interface InvokeOptions<ReturnType> {
channel?: NTChannel
classNameIsRegister?: boolean
cbCmd?: string | string[]
cmdCB?: (payload: ReturnType) => boolean
cmdCB?: (payload: ReturnType, result: unknown) => boolean
afterFirstCmd?: boolean // 是否在methodName调用完之后再去hook cbCmd
timeout?: number
}
export function invoke<
R extends Awaited<ReturnType<NTService[S][M] extends (...args: any) => any ? NTService[S][M] : any>>,
R extends Awaited<ReturnType<Extract<NTService[S][M], (...args: any) => any>>>,
S extends keyof NTService = any,
M extends keyof NTService[S] & string = any
>(method: `${unknown extends `${S}/${M}` ? `${S}/${M}` : string}`, args?: unknown[], options: InvokeOptions<R> = {}) {
>(method: Extract<unknown, `${S}/${M}`> | string, args: unknown[], options: InvokeOptions<R> = {}) {
const className = options.className ?? NTClass.NT_API
const channel = options.channel ?? NTChannel.IPC_UP_2
const timeout = options.timeout ?? 5000
const afterFirstCmd = options.afterFirstCmd ?? true
const uuid = randomUUID()
let eventName = className + '-' + channel[channel.length - 1]
if (options.classNameIsRegister) {
eventName += '-register'
}
const apiArgs = [method, ...(args ?? [])]
//log('callNTQQApi', channel, eventName, apiArgs, uuid)
return new Promise((resolve: (data: R) => void, reject) => {
return new Promise<R>((resolve, reject) => {
const apiArgs = [method, ...args]
const callbackId = randomUUID()
let success = false
if (!options.cbCmd) {
// QQ后端会返回结果并且可以根据uuid识别
hookApiCallbacks[uuid] = (r: R) => {
hookApiCallbacks[callbackId] = res => {
success = true
resolve(r)
resolve(res)
}
}
else {
let result: unknown
// 这里的callback比较特殊QQ后端先返回是否调用成功再返回一条结果数据
const secondCallback = () => {
const hookId = registerReceiveHook<R>(options.cbCmd!, (payload) => {
// log(methodName, "second callback", cbCmd, payload, cmdCB);
if (!!options.cmdCB) {
if (options.cmdCB(payload)) {
if (options.cmdCB(payload, result)) {
removeReceiveHook(hookId)
success = true
resolve(payload)
@@ -177,14 +152,14 @@ export function invoke<
})
}
!afterFirstCmd && secondCallback()
hookApiCallbacks[uuid] = (result: GeneralCallResult) => {
if (result?.result === 0 || result === undefined) {
//log(`${params.methodName} callback`, result)
hookApiCallbacks[callbackId] = (res: GeneralCallResult) => {
result = res
if (res?.result === 0 || ['undefined', 'number'].includes(typeof res)) {
afterFirstCmd && secondCallback()
}
else {
log('ntqq api call failed,', method, result)
reject(`ntqq api call failed, ${method}, ${result.errMsg}`)
log('ntqq api call failed,', method, res)
reject(`ntqq api call failed, ${method}, ${res.errMsg}`)
}
}
}
@@ -203,7 +178,7 @@ export function invoke<
},
},
},
{ type: 'request', callbackId: uuid, eventName },
{ type: 'request', callbackId, eventName },
apiArgs,
)
})

View File

@@ -168,9 +168,10 @@ export interface NodeIKernelMsgService {
getLastMessageList(peer: Peer[]): Promise<unknown>
getAioFirstViewLatestMsgs(peer: Peer, num: number): unknown
getAioFirstViewLatestMsgs(peer: Peer, num: number): Promise<GeneralCallResult & {
msgList: RawMessage[]
}>
//deprecated 从9.9.15-26702版本开始该接口已经废弃请使用getMsgsEx
getMsgs(peer: Peer, msgId: string, count: unknown, queryOrder: boolean): Promise<unknown>
getMsgsIncludeSelf(peer: Peer, msgId: string, count: number, queryOrder: boolean): Promise<GeneralCallResult & {

View File

@@ -177,8 +177,6 @@ export interface NodeIKernelRichMediaService {
renameGroupFolder(arg1: unknown, arg2: unknown, arg3: unknown): unknown
deleteGroupFolder(arg1: unknown, arg2: unknown): unknown
deleteTransferInfo(arg1: unknown, arg2: unknown): unknown
cancelTransferTask(arg1: unknown, arg2: unknown, arg3: unknown): unknown

View File

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

View File

@@ -275,7 +275,7 @@ export interface PicElement {
thumbPath: Map<number, string>
picWidth: number
picHeight: number
fileSize: number
fileSize: string
fileName: string
fileUuid: string
md5HexStr?: string
@@ -480,6 +480,8 @@ export interface RawMessage {
sourceMsgIsIncPic: boolean // 原消息是否有图片
sourceMsgText: string
replayMsgSeq: string // 源消息的msgSeq可以通过这个找到源消息的msgId
senderUidStr: string
replyMsgTime: string
}
textElement: {
atType: AtType

View File

@@ -11,7 +11,8 @@ import {
NodeIKernelTipOffService,
NodeIKernelSearchService
} from './services'
import os from 'node:os'
import { constants } from 'node:os'
import { Dict } from 'cosmokit'
const Process = require('node:process')
export interface NodeIQQNTWrapperSession {
@@ -72,7 +73,7 @@ const constructor = [
Process.dlopenOrig = Process.dlopen
Process.dlopen = function (module, filename, flags = os.constants.dlopen.RTLD_LAZY) {
Process.dlopen = function (module: Dict, filename: string, flags = constants.dlopen.RTLD_LAZY) {
const dlopenRet = this.dlopenOrig(module, filename, flags)
for (let export_name in module.exports) {
module.exports[export_name] = new Proxy(module.exports[export_name], {

View File

@@ -1,6 +1,5 @@
import BaseAction from '../BaseAction'
import fsPromise from 'node:fs/promises'
import { getConfigUtil } from '@/common/config'
import { ActionName } from '../types'
import { Peer, ElementType } from '@/ntqqapi/types'
import { MessageUnique } from '@/common/utils/messageUnique'
@@ -20,7 +19,7 @@ export interface GetFileResponse {
export abstract class GetFileBase extends BaseAction<GetFilePayload, GetFileResponse> {
// forked from https://github.com/NapNeko/NapCatQQ/blob/6f6b258f22d7563f15d84e7172c4d4cbb547f47e/src/onebot11/action/file/GetFile.ts#L44
protected async _handle(payload: GetFilePayload): Promise<GetFileResponse> {
const { enableLocalFile2Url } = getConfigUtil().getConfig()
const { enableLocalFile2Url } = this.adapter.config
let fileCache = await MessageUnique.getFileCacheById(String(payload.file))
if (!fileCache?.length) {

View File

@@ -1,7 +1,6 @@
import { GetFileBase, GetFilePayload, GetFileResponse } from './GetFile'
import { ActionName } from '../types'
import { decodeSilk } from '@/common/utils/audio'
import { getConfigUtil } from '@/common/config'
import path from 'node:path'
import fs from 'node:fs'
@@ -17,7 +16,7 @@ export default class GetRecord extends GetFileBase {
res.file = await decodeSilk(this.ctx, res.file!, payload.out_format)
res.file_name = path.basename(res.file)
res.file_size = fs.statSync(res.file).size.toString()
if (getConfigUtil().getConfig().enableLocalFile2Url) {
if (this.adapter.config.enableLocalFile2Url) {
res.base64 = fs.readFileSync(res.file, 'base64')
}
return res

View File

@@ -7,7 +7,7 @@ interface Payload {
parent_id?: '/'
}
export class GoCQHTTPCreateGroupFileFolder extends BaseAction<Payload, null> {
export class CreateGroupFileFolder extends BaseAction<Payload, null> {
actionName = ActionName.GoCQHTTP_CreateGroupFileFolder
async _handle(payload: Payload) {

View File

@@ -6,7 +6,7 @@ interface Payload {
message_id: number | string
}
export default class GoCQHTTPDelEssenceMsg extends BaseAction<Payload, any> {
export class DelEssenceMsg extends BaseAction<Payload, any> {
actionName = ActionName.GoCQHTTP_DelEssenceMsg;
protected async _handle(payload: Payload): Promise<any> {

View File

@@ -7,7 +7,7 @@ interface Payload {
busid?: 102
}
export class GoCQHTTPDelGroupFile extends BaseAction<Payload, null> {
export class DelGroupFile extends BaseAction<Payload, null> {
actionName = ActionName.GoCQHTTP_DelGroupFile
async _handle(payload: Payload) {

View File

@@ -6,7 +6,7 @@ interface Payload {
folder_id: string
}
export class GoCQHTTPDelGroupFolder extends BaseAction<Payload, null> {
export class DelGroupFolder extends BaseAction<Payload, null> {
actionName = ActionName.GoCQHTTP_DelGroupFolder
async _handle(payload: Payload) {

View File

@@ -6,6 +6,7 @@ import { ActionName } from '../types'
import { calculateFileMD5, fetchFile } from '@/common/utils'
import { TEMP_DIR } from '@/common/globalVars'
import { randomUUID } from 'node:crypto'
import { Dict } from 'cosmokit'
interface Payload {
thread_count?: number
@@ -19,7 +20,7 @@ interface FileResponse {
file: string
}
export default class GoCQHTTPDownloadFile extends BaseAction<Payload, FileResponse> {
export class DownloadFile extends BaseAction<Payload, FileResponse> {
actionName = ActionName.GoCQHTTP_DownloadFile
protected async _handle(payload: Payload): Promise<FileResponse> {
@@ -51,7 +52,7 @@ export default class GoCQHTTPDownloadFile extends BaseAction<Payload, FileRespon
}
getHeaders(headersIn?: string | string[]): Record<string, string> {
const headers = {}
const headers: Dict = {}
if (typeof headersIn == 'string') {
headersIn = headersIn.split('[\\r\\n]')
}

View File

@@ -13,7 +13,7 @@ interface Response {
messages: (OB11Message & { content: OB11MessageData })[]
}
export class GoCQHTTGetForwardMsgAction extends BaseAction<Payload, Response> {
export class GetForwardMsg extends BaseAction<Payload, Response> {
actionName = ActionName.GoCQHTTP_GetForwardMsg
protected async _handle(payload: Payload): Promise<any> {
const msgId = payload.id || payload.message_id

View File

@@ -0,0 +1,25 @@
import BaseAction from '../BaseAction'
import { ActionName } from '../types'
interface Payload {
group_id: number | string
}
interface Response {
can_at_all: boolean
remain_at_all_count_for_group: number
remain_at_all_count_for_uin: number
}
export class GetGroupAtAllRemain extends BaseAction<Payload, Response> {
actionName = ActionName.GoCQHTTP_GetGroupAtAllRemain
async _handle(payload: Payload) {
const data = await this.ctx.ntGroupApi.getGroupRemainAtTimes(payload.group_id.toString())
return {
can_at_all: data.atInfo.canAtAll,
remain_at_all_count_for_group: data.atInfo.RemainAtAllCountForGroup,
remain_at_all_count_for_uin: data.atInfo.RemainAtAllCountForUin
}
}
}

View File

@@ -8,8 +8,8 @@ import { MessageUnique } from '@/common/utils/messageUnique'
interface Payload {
group_id: number | string
message_seq?: number
count?: number
message_seq?: number | string
count?: number | string
reverseOrder?: boolean
}
@@ -17,7 +17,7 @@ interface Response {
messages: OB11Message[]
}
export default class GoCQHTTPGetGroupMsgHistory extends BaseAction<Payload, Response> {
export class GetGroupMsgHistory extends BaseAction<Payload, Response> {
actionName = ActionName.GoCQHTTP_GetGroupMsgHistory
protected async _handle(payload: Payload): Promise<Response> {
@@ -27,13 +27,13 @@ export default class GoCQHTTPGetGroupMsgHistory extends BaseAction<Payload, Resp
let msgList: RawMessage[] | undefined
// 包含 message_seq 0
if (!payload.message_seq) {
msgList = (await this.ctx.ntMsgApi.getLastestMsgByUids(peer, count))?.msgList
msgList = (await this.ctx.ntMsgApi.getAioFirstViewLatestMsgs(peer, +count)).msgList
} else {
const startMsgId = (await MessageUnique.getMsgIdAndPeerByShortId(payload.message_seq))?.MsgId
if (!startMsgId) throw `消息${payload.message_seq}不存在`
msgList = (await this.ctx.ntMsgApi.getMsgHistory(peer, startMsgId, count)).msgList
const startMsgId = (await MessageUnique.getMsgIdAndPeerByShortId(+payload.message_seq))?.MsgId
if (!startMsgId) throw new Error(`消息${payload.message_seq}不存在`)
msgList = (await this.ctx.ntMsgApi.getMsgHistory(peer, startMsgId, +count)).msgList
}
if (!msgList?.length) throw '未找到消息'
if (!msgList?.length) throw new Error('未找到消息')
if (isReverseOrder) msgList.reverse()
await Promise.all(
msgList.map(async msg => {

View File

@@ -0,0 +1,62 @@
import BaseAction from '../BaseAction'
import { ActionName } from '../types'
import { OB11GroupFile, OB11GroupFileFolder } from '../../types'
interface Payload {
group_id: string | number
file_count: string | number
}
interface Response {
files: OB11GroupFile[]
folders: OB11GroupFileFolder[]
}
export class GetGroupRootFiles extends BaseAction<Payload, Response> {
actionName = ActionName.GoCQHTTP_GetGroupRootFiles
async _handle(payload: Payload) {
const data = await this.ctx.ntGroupApi.getGroupFileList(payload.group_id.toString(), {
sortType: 1,
fileCount: +(payload.file_count ?? 50),
startIndex: 0,
sortOrder: 2,
showOnlinedocFolder: 0,
})
this.ctx.logger.info(data)
return {
files: data.filter(item => item.fileInfo)
.map(item => {
const file = item.fileInfo!
return {
group_id: +item.peerId,
file_id: file.fileId,
file_name: file.fileName,
busid: file.busId,
file_size: +file.fileSize,
upload_time: file.uploadTime,
dead_time: file.deadTime,
modify_time: file.modifyTime,
download_times: file.downloadTimes,
uploader: +file.uploaderUin,
uploader_name: file.uploaderName
}
}),
folders: data.filter(item => item.folderInfo)
.map(item => {
const folder = item.folderInfo!
return {
group_id: +item.peerId,
folder_id: folder.folderId,
folder_name: folder.folderName,
create_time: folder.createTime,
creator: +folder.createUin,
creator_name: folder.creatorName,
total_file_count: folder.totalFileCount
}
})
}
}
}

View File

@@ -24,7 +24,7 @@ interface Response {
}[]
}
export class GoCQHTTPGetGroupSystemMsg extends BaseAction<void, Response> {
export class GetGroupSystemMsg extends BaseAction<void, Response> {
actionName = ActionName.GoCQHTTP_GetGroupSystemMsg
async _handle(payload: void) {

View File

@@ -10,7 +10,7 @@ interface Payload {
user_id: number | string
}
export default class GoCQHTTPGetStrangerInfo extends BaseAction<Payload, OB11User> {
export class GetStrangerInfo extends BaseAction<Payload, OB11User> {
actionName = ActionName.GoCQHTTP_GetStrangerInfo
protected async _handle(payload: Payload): Promise<OB11User> {

View File

@@ -5,7 +5,7 @@ interface Payload {
message_id: number
}
export default class GoCQHTTPMarkMsgAsRead extends BaseAction<Payload, null> {
export class MarkMsgAsRead extends BaseAction<Payload, null> {
actionName = ActionName.GoCQHTTP_MarkMsgAsRead
protected async _handle(payload: Payload): Promise<null> {

View File

@@ -7,7 +7,7 @@ interface Payload {
operation: QuickOperation
}
export class GoCQHTTHandleQuickOperation extends BaseAction<Payload, null> {
export class HandleQuickOperation extends BaseAction<Payload, null> {
actionName = ActionName.GoCQHTTP_HandleQuickOperation
protected async _handle(payload: Payload): Promise<null> {
handleQuickOperation(this.ctx, payload.context, payload.operation).catch(e => this.ctx.logger.error(e))

View File

@@ -3,7 +3,7 @@ import { OB11PostSendMsg } from '../../types'
import { ActionName } from '../types'
import { convertMessage2List } from '../../helper/createMessage'
export class GoCQHTTPSendForwardMsg extends SendMsg {
export class SendForwardMsg extends SendMsg {
actionName = ActionName.GoCQHTTP_SendForwardMsg
protected async check(payload: OB11PostSendMsg) {
@@ -12,10 +12,10 @@ export class GoCQHTTPSendForwardMsg extends SendMsg {
}
}
export class GoCQHTTPSendPrivateForwardMsg extends GoCQHTTPSendForwardMsg {
export class SendPrivateForwardMsg extends SendForwardMsg {
actionName = ActionName.GoCQHTTP_SendPrivateForwardMsg
}
export class GoCQHTTPSendGroupForwardMsg extends GoCQHTTPSendForwardMsg {
export class SendGroupForwardMsg extends SendForwardMsg {
actionName = ActionName.GoCQHTTP_SendGroupForwardMsg
}

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

@@ -6,7 +6,7 @@ interface Payload {
message_id: number | string
}
export default class GoCQHTTPSetEssenceMsg extends BaseAction<Payload, any> {
export class SetEssenceMsg extends BaseAction<Payload, any> {
actionName = ActionName.GoCQHTTP_SetEssenceMsg;
protected async _handle(payload: Payload): Promise<any> {

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 GoCQHTTPUploadGroupFile 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 GoCQHTTPUploadGroupFile 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 GoCQHTTPUploadPrivateFile 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

@@ -16,11 +16,11 @@ import CanSendRecord from './system/CanSendRecord'
import CanSendImage from './system/CanSendImage'
import GetStatus from './system/GetStatus'
import {
GoCQHTTPSendForwardMsg,
GoCQHTTPSendGroupForwardMsg,
GoCQHTTPSendPrivateForwardMsg,
SendForwardMsg,
SendGroupForwardMsg,
SendPrivateForwardMsg,
} from './go-cqhttp/SendForwardMsg'
import GoCQHTTPGetStrangerInfo from './go-cqhttp/GetStrangerInfo'
import { GetStrangerInfo } from './go-cqhttp/GetStrangerInfo'
import SendLike from './user/SendLike'
import SetGroupAddRequest from './group/SetGroupAddRequest'
import SetGroupLeave from './group/SetGroupLeave'
@@ -35,29 +35,33 @@ import SetGroupAdmin from './group/SetGroupAdmin'
import SetGroupCard from './group/SetGroupCard'
import GetImage from './file/GetImage'
import GetRecord from './file/GetRecord'
import GoCQHTTPMarkMsgAsRead from './msg/MarkMsgAsRead'
import { MarkMsgAsRead } from './go-cqhttp/MarkMsgAsRead'
import CleanCache from './system/CleanCache'
import { GoCQHTTPUploadGroupFile, GoCQHTTPUploadPrivateFile } from './go-cqhttp/UploadFile'
import { UploadGroupFile, UploadPrivateFile } from './go-cqhttp/UploadFile'
import { GetConfigAction, SetConfigAction } from './llonebot/Config'
import GetGroupAddRequest from './llonebot/GetGroupAddRequest'
import SetQQAvatar from './llonebot/SetQQAvatar'
import GoCQHTTPDownloadFile from './go-cqhttp/DownloadFile'
import GoCQHTTPGetGroupMsgHistory from './go-cqhttp/GetGroupMsgHistory'
import { DownloadFile } from './go-cqhttp/DownloadFile'
import { GetGroupMsgHistory } from './go-cqhttp/GetGroupMsgHistory'
import GetFile from './file/GetFile'
import { GoCQHTTGetForwardMsgAction } from './go-cqhttp/GetForwardMsg'
import { GetForwardMsg } from './go-cqhttp/GetForwardMsg'
import { GetCookies } from './user/GetCookie'
import { SetMsgEmojiLike } from './msg/SetMsgEmojiLike'
import { ForwardFriendSingleMsg, ForwardGroupSingleMsg } from './msg/ForwardSingleMsg'
import { GetGroupEssence } from './group/GetGroupEssence'
import { GetGroupHonorInfo } from './group/GetGroupHonorInfo'
import { GoCQHTTHandleQuickOperation } from './go-cqhttp/QuickOperation'
import GoCQHTTPSetEssenceMsg from './go-cqhttp/SetEssenceMsg'
import GoCQHTTPDelEssenceMsg from './go-cqhttp/DelEssenceMsg'
import { HandleQuickOperation } from './go-cqhttp/QuickOperation'
import { SetEssenceMsg } from './go-cqhttp/SetEssenceMsg'
import { DelEssenceMsg } from './go-cqhttp/DelEssenceMsg'
import GetEvent from './llonebot/GetEvent'
import { GoCQHTTPDelGroupFile } from './go-cqhttp/DelGroupFile'
import { GoCQHTTPGetGroupSystemMsg } from './go-cqhttp/GetGroupSystemMsg'
import { GoCQHTTPCreateGroupFileFolder } from './go-cqhttp/CreateGroupFileFolder'
import { GoCQHTTPDelGroupFolder } from './go-cqhttp/DelGroupFolder'
import { DelGroupFile } from './go-cqhttp/DelGroupFile'
import { GetGroupSystemMsg } from './go-cqhttp/GetGroupSystemMsg'
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 = [
@@ -69,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),
@@ -105,24 +110,27 @@ export function initActionMap(adapter: Adapter) {
//以下为go-cqhttp api
new GetGroupEssence(adapter),
new GetGroupHonorInfo(adapter),
new GoCQHTTPSendForwardMsg(adapter),
new GoCQHTTPSendGroupForwardMsg(adapter),
new GoCQHTTPSendPrivateForwardMsg(adapter),
new GoCQHTTPGetStrangerInfo(adapter),
new GoCQHTTPDownloadFile(adapter),
new SendForwardMsg(adapter),
new SendGroupForwardMsg(adapter),
new SendPrivateForwardMsg(adapter),
new GetStrangerInfo(adapter),
new DownloadFile(adapter),
new GetGuildList(adapter),
new GoCQHTTPMarkMsgAsRead(adapter),
new GoCQHTTPUploadGroupFile(adapter),
new GoCQHTTPUploadPrivateFile(adapter),
new GoCQHTTPGetGroupMsgHistory(adapter),
new GoCQHTTGetForwardMsgAction(adapter),
new GoCQHTTHandleQuickOperation(adapter),
new GoCQHTTPSetEssenceMsg(adapter),
new GoCQHTTPDelEssenceMsg(adapter),
new GoCQHTTPDelGroupFile(adapter),
new GoCQHTTPGetGroupSystemMsg(adapter),
new GoCQHTTPCreateGroupFileFolder(adapter),
new GoCQHTTPDelGroupFolder(adapter)
new MarkMsgAsRead(adapter),
new UploadGroupFile(adapter),
new UploadPrivateFile(adapter),
new GetGroupMsgHistory(adapter),
new GetForwardMsg(adapter),
new HandleQuickOperation(adapter),
new SetEssenceMsg(adapter),
new DelEssenceMsg(adapter),
new DelGroupFile(adapter),
new GetGroupSystemMsg(adapter),
new CreateGroupFileFolder(adapter),
new DelGroupFolder(adapter),
new GetGroupAtAllRemain(adapter),
new GetGroupRootFiles(adapter),
new SendGroupNotice(adapter)
]
const actionMap = new Map<string, BaseAction<any, any>>()
for (const action of actionHandlers) {

View File

@@ -14,8 +14,8 @@ export default class Debug extends BaseAction<Payload, any> {
const { ntMsgApi, ntFileApi, ntFileCacheApi, ntFriendApi, ntGroupApi, ntUserApi, ntWindowApi } = this.ctx
const ntqqApi = [ntMsgApi, ntFriendApi, ntGroupApi, ntUserApi, ntFileApi, ntFileCacheApi, ntWindowApi]
for (const ntqqApiClass of ntqqApi) {
const method = ntqqApiClass[payload.method] as Function
if (method) {
const method = ntqqApiClass[payload.method as keyof typeof ntqqApiClass]
if (method && method instanceof Function) {
const result = method.apply(ntqqApiClass, payload.args)
if (method.constructor.name === 'AsyncFunction') {
return await result

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

@@ -11,13 +11,17 @@ class DeleteMsg extends BaseAction<Payload, void> {
protected async _handle(payload: Payload) {
if (!payload.message_id) {
throw Error('message_id不能为空')
throw new Error('参数message_id不能为空')
}
const msg = await MessageUnique.getMsgIdAndPeerByShortId(+payload.message_id)
if (!msg) {
throw `消息${payload.message_id}不存在`
throw new Error(`消息${payload.message_id}不存在`)
}
const data = await this.ctx.ntMsgApi.recallMsg(msg.Peer, [msg.MsgId])
if (data.result !== 0) {
this.ctx.logger.error('delete_msg', payload.message_id, data)
throw new Error(`消息撤回失败`)
}
await this.ctx.ntMsgApi.recallMsg(msg.Peer, [msg.MsgId])
}
}

View File

@@ -13,53 +13,22 @@ import {
OB11MessageNode,
OB11PostSendMsg,
} from '../../types'
import fs from 'node:fs'
import BaseAction from '../BaseAction'
import { ActionName, BaseCheckResult } from '../types'
import fs from 'node:fs'
import { getConfigUtil } from '@/common/config'
import { CustomMusicSignPostData, IdMusicSignPostData, MusicSign, MusicSignPostData } from '@/common/utils/sign'
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())
const isBuddy = await this.ctx.ntFriendApi.isBuddy(Uid!)
//console.log("[调试代码] UIN:", payload.user_id, " UID:", Uid, " IsBuddy:", isBuddy)
return {
chatType: isBuddy ? ChatType.friend : ChatType.temp,
peerUid: Uid!,
guildId: payload.group_id?.toString() || '' //临时主动发起时需要传入群号
}
}
throw '请指定 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 +53,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',
@@ -106,7 +75,7 @@ export class SendMsg extends BaseAction<OB11PostSendMsg, ReturnDataType> {
else if (this.getSpecialMsgNum(messages, OB11MessageDataType.music)) {
const music = messages[0] as OB11MessageMusic
if (music) {
const { musicSignUrl } = getConfigUtil().getConfig()
const { musicSignUrl } = this.adapter.config
if (!musicSignUrl) {
throw '音乐签名地址未配置'
}
@@ -160,6 +129,9 @@ export class SendMsg extends BaseAction<OB11PostSendMsg, ReturnDataType> {
}
}
const returnMsg = await sendMsg(this.ctx, peer, sendElements, deleteAfterSentFiles)
if (!returnMsg) {
throw new Error('消息发送失败')
}
return { message_id: returnMsg.msgShortId! }
}
@@ -251,9 +223,12 @@ export class SendMsg extends BaseAction<OB11PostSendMsg, ReturnDataType> {
// log("分割后的转发节点", sendElementsSplit)
for (const eles of sendElementsSplit) {
const nodeMsg = await sendMsg(this.ctx, selfPeer, eles, [], true)
if (!nodeMsg) {
this.ctx.logger.warn('转发节点生成失败', eles)
continue
}
nodeMsgIds.push(nodeMsg.msgId)
await this.ctx.sleep(400)
this.ctx.logger.info('转发节点生成成功', nodeMsg.msgId)
}
deleteAfterSentFiles.map((f) => fs.unlink(f, () => {
}))

View File

@@ -8,7 +8,7 @@ interface ReturnType {
export default class CanSendRecord extends BaseAction<any, ReturnType> {
actionName = ActionName.CanSendRecord
protected async _handle(payload): Promise<ReturnType> {
protected async _handle(payload: void): Promise<ReturnType> {
return {
yes: 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',
@@ -76,5 +77,8 @@ export enum ActionName {
GoCQHTTP_DelGroupFile = 'delete_group_file',
GoCQHTTP_GetGroupSystemMsg = 'get_group_system_msg',
GoCQHTTP_CreateGroupFileFolder = 'create_group_file_folder',
GoCQHTTP_DelGroupFolder = 'delete_group_folder'
GoCQHTTP_DelGroupFolder = 'delete_group_folder',
GoCQHTTP_GetGroupAtAllRemain = 'get_group_at_all_remain',
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 = ''
@@ -340,10 +342,13 @@ class OneBot11Adapter extends Service {
Object.assign(this.config, {
...config.ob11,
heartInterval: config.heartInterval,
token: config.token!,
debug: config.debug!,
reportSelfMessage: config.reportSelfMessage!,
msgCacheExpire: config.msgCacheExpire!,
token: config.token,
debug: config.debug,
reportSelfMessage: config.reportSelfMessage,
msgCacheExpire: config.msgCacheExpire,
musicSignUrl: config.musicSignUrl,
enableLocalFile2Url: config.enableLocalFile2Url,
ffmpeg: config.ffmpeg
})
}
@@ -415,6 +420,9 @@ namespace OneBot11Adapter {
debug: boolean
reportSelfMessage: boolean
msgCacheExpire: number
musicSignUrl?: string
enableLocalFile2Url: boolean
ffmpeg?: string
}
}

View File

@@ -10,6 +10,7 @@ import { OB11Message } from '../types'
import { OB11BaseEvent } from '../event/OB11BaseEvent'
import { handleQuickOperation, QuickOperationEvent } from '../helper/quickOperation'
import { OB11HeartbeatEvent } from '../event/meta/OB11HeartbeatEvent'
import { Dict } from 'cosmokit'
type RegisterHandler = (res: Response, payload: any) => Promise<any>
@@ -50,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) {
@@ -135,6 +137,7 @@ namespace OB11Http {
port: number
token?: string
actionMap: Map<string, BaseAction<any, any>>
listenLocalhost: boolean
}
}
@@ -159,7 +162,7 @@ class OB11HttpPost {
public async emitEvent(event: OB11BaseEvent | OB11Message) {
const msgStr = JSON.stringify(event)
const headers = {
const headers: Dict = {
'Content-Type': 'application/json',
'x-self-id': selfInfo.uin,
}

View File

@@ -14,16 +14,21 @@ import { version } from '../../version'
class OB11WebSocket {
private wsServer?: WebSocketServer
private wsClients: WebSocket[] = []
private wsClients: { socket: WebSocket; emitEvent: boolean }[] = []
constructor(protected ctx: Context, public config: OB11WebSocket.Config) {
}
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()
@@ -31,7 +36,7 @@ class OB11WebSocket {
}
this.wsServer?.on('connection', (socket, req) => {
this.authorize(socket, req)
this.connect(socket)
this.connect(socket, req)
})
}
@@ -53,8 +58,8 @@ class OB11WebSocket {
}
public async emitEvent(event: OB11BaseEvent | OB11Message) {
this.wsClients.forEach(socket => {
if (socket.readyState === WebSocket.OPEN) {
this.wsClients.forEach(({ socket, emitEvent }) => {
if (emitEvent && socket.readyState === WebSocket.OPEN) {
socket.send(JSON.stringify(event))
this.ctx.logger.info('WebSocket 事件上报', socket.url ?? '', event.post_type)
}
@@ -70,8 +75,8 @@ class OB11WebSocket {
return
}
socket.send(JSON.stringify(data))
if (data['post_type']) {
this.ctx.logger.info('WebSocket 事件上报', socket.url ?? '', data['post_type'])
if ('post_type' in data) {
this.ctx.logger.info('WebSocket 事件上报', socket.url ?? '', data.post_type)
}
}
@@ -122,33 +127,40 @@ class OB11WebSocket {
}
}
private connect(socket: WebSocket) {
try {
this.reply(socket, new OB11LifeCycleEvent(LifeCycleSubType.CONNECT))
} catch (e) {
this.ctx.logger.error('发送生命周期失败', e)
private connect(socket: WebSocket, req: IncomingMessage) {
const url = req.url?.split('?').shift()
if (['/api', '/api/', '/', undefined].includes(url)) {
socket.on('message', msg => {
this.handleAction(socket, msg.toString())
})
}
if (['/event', '/event/', '/', undefined].includes(url)) {
try {
this.reply(socket, new OB11LifeCycleEvent(LifeCycleSubType.CONNECT))
} catch (e) {
this.ctx.logger.error('发送生命周期失败', e)
}
const disposeHeartBeat = this.ctx.setInterval(() => {
this.reply(socket, new OB11HeartbeatEvent(selfInfo.online!, true, this.config.heartInterval))
}, this.config.heartInterval)
socket.on('close', () => {
disposeHeartBeat()
this.ctx.logger.info('有一个 Websocket 连接断开')
})
}
socket.on('error', err => this.ctx.logger.error(err.message))
socket.on('message', msg => {
this.handleAction(socket, msg.toString())
})
socket.on('ping', () => {
socket.pong()
})
const disposeHeartBeat = this.ctx.setInterval(() => {
this.reply(socket, new OB11HeartbeatEvent(selfInfo.online!, true, this.config.heartInterval))
}, this.config.heartInterval)
socket.on('close', () => {
disposeHeartBeat()
this.ctx.logger.info('有一个 Websocket 连接断开')
this.wsClients.push({
socket,
emitEvent: ['/event', '/event/', '/', undefined].includes(url)
})
this.wsClients.push(socket)
}
}
@@ -158,6 +170,7 @@ namespace OB11WebSocket {
heartInterval: number
token?: string
actionMap: Map<string, BaseAction<any, any>>
listenLocalhost: boolean
}
}
@@ -192,8 +205,8 @@ class OB11WebSocketReverse {
return
}
socket.send(JSON.stringify(data))
if (data['post_type']) {
this.ctx.logger.info('WebSocket 事件上报', socket.url ?? '', data['post_type'])
if ('post_type' in data) {
this.ctx.logger.info('WebSocket 事件上报', socket.url ?? '', data.post_type)
}
}

View File

@@ -44,7 +44,7 @@ export function decodeCQCode(source: string): OB11MessageData[] {
return elements
}
export function encodeCQCode(data: OB11MessageData) {
export function encodeCQCode(input: OB11MessageData) {
const CQCodeEscapeText = (text: string) => {
return text.replace(/\&/g, '&amp;').replace(/\[/g, '&#91;').replace(/\]/g, '&#93;')
}
@@ -53,21 +53,20 @@ export function encodeCQCode(data: OB11MessageData) {
return text.replace(/\&/g, '&amp;').replace(/\[/g, '&#91;').replace(/\]/g, '&#93;').replace(/,/g, '&#44;')
}
if (data.type === 'text') {
return CQCodeEscapeText(data.data.text)
if (input.type === 'text') {
return CQCodeEscapeText(input.data.text)
}
let result = '[CQ:' + data.type
for (const name in data.data) {
const value = data.data[name]
let result = '[CQ:' + input.type
for (const [key, value] of Object.entries(input.data)) {
if (value === undefined) {
continue
}
try {
const text = value.toString()
result += `,${name}=${CQCodeEscape(text)}`
result += `,${key}=${CQCodeEscape(text)}`
} catch (error) {
// If it can't be converted, skip this name-value pair
// If it can't be converted, skip this key-value pair
}
}
result += ']'

View File

@@ -1,4 +1,4 @@
import fastXmlParser from 'fast-xml-parser'
import { XMLParser } from 'fast-xml-parser'
import {
OB11Group,
OB11GroupMember,
@@ -32,29 +32,28 @@ import { OB11GroupBanEvent } from './event/notice/OB11GroupBanEvent'
import { OB11GroupUploadNoticeEvent } from './event/notice/OB11GroupUploadNoticeEvent'
import { OB11GroupNoticeEvent } from './event/notice/OB11GroupNoticeEvent'
import { calcQQLevel } from '../common/utils/misc'
import { getConfigUtil } from '../common/config'
import { OB11GroupTitleEvent } from './event/notice/OB11GroupTitleEvent'
import { OB11GroupCardEvent } from './event/notice/OB11GroupCardEvent'
import { OB11GroupDecreaseEvent } from './event/notice/OB11GroupDecreaseEvent'
import { OB11GroupMsgEmojiLikeEvent } from './event/notice/OB11MsgEmojiLikeEvent'
import { mFaceCache } from '../ntqqapi/entities'
import { OB11FriendAddNoticeEvent } from './event/notice/OB11FriendAddNoticeEvent'
import { OB11FriendRecallNoticeEvent } from './event/notice/OB11FriendRecallNoticeEvent'
import { OB11GroupRecallNoticeEvent } from './event/notice/OB11GroupRecallNoticeEvent'
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'
import OneBot11Adapter from './adapter'
export namespace OB11Entities {
export async function message(ctx: Context, msg: RawMessage): Promise<OB11Message> {
let config = getConfigUtil().getConfig()
const {
debug,
ob11: { messagePostFormat },
} = config
messagePostFormat,
} = ctx.config as OneBot11Adapter.Config
const selfUin = selfInfo.uin
const resMsg: OB11Message = {
self_id: parseInt(selfUin),
@@ -96,19 +95,16 @@ export namespace OB11Entities {
resMsg.sub_type = 'group'
const ret = await ctx.ntMsgApi.getTempChatInfo(ChatType2.KCHATTYPETEMPC2CFROMGROUP, msg.senderUid)
if (ret?.result === 0) {
resMsg.group_id = parseInt(ret.tmpChatInfo!.groupCode)
resMsg.sender.nickname = ret.tmpChatInfo!.fromNick
resMsg.temp_source = Number(ret.tmpChatInfo?.groupCode)
resMsg.sender.nickname = ret.tmpChatInfo?.fromNick!
} else {
resMsg.group_id = 284840486 //兜底数据
resMsg.temp_source = 284840486 //兜底数据
resMsg.sender.nickname = '临时会话'
}
}
for (let element of msg.elements) {
let message_data: OB11MessageData = {
data: {} as any,
type: 'unknown' as any,
}
let messageSegment: OB11MessageData | undefined
if (element.textElement && element.textElement?.atType !== AtType.notAt) {
let qq: string
let name: string | undefined
@@ -129,7 +125,7 @@ export namespace OB11Entities {
name = content.replace('@', '')
}
}
message_data = {
messageSegment = {
type: OB11MessageDataType.at,
data: {
qq: qq!,
@@ -138,58 +134,61 @@ export namespace OB11Entities {
}
}
else if (element.textElement) {
message_data['type'] = OB11MessageDataType.text
let text = element.textElement.content
const text = element.textElement.content
if (!text.trim()) {
continue
}
message_data['data']['text'] = text
messageSegment = {
type: OB11MessageDataType.text,
data: {
text
}
}
}
else if (element.replyElement) {
message_data['type'] = OB11MessageDataType.reply
const { replyElement } = element
const peer = {
chatType: msg.chatType,
peerUid: msg.peerUid,
guildId: ''
}
try {
const records = msg.records.find(msgRecord => msgRecord.msgId === element.replyElement.sourceMsgIdInRecords)
if (!records) throw new Error('找不到回复消息')
let replyMsg = (await ctx.ntMsgApi.getMsgsBySeqAndCount({
peerUid: msg.peerUid,
guildId: '',
chatType: msg.chatType,
}, element.replyElement.replayMsgSeq, 1, true, true))?.msgList[0]
if (!replyMsg || records.msgRandom !== replyMsg.msgRandom) {
const peer = {
chatType: msg.chatType,
peerUid: msg.peerUid,
guildId: '',
}
replyMsg = (await ctx.ntMsgApi.getSingleMsg(peer, element.replyElement.replayMsgSeq))?.msgList[0]
const { replayMsgSeq, replyMsgTime, senderUidStr } = replyElement
const records = msg.records.find(msgRecord => msgRecord.msgId === replyElement.sourceMsgIdInRecords)
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,
data: {
id: MessageUnique.createMsg(peer, replyMsg ? replyMsg.msgId : records.msgId).toString()
}
}
message_data['data']['id'] = replyMsg && MessageUnique.createMsg({
peerUid: msg.peerUid,
guildId: '',
chatType: msg.chatType,
}, replyMsg.msgId)?.toString()
} catch (e: any) {
ctx.logger.error('获取不到引用的消息', e.stack, element.replyElement.replayMsgSeq)
ctx.logger.error('获取不到引用的消息', replyElement, e.stack)
continue
}
}
else if (element.picElement) {
message_data['type'] = OB11MessageDataType.image
const { picElement } = element
/*let fileName = picElement.fileName
const isGif = picElement.picType === PicType.gif
if (isGif && !fileName.endsWith('.gif')) {
fileName += '.gif'
}*/
message_data['data']['file'] = picElement.fileName
message_data['data']['subType'] = picElement.picSubType
//message_data['data']['file_id'] = picElement.fileUuid
message_data['data']['url'] = await ctx.ntFileApi.getImageUrl(picElement)
message_data['data']['file_size'] = picElement.fileSize
const fileSize = picElement.fileSize ?? '0'
messageSegment = {
type: OB11MessageDataType.image,
data: {
file: picElement.fileName,
subType: picElement.picSubType,
url: await ctx.ntFileApi.getImageUrl(picElement),
file_size: fileSize,
}
}
MessageUnique.addFileCache({
peerUid: msg.peerUid,
msgId: msg.msgId,
@@ -198,21 +197,26 @@ export namespace OB11Entities {
elementId: element.elementId,
elementType: element.elementType,
fileName: picElement.fileName,
fileSize: String(picElement.fileSize || '0'),
fileUuid: picElement.fileUuid
fileUuid: picElement.fileUuid,
fileSize,
})
}
else if (element.videoElement) {
message_data['type'] = OB11MessageDataType.video
const { videoElement } = element
message_data['data']['file'] = videoElement.fileName
message_data['data']['path'] = videoElement.filePath
//message_data['data']['file_id'] = videoElement.fileUuid
message_data['data']['file_size'] = videoElement.fileSize
message_data['data']['url'] = await ctx.ntFileApi.getVideoUrl({
const videoUrl = await ctx.ntFileApi.getVideoUrl({
chatType: msg.chatType,
peerUid: msg.peerUid,
}, msg.msgId, element.elementId)
const fileSize = videoElement.fileSize ?? '0'
messageSegment = {
type: OB11MessageDataType.video,
data: {
file: videoElement.fileName,
url: videoUrl || pathToFileURL(videoElement.filePath).href,
path: videoElement.filePath,
file_size: fileSize,
}
}
MessageUnique.addFileCache({
peerUid: msg.peerUid,
msgId: msg.msgId,
@@ -221,17 +225,23 @@ export namespace OB11Entities {
elementId: element.elementId,
elementType: element.elementType,
fileName: videoElement.fileName,
fileSize: String(videoElement.fileSize || '0'),
fileUuid: videoElement.fileUuid!
fileUuid: videoElement.fileUuid!,
fileSize,
})
}
else if (element.fileElement) {
message_data['type'] = OB11MessageDataType.file
const { fileElement } = element
message_data['data']['file'] = fileElement.fileName
message_data['data']['path'] = fileElement.filePath
message_data['data']['file_id'] = fileElement.fileUuid
message_data['data']['file_size'] = fileElement.fileSize
const fileSize = fileElement.fileSize ?? '0'
messageSegment = {
type: OB11MessageDataType.file,
data: {
file: fileElement.fileName,
url: pathToFileURL(fileElement.filePath).href,
file_id: fileElement.fileUuid,
path: fileElement.filePath,
file_size: fileSize,
}
}
MessageUnique.addFileCache({
peerUid: msg.peerUid,
msgId: msg.msgId,
@@ -240,17 +250,22 @@ export namespace OB11Entities {
elementId: element.elementId,
elementType: element.elementType,
fileName: fileElement.fileName,
fileSize: String(fileElement.fileSize || '0'),
fileUuid: fileElement.fileUuid!
fileUuid: fileElement.fileUuid!,
fileSize,
})
}
else if (element.pttElement) {
message_data['type'] = OB11MessageDataType.voice
const { pttElement } = element
message_data['data']['file'] = pttElement.fileName
message_data['data']['path'] = pttElement.filePath
//message_data['data']['file_id'] = pttElement.fileUuid
message_data['data']['file_size'] = pttElement.fileSize
const fileSize = pttElement.fileSize ?? '0'
messageSegment = {
type: OB11MessageDataType.voice,
data: {
file: pttElement.fileName,
url: pathToFileURL(pttElement.filePath).href,
path: pttElement.filePath,
file_size: fileSize,
}
}
MessageUnique.addFileCache({
peerUid: msg.peerUid,
msgId: msg.msgId,
@@ -259,59 +274,91 @@ export namespace OB11Entities {
elementId: element.elementId,
elementType: element.elementType,
fileName: pttElement.fileName,
fileSize: String(pttElement.fileSize || '0'),
fileUuid: pttElement.fileUuid
fileUuid: pttElement.fileUuid,
fileSize,
})
}
else if (element.arkElement) {
message_data['type'] = OB11MessageDataType.json
message_data['data']['data'] = element.arkElement.bytesData
const { arkElement } = element
messageSegment = {
type: OB11MessageDataType.json,
data: {
data: arkElement.bytesData
}
}
}
else if (element.faceElement) {
const faceId = element.faceElement.faceIndex
const { faceElement } = element
const faceId = faceElement.faceIndex
if (faceId === FaceIndex.dice) {
message_data['type'] = OB11MessageDataType.dice
message_data['data']['result'] = element.faceElement.resultId
messageSegment = {
type: OB11MessageDataType.dice,
data: {
result: faceElement.resultId!
}
}
}
else if (faceId === FaceIndex.RPS) {
message_data['type'] = OB11MessageDataType.RPS
message_data['data']['result'] = element.faceElement.resultId
messageSegment = {
type: OB11MessageDataType.RPS,
data: {
result: faceElement.resultId!
}
}
}
else {
message_data['type'] = OB11MessageDataType.face
message_data['data']['id'] = element.faceElement.faceIndex.toString()
messageSegment = {
type: OB11MessageDataType.face,
data: {
id: faceId.toString()
}
}
}
}
else if (element.marketFaceElement) {
message_data['type'] = OB11MessageDataType.mface
message_data['data']['summary'] = element.marketFaceElement.faceName
const md5 = element.marketFaceElement.emojiId
const { marketFaceElement } = element
const { emojiId } = marketFaceElement
// 取md5的前两位
const dir = md5.substring(0, 2)
const dir = emojiId.substring(0, 2)
// 获取组装url
// const url = `https://p.qpic.cn/CDN_STATIC/0/data/imgcache/htdocs/club/item/parcel/item/${dir}/${md5}/300x300.gif?max_age=31536000`
const url = `https://gxh.vip.qq.com/club/item/parcel/item/${dir}/${md5}/raw300.gif`
message_data['data']['url'] = url
message_data['data']['emoji_id'] = element.marketFaceElement.emojiId
message_data['data']['emoji_package_id'] = String(element.marketFaceElement.emojiPackageId)
message_data['data']['key'] = element.marketFaceElement.key
mFaceCache.set(md5, element.marketFaceElement.faceName!)
const url = `https://gxh.vip.qq.com/club/item/parcel/item/${dir}/${emojiId}/raw300.gif`
messageSegment = {
type: OB11MessageDataType.mface,
data: {
summary: marketFaceElement.faceName!,
url,
emoji_id: emojiId,
emoji_package_id: marketFaceElement.emojiPackageId,
key: marketFaceElement.key
}
}
//mFaceCache.set(emojiId, element.marketFaceElement.faceName!)
}
else if (element.markdownElement) {
message_data['type'] = OB11MessageDataType.markdown
message_data['data']['data'] = element.markdownElement.content
const { markdownElement } = element
messageSegment = {
type: OB11MessageDataType.markdown,
data: {
data: markdownElement.content
}
}
}
else if (element.multiForwardMsgElement) {
message_data['type'] = OB11MessageDataType.forward
message_data['data']['id'] = msg.msgId
messageSegment = {
type: OB11MessageDataType.forward,
data: {
id: msg.msgId
}
}
}
if ((message_data.type as string) !== 'unknown' && message_data.data) {
const cqCode = encodeCQCode(message_data)
if (messageSegment) {
const cqCode = encodeCQCode(messageSegment)
if (messagePostFormat === 'string') {
(resMsg.message as string) += cqCode
} else {
(resMsg.message as OB11MessageData[]).push(messageSegment)
}
else (resMsg.message as OB11MessageData[]).push(message_data)
resMsg.raw_message += cqCode
}
}
@@ -467,35 +514,33 @@ export namespace OB11Entities {
// <url jp= \"\" msgseq=\"74711\" col=\"3\" txt=\"消息:\"/>
// <face type=\"1\" id=\"76\"> </face>
// </gtip>",
const emojiLikeData = new fastXmlParser.XMLParser({
const emojiLikeData = new XMLParser({
ignoreAttributes: false,
attributeNamePrefix: '',
}).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)
@@ -723,6 +768,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

@@ -54,7 +54,7 @@ export async function createSendElements(
let isAdmin: boolean = true
if (groupCode) {
try {
remainAtAllCount = (await ctx.ntGroupApi.getGroupAtAllRemainCount(groupCode)).atInfo
remainAtAllCount = (await ctx.ntGroupApi.getGroupRemainAtTimes(groupCode)).atInfo
.RemainAtAllCountForUin
ctx.logger.info(`${groupCode}剩余at全体次数`, remainAtAllCount)
const self = await ctx.ntGroupApi.getGroupMember(groupCode, selfInfo.uin)
@@ -270,8 +270,40 @@ export async function sendMsg(
const timeout = 10000 + (totalSize / 1024 / 256 * 1000) // 10s Basic Timeout + PredictTime( For File 512kb/s )
//log('设置消息超时时间', timeout)
const returnMsg = await ctx.ntMsgApi.sendMsg(peer, sendElements, waitComplete, timeout)
returnMsg.msgShortId = MessageUnique.createMsg(peer, returnMsg.msgId)
ctx.logger.info('消息发送', returnMsg.msgShortId)
deleteAfterSentFiles.map(path => fsPromise.unlink(path))
return returnMsg
if (returnMsg) {
returnMsg.msgShortId = MessageUnique.createMsg(peer, returnMsg.msgId)
ctx.logger.info('消息发送', returnMsg.msgShortId)
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 {
@@ -91,6 +93,7 @@ export interface OB11Message {
font: number
post_type?: EventType
raw?: RawMessage
temp_source?: number
}
export interface OB11ForwardMessage extends OB11Message {
@@ -133,20 +136,21 @@ export interface OB11MessageMFace {
emoji_package_id: number
emoji_id: string
key: string
summary: string
summary?: string
url?: string
}
}
export interface OB11MessageDice {
type: OB11MessageDataType.dice
data: {
result: number
result: number /* intended */ | string /* in fact */
}
}
export interface OB11MessageRPS {
type: OB11MessageDataType.RPS
data: {
result: number
result: number | string
}
}
@@ -171,6 +175,7 @@ export interface OB11MessageFileBase {
name?: string
file: string
url?: string
file_size?: string //扩展
}
}
@@ -184,14 +189,24 @@ export interface OB11MessageImage extends OB11MessageFileBase {
export interface OB11MessageRecord extends OB11MessageFileBase {
type: OB11MessageDataType.voice
data: OB11MessageFileBase['data'] & {
path?: string //扩展
}
}
export interface OB11MessageFile extends OB11MessageFileBase {
type: OB11MessageDataType.file
data: OB11MessageFileBase['data'] & {
file_id?: string
path?: string
}
}
export interface OB11MessageVideo extends OB11MessageFileBase {
type: OB11MessageDataType.video
data: OB11MessageFileBase['data'] & {
path?: string //扩展
}
}
export interface OB11MessageAt {
@@ -298,3 +313,27 @@ export interface OB11Status {
online: boolean | null
good: boolean
}
export interface OB11GroupFile {
group_id: number
file_id: string
file_name: string
busid: number
file_size: number
upload_time: number
dead_time: number
modify_time: number
download_times: number
uploader: number
uploader_name: string
}
export interface OB11GroupFileFolder {
group_id: number
folder_id: string
folder_name: string
create_time: number
creator: number
creator_name: string
total_file_count: number
}

View File

@@ -38,11 +38,9 @@ window.customElements.define(
const buttonClick = () => {
const isHidden = this._context.classList.toggle('hidden')
window[`${isHidden ? 'remove' : 'add'}EventListener`]('pointerdown', windowPointerDown)
}
const windowPointerDown = ({ target }) => {
if (!this.contains(target)) buttonClick()
window[`${isHidden ? 'remove' : 'add'}EventListener`]('pointerdown', ({ target }) => {
if (!this.contains(target as any)) buttonClick()
})
}
this._button.addEventListener('click', buttonClick)

View File

@@ -1,8 +1,10 @@
import { CheckVersion } from '../common/types'
import { CheckVersion, Config } from '../common/types'
import { SettingButton, SettingItem, SettingList, SettingSwitch, SettingSelect } from './components'
import { version } from '../version'
// @ts-ignore
import StyleRaw from './style.css?raw'
import { version } from '../version'
type HostsType = 'httpHosts' | 'wsHosts'
function isEmpty(value: unknown) {
return value === undefined || value === null || value === ''
@@ -10,17 +12,20 @@ function isEmpty(value: unknown) {
async function onSettingWindowCreated(view: Element) {
//window.llonebot.log('setting window created')
let config = await window.llonebot.getConfig()
let ob11Config = { ...config.ob11 }
const config = await window.llonebot.getConfig()
const ob11Config = { ...config.ob11 }
const setConfig = (key: string, value: any) => {
const configKey = key.split('.')
if (key.indexOf('ob11') === 0) {
if (configKey.length === 2) ob11Config[configKey[1]] = value
else ob11Config[key] = value
if (key.startsWith('ob11')) {
if (configKey.length === 2) Object.assign(ob11Config, { [configKey[1]]: value })
else Object.assign(ob11Config, { [key]: value })
} else {
if (configKey.length === 2) config[configKey[0]][configKey[1]] = value
else config[key] = value
if (configKey.length === 2) {
Object.assign(config[configKey[0] as keyof Config[keyof Config]], { [configKey[1]]: value })
} else {
Object.assign(config, { [key]: value })
}
if (!['heartInterval', 'token', 'ffmpeg'].includes(key)) {
window.llonebot.setConfig(false, config)
}
@@ -161,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')),
]),
@@ -244,7 +254,7 @@ async function onSettingWindowCreated(view: Element) {
window.LiteLoader.api.openExternal('https://llonebot.github.io/')
})
// 生成反向地址列表
const buildHostListItem = (type: string, host: string, index: number, inputAttrs: any = {}) => {
const buildHostListItem = (type: HostsType, host: string, index: number, inputAttrs: any = {}) => {
const dom = {
container: document.createElement('setting-item'),
input: document.createElement('input'),
@@ -276,7 +286,7 @@ async function onSettingWindowCreated(view: Element) {
return dom.container
}
const buildHostList = (hosts: string[], type: string, inputAttr: any = {}) => {
const buildHostList = (hosts: string[], type: HostsType, inputAttr: any = {}) => {
const result: HTMLElement[] = []
hosts.forEach((host, index) => {
@@ -285,12 +295,12 @@ async function onSettingWindowCreated(view: Element) {
return result
}
const addReverseHost = (type: string, doc: Document = document, inputAttr: any = {}) => {
const addReverseHost = (type: HostsType, doc: Document = document, inputAttr: any = {}) => {
const hostContainerDom = doc.body.querySelector(`#config-ob11-${type}-list`)
hostContainerDom?.appendChild(buildHostListItem(type, '', ob11Config[type].length, inputAttr))
ob11Config[type].push('')
}
const initReverseHost = (type: string, doc: Document = document) => {
const initReverseHost = (type: HostsType, doc: Document = document) => {
const hostContainerDom = doc.body.querySelector(`#config-ob11-${type}-list`)
;[...hostContainerDom?.childNodes!].forEach((dom) => dom.remove())
buildHostList(ob11Config[type], type).forEach((dom) => {
@@ -431,7 +441,7 @@ function init() {
}
if (location.hash === '#/blank') {
globalThis.navigation.addEventListener('navigatesuccess', init, { once: true })
globalThis.navigation?.addEventListener('navigatesuccess', init, { once: true })
} else {
init()
}

View File

@@ -1 +1 @@
export const version = '3.31.2'
export const version = '3.31.8'

View File

@@ -1,10 +1,10 @@
{
"compilerOptions": {
"target": "ESNext",
"module": "commonjs",
"module": "CommonJS",
"outDir": "./dist",
"strict": true,
"noImplicitAny": false,
"isolatedModules": true,
"esModuleInterop": true,
"allowSyntheticDefaultImports": true,
"experimentalDecorators": true,