Compare commits

..

19 Commits

Author SHA1 Message Date
idranme
59a11faa7f Merge pull request #352 from LLOneBot/dev
3.29.5
2024-08-19 17:40:30 +08:00
idranme
3b3795c946 chore: v3.29.5 2024-08-19 17:38:42 +08:00
idranme
ff18937828 fix 2024-08-19 17:29:58 +08:00
idranme
65d02d7f21 Merge pull request #351 from LLOneBot/main
merge
2024-08-19 12:59:10 +08:00
idranme
9cb8ba017e Merge pull request #350 from snsin09/nocache
ws修复必须no_cache参数
2024-08-19 12:55:27 +08:00
yota
1e579858b8 ws修复必须no_cache参数 2024-08-19 09:47:24 +08:00
idranme
db0c800851 Merge pull request #347 from LLOneBot/dev
3.29.4
2024-08-18 21:09:15 +08:00
idranme
e912911dd8 chore: v3.29.4 2024-08-18 21:04:30 +08:00
idranme
2245d0d3de fix 2024-08-18 20:58:26 +08:00
idranme
a56eac0251 Merge pull request #345 from LLOneBot/main
merge
2024-08-18 16:45:02 +08:00
linyuchen
8be0562c19 Merge pull request #344 from LLOneBot/linyuchen-patch-1
Fix: typo
2024-08-17 23:46:12 +08:00
linyuchen
f4c77f3e20 Fix: typo 2024-08-17 23:45:41 +08:00
linyuchen
508e6f2928 Merge pull request #342 from gfhdhytghd/patch-1
Update LICENSE
2024-08-17 16:20:50 +08:00
lin
9353cb0432 Update LICENSE
修改许可证以在法律层面上禁止宣传
2024-08-17 14:21:27 +08:00
idranme
816e07f47c Merge pull request #341 from LLOneBot/dev
3.29.3
2024-08-16 22:27:41 +08:00
idranme
46b1e8e67d chore: v3.29.3 2024-08-16 22:25:17 +08:00
idranme
8542594181 fix 2024-08-16 21:58:05 +08:00
idranme
0d7aa9bd2c fix 2024-08-16 21:28:43 +08:00
idranme
a47ee4c3e4 fix 2024-08-16 09:53:23 +08:00
34 changed files with 380 additions and 367 deletions

View File

@@ -1,4 +1,4 @@
MIT License MIT Without Public Sicial Media Promotion License
Copyright (c) 2024 LLOneBot Copyright (c) 2024 LLOneBot
@@ -19,3 +19,7 @@ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE. SOFTWARE.
You may use this software in accordance with the above terms, but you are not
allowed to promote this project or your projects based on this project on any
public social media.

View File

@@ -1,6 +1,6 @@
# LLOneBot # LLOneBot
LiteLoaderQQNT 插件,实现 OneBot 11 协议,用 QQ 机器人开发 LiteLoaderQQNT 插件,实现 OneBot 11 协议,用 QQ 机器人开发
> [!CAUTION]\ > [!CAUTION]\
> **请不要在 QQ 官方群聊和任何影响力较大的简中互联网平台(包括但不限于: 哔哩哔哩,微博,知乎,抖音等)发布和讨论*任何*与本插件存在相关性的信息** > **请不要在 QQ 官方群聊和任何影响力较大的简中互联网平台(包括但不限于: 哔哩哔哩,微博,知乎,抖音等)发布和讨论*任何*与本插件存在相关性的信息**

View File

@@ -3,8 +3,8 @@
"type": "extension", "type": "extension",
"name": "LLOneBot", "name": "LLOneBot",
"slug": "LLOneBot", "slug": "LLOneBot",
"description": "实现 OneBot 11 协议,用 QQ 机器人开发", "description": "实现 OneBot 11 协议,用 QQ 机器人开发",
"version": "3.29.2", "version": "3.29.5",
"icon": "./icon.webp", "icon": "./icon.webp",
"authors": [ "authors": [
{ {

View File

@@ -16,15 +16,15 @@
"author": "", "author": "",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@minatojs/driver-sqlite": "^4.4.1", "@minatojs/driver-sqlite": "^4.5.0",
"compressing": "^1.10.1", "compressing": "^1.10.1",
"cordis": "^3.17.9", "cordis": "^3.18.0",
"cors": "^2.8.5", "cors": "^2.8.5",
"express": "^4.19.2", "express": "^4.19.2",
"fast-xml-parser": "^4.4.1", "fast-xml-parser": "^4.4.1",
"file-type": "^19.4.0", "file-type": "^19.4.1",
"fluent-ffmpeg": "^2.1.3", "fluent-ffmpeg": "^2.1.3",
"minato": "^3.4.3", "minato": "^3.5.0",
"silk-wasm": "^3.6.1", "silk-wasm": "^3.6.1",
"ws": "^8.18.0" "ws": "^8.18.0"
}, },
@@ -34,10 +34,10 @@
"@types/fluent-ffmpeg": "^2.1.25", "@types/fluent-ffmpeg": "^2.1.25",
"@types/node": "^20.14.15", "@types/node": "^20.14.15",
"@types/ws": "^8.5.12", "@types/ws": "^8.5.12",
"electron": "^29.1.4", "electron": "^31.4.0",
"electron-vite": "^2.3.0", "electron-vite": "^2.3.0",
"typescript": "^5.5.4", "typescript": "^5.5.4",
"vite": "^5.4.0", "vite": "^5.4.1",
"vite-plugin-cp": "^4.0.8" "vite-plugin-cp": "^4.0.8"
}, },
"packageManager": "yarn@4.4.0" "packageManager": "yarn@4.4.0"

View File

@@ -6,7 +6,7 @@ const manifest = {
type: 'extension', type: 'extension',
name: 'LLOneBot', name: 'LLOneBot',
slug: 'LLOneBot', slug: 'LLOneBot',
description: '实现 OneBot 11 协议,用 QQ 机器人开发', description: '实现 OneBot 11 协议,用 QQ 机器人开发',
version, version,
icon: './icon.webp', icon: './icon.webp',
authors: [ authors: [

View File

@@ -1,17 +0,0 @@
import { Level } from 'level'
const db = new Level(process.env['level_db_path'] as string, { valueEncoding: 'json' })
async function getGroupNotify() {
let keys = await db.keys().all()
let result: string[] = []
for (const key of keys) {
// console.log(key)
if (key.startsWith('group_notify_')) {
result.push(key)
}
}
return result
}
getGroupNotify().then(console.log)

View File

@@ -5,9 +5,7 @@ import path from 'node:path'
import { getSelfUin } from './data' import { getSelfUin } from './data'
import { DATA_DIR } from './utils' import { DATA_DIR } from './utils'
export const HOOK_LOG = false //export const HOOK_LOG = false
export const ALLOW_SEND_TEMP_MSG = false
export class ConfigUtil { export class ConfigUtil {
private readonly configPath: string private readonly configPath: string

View File

@@ -1,6 +1,5 @@
import { import {
type Friend, type Friend,
type Group,
type GroupMember, type GroupMember,
type SelfInfo, type SelfInfo,
} from '../ntqqapi/types' } from '../ntqqapi/types'
@@ -11,8 +10,8 @@ import { isNumeric } from './utils/helper'
import { NTQQFriendApi, NTQQUserApi } from '../ntqqapi/api' import { NTQQFriendApi, NTQQUserApi } from '../ntqqapi/api'
import { RawMessage } from '../ntqqapi/types' import { RawMessage } from '../ntqqapi/types'
import { getConfigUtil } from './config' import { getConfigUtil } from './config'
import { getBuildVersion } from './utils/QQBasicInfo'
export let groups: Group[] = []
export let friends: Friend[] = [] export let friends: Friend[] = []
export const llonebotError: LLOneBotError = { export const llonebotError: LLOneBotError = {
ffmpegError: '', ffmpegError: '',
@@ -24,10 +23,10 @@ export const llonebotError: LLOneBotError = {
export const groupMembers: Map<string, Map<string, GroupMember>> = new Map<string, Map<string, GroupMember>>() export const groupMembers: Map<string, Map<string, GroupMember>> = new Map<string, Map<string, GroupMember>>()
export async function getFriend(uinOrUid: string): Promise<Friend | undefined> { export async function getFriend(uinOrUid: string): Promise<Friend | undefined> {
let filterKey = isNumeric(uinOrUid.toString()) ? 'uin' : 'uid' const filterKey: 'uin' | 'uid' = isNumeric(uinOrUid.toString()) ? 'uin' : 'uid'
let filterValue = uinOrUid const filterValue = uinOrUid
let friend = friends.find((friend) => friend[filterKey] === filterValue.toString()) let friend = friends.find((friend) => friend[filterKey] === filterValue.toString())
if (!friend) { if (!friend && getBuildVersion() < 26702) {
try { try {
const _friends = await NTQQFriendApi.getFriends(true) const _friends = await NTQQFriendApi.getFriends(true)
friend = _friends.find((friend) => friend[filterKey] === filterValue.toString()) friend = _friends.find((friend) => friend[filterKey] === filterValue.toString())
@@ -41,39 +40,15 @@ export async function getFriend(uinOrUid: string): Promise<Friend | undefined> {
return friend return friend
} }
export async function getGroup(qq: string): Promise<Group | undefined> { export async function getGroupMember(groupCode: string | number, memberUinOrUid: string | number) {
let group = groups.find((group) => group.groupCode === qq.toString()) const groupCodeStr = groupCode.toString()
if (!group) { const memberUinOrUidStr = memberUinOrUid.toString()
try { let members = groupMembers.get(groupCodeStr)
const _groups = await NTQQGroupApi.getGroups(true)
group = _groups.find((group) => group.groupCode === qq.toString())
if (group) {
groups.push(group)
}
} catch (e) {
}
}
return group
}
export function deleteGroup(groupCode: string) {
const groupIndex = groups.findIndex((group) => group.groupCode === groupCode.toString())
// log(groups, groupCode, groupIndex);
if (groupIndex !== -1) {
log('删除群', groupCode)
groups.splice(groupIndex, 1)
}
}
export async function getGroupMember(groupQQ: string | number, memberUinOrUid: string | number) {
groupQQ = groupQQ.toString()
memberUinOrUid = memberUinOrUid.toString()
let members = groupMembers.get(groupQQ)
if (!members) { if (!members) {
try { try {
members = await NTQQGroupApi.getGroupMembers(groupQQ) members = await NTQQGroupApi.getGroupMembers(groupCodeStr)
// 更新群成员列表 // 更新群成员列表
groupMembers.set(groupQQ, members) groupMembers.set(groupCodeStr, members)
} }
catch (e) { catch (e) {
return null return null
@@ -81,16 +56,17 @@ export async function getGroupMember(groupQQ: string | number, memberUinOrUid: s
} }
const getMember = () => { const getMember = () => {
let member: GroupMember | undefined = undefined let member: GroupMember | undefined = undefined
if (isNumeric(memberUinOrUid)) { if (isNumeric(memberUinOrUidStr)) {
member = Array.from(members!.values()).find(member => member.uin === memberUinOrUid) member = Array.from(members!.values()).find(member => member.uin === memberUinOrUidStr)
} else { } else {
member = members!.get(memberUinOrUid) member = members!.get(memberUinOrUidStr)
} }
return member return member
} }
let member = getMember() let member = getMember()
if (!member) { if (!member) {
members = await NTQQGroupApi.getGroupMembers(groupQQ) members = await NTQQGroupApi.getGroupMembers(groupCodeStr)
groupMembers.set(groupCodeStr, members)
member = getMember() member = getMember()
} }
return member return member

View File

@@ -50,3 +50,15 @@ export interface FileCache {
elementId: string elementId: string
elementType: number elementType: number
} }
export interface FileCacheV2 {
fileName: string
fileSize: string
fileUuid: string
msgId: string
msgTime: number
peerUid: string
chatType: number
elementId: string
elementType: number
}

View File

@@ -7,7 +7,7 @@ import SQLite from '@minatojs/driver-sqlite'
import fsPromise from 'node:fs/promises' import fsPromise from 'node:fs/promises'
import fs from 'node:fs' import fs from 'node:fs'
import path from 'node:path' import path from 'node:path'
import { FileCache } from '../types' import { FileCacheV2 } from '../types'
interface SQLiteTables extends Tables { interface SQLiteTables extends Tables {
message: { message: {
@@ -16,7 +16,7 @@ interface SQLiteTables extends Tables {
chatType: number chatType: number
peerUid: string peerUid: string
} }
file: FileCache file_v2: FileCacheV2
} }
interface MsgIdAndPeerByShortId { interface MsgIdAndPeerByShortId {
@@ -52,16 +52,19 @@ class MessageUniqueWrapper {
}, { }, {
primary: 'shortId' primary: 'shortId'
}) })
database.extend('file', { database.extend('file_v2', {
fileName: 'string', fileName: 'string',
fileSize: 'string', fileSize: 'string',
fileUuid: 'string(128)',
msgId: 'string(24)', msgId: 'string(24)',
msgTime: 'unsigned(10)',
peerUid: 'string(24)', peerUid: 'string(24)',
chatType: 'unsigned', chatType: 'unsigned',
elementId: 'string(24)', elementId: 'string(24)',
elementType: 'unsigned', elementType: 'unsigned',
}, { }, {
primary: 'fileName' primary: 'fileUuid',
indexes: ['fileName']
}) })
this.db = database this.db = database
} }
@@ -142,12 +145,18 @@ class MessageUniqueWrapper {
this.msgDataMap.resize(maxSize) this.msgDataMap.resize(maxSize)
} }
addFileCache(data: FileCache) { addFileCache(data: FileCacheV2) {
return this.db?.upsert('file', [data], 'fileName') return this.db?.upsert('file_v2', [data], 'fileUuid')
} }
getFileCache(fileName: string) { getFileCacheByName(fileName: string) {
return this.db?.get('file', { fileName }) return this.db?.get('file_v2', { fileName }, {
sort: { msgTime: 'desc' }
})
}
getFileCacheById(fileUuid: string) {
return this.db?.get('file_v2', { fileUuid })
} }
} }

View File

@@ -16,7 +16,7 @@ export class RequestUtil {
const redirectUrl = new URL(res.headers.location, url); const redirectUrl = new URL(res.headers.location, url);
RequestUtil.HttpsGetCookies(redirectUrl.href).then((redirectCookies) => { RequestUtil.HttpsGetCookies(redirectUrl.href).then((redirectCookies) => {
// 合并重定向过程中的cookies // 合并重定向过程中的cookies
log('redirectCookies', redirectCookies) //log('redirectCookies', redirectCookies)
cookies = { ...cookies, ...redirectCookies }; cookies = { ...cookies, ...redirectCookies };
resolve(cookies); resolve(cookies);
}); });
@@ -33,7 +33,7 @@ export class RequestUtil {
}); });
if (res.headers['set-cookie']) { if (res.headers['set-cookie']) {
// console.log(res.headers['set-cookie']); // console.log(res.headers['set-cookie']);
log('set-cookie', url, res.headers['set-cookie']); //log('set-cookie', url, res.headers['set-cookie']);
res.headers['set-cookie'].forEach((cookie) => { res.headers['set-cookie'].forEach((cookie) => {
const parts = cookie.split(';')[0].split('='); const parts = cookie.split(';')[0].split('=');
const key = parts[0]; const key = parts[0];

View File

@@ -16,7 +16,6 @@ import {
import { ob11WebsocketServer } from '../onebot11/server/ws/WebsocketServer' import { ob11WebsocketServer } from '../onebot11/server/ws/WebsocketServer'
import { DATA_DIR, TEMP_DIR } from '../common/utils' import { DATA_DIR, TEMP_DIR } from '../common/utils'
import { import {
getGroupMember,
llonebotError, llonebotError,
setSelfInfo, setSelfInfo,
getSelfInfo, getSelfInfo,
@@ -28,7 +27,7 @@ import { hookNTQQApiCall, hookNTQQApiReceive, ReceiveCmdS, registerReceiveHook,
import { OB11Constructor } from '../onebot11/constructor' import { OB11Constructor } from '../onebot11/constructor'
import { import {
FriendRequestNotify, FriendRequestNotify,
GroupNotifies, GroupNotify,
GroupNotifyTypes, GroupNotifyTypes,
RawMessage, RawMessage,
BuddyReqType, BuddyReqType,
@@ -245,6 +244,7 @@ function onLoad() {
log('report self message error: ', e.stack.toString()) log('report self message error: ', e.stack.toString())
} }
}) })
const processedGroupNotify: string[] = []
registerReceiveHook<{ registerReceiveHook<{
doubt: boolean doubt: boolean
oldestUnreadSeq: string oldestUnreadSeq: string
@@ -252,48 +252,43 @@ function onLoad() {
}>(ReceiveCmdS.UNREAD_GROUP_NOTIFY, async (payload) => { }>(ReceiveCmdS.UNREAD_GROUP_NOTIFY, async (payload) => {
if (payload.unreadCount) { if (payload.unreadCount) {
// log("开始获取群通知详情") // log("开始获取群通知详情")
let notify: GroupNotifies let notifies: GroupNotify[]
try { try {
notify = await NTQQGroupApi.getGroupNotifies() notifies = (await NTQQGroupApi.getSingleScreenNotifies(14)).slice(0, payload.unreadCount)
} catch (e) { } catch (e) {
// log("获取群通知详情失败", e); // log("获取群通知详情失败", e);
return return
} }
const notifies = notify.notifies.slice(0, payload.unreadCount)
// log("获取群通知详情完成", notifies, payload);
for (const notify of notifies) { for (const notify of notifies) {
try { try {
notify.time = Date.now() notify.time = Date.now()
const notifyTime = parseInt(notify.seq) / 1000 const notifyTime = parseInt(notify.seq) / 1000
if (notifyTime < startTime) { const flag = notify.group.groupCode + '|' + notify.seq + '|' + notify.type
if (notifyTime < startTime || processedGroupNotify.includes(flag)) {
continue continue
} }
log('收到群通知', notify) processedGroupNotify.push(flag)
const flag = notify.group.groupCode + '|' + notify.seq + '|' + notify.type
if (notify.type == GroupNotifyTypes.MEMBER_EXIT || notify.type == GroupNotifyTypes.KICK_MEMBER) { if (notify.type == GroupNotifyTypes.MEMBER_EXIT || notify.type == GroupNotifyTypes.KICK_MEMBER) {
log('有成员退出通知', notify) log('有成员退出通知', notify)
try { const member1Uin = (await NTQQUserApi.getUinByUid(notify.user1.uid))!
const member1 = await NTQQUserApi.getUserDetailInfo(notify.user1.uid) let operatorId = member1Uin
let operatorId = member1.uin
let subType: GroupDecreaseSubType = 'leave' let subType: GroupDecreaseSubType = 'leave'
if (notify.user2.uid) { if (notify.user2.uid) {
// 是被踢的 // 是被踢的
const member2 = await getGroupMember(notify.group.groupCode, notify.user2.uid) const member2Uin = await NTQQUserApi.getUinByUid(notify.user2.uid)
operatorId = member2?.uin! if (member2Uin) {
operatorId = member2Uin
}
subType = 'kick' subType = 'kick'
} }
let groupDecreaseEvent = new OB11GroupDecreaseEvent( const groupDecreaseEvent = new OB11GroupDecreaseEvent(
parseInt(notify.group.groupCode), parseInt(notify.group.groupCode),
parseInt(member1.uin), parseInt(member1Uin),
parseInt(operatorId), parseInt(operatorId),
subType, subType,
) )
postOb11Event(groupDecreaseEvent, true) postOb11Event(groupDecreaseEvent, true)
} catch (e: any) {
log('获取群通知的成员信息失败', notify, e.stack.toString())
}
} }
else if ([GroupNotifyTypes.JOIN_REQUEST, GroupNotifyTypes.JOIN_REQUEST_BY_INVITED].includes(notify.type)) { else if ([GroupNotifyTypes.JOIN_REQUEST, GroupNotifyTypes.JOIN_REQUEST_BY_INVITED].includes(notify.type)) {
log('有加群请求') log('有加群请求')

View File

@@ -8,7 +8,7 @@ import { CacheClassFuncAsyncExtend } from '@/common/utils/helper'
import { LimitedHashTable } from '@/common/utils/table' import { LimitedHashTable } from '@/common/utils/table'
export class NTQQFriendApi { export class NTQQFriendApi {
/** >=26702 应使用 getBuddyV2 */ /** 大于或等于 26702 应使用 getBuddyV2 */
static async getFriends(forced = false) { static async getFriends(forced = false) {
const data = await callNTQQApi<{ const data = await callNTQQApi<{
data: { data: {

View File

@@ -1,10 +1,11 @@
import { ReceiveCmdS } from '../hook' import { ReceiveCmdS } from '../hook'
import { Group, GroupMember, GroupMemberRole, GroupNotifies, GroupRequestOperateTypes } from '../types' import { Group, GroupMember, GroupMemberRole, GroupNotifies, GroupRequestOperateTypes, GroupNotify } from '../types'
import { callNTQQApi, GeneralCallResult, NTQQApiMethod } from '../ntcall' import { callNTQQApi, GeneralCallResult, NTQQApiMethod } from '../ntcall'
import { NTQQWindowApi, NTQQWindows } from './window' import { NTQQWindowApi, NTQQWindows } from './window'
import { getSession } from '../wrapper' import { getSession } from '../wrapper'
import { NTEventDispatch } from '@/common/utils/EventTask' import { NTEventDispatch } from '@/common/utils/EventTask'
import { NodeIKernelGroupListener } from '../listeners' import { NodeIKernelGroupListener } from '../listeners'
import { NodeIKernelGroupService } from '../services'
export class NTQQGroupApi { export class NTQQGroupApi {
static async activateMemberListChange() { static async activateMemberListChange() {
@@ -45,12 +46,29 @@ export class NTQQGroupApi {
'NodeIKernelGroupListener/onGroupListUpdate', 'NodeIKernelGroupListener/onGroupListUpdate',
1, 1,
5000, 5000,
(updateType) => true, () => true,
forced forced
) )
return groupList return groupList
} }
static async getGroupMemberV2(GroupCode: string, uid: string, forced = false) {
type ListenerType = NodeIKernelGroupListener['onMemberInfoChange']
type EventType = NodeIKernelGroupService['getMemberInfo']
const [, , , _members] = await NTEventDispatch.CallNormalEvent<EventType, ListenerType>
(
'NodeIKernelGroupService/getMemberInfo',
'NodeIKernelGroupListener/onMemberInfoChange',
1,
5000,
(groupCode: string, changeType: number, members: Map<string, GroupMember>) => {
return groupCode == GroupCode && members.has(uid)
},
GroupCode, [uid], forced,
)
return _members.get(uid)
}
static async getGroupMembers(groupQQ: string, num = 3000): Promise<Map<string, GroupMember>> { static async getGroupMembers(groupQQ: string, num = 3000): Promise<Map<string, GroupMember>> {
const session = getSession() const session = getSession()
const groupService = session?.getGroupService() const groupService = session?.getGroupService()
@@ -100,6 +118,36 @@ export class NTQQGroupApi {
) )
} }
static async getSingleScreenNotifies(num: number) {
const [_retData, _doubt, _seq, notifies] = await NTEventDispatch.CallNormalEvent
<(arg1: boolean, arg2: string, arg3: number) => Promise<any>, (doubt: boolean, seq: string, notifies: GroupNotify[]) => void>
(
'NodeIKernelGroupService/getSingleScreenNotifies',
'NodeIKernelGroupListener/onGroupSingleScreenNotifies',
1,
5000,
() => true,
false,
'',
num,
)
return notifies
}
static async delGroupFile(groupCode: string, files: string[]) {
const session = getSession()
return session?.getRichMediaService().deleteGroupFile(groupCode, [102], files)!
}
static DelGroupFile = NTQQGroupApi.delGroupFile
static async delGroupFileFolder(groupCode: string, folderId: string) {
const session = getSession()
return session?.getRichMediaService().deleteGroupFolder(groupCode, folderId)!
}
static DelGroupFileFolder = NTQQGroupApi.delGroupFileFolder
static async handleGroupRequest(flag: string, operateType: GroupRequestOperateTypes, reason?: string) { static async handleGroupRequest(flag: string, operateType: GroupRequestOperateTypes, reason?: string) {
const flagitem = flag.split('|') const flagitem = flag.split('|')
const groupCode = flagitem[0] const groupCode = flagitem[0]

View File

@@ -138,45 +138,47 @@ export class WebApi {
return ret return ret
} }
@CacheClassFuncAsync(3600 * 1000, 'webapi_get_group_members')
static async getGroupMembers(GroupCode: string, cached: boolean = true): Promise<WebApiGroupMember[]> { static async getGroupMembers(GroupCode: string, cached: boolean = true): Promise<WebApiGroupMember[]> {
//logDebug('webapi 获取群成员', GroupCode) const memberData: Array<WebApiGroupMember> = new Array<WebApiGroupMember>()
let MemberData: Array<WebApiGroupMember> = new Array<WebApiGroupMember>() const cookieObject = await NTQQUserApi.getCookies('qun.qq.com')
try { const cookieStr = Object.entries(cookieObject).map(([key, value]) => `${key}=${value}`).join('; ')
const CookiesObject = await NTQQUserApi.getCookies('qun.qq.com')
const CookieValue = Object.entries(CookiesObject).map(([key, value]) => `${key}=${value}`).join('; ')
const Bkn = WebApi.genBkn(CookiesObject.skey)
const retList: Promise<WebApiGroupMemberRet>[] = [] const retList: Promise<WebApiGroupMemberRet>[] = []
const fastRet = await RequestUtil.HttpGetJson<WebApiGroupMemberRet>('https://qun.qq.com/cgi-bin/qun_mgr/search_group_members?st=0&end=40&sort=1&gc=' + GroupCode + '&bkn=' + Bkn, 'POST', '', { 'Cookie': CookieValue }); const params = new URLSearchParams({
st: '0',
end: '40',
sort: '1',
gc: GroupCode,
bkn: WebApi.genBkn(cookieObject.skey)
})
const fastRet = await RequestUtil.HttpGetJson<WebApiGroupMemberRet>(`https://qun.qq.com/cgi-bin/qun_mgr/search_group_members?${params}`, 'POST', '', { 'Cookie': cookieStr })
if (!fastRet?.count || fastRet?.errcode !== 0 || !fastRet?.mems) { if (!fastRet?.count || fastRet?.errcode !== 0 || !fastRet?.mems) {
return [] return []
} else { } else {
for (const key in fastRet.mems) { for (const member of fastRet.mems) {
MemberData.push(fastRet.mems[key]) memberData.push(member)
} }
} }
//初始化获取PageNum const pageNum = Math.ceil(fastRet.count / 40)
const PageNum = Math.ceil(fastRet.count / 40)
//遍历批量请求 //遍历批量请求
for (let i = 2; i <= PageNum; i++) { for (let i = 2; i <= pageNum; i++) {
const ret: Promise<WebApiGroupMemberRet> = RequestUtil.HttpGetJson<WebApiGroupMemberRet>('https://qun.qq.com/cgi-bin/qun_mgr/search_group_members?st=' + (i - 1) * 40 + '&end=' + i * 40 + '&sort=1&gc=' + GroupCode + '&bkn=' + Bkn, 'POST', '', { 'Cookie': CookieValue }); params.set('st', String((i - 1) * 40))
params.set('end', String(i * 40))
const ret = RequestUtil.HttpGetJson<WebApiGroupMemberRet>(`https://qun.qq.com/cgi-bin/qun_mgr/search_group_members?${params}`, 'POST', '', { 'Cookie': cookieStr })
retList.push(ret) retList.push(ret)
} }
//批量等待 //批量等待
for (let i = 1; i <= PageNum; i++) { for (let i = 1; i <= pageNum; i++) {
const ret = await (retList[i]) const ret = await (retList[i])
if (!ret?.count || ret?.errcode !== 0 || !ret?.mems) { if (!ret?.count || ret?.errcode !== 0 || !ret?.mems) {
continue continue
} }
for (const key in ret.mems) { for (const member of ret.mems) {
MemberData.push(ret.mems[key]) memberData.push(member)
} }
} }
} catch { return memberData
return MemberData
}
return MemberData
} }
// public static async addGroupDigest(groupCode: string, msgSeq: string) { // public static async addGroupDigest(groupCode: string, msgSeq: string) {
// const url = `https://qun.qq.com/cgi-bin/group_digest/cancel_digest?random=665&X-CROSS-ORIGIN=fetch&group_code=${groupCode}&msg_seq=${msgSeq}&msg_random=444021292`; // const url = `https://qun.qq.com/cgi-bin/group_digest/cancel_digest?random=665&X-CROSS-ORIGIN=fetch&group_code=${groupCode}&msg_seq=${msgSeq}&msg_random=444021292`;
// const res = await this.request(url); // const res = await this.request(url);

View File

@@ -4,27 +4,20 @@ import { NTQQMsgApi } from './api/msg'
import { import {
CategoryFriend, CategoryFriend,
ChatType, ChatType,
FriendV2,
Group,
GroupMember, GroupMember,
GroupMemberRole, GroupMemberRole,
RawMessage, RawMessage,
SimpleInfo, User, SimpleInfo, User,
} from './types' } from './types'
import { import {
deleteGroup,
friends, friends,
getFriend, getFriend,
getGroupMember, getGroupMember,
groups,
getSelfUin,
setSelfInfo setSelfInfo
} from '@/common/data' } from '@/common/data'
import { OB11GroupDecreaseEvent } from '../onebot11/event/notice/OB11GroupDecreaseEvent'
import { postOb11Event } from '../onebot11/server/post-ob11-event' import { postOb11Event } from '../onebot11/server/post-ob11-event'
import { getConfigUtil, HOOK_LOG } from '@/common/config' import { getConfigUtil } from '@/common/config'
import fs from 'node:fs' import fs from 'node:fs'
import { NTQQGroupApi } from './api/group'
import { log } from '@/common/utils' import { log } from '@/common/utils'
import { randomUUID } from 'node:crypto' import { randomUUID } from 'node:crypto'
import { MessageUnique } from '../common/utils/MessageUnique' import { MessageUnique } from '../common/utils/MessageUnique'
@@ -88,7 +81,7 @@ export function hookNTQQApiReceive(window: BrowserWindow) {
const originalSend = window.webContents.send const originalSend = window.webContents.send
const patchSend = (channel: string, ...args: NTQQApiReturnData) => { const patchSend = (channel: string, ...args: NTQQApiReturnData) => {
// console.log("hookNTQQApiReceive", channel, args) // console.log("hookNTQQApiReceive", channel, args)
let isLogger = false /*let isLogger = false
try { try {
isLogger = args[0]?.eventName?.startsWith('ns-LoggerApi') isLogger = args[0]?.eventName?.startsWith('ns-LoggerApi')
} catch (e) { } } catch (e) { }
@@ -98,7 +91,7 @@ export function hookNTQQApiReceive(window: BrowserWindow) {
} catch (e) { } catch (e) {
log('hook log error', e, args) log('hook log error', e, args)
} }
} }*/
try { try {
if (args?.[1] instanceof Array) { if (args?.[1] instanceof Array) {
for (let receiveData of args?.[1]) { for (let receiveData of args?.[1]) {
@@ -152,9 +145,9 @@ export function hookNTQQApiCall(window: BrowserWindow) {
isLogger = args[3][0].eventName.startsWith('ns-LoggerApi') isLogger = args[3][0].eventName.startsWith('ns-LoggerApi')
} catch (e) { } } catch (e) { }
if (!isLogger) { if (!isLogger) {
try { /*try {
HOOK_LOG && log('call NTQQ api', thisArg, args) HOOK_LOG && log('call NTQQ api', thisArg, args)
} catch (e) { } } catch (e) { }*/
try { try {
const _args: unknown[] = args[3][1] const _args: unknown[] = args[3][1]
const cmdName: NTQQApiMethod = _args[0] as NTQQApiMethod const cmdName: NTQQApiMethod = _args[0] as NTQQApiMethod
@@ -188,16 +181,16 @@ export function hookNTQQApiCall(window: BrowserWindow) {
const proxyIpcInvoke = new Proxy(ipc_invoke_proxy, { const proxyIpcInvoke = new Proxy(ipc_invoke_proxy, {
apply(target, thisArg, args) { apply(target, thisArg, args) {
// console.log(args); // console.log(args);
HOOK_LOG && log('call NTQQ invoke api', thisArg, args) //HOOK_LOG && log('call NTQQ invoke api', thisArg, args)
args[0]['_replyChannel']['sendReply'] = new Proxy(args[0]['_replyChannel']['sendReply'], { args[0]['_replyChannel']['sendReply'] = new Proxy(args[0]['_replyChannel']['sendReply'], {
apply(sendtarget, sendthisArg, sendargs) { apply(sendtarget, sendthisArg, sendargs) {
sendtarget.apply(sendthisArg, sendargs) sendtarget.apply(sendthisArg, sendargs)
}, },
}) })
let ret = target.apply(thisArg, args) let ret = target.apply(thisArg, args)
try { /*try {
HOOK_LOG && log('call NTQQ invoke api return', ret) HOOK_LOG && log('call NTQQ invoke api return', ret)
} catch (e) { } } catch (e) { }*/
return ret return ret
}, },
}) })
@@ -242,9 +235,9 @@ export function removeReceiveHook(id: string) {
receiveHooks.splice(index, 1) receiveHooks.splice(index, 1)
} }
let activatedGroups: string[] = [] //let activatedGroups: string[] = []
async function updateGroups(_groups: Group[], needUpdate: boolean = true) { /*async function updateGroups(_groups: Group[], needUpdate: boolean = true) {
for (let group of _groups) { for (let group of _groups) {
log('update group', group.groupCode) log('update group', group.groupCode)
if (group.privilegeFlag === 0) { if (group.privilegeFlag === 0) {
@@ -269,9 +262,9 @@ async function updateGroups(_groups: Group[], needUpdate: boolean = true) {
} }
} }
} }
} }*/
async function processGroupEvent(payload: { groupList: Group[] }) { /*async function processGroupEvent(payload: { groupList: Group[] }) {
try { try {
const newGroupList = payload.groupList const newGroupList = payload.groupList
for (const group of newGroupList) { for (const group of newGroupList) {
@@ -322,12 +315,12 @@ async function processGroupEvent(payload: { groupList: Group[] }) {
updateGroups(payload.groupList).then() updateGroups(payload.groupList).then()
log('更新群信息错误', e.stack.toString()) log('更新群信息错误', e.stack.toString())
} }
} }*/
export async function startHook() { export async function startHook() {
// 群列表变动 // 群列表变动
registerReceiveHook<{ groupList: Group[]; updateType: number }>(ReceiveCmdS.GROUPS, (payload) => { /*registerReceiveHook<{ groupList: Group[]; updateType: number }>(ReceiveCmdS.GROUPS, (payload) => {
// updateType 3是群列表变动2是群成员变动 // updateType 3是群列表变动2是群成员变动
// log("群列表变动", payload.updateType, payload.groupList) // log("群列表变动", payload.updateType, payload.groupList)
if (payload.updateType != 2) { if (payload.updateType != 2) {
@@ -350,7 +343,7 @@ export async function startHook() {
processGroupEvent(payload).then() processGroupEvent(payload).then()
} }
} }
}) })*/
registerReceiveHook<{ registerReceiveHook<{
groupCode: string groupCode: string

View File

@@ -1,7 +1,6 @@
import { ipcMain } from 'electron' import { ipcMain } from 'electron'
import { hookApiCallbacks, ReceiveCmd, ReceiveCmdS, registerReceiveHook, removeReceiveHook } from './hook' import { hookApiCallbacks, ReceiveCmd, ReceiveCmdS, registerReceiveHook, removeReceiveHook } from './hook'
import { log } from '../common/utils/log' import { log } from '../common/utils/log'
import { HOOK_LOG } from '../common/config'
import { randomUUID } from 'node:crypto' import { randomUUID } from 'node:crypto'
export enum NTQQApiClass { export enum NTQQApiClass {
@@ -15,6 +14,7 @@ export enum NTQQApiClass {
SKEY_API = 'ns-SkeyApi', SKEY_API = 'ns-SkeyApi',
GROUP_HOME_WORK = 'ns-GroupHomeWork', GROUP_HOME_WORK = 'ns-GroupHomeWork',
GROUP_ESSENCE = 'ns-GroupEssence', GROUP_ESSENCE = 'ns-GroupEssence',
NODE_STORE_API = 'ns-NodeStoreApi'
} }
export enum NTQQApiMethod { export enum NTQQApiMethod {
@@ -129,7 +129,7 @@ export function callNTQQApi<ReturnType>(params: NTQQApiParams) {
timeout = timeout ?? 5 timeout = timeout ?? 5
afterFirstCmd = afterFirstCmd ?? true afterFirstCmd = afterFirstCmd ?? true
const uuid = randomUUID() const uuid = randomUUID()
HOOK_LOG && log('callNTQQApi', channel, className, methodName, args, uuid) //HOOK_LOG && log('callNTQQApi', channel, className, methodName, args, uuid)
return new Promise((resolve: (data: ReturnType) => void, reject) => { return new Promise((resolve: (data: ReturnType) => void, reject) => {
// log("callNTQQApiPromise", channel, className, methodName, args, uuid) // log("callNTQQApiPromise", channel, className, methodName, args, uuid)
const _timeout = timeout * 1000 const _timeout = timeout * 1000
@@ -203,24 +203,3 @@ export interface GeneralCallResult {
result: number // 0: success result: number // 0: success
errMsg: string errMsg: string
} }
export class NTQQApi {
static async call(className: NTQQApiClass, cmdName: string, args: any[]) {
return await callNTQQApi<GeneralCallResult>({
className,
methodName: cmdName,
args: [...args],
})
}
static async fetchUnitedCommendConfig() {
return await callNTQQApi<GeneralCallResult>({
methodName: NTQQApiMethod.FETCH_UNITED_COMMEND_CONFIG,
args: [
{
groups: ['100243'],
},
],
})
}
}

View File

@@ -45,7 +45,7 @@ export enum GroupMemberRole {
} }
export interface GroupMember { export interface GroupMember {
memberSpecialTitle: string memberSpecialTitle?: string
avatarPath: string avatarPath: string
cardName: string cardName: string
cardType: number cardType: number
@@ -60,4 +60,7 @@ export interface GroupMember {
isRobot: boolean isRobot: boolean
sex?: Sex sex?: Sex
qqLevel?: QQLevel qqLevel?: QQLevel
isChangeRole: boolean
joinTime: string
lastSpeakTime: string
} }

View File

@@ -10,7 +10,7 @@ export class OB11Response {
data: data, data: data,
message: message, message: message,
wording: message, wording: message,
echo: null, echo: undefined,
} }
} }

View File

@@ -23,66 +23,12 @@ export abstract class GetFileBase extends BaseAction<GetFilePayload, GetFileResp
// forked from https://github.com/NapNeko/NapCatQQ/blob/6f6b258f22d7563f15d84e7172c4d4cbb547f47e/src/onebot11/action/file/GetFile.ts#L44 // forked from https://github.com/NapNeko/NapCatQQ/blob/6f6b258f22d7563f15d84e7172c4d4cbb547f47e/src/onebot11/action/file/GetFile.ts#L44
protected async _handle(payload: GetFilePayload): Promise<GetFileResponse> { protected async _handle(payload: GetFilePayload): Promise<GetFileResponse> {
const { enableLocalFile2Url } = getConfigUtil().getConfig() const { enableLocalFile2Url } = getConfigUtil().getConfig()
let UuidData: {
high: string
low: string
} | undefined
try {
UuidData = UUIDConverter.decode(payload.file)
if (UuidData) {
const peerUin = UuidData.high
const msgId = UuidData.low
const isGroup: boolean = !!(await NTQQGroupApi.getGroups(false)).find(e => e.groupCode == peerUin)
let peer: Peer | undefined
//识别Peer
if (isGroup) {
peer = { chatType: ChatType.group, peerUid: peerUin }
}
const PeerUid = await NTQQUserApi.getUidByUinV2(peerUin)
if (PeerUid) {
const isBuddy = await NTQQFriendApi.isBuddy(PeerUid)
if (isBuddy) {
peer = { chatType: ChatType.friend, peerUid: PeerUid }
} else {
peer = { chatType: ChatType.temp, peerUid: PeerUid }
}
}
if (!peer) {
throw new Error('chattype not support')
}
const msgList = await NTQQMsgApi.getMsgsByMsgId(peer, [msgId])
if (msgList.msgList.length === 0) {
throw new Error('msg not found')
}
const msg = msgList.msgList[0]
const findEle = msg.elements.find(e => e.elementType == ElementType.VIDEO || e.elementType == ElementType.FILE || e.elementType == ElementType.PTT)
if (!findEle) {
throw new Error('element not found')
}
const downloadPath = await NTQQFileApi.downloadMedia(msgId, msg.chatType, msg.peerUid, findEle.elementId, '', '')
const fileSize = findEle?.videoElement?.fileSize || findEle?.fileElement?.fileSize || findEle?.pttElement?.fileSize || '0'
const fileName = findEle?.videoElement?.fileName || findEle?.fileElement?.fileName || findEle?.pttElement?.fileName || ''
const res: GetFileResponse = {
file: downloadPath,
url: downloadPath,
file_size: fileSize,
file_name: fileName,
}
if (enableLocalFile2Url && downloadPath) {
try {
res.base64 = await fsPromise.readFile(downloadPath, 'base64')
} catch (e) {
throw new Error('文件下载失败. ' + e)
}
}
//不手动删除?文件持久化了
return res
}
} catch {
let fileCache = await MessageUnique.getFileCacheById(String(payload.file))
if (!fileCache?.length) {
fileCache = await MessageUnique.getFileCacheByName(String(payload.file))
} }
const fileCache = await MessageUnique.getFileCache(String(payload.file))
if (fileCache?.length) { if (fileCache?.length) {
const downloadPath = await NTQQFileApi.downloadMedia( const downloadPath = await NTQQFileApi.downloadMedia(
fileCache[0].msgId, fileCache[0].msgId,

View File

@@ -0,0 +1,17 @@
import BaseAction from '../BaseAction'
import { ActionName } from '../types'
import { NTQQGroupApi } from '@/ntqqapi/api'
interface Payload {
group_id: string | number
file_id: string
busid?: 102
}
export class GoCQHTTPDelGroupFile extends BaseAction<Payload, void> {
actionName = ActionName.GoCQHTTP_DelGroupFile
async _handle(payload: Payload) {
await NTQQGroupApi.delGroupFile(payload.group_id.toString(), [payload.file_id])
}
}

View File

@@ -1,6 +1,5 @@
import fs from 'node:fs' import fs from 'node:fs'
import BaseAction from '../BaseAction' import BaseAction from '../BaseAction'
import { getGroup } from '@/common/data'
import { ActionName } from '../types' import { ActionName } from '../types'
import { SendMsgElementConstructor } from '@/ntqqapi/constructor' import { SendMsgElementConstructor } from '@/ntqqapi/constructor'
import { ChatType, SendFileElement } from '@/ntqqapi/types' import { ChatType, SendFileElement } from '@/ntqqapi/types'
@@ -22,10 +21,6 @@ export class GoCQHTTPUploadGroupFile extends BaseAction<Payload, null> {
actionName = ActionName.GoCQHTTP_UploadGroupFile actionName = ActionName.GoCQHTTP_UploadGroupFile
protected async _handle(payload: Payload): Promise<null> { protected async _handle(payload: Payload): Promise<null> {
const group = await getGroup(payload.group_id?.toString()!)
if (!group) {
throw new Error(`群组${payload.group_id}不存在`)
}
let file = payload.file let file = payload.file
if (fs.existsSync(file)) { if (fs.existsSync(file)) {
file = `file://${file}` file = `file://${file}`
@@ -34,8 +29,11 @@ export class GoCQHTTPUploadGroupFile extends BaseAction<Payload, null> {
if (!downloadResult.success) { if (!downloadResult.success) {
throw new Error(downloadResult.errMsg) throw new Error(downloadResult.errMsg)
} }
const sendFileEle: SendFileElement = await SendMsgElementConstructor.file(downloadResult.path, payload.name, payload.folder_id) const sendFileEle = await SendMsgElementConstructor.file(downloadResult.path, payload.name, payload.folder_id)
await sendMsg({ chatType: ChatType.group, peerUid: group.groupCode }, [sendFileEle], [], true) await sendMsg({
chatType: ChatType.group,
peerUid: payload.group_id?.toString()!,
}, [sendFileEle], [], true)
return null return null
} }
} }

View File

@@ -1,18 +1,18 @@
import { OB11Group } from '../../types' import { OB11Group } from '../../types'
import { getGroup } from '../../../common/data'
import { OB11Constructor } from '../../constructor' import { OB11Constructor } from '../../constructor'
import BaseAction from '../BaseAction' import BaseAction from '../BaseAction'
import { ActionName } from '../types' import { ActionName } from '../types'
import { NTQQGroupApi } from '@/ntqqapi/api'
interface PayloadType { interface Payload {
group_id: number group_id: number | string
} }
class GetGroupInfo extends BaseAction<PayloadType, OB11Group> { class GetGroupInfo extends BaseAction<Payload, OB11Group> {
actionName = ActionName.GetGroupInfo actionName = ActionName.GetGroupInfo
protected async _handle(payload: PayloadType) { protected async _handle(payload: Payload) {
const group = await getGroup(payload.group_id.toString()) const group = (await NTQQGroupApi.getGroups()).find(e => e.groupCode == payload.group_id.toString())
if (group) { if (group) {
return OB11Constructor.group(group) return OB11Constructor.group(group)
} else { } else {

View File

@@ -1,10 +1,8 @@
import { OB11Group } from '../../types' import { OB11Group } from '../../types'
import { OB11Constructor } from '../../constructor' import { OB11Constructor } from '../../constructor'
import { groups } from '../../../common/data'
import BaseAction from '../BaseAction' import BaseAction from '../BaseAction'
import { ActionName } from '../types' import { ActionName } from '../types'
import { NTQQGroupApi } from '../../../ntqqapi/api' import { NTQQGroupApi } from '../../../ntqqapi/api'
import { log } from '../../../common/utils'
interface Payload { interface Payload {
no_cache: boolean | string no_cache: boolean | string
@@ -14,14 +12,8 @@ class GetGroupList extends BaseAction<Payload, OB11Group[]> {
actionName = ActionName.GetGroupList actionName = ActionName.GetGroupList
protected async _handle(payload: Payload) { protected async _handle(payload: Payload) {
if (groups.length === 0 || payload?.no_cache === true || payload?.no_cache === 'true') { const groupList = await NTQQGroupApi.getGroups(payload?.no_cache === true || payload?.no_cache === 'true')
try { return OB11Constructor.groups(groupList)
const groups = await NTQQGroupApi.getGroups(true)
log('强制刷新群列表, 数量:', groups.length)
return OB11Constructor.groups(groups)
} catch (e) {}
}
return OB11Constructor.groups(groups)
} }
} }

View File

@@ -1,30 +1,44 @@
import { OB11GroupMember } from '../../types' import { OB11GroupMember } from '../../types'
import { getGroupMember } from '../../../common/data' import { getGroupMember, getSelfUid } from '@/common/data'
import { OB11Constructor } from '../../constructor' import { OB11Constructor } from '../../constructor'
import BaseAction from '../BaseAction' import BaseAction from '../BaseAction'
import { ActionName } from '../types' import { ActionName } from '../types'
import { NTQQUserApi } from '../../../ntqqapi/api/user' import { NTQQUserApi, WebApi } from '@/ntqqapi/api'
import { log } from '../../../common/utils/log' import { isNull } from '@/common/utils/helper'
import { isNull } from '../../../common/utils/helper'
export interface PayloadType { interface Payload {
group_id: number group_id: number | string
user_id: number user_id: number | string
} }
class GetGroupMemberInfo extends BaseAction<PayloadType, OB11GroupMember> { class GetGroupMemberInfo extends BaseAction<Payload, OB11GroupMember> {
actionName = ActionName.GetGroupMemberInfo actionName = ActionName.GetGroupMemberInfo
protected async _handle(payload: PayloadType) { protected async _handle(payload: Payload) {
const member = await getGroupMember(payload.group_id.toString(), payload.user_id.toString()) const member = await getGroupMember(payload.group_id.toString(), payload.user_id.toString())
if (member) { if (member) {
if (isNull(member.sex)) { if (isNull(member.sex)) {
log('获取群成员详细信息') //log('获取群成员详细信息')
let info = await NTQQUserApi.getUserDetailInfo(member.uid, true) const info = await NTQQUserApi.getUserDetailInfo(member.uid, true)
log('群成员详细信息结果', info) //log('群成员详细信息结果', info)
Object.assign(member, info) Object.assign(member, info)
} }
return OB11Constructor.groupMember(payload.group_id.toString(), member) const ret = OB11Constructor.groupMember(payload.group_id.toString(), member)
const self = await getGroupMember(payload.group_id.toString(), getSelfUid())
if (self?.role === 3 || self?.role === 4) {
const webGroupMembers = await WebApi.getGroupMembers(payload.group_id.toString())
const target = webGroupMembers.find(e => e?.uin && e.uin === ret.user_id)
if (target) {
ret.join_time = target.join_time
ret.last_sent_time = target.last_speak_time
ret.qage = target.qage
ret.level = target.lv.level.toString()
}
}
const date = Math.round(Date.now() / 1000)
ret.last_sent_time ||= Number(member.lastSpeakTime || date)
ret.join_time ||= Number(member.joinTime || date)
return ret
} else { } else {
throw `群成员${payload.user_id}不存在` throw `群成员${payload.user_id}不存在`
} }

View File

@@ -1,31 +1,58 @@
import { OB11GroupMember } from '../../types' import { OB11GroupMember } from '../../types'
import { getGroup } from '../../../common/data'
import { OB11Constructor } from '../../constructor' import { OB11Constructor } from '../../constructor'
import BaseAction from '../BaseAction' import BaseAction from '../BaseAction'
import { ActionName } from '../types' import { ActionName } from '../types'
import { NTQQGroupApi } from '../../../ntqqapi/api/group' import { NTQQGroupApi, WebApi } from '@/ntqqapi/api'
import { log } from '../../../common/utils' import { getSelfUid } from '@/common/data'
export interface PayloadType { interface Payload {
group_id: number group_id: number | string
no_cache: boolean | string no_cache: boolean | string
} }
class GetGroupMemberList extends BaseAction<PayloadType, OB11GroupMember[]> { class GetGroupMemberList extends BaseAction<Payload, OB11GroupMember[]> {
actionName = ActionName.GetGroupMemberList actionName = ActionName.GetGroupMemberList
protected async _handle(payload: PayloadType) { protected async _handle(payload: Payload) {
const group = await getGroup(payload.group_id.toString()) const groupMembers = await NTQQGroupApi.getGroupMembers(payload.group_id.toString())
if (group) { const groupMembersArr = Array.from(groupMembers.values())
if (!group.members?.length || payload.no_cache === true || payload.no_cache === 'true') {
const members = await NTQQGroupApi.getGroupMembers(payload.group_id.toString()) let _groupMembers = groupMembersArr.map(item => {
group.members = Array.from(members.values()) return OB11Constructor.groupMember(payload.group_id.toString(), item)
log('强制刷新群成员列表, 数量: ', group.members.length) })
const MemberMap: Map<number, OB11GroupMember> = new Map<number, OB11GroupMember>()
const date = Math.round(Date.now() / 1000)
for (let i = 0, len = _groupMembers.length; i < len; i++) {
// 保证基础数据有这个 同时避免群管插件过于依赖这个杀了
_groupMembers[i].join_time = date
_groupMembers[i].last_sent_time = date
MemberMap.set(_groupMembers[i].user_id, _groupMembers[i])
} }
return OB11Constructor.groupMembers(group)
} else { const selfRole = groupMembers.get(getSelfUid())?.role
throw `${payload.group_id}不存在` const isPrivilege = selfRole === 3 || selfRole === 4
if (isPrivilege) {
const webGroupMembers = await WebApi.getGroupMembers(payload.group_id.toString())
for (let i = 0, len = webGroupMembers.length; i < len; i++) {
if (!webGroupMembers[i]?.uin) {
continue
} }
const MemberData = MemberMap.get(webGroupMembers[i]?.uin)
if (MemberData) {
MemberData.join_time = webGroupMembers[i]?.join_time
MemberData.last_sent_time = webGroupMembers[i]?.last_speak_time
MemberData.qage = webGroupMembers[i]?.qage
MemberData.level = webGroupMembers[i]?.lv.level.toString()
MemberMap.set(webGroupMembers[i]?.uin, MemberData)
}
}
}
_groupMembers = Array.from(MemberMap.values())
return _groupMembers
} }
} }

View File

@@ -53,6 +53,7 @@ import { GoCQHTTHandleQuickOperation } from './go-cqhttp/QuickOperation'
import GoCQHTTPSetEssenceMsg from './go-cqhttp/SetEssenceMsg' import GoCQHTTPSetEssenceMsg from './go-cqhttp/SetEssenceMsg'
import GoCQHTTPDelEssenceMsg from './go-cqhttp/DelEssenceMsg' import GoCQHTTPDelEssenceMsg from './go-cqhttp/DelEssenceMsg'
import GetEvent from './llonebot/GetEvent' import GetEvent from './llonebot/GetEvent'
import { GoCQHTTPDelGroupFile } from './go-cqhttp/DelGroupFile'
export const actionHandlers = [ export const actionHandlers = [
@@ -113,7 +114,8 @@ export const actionHandlers = [
new GoCQHTTGetForwardMsgAction(), new GoCQHTTGetForwardMsgAction(),
new GoCQHTTHandleQuickOperation(), new GoCQHTTHandleQuickOperation(),
new GoCQHTTPSetEssenceMsg(), new GoCQHTTPSetEssenceMsg(),
new GoCQHTTPDelEssenceMsg() new GoCQHTTPDelEssenceMsg(),
new GoCQHTTPDelGroupFile()
] ]
function initActionMap() { function initActionMap() {

View File

@@ -6,7 +6,7 @@ import {
RawMessage, RawMessage,
SendMessageElement, SendMessageElement,
} from '@/ntqqapi/types' } from '@/ntqqapi/types'
import { getGroup, getGroupMember, getSelfUid, getSelfUin } from '@/common/data' import { getGroupMember, getSelfUid, getSelfUin } from '@/common/data'
import { import {
OB11MessageCustomMusic, OB11MessageCustomMusic,
OB11MessageData, OB11MessageData,
@@ -289,12 +289,12 @@ export async function sendMsg(
log('文件大小计算失败', e, fileElement) log('文件大小计算失败', e, fileElement)
} }
} }
log('发送消息总大小', totalSize, 'bytes') //log('发送消息总大小', totalSize, 'bytes')
let timeout = ((totalSize / 1024 / 100) * 1000) + 5000 // 100kb/s const timeout = 10000 + (totalSize / 1024 / 256 * 1000) // 10s Basic Timeout + PredictTime( For File 512kb/s )
log('设置消息超时时间', timeout) //log('设置消息超时时间', timeout)
const returnMsg = await NTQQMsgApi.sendMsg(peer, sendElements, waitComplete, timeout) const returnMsg = await NTQQMsgApi.sendMsg(peer, sendElements, waitComplete, timeout)
log('消息发送结果', returnMsg)
returnMsg.msgShortId = MessageUnique.createMsg(peer, returnMsg.msgId) returnMsg.msgShortId = MessageUnique.createMsg(peer, returnMsg.msgId)
log('消息发送', returnMsg.msgShortId)
deleteAfterSentFiles.map(path => fsPromise.unlink(path)) deleteAfterSentFiles.map(path => fsPromise.unlink(path))
return returnMsg return returnMsg
} }
@@ -305,10 +305,9 @@ async function createContext(payload: OB11PostSendMsg, contextMode: ContextMode)
// This redundant design of Ob11 here should be blamed. // This redundant design of Ob11 here should be blamed.
if ((contextMode === ContextMode.Group || contextMode === ContextMode.Normal) && payload.group_id) { if ((contextMode === ContextMode.Group || contextMode === ContextMode.Normal) && payload.group_id) {
const group = (await getGroup(payload.group_id))! // checked before
return { return {
chatType: ChatType.group, chatType: ChatType.group,
peerUid: group.groupCode peerUid: payload.group_id.toString(),
} }
} }
if ((contextMode === ContextMode.Private || contextMode === ContextMode.Normal) && payload.user_id) { if ((contextMode === ContextMode.Private || contextMode === ContextMode.Normal) && payload.user_id) {
@@ -318,7 +317,7 @@ async function createContext(payload: OB11PostSendMsg, contextMode: ContextMode)
return { return {
chatType: isBuddy ? ChatType.friend : ChatType.temp, chatType: isBuddy ? ChatType.friend : ChatType.temp,
peerUid: Uid!, peerUid: Uid!,
guildId: payload.group_id || ''//临时主动发起时需要传入群号 guildId: payload.group_id?.toString() || '' //临时主动发起时需要传入群号
} }
} }
throw '请指定 group_id 或 user_id' throw '请指定 group_id 或 user_id'
@@ -343,12 +342,6 @@ export class SendMsg extends BaseAction<OB11PostSendMsg, ReturnDataType> {
message: '音乐消息不可以和其他消息混在一起发送', message: '音乐消息不可以和其他消息混在一起发送',
} }
} }
if (payload.message_type !== 'private' && payload.group_id && !(await getGroup(payload.group_id))) {
return {
valid: false,
message: `${payload.group_id}不存在`,
}
}
if (payload.user_id && payload.message_type !== 'group') { if (payload.user_id && payload.message_type !== 'group') {
const uid = await NTQQUserApi.getUidByUin(payload.user_id.toString()) const uid = await NTQQUserApi.getUidByUin(payload.user_id.toString())
const isBuddy = await NTQQFriendApi.isBuddy(uid!) const isBuddy = await NTQQFriendApi.isBuddy(uid!)

View File

@@ -73,4 +73,5 @@ export enum ActionName {
GetGroupHonorInfo = "get_group_honor_info", GetGroupHonorInfo = "get_group_honor_info",
GoCQHTTP_SetEssenceMsg = 'set_essence_msg', GoCQHTTP_SetEssenceMsg = 'set_essence_msg',
GoCQHTTP_DelEssenceMsg = 'delete_essence_msg', GoCQHTTP_DelEssenceMsg = 'delete_essence_msg',
GoCQHTTP_DelGroupFile = 'delete_group_file',
} }

View File

@@ -17,7 +17,6 @@ import {
Group, Group,
Peer, Peer,
GroupMember, GroupMember,
PicType,
RawMessage, RawMessage,
SelfInfo, SelfInfo,
Sex, Sex,
@@ -26,11 +25,10 @@ import {
FriendV2, FriendV2,
ChatType2 ChatType2
} from '../ntqqapi/types' } from '../ntqqapi/types'
import { deleteGroup, getGroupMember, getSelfUin } from '../common/data' import { getGroupMember, getSelfUin } from '../common/data'
import { EventType } from './event/OB11BaseEvent' import { EventType } from './event/OB11BaseEvent'
import { encodeCQCode } from './cqcode' import { encodeCQCode } from './cqcode'
import { MessageUnique } from '../common/utils/MessageUnique' import { MessageUnique } from '../common/utils/MessageUnique'
import { UUIDConverter } from '../common/utils/helper'
import { OB11GroupIncreaseEvent } from './event/notice/OB11GroupIncreaseEvent' import { OB11GroupIncreaseEvent } from './event/notice/OB11GroupIncreaseEvent'
import { OB11GroupBanEvent } from './event/notice/OB11GroupBanEvent' import { OB11GroupBanEvent } from './event/notice/OB11GroupBanEvent'
import { OB11GroupUploadNoticeEvent } from './event/notice/OB11GroupUploadNoticeEvent' import { OB11GroupUploadNoticeEvent } from './event/notice/OB11GroupUploadNoticeEvent'
@@ -192,41 +190,61 @@ export class OB11Constructor {
}*/ }*/
message_data['data']['file'] = picElement.fileName message_data['data']['file'] = picElement.fileName
message_data['data']['subType'] = picElement.picSubType message_data['data']['subType'] = picElement.picSubType
message_data['data']['file_id'] = UUIDConverter.encode(msg.peerUin, msg.msgId) //message_data['data']['file_id'] = picElement.fileUuid
message_data['data']['url'] = await NTQQFileApi.getImageUrl(picElement) message_data['data']['url'] = await NTQQFileApi.getImageUrl(picElement)
message_data['data']['file_size'] = picElement.fileSize message_data['data']['file_size'] = picElement.fileSize
MessageUnique.addFileCache({ MessageUnique.addFileCache({
peerUid: msg.peerUid, peerUid: msg.peerUid,
msgId: msg.msgId, msgId: msg.msgId,
msgTime: +msg.msgTime,
chatType: msg.chatType, chatType: msg.chatType,
elementId: element.elementId, elementId: element.elementId,
elementType: element.elementType, elementType: element.elementType,
fileName: picElement.fileName, fileName: picElement.fileName,
fileSize: String(picElement.fileSize || '0'), fileSize: String(picElement.fileSize || '0'),
fileUuid: picElement.fileUuid
}) })
} }
else if (element.videoElement || element.fileElement) { else if (element.videoElement) {
const videoOrFileElement = element.videoElement || element.fileElement message_data['type'] = OB11MessageDataType.video
message_data['type'] = element.videoElement ? OB11MessageDataType.video : OB11MessageDataType.file const { videoElement } = element
message_data['data']['file'] = videoOrFileElement.fileName message_data['data']['file'] = videoElement.fileName
message_data['data']['path'] = videoOrFileElement.filePath message_data['data']['path'] = videoElement.filePath
message_data['data']['file_id'] = UUIDConverter.encode(msg.peerUin, msg.msgId) //message_data['data']['file_id'] = videoElement.fileUuid
message_data['data']['file_size'] = videoOrFileElement.fileSize message_data['data']['file_size'] = videoElement.fileSize
if (element.videoElement) {
message_data['data']['url'] = await NTQQFileApi.getVideoUrl({ message_data['data']['url'] = await NTQQFileApi.getVideoUrl({
chatType: msg.chatType, chatType: msg.chatType,
peerUid: msg.peerUid, peerUid: msg.peerUid,
}, msg.msgId, element.elementId, }, msg.msgId, element.elementId)
)
}
MessageUnique.addFileCache({ MessageUnique.addFileCache({
peerUid: msg.peerUid, peerUid: msg.peerUid,
msgId: msg.msgId, msgId: msg.msgId,
msgTime: +msg.msgTime,
chatType: msg.chatType, chatType: msg.chatType,
elementId: element.elementId, elementId: element.elementId,
elementType: element.elementType, elementType: element.elementType,
fileName: videoOrFileElement.fileName, fileName: videoElement.fileName,
fileSize: String(videoOrFileElement.fileSize || '0') fileSize: String(videoElement.fileSize || '0'),
fileUuid: videoElement.fileUuid!
})
}
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
MessageUnique.addFileCache({
peerUid: msg.peerUid,
msgId: msg.msgId,
msgTime: +msg.msgTime,
chatType: msg.chatType,
elementId: element.elementId,
elementType: element.elementType,
fileName: fileElement.fileName,
fileSize: String(fileElement.fileSize || '0'),
fileUuid: fileElement.fileUuid!
}) })
} }
else if (element.pttElement) { else if (element.pttElement) {
@@ -234,16 +252,18 @@ export class OB11Constructor {
const { pttElement } = element const { pttElement } = element
message_data['data']['file'] = pttElement.fileName message_data['data']['file'] = pttElement.fileName
message_data['data']['path'] = pttElement.filePath message_data['data']['path'] = pttElement.filePath
message_data['data']['file_id'] = UUIDConverter.encode(msg.peerUin, msg.msgId) //message_data['data']['file_id'] = pttElement.fileUuid
message_data['data']['file_size'] = pttElement.fileSize message_data['data']['file_size'] = pttElement.fileSize
MessageUnique.addFileCache({ MessageUnique.addFileCache({
peerUid: msg.peerUid, peerUid: msg.peerUid,
msgId: msg.msgId, msgId: msg.msgId,
msgTime: +msg.msgTime,
chatType: msg.chatType, chatType: msg.chatType,
elementId: element.elementId, elementId: element.elementId,
elementType: element.elementType, elementType: element.elementType,
fileName: pttElement.fileName, fileName: pttElement.fileName,
fileSize: String(pttElement.fileSize || '0') fileSize: String(pttElement.fileSize || '0'),
fileUuid: pttElement.fileUuid
}) })
} }
else if (element.arkElement) { else if (element.arkElement) {
@@ -358,7 +378,7 @@ export class OB11Constructor {
const groupElement = grayTipElement?.groupElement const groupElement = grayTipElement?.groupElement
if (groupElement) { if (groupElement) {
// log("收到群提示消息", groupElement) // log("收到群提示消息", groupElement)
if (groupElement.type == TipGroupElementType.memberIncrease) { if (groupElement.type === TipGroupElementType.memberIncrease) {
log('收到群成员增加消息', groupElement) log('收到群成员增加消息', groupElement)
await sleep(1000) await sleep(1000)
const member = await getGroupMember(msg.peerUid, groupElement.memberUid) const member = await getGroupMember(msg.peerUid, groupElement.memberUid)
@@ -406,25 +426,26 @@ export class OB11Constructor {
) )
} }
} }
else if (groupElement.type == TipGroupElementType.kicked) { else if (groupElement.type === TipGroupElementType.kicked) {
log(`收到我被踢出或退群提示, 群${msg.peerUid}`, groupElement) log(`收到我被踢出或退群提示, 群${msg.peerUid}`, groupElement)
deleteGroup(msg.peerUid)
NTQQGroupApi.quitGroup(msg.peerUid).then() NTQQGroupApi.quitGroup(msg.peerUid).then()
const selfUin = getSelfUin()
try { try {
const adminUin = const adminUin = (await getGroupMember(msg.peerUid, groupElement.adminUid))?.uin || (await NTQQUserApi.getUidByUin(groupElement.adminUid))
(await getGroupMember(msg.peerUid, groupElement.adminUid))?.uin ||
(await NTQQUserApi.getUserDetailInfo(groupElement.adminUid))?.uin
if (adminUin) { if (adminUin) {
return new OB11GroupDecreaseEvent( return new OB11GroupDecreaseEvent(
parseInt(msg.peerUid), parseInt(msg.peerUid),
parseInt(selfUin), parseInt(getSelfUin()),
parseInt(adminUin), parseInt(adminUin),
'kick_me', 'kick_me'
) )
} }
} catch (e) { } catch (e) {
return new OB11GroupDecreaseEvent(parseInt(msg.peerUid), parseInt(selfUin), 0, 'leave') return new OB11GroupDecreaseEvent(
parseInt(msg.peerUid),
parseInt(getSelfUin()),
0,
'leave'
)
} }
} }
} }
@@ -677,7 +698,7 @@ export class OB11Constructor {
sex: OB11Constructor.sex(member.sex!), sex: OB11Constructor.sex(member.sex!),
age: 0, age: 0,
area: '', area: '',
level: 0, level: '0',
qq_level: (member.qqLevel && calcQQLevel(member.qqLevel)) || 0, qq_level: (member.qqLevel && calcQQLevel(member.qqLevel)) || 0,
join_time: 0, // 暂时没法获取 join_time: 0, // 暂时没法获取
last_sent_time: 0, // 暂时没法获取 last_sent_time: 0, // 暂时没法获取

View File

@@ -61,13 +61,15 @@ export function postOb11Event(msg: PostEventType, reportSelf = false, postWs = t
body: msgStr, body: msgStr,
}).then( }).then(
async (res) => { async (res) => {
log(`新消息事件HTTP上报成功: ${host} `, msgStr) if (msg.post_type) {
log(`HTTP 事件上报: ${host} `, msg.post_type)
}
try { try {
const resJson = await res.json() const resJson = await res.json()
log(`新消息事件HTTP上报返回快速操作: `, JSON.stringify(resJson)) log(`新消息事件HTTP上报返回快速操作: `, JSON.stringify(resJson))
handleQuickOperation(msg as QuickOperationEvent, resJson).then().catch(log); handleQuickOperation(msg as QuickOperationEvent, resJson).then().catch(log);
} catch (e) { } catch (e) {
log(`新消息事件HTTP上报没有返回快速操作不需要处理`) //log(`新消息事件HTTP上报没有返回快速操作不需要处理`)
return return
} }
}, },

View File

@@ -1,18 +1,15 @@
import { WebSocket as WebSocketClass } from 'ws' import { WebSocket as WebSocketClass } from 'ws'
import { OB11Response } from '../../action/OB11Response'
import { PostEventType } from '../post-ob11-event' import { PostEventType } from '../post-ob11-event'
import { log } from '../../../common/utils/log' import { log } from '@/common/utils/log'
import { isNull } from '../../../common/utils/helper' import { OB11Return } from '../../types'
export function wsReply(wsClient: WebSocketClass, data: OB11Response | PostEventType) { export function wsReply(wsClient: WebSocketClass, data: OB11Return<any> | PostEventType) {
try { try {
const packet = Object.assign({}, data) wsClient.send(JSON.stringify(data))
if (isNull(packet['echo'])) { if (data['post_type']) {
delete packet['echo'] log('WebSocket 事件上报', wsClient.url ?? '', data['post_type'])
} }
wsClient.send(JSON.stringify(packet))
//log('ws 消息上报', wsClient.url || '', data)
} catch (e: any) { } catch (e: any) {
log('websocket 回复失败', e.stack, data) log('WebSocket 上报失败', e.stack, data)
} }
} }

View File

@@ -36,7 +36,7 @@ export interface OB11GroupMember {
age?: number age?: number
join_time?: number join_time?: number
last_sent_time?: number last_sent_time?: number
level?: number level?: string
qq_level?: number qq_level?: number
role?: OB11GroupMemberRole role?: OB11GroupMemberRole
title?: string title?: string
@@ -48,6 +48,7 @@ export interface OB11GroupMember {
shut_up_timestamp?: number shut_up_timestamp?: number
// 以下为扩展字段 // 以下为扩展字段
is_robot?: boolean is_robot?: boolean
qage?: number
} }
export interface OB11Group { export interface OB11Group {

View File

@@ -1 +1 @@
export const version = '3.29.2' export const version = '3.29.5'