This commit is contained in:
idranme 2024-08-28 06:49:46 +08:00
parent 5501980ab3
commit 7cb94cb8b8
No known key found for this signature in database
GPG Key ID: 926F7B5B668E495F
33 changed files with 382 additions and 602 deletions

View File

@ -1,9 +1,26 @@
import fs from 'node:fs'
import { Config, OB11Config } from './types'
import { mergeNewProperties } from './utils/helper'
import path from 'node:path'
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]
}
}
})
}
export class ConfigUtil {
private readonly configPath: string
private config: Config | null = null

View File

@ -1,9 +1,25 @@
import fs from 'fs'
import path from 'node:path'
import { truncateString } from './index'
import { getConfigUtil } from '../config'
import { LOG_DIR } from '../globalVars'
function truncateString(obj: any, maxLength = 500) {
if (obj !== null && typeof obj === 'object') {
Object.keys(obj).forEach((key) => {
if (typeof obj[key] === 'string') {
// 如果是字符串且超过指定长度,则截断
if (obj[key].length > maxLength) {
obj[key] = obj[key].substring(0, maxLength) + '...'
}
} else if (typeof obj[key] === 'object') {
// 如果是对象或数组,则递归调用
truncateString(obj[key], maxLength)
}
})
}
return obj
}
export const logFileName = `llonebot-${new Date().toLocaleString('zh-CN')}.log`.replace(/\//g, '-').replace(/:/g, '-')
export function log(...msg: any[]) {

View File

@ -1,51 +0,0 @@
import path from 'node:path'
import os from 'node:os'
export const exePath = process.execPath
function getPKGPath() {
let p = path.join(path.dirname(exePath), 'resources', 'app', 'package.json')
if (os.platform() === 'darwin') {
p = path.join(path.dirname(path.dirname(exePath)), 'Resources', 'app', 'package.json')
}
return p
}
export const pkgInfoPath = getPKGPath()
let configVersionInfoPath: string
if (os.platform() !== 'linux') {
configVersionInfoPath = path.join(path.dirname(exePath), 'resources', 'app', 'versions', 'config.json')
}
else {
const userPath = os.homedir()
const appDataPath = path.resolve(userPath, './.config/QQ')
configVersionInfoPath = path.resolve(appDataPath, './versions/config.json')
}
if (typeof configVersionInfoPath !== 'string') {
throw new Error('Something went wrong when load QQ info path')
}
export { configVersionInfoPath }
type QQPkgInfo = {
version: string
buildVersion: string
platform: string
eleArch: string
}
export const qqPkgInfo: QQPkgInfo = require(pkgInfoPath)
// platform_type: 3,
// app_type: 4,
// app_version: '9.9.9-23159',
// qua: 'V1_WIN_NQ_9.9.9_23159_GW_B',
// appid: '537213764',
// platVer: '10.0.26100',
// clientVer: '9.9.9-23159',
export function getBuildVersion(): number {
return +qqPkgInfo.buildVersion
}

View File

@ -56,34 +56,6 @@ export function calculateFileMD5(filePath: string): Promise<string> {
})
}
export interface HttpDownloadOptions {
url: string
headers?: Record<string, string> | string
}
export async function httpDownload(options: string | HttpDownloadOptions): Promise<Buffer> {
let url: string
let headers: Record<string, string> = {
'User-Agent':
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/94.0.4606.71 Safari/537.36',
}
if (typeof options === 'string') {
url = options
} else {
url = options.url
if (options.headers) {
if (typeof options.headers === 'string') {
headers = JSON.parse(options.headers)
} else {
headers = options.headers
}
}
}
const fetchRes = await fetch(url, { headers })
if (!fetchRes.ok) throw new Error(`下载文件失败: ${fetchRes.statusText}`)
return Buffer.from(await fetchRes.arrayBuffer())
}
export enum FileUriType {
Unknown = 0,
FileURL = 1,
@ -117,10 +89,11 @@ interface FetchFileRes {
url: string
}
async function fetchFile(url: string): Promise<FetchFileRes> {
export async function fetchFile(url: string, headersInit?: Record<string, string>): Promise<FetchFileRes> {
const headers: Record<string, string> = {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/94.0.4606.71 Safari/537.36',
'Host': new URL(url).hostname
'Host': new URL(url).hostname,
...headersInit
}
const raw = await fetch(url, { headers }).catch((err) => {
if (err.cause) {

View File

@ -1,169 +0,0 @@
export function truncateString(obj: any, maxLength = 500) {
if (obj !== null && typeof obj === 'object') {
Object.keys(obj).forEach((key) => {
if (typeof obj[key] === 'string') {
// 如果是字符串且超过指定长度,则截断
if (obj[key].length > maxLength) {
obj[key] = obj[key].substring(0, maxLength) + '...'
}
} else if (typeof obj[key] === 'object') {
// 如果是对象或数组,则递归调用
truncateString(obj[key], maxLength)
}
})
}
return obj
}
export function isNumeric(str: string) {
return /^\d+$/.test(str)
}
export function sleep(ms: number): Promise<void> {
return new Promise((resolve) => setTimeout(resolve, ms))
}
// 在保证老对象已有的属性不变化的情况下将新对象的属性复制到老对象
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]
}
}
})
}
export function isNull(value: unknown) {
return value === undefined || value === null
}
/**
*
* @param str
* @param maxLength
* @returns
*/
export function wrapText(str: string, maxLength: number): string {
// 初始化一个空字符串用于存放结果
let result: string = ''
// 循环遍历字符串每次步进maxLength个字符
for (let i = 0; i < str.length; i += maxLength) {
// 从i开始截取长度为maxLength的字符串段并添加到结果字符串
// 如果不是第一段,先添加一个换行符
if (i > 0) result += '\n'
result += str.substring(i, i + maxLength)
}
return result
}
/**
* key生成缓存键
* @param ttl
* @param customKey
* @returns
*/
export function cacheFunc(ttl: number, customKey: string = '') {
const cache = new Map<string, { expiry: number; value: any }>()
return function (target: any, propertyKey: string, descriptor: PropertyDescriptor): PropertyDescriptor {
const originalMethod = descriptor.value
const className = target.constructor.name // 获取类名
const methodName = propertyKey // 获取方法名
descriptor.value = async function (...args: any[]) {
const cacheKey = `${customKey}${className}.${methodName}:${JSON.stringify(args)}`
const cached = cache.get(cacheKey)
if (cached && cached.expiry > Date.now()) {
return cached.value
} else {
const result = await originalMethod.apply(this, args)
cache.set(cacheKey, { value: result, expiry: Date.now() + ttl })
return result
}
}
return descriptor
}
}
export function CacheClassFuncAsync(ttl = 3600 * 1000, customKey = '') {
function logExecutionTime(target: any, methodName: string, descriptor: PropertyDescriptor) {
const cache = new Map<string, { expiry: number; value: any }>()
const originalMethod = descriptor.value
descriptor.value = async function (...args: any[]) {
const key = `${customKey}${String(methodName)}.(${args.map(arg => JSON.stringify(arg)).join(', ')})`
cache.forEach((value, key) => {
if (value.expiry < Date.now()) {
cache.delete(key)
}
})
const cachedValue = cache.get(key)
if (cachedValue && cachedValue.expiry > Date.now()) {
return cachedValue.value
}
const result = await originalMethod.apply(this, args)
cache.set(key, { expiry: Date.now() + ttl, value: result })
return result
}
}
return logExecutionTime
}
export function CacheClassFuncAsyncExtend(ttl: number = 3600 * 1000, customKey: string = '', checker: any = (...data: any[]) => { return true }) {
function logExecutionTime(target: any, methodName: string, descriptor: PropertyDescriptor) {
const cache = new Map<string, { expiry: number; value: any }>()
const originalMethod = descriptor.value
descriptor.value = async function (...args: any[]) {
const key = `${customKey}${String(methodName)}.(${args.map(arg => JSON.stringify(arg)).join(', ')})`
cache.forEach((value, key) => {
if (value.expiry < Date.now()) {
cache.delete(key)
}
})
const cachedValue = cache.get(key)
if (cachedValue && cachedValue.expiry > Date.now()) {
return cachedValue.value
}
const result = await originalMethod.apply(this, args)
if (!checker(...args, result)) {
return result //丢弃缓存
}
cache.set(key, { expiry: Date.now() + ttl, value: result })
return result
}
}
return logExecutionTime
}
// forked from https://github.com/NapNeko/NapCatQQ/blob/6f6b258f22d7563f15d84e7172c4d4cbb547f47e/src/common/utils/helper.ts#L14
export class UUIDConverter {
static encode(highStr: string, lowStr: string): string {
const high = BigInt(highStr)
const low = BigInt(lowStr)
const highHex = high.toString(16).padStart(16, '0')
const lowHex = low.toString(16).padStart(16, '0')
const combinedHex = highHex + lowHex
const uuid = `${combinedHex.substring(0, 8)}-${combinedHex.substring(8, 12)}-${combinedHex.substring(
12,
16,
)}-${combinedHex.substring(16, 20)}-${combinedHex.substring(20)}`
return uuid
}
static decode(uuid: string): { high: string; low: string } {
const hex = uuid.replace(/-/g, '')
const high = BigInt('0x' + hex.substring(0, 16))
const low = BigInt('0x' + hex.substring(16))
return { high: high.toString(), low: low.toString() }
}
}

View File

@ -1,9 +1,7 @@
export * from './file'
export * from './helper'
export * from './misc'
export * from './LegacyLog'
export * from './qqlevel'
export * from './QQBasicInfo'
export * from './misc'
export * from './upgrade'
export { getVideoInfo } from './video'
export { checkFfmpeg } from './video'
export { getVideoInfo, checkFfmpeg } from './video'
export { encodeSilk } from './audio'

15
src/common/utils/misc.ts Normal file
View File

@ -0,0 +1,15 @@
import { QQLevel } from '@/ntqqapi/types'
export function isNumeric(str: string) {
return /^\d+$/.test(str)
}
export function calcQQLevel(level: QQLevel) {
const { crownNum, sunNum, moonNum, starNum } = level
return crownNum * 64 + sunNum * 16 + moonNum * 4 + starNum
}
export function getBuildVersion(): number {
const version: string = globalThis.LiteLoader.versions.qqnt
return +version.split('-')[1]
}

View File

@ -1,7 +0,0 @@
// QQ等级换算
import { QQLevel } from '../../ntqqapi/types'
export function calcQQLevel(level: QQLevel) {
const { crownNum, sunNum, moonNum, starNum } = level
return crownNum * 64 + sunNum * 16 + moonNum * 4 + starNum
}

View File

@ -1,8 +1,8 @@
import { version } from '../../version'
import * as path from 'node:path'
import * as fs from 'node:fs'
import { copyFolder, httpDownload, log } from '.'
import path from 'node:path'
import compressing from 'compressing'
import { writeFile } from 'node:fs/promises'
import { version } from '../../version'
import { copyFolder, log, fetchFile } from '.'
import { PLUGIN_DIR, TEMP_DIR } from '../globalVars'
const downloadMirrorHosts = ['https://mirror.ghproxy.com/']
@ -34,8 +34,8 @@ export async function upgradeLLOneBot() {
// 多镜像下载
for (const mirrorGithub of downloadMirrorHosts) {
try {
const buffer = await httpDownload(mirrorGithub + downloadUrl)
fs.writeFileSync(filePath, buffer)
const res = await fetchFile(mirrorGithub + downloadUrl)
await writeFile(filePath, res.data)
downloadSuccess = true
break
} catch (e) {
@ -89,7 +89,7 @@ export async function getRemoteVersionByMirror(mirrorGithub: string) {
let releasePage = 'error'
try {
releasePage = (await httpDownload(mirrorGithub + '/LLOneBot/LLOneBot/releases')).toString()
releasePage = (await fetchFile(mirrorGithub + '/LLOneBot/LLOneBot/releases')).data.toString()
// log("releasePage", releasePage);
if (releasePage === 'error') return ''
return releasePage.match(new RegExp('(?<=(tag/v)).*?(?=("))'))?.[0]

8
src/global.d.ts vendored
View File

@ -1,8 +1,10 @@
import { type LLOneBot } from './preload'
import type { LLOneBot } from './preload'
import { Dict } from 'cosmokit'
declare global {
interface Window {
llonebot: LLOneBot
LiteLoader: Record<string, any>
LiteLoader: Dict
}
}
var LiteLoader: Dict
}

View File

@ -3,9 +3,9 @@ import { Context, Logger } from 'cordis'
import { appendFile } from 'node:fs'
import { LOG_DIR, selfInfo } from '@/common/globalVars'
import { noop } from 'cosmokit'
import { getConfigUtil } from '../common/config'
interface Config {
enable: boolean
filename: string
}
@ -13,9 +13,8 @@ export default class Log {
static name = 'logger'
constructor(ctx: Context, cfg: Config) {
// fetch data from the database
Logger.targets.splice(0, Logger.targets.length)
if (!getConfigUtil().getConfig().log) {
if (!cfg.enable) {
return
}
const file = path.join(LOG_DIR, cfg.filename)

View File

@ -50,7 +50,7 @@ function onLoad() {
}
if (!fs.existsSync(LOG_DIR)) {
fs.mkdirSync(LOG_DIR, { recursive: true })
fs.mkdirSync(LOG_DIR)
}
ipcMain.handle(CHANNEL_CHECK_VERSION, async (event, arg) => {
@ -152,10 +152,11 @@ function onLoad() {
return
}
if (!fs.existsSync(TEMP_DIR)) {
fs.mkdirSync(TEMP_DIR, { recursive: true })
fs.mkdirSync(TEMP_DIR)
}
const ctx = new Context()
ctx.plugin(Log, {
enable: config.log!,
filename: logFileName
})
ctx.plugin(NTQQFileApi)

View File

@ -8,7 +8,7 @@ import { NTEventDispatch } from '@/common/utils/EventTask'
import { NodeIKernelGroupListener } from '../listeners'
import { NodeIKernelGroupService } from '../services'
import { Service, Context } from 'cordis'
import { isNumeric } from '@/common/utils/helper'
import { isNumeric } from '@/common/utils/misc'
declare module 'cordis' {
interface Context {

View File

@ -129,22 +129,6 @@ export class NTQQWebApi extends Service {
super(ctx, 'ntWebApi', true)
}
async getGroupEssenceMsg(GroupCode: string, page_start: string): Promise<GroupEssenceMsgRet | undefined> {
const { cookies: CookieValue, bkn: Bkn } = await this.ctx.ntUserApi.getCookies('qun.qq.com')
const url = 'https://qun.qq.com/cgi-bin/group_digest/digest_list?bkn=' + Bkn + '&group_code=' + GroupCode + '&page_start=' + page_start + '&page_limit=20'
let ret: GroupEssenceMsgRet
try {
ret = await RequestUtil.HttpGetJson<GroupEssenceMsgRet>(url, 'GET', '', { 'Cookie': CookieValue })
} catch {
return undefined
}
//console.log(url, CookieValue)
if (ret.retcode !== 0) {
return undefined
}
return ret
}
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')

View File

@ -18,9 +18,9 @@ import ffmpeg from 'fluent-ffmpeg'
import { calculateFileMD5, isGIF } from '../common/utils/file'
import { defaultVideoThumb, getVideoInfo } from '../common/utils/video'
import { encodeSilk } from '../common/utils/audio'
import { isNull } from '../common/utils'
import faceConfig from './helper/face_config.json'
import { Context } from 'cordis'
import { isNullable } from 'cosmokit'
export const mFaceCache = new Map<string, string>() // emojiId -> faceName
@ -315,7 +315,7 @@ export namespace SendMsgElementConstructor {
// 实际测试并不能控制结果
// 随机1到6
if (isNull(resultId)) resultId = Math.floor(Math.random() * 6) + 1
if (isNullable(resultId)) resultId = Math.floor(Math.random() * 6) + 1
return {
elementType: ElementType.FACE,
elementId: '',
@ -337,7 +337,7 @@ export namespace SendMsgElementConstructor {
// 猜拳(石头剪刀布)表情
export function rps(resultId: number | null): SendFaceElement {
// 实际测试并不能控制结果
if (isNull(resultId)) resultId = Math.floor(Math.random() * 3) + 1
if (isNullable(resultId)) resultId = Math.floor(Math.random() * 3) + 1
return {
elementType: ElementType.FACE,
elementId: '',

View File

@ -5,8 +5,8 @@ import { MessageUnique } from '../common/utils/MessageUnique'
import { NTEventDispatch } from '../common/utils/EventTask'
import { wrapperConstructor, getSession } from './wrapper'
import { Config as LLOBConfig } from '../common/types'
import { llonebotError, TEMP_DIR } from '../common/globalVars'
import { isNumeric } from '../common/utils/helper'
import { llonebotError } from '../common/globalVars'
import { isNumeric } from '../common/utils/misc'
import { NTMethod } from './ntcall'
import {
RawMessage,

View File

@ -1,9 +1,9 @@
import BaseAction from '../BaseAction'
import { ActionName } from '../types'
import fs from 'fs'
import fsPromise from 'fs/promises'
import path from 'node:path'
import { calculateFileMD5, httpDownload } from '@/common/utils'
import { ActionName } from '../types'
import { calculateFileMD5, fetchFile } from '@/common/utils'
import { TEMP_DIR } from '@/common/globalVars'
import { randomUUID } from 'node:crypto'
@ -31,8 +31,8 @@ export default class GoCQHTTPDownloadFile extends BaseAction<Payload, FileRespon
await fsPromise.writeFile(filePath, payload.base64, 'base64')
} else if (payload.url) {
const headers = this.getHeaders(payload.headers)
const buffer = await httpDownload({ url: payload.url, headers: headers })
await fsPromise.writeFile(filePath, buffer)
const res = await fetchFile(payload.url, headers)
await fsPromise.writeFile(filePath, res.data)
} else {
throw new Error('不存在任何文件, 无法下载')
}

View File

@ -2,9 +2,9 @@ import BaseAction from '../BaseAction'
import { OB11User } from '../../types'
import { OB11Constructor } from '../../constructor'
import { ActionName } from '../types'
import { getBuildVersion } from '@/common/utils/QQBasicInfo'
import { getBuildVersion } from '@/common/utils'
import { OB11UserSex } from '../../types'
import { calcQQLevel } from '@/common/utils/qqlevel'
import { calcQQLevel } from '@/common/utils/misc'
interface Payload {
user_id: number | string

View File

@ -1,5 +1,5 @@
import BaseAction from '../BaseAction'
import { handleQuickOperation, QuickOperation, QuickOperationEvent } from '../../helper/quick-operation'
import { handleQuickOperation, QuickOperation, QuickOperationEvent } from '../../helper/quickOperation'
import { ActionName } from '../types'
interface Payload {

View File

@ -1,6 +1,7 @@
import SendMsg, { convertMessage2List } from '../msg/SendMsg'
import SendMsg from '../msg/SendMsg'
import { OB11PostSendMsg } from '../../types'
import { ActionName } from '../types'
import { convertMessage2List } from '../../helper/createMessage'
export class GoCQHTTPSendForwardMsg extends SendMsg {
actionName = ActionName.GoCQHTTP_SendForwardMsg

View File

@ -5,7 +5,7 @@ import { SendMsgElementConstructor } from '@/ntqqapi/constructor'
import { ChatType, SendFileElement } from '@/ntqqapi/types'
import { uri2local } from '@/common/utils'
import { Peer } from '@/ntqqapi/types'
import { sendMsg } from '../msg/SendMsg'
import { sendMsg } from '../../helper/createMessage'
interface Payload {
user_id: number | string

View File

@ -1,9 +1,9 @@
import BaseAction from '../BaseAction'
import { OB11GroupMember } from '../../types'
import { OB11Constructor } from '../../constructor'
import BaseAction from '../BaseAction'
import { ActionName } from '../types'
import { isNull } from '@/common/utils/helper'
import { selfInfo } from '@/common/globalVars'
import { isNullable } from 'cosmokit'
interface Payload {
group_id: number | string
@ -16,7 +16,7 @@ class GetGroupMemberInfo extends BaseAction<Payload, OB11GroupMember> {
protected async _handle(payload: Payload) {
const member = await this.ctx.ntGroupApi.getGroupMember(payload.group_id.toString(), payload.user_id.toString())
if (member) {
if (isNull(member.sex)) {
if (isNullable(member.sex)) {
//log('获取群成员详细信息')
const info = await this.ctx.ntUserApi.getUserDetailInfo(member.uid, true)
//log('群成员详细信息结果', info)

View File

@ -1,6 +1,6 @@
import BaseAction from '../BaseAction'
import { ActionName } from '../types'
import { getHttpEvent } from '../../helper/event-for-http'
import { getHttpEvent } from '../../helper/eventForHttp'
import { OB11Message } from '../../types'
import { OB11BaseEvent } from '../../event/OB11BaseEvent'

View File

@ -1,6 +1,6 @@
import BaseAction from '../BaseAction'
import { OB11Message } from '../../types'
import { OB11Constructor } from '../../constructor'
import BaseAction from '../BaseAction'
import { ActionName } from '../types'
import { MessageUnique } from '@/common/utils/MessageUnique'
@ -14,7 +14,6 @@ class GetMsg extends BaseAction<PayloadType, OB11Message> {
actionName = ActionName.GetMsg
protected async _handle(payload: PayloadType) {
// log("history msg ids", Object.keys(msgHistory));
if (!payload.message_id) {
throw '参数message_id不能为空'
}

View File

@ -1,8 +1,6 @@
import {
AtType,
ChatType,
ElementType,
GroupMemberRole,
RawMessage,
SendMessageElement,
} from '@/ntqqapi/types'
@ -11,26 +9,19 @@ import {
OB11MessageData,
OB11MessageDataType,
OB11MessageJson,
OB11MessageMixType,
OB11MessageMusic,
OB11MessageNode,
OB11PostSendMsg,
} from '../../types'
import { SendMsgElementConstructor } from '@/ntqqapi/constructor'
import BaseAction from '../BaseAction'
import { ActionName, BaseCheckResult } from '../types'
import fs from 'node:fs'
import fsPromise from 'node:fs/promises'
import { decodeCQCode } from '../../cqcode'
import { getConfigUtil } from '@/common/config'
import { sleep } from '@/common/utils/helper'
import { uri2local } from '@/common/utils'
import { CustomMusicSignPostData, IdMusicSignPostData, MusicSign, MusicSignPostData } from '@/common/utils/sign'
import { Peer } from '@/ntqqapi/types/msg'
import { MessageUnique } from '@/common/utils/MessageUnique'
import { OB11MessageFileBase } from '../../types'
import { Context } from 'cordis'
import { selfInfo } from '@/common/globalVars'
import { convertMessage2List, createSendElements, sendMsg } from '../../helper/createMessage'
export interface ReturnDataType {
message_id: number
@ -42,266 +33,6 @@ export enum ContextMode {
Group = 2
}
interface MessageContext {
deleteAfterSentFiles: string[]
peer: Peer
}
export function convertMessage2List(message: OB11MessageMixType, autoEscape = false) {
if (typeof message === 'string') {
if (autoEscape === true) {
message = [
{
type: OB11MessageDataType.text,
data: {
text: message,
},
},
]
}
else {
message = decodeCQCode(message.toString())
}
}
else if (!Array.isArray(message)) {
message = [message]
}
return message
}
// forked from https://github.com/NapNeko/NapCatQQ/blob/6f6b258f22d7563f15d84e7172c4d4cbb547f47e/src/onebot11/action/msg/SendMsg/create-send-elements.ts#L26
async function handleOb11FileLikeMessage(
ctx: Context,
{ data: inputdata }: OB11MessageFileBase,
{ deleteAfterSentFiles }: Pick<MessageContext, 'deleteAfterSentFiles'>,
) {
//有的奇怪的框架将url作为参数 而不是file 此时优先url 同时注意可能传入的是非file://开头的目录 By Mlikiowa
const {
path,
isLocal,
fileName,
errMsg,
success,
} = (await uri2local(inputdata?.url || inputdata.file))
if (!success) {
ctx.logger.error('文件下载失败', errMsg)
throw Error('文件下载失败' + errMsg)
}
if (!isLocal) { // 只删除http和base64转过来的文件
deleteAfterSentFiles.push(path)
}
return { path, fileName: inputdata.name || fileName }
}
export async function createSendElements(
ctx: Context,
messageData: OB11MessageData[],
peer: Peer,
ignoreTypes: OB11MessageDataType[] = [],
) {
let sendElements: SendMessageElement[] = []
let deleteAfterSentFiles: string[] = []
for (let sendMsg of messageData) {
if (ignoreTypes.includes(sendMsg.type)) {
continue
}
switch (sendMsg.type) {
case OB11MessageDataType.text: {
const text = sendMsg.data?.text
if (text) {
sendElements.push(SendMsgElementConstructor.text(sendMsg.data!.text))
}
}
break
case OB11MessageDataType.at: {
if (!peer) {
continue
}
if (sendMsg.data?.qq) {
const atQQ = String(sendMsg.data.qq)
if (atQQ === 'all') {
// todo查询剩余的at全体次数
const groupCode = peer.peerUid
let remainAtAllCount = 1
let isAdmin: boolean = true
if (groupCode) {
try {
remainAtAllCount = (await ctx.ntGroupApi.getGroupAtAllRemainCount(groupCode)).atInfo
.RemainAtAllCountForUin
ctx.logger.info(`${groupCode}剩余at全体次数`, remainAtAllCount)
const self = await ctx.ntGroupApi.getGroupMember(groupCode, selfInfo.uin)
isAdmin = self?.role === GroupMemberRole.admin || self?.role === GroupMemberRole.owner
} catch (e) {
}
}
if (isAdmin && remainAtAllCount > 0) {
sendElements.push(SendMsgElementConstructor.at(atQQ, atQQ, AtType.atAll, '@全体成员'))
}
}
else if (peer.chatType === ChatType.group) {
const atMember = await ctx.ntGroupApi.getGroupMember(peer.peerUid, atQQ)
if (atMember) {
const display = `@${atMember.cardName || atMember.nick}`
sendElements.push(
SendMsgElementConstructor.at(atQQ, atMember.uid, AtType.atUser, display),
)
} else {
const atNmae = sendMsg.data?.name
const uid = await ctx.ntUserApi.getUidByUin(atQQ) || ''
const display = atNmae ? `@${atNmae}` : ''
sendElements.push(
SendMsgElementConstructor.at(atQQ, uid, AtType.atUser, display),
)
}
}
}
}
break
case OB11MessageDataType.reply: {
if (sendMsg.data?.id) {
const replyMsgId = await MessageUnique.getMsgIdAndPeerByShortId(+sendMsg.data.id)
if (!replyMsgId) {
ctx.logger.warn('回复消息不存在', replyMsgId)
continue
}
const replyMsg = (await ctx.ntMsgApi.getMsgsByMsgId(
replyMsgId.Peer,
[replyMsgId.MsgId!]
)).msgList[0]
if (replyMsg) {
sendElements.push(
SendMsgElementConstructor.reply(
replyMsg.msgSeq,
replyMsg.msgId,
replyMsg.senderUin!,
replyMsg.senderUin!,
),
)
}
}
}
break
case OB11MessageDataType.face: {
const faceId = sendMsg.data?.id
if (faceId) {
sendElements.push(SendMsgElementConstructor.face(parseInt(faceId)))
}
}
break
case OB11MessageDataType.mface: {
sendElements.push(
SendMsgElementConstructor.mface(
sendMsg.data.emoji_package_id,
sendMsg.data.emoji_id,
sendMsg.data.key,
sendMsg.data.summary,
),
)
}
break
case OB11MessageDataType.image: {
const res = await SendMsgElementConstructor.pic(
ctx,
(await handleOb11FileLikeMessage(ctx, sendMsg, { deleteAfterSentFiles })).path,
sendMsg.data.summary || '',
sendMsg.data.subType || 0
)
deleteAfterSentFiles.push(res.picElement.sourcePath)
sendElements.push(res)
}
break
case OB11MessageDataType.file: {
const { path, fileName } = await handleOb11FileLikeMessage(ctx, sendMsg, { deleteAfterSentFiles })
sendElements.push(await SendMsgElementConstructor.file(ctx, path, fileName))
}
break
case OB11MessageDataType.video: {
const { path, fileName } = await handleOb11FileLikeMessage(ctx, sendMsg, { deleteAfterSentFiles })
let thumb = sendMsg.data.thumb
if (thumb) {
const uri2LocalRes = await uri2local(thumb)
if (uri2LocalRes.success) thumb = uri2LocalRes.path
}
const res = await SendMsgElementConstructor.video(ctx, path, fileName, thumb)
deleteAfterSentFiles.push(res.videoElement.filePath)
sendElements.push(res)
}
break
case OB11MessageDataType.voice: {
const { path } = await handleOb11FileLikeMessage(ctx, sendMsg, { deleteAfterSentFiles })
sendElements.push(await SendMsgElementConstructor.ptt(ctx, path))
}
break
case OB11MessageDataType.json: {
sendElements.push(SendMsgElementConstructor.ark(sendMsg.data.data))
}
break
case OB11MessageDataType.poke: {
let qq = sendMsg.data?.qq || sendMsg.data?.id
}
break
case OB11MessageDataType.dice: {
const resultId = sendMsg.data?.result
sendElements.push(SendMsgElementConstructor.dice(resultId))
}
break
case OB11MessageDataType.RPS: {
const resultId = sendMsg.data?.result
sendElements.push(SendMsgElementConstructor.rps(resultId))
}
break
}
}
return {
sendElements,
deleteAfterSentFiles,
}
}
export async function sendMsg(
ctx: Context,
peer: Peer,
sendElements: SendMessageElement[],
deleteAfterSentFiles: string[],
waitComplete = true,
) {
if (!sendElements.length) {
throw '消息体无法解析,请检查是否发送了不支持的消息类型'
}
// 计算发送的文件大小
let totalSize = 0
for (const fileElement of sendElements) {
try {
if (fileElement.elementType === ElementType.PTT) {
totalSize += fs.statSync(fileElement.pttElement.filePath).size
}
if (fileElement.elementType === ElementType.FILE) {
totalSize += fs.statSync(fileElement.fileElement.filePath).size
}
if (fileElement.elementType === ElementType.VIDEO) {
totalSize += fs.statSync(fileElement.videoElement.filePath).size
}
if (fileElement.elementType === ElementType.PIC) {
totalSize += fs.statSync(fileElement.picElement.sourcePath).size
}
} catch (e) {
ctx.logger.warn('文件大小计算失败', e, fileElement)
}
}
//log('发送消息总大小', totalSize, 'bytes')
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
}
export class SendMsg extends BaseAction<OB11PostSendMsg, ReturnDataType> {
actionName = ActionName.SendMsg
@ -422,7 +153,6 @@ export class SendMsg extends BaseAction<OB11PostSendMsg, ReturnDataType> {
} as OB11MessageJson
}
}
// log("send msg:", peer, sendElements)
const { sendElements, deleteAfterSentFiles } = await createSendElements(this.ctx, messages, peer)
if (sendElements.length === 1) {
if (sendElements[0] === null) {
@ -462,7 +192,7 @@ export class SendMsg extends BaseAction<OB11PostSendMsg, ReturnDataType> {
sendElements,
true,
)
await sleep(400)
await this.ctx.sleep(400)
return nodeMsg
} catch (e) {
this.ctx.logger.warn(e, '克隆转发消息失败,将忽略本条消息', msg)
@ -522,7 +252,7 @@ export class SendMsg extends BaseAction<OB11PostSendMsg, ReturnDataType> {
for (const eles of sendElementsSplit) {
const nodeMsg = await sendMsg(this.ctx, selfPeer, eles, [], true)
nodeMsgIds.push(nodeMsg.msgId)
await sleep(400)
await this.ctx.sleep(400)
this.ctx.logger.info('转发节点生成成功', nodeMsg.msgId)
}
deleteAfterSentFiles.map((f) => fs.unlink(f, () => {

View File

@ -2,7 +2,7 @@ import BaseAction from '../BaseAction'
import { OB11User } from '../../types'
import { OB11Constructor } from '../../constructor'
import { ActionName } from '../types'
import { getBuildVersion } from '@/common/utils/QQBasicInfo'
import { getBuildVersion } from '@/common/utils'
interface Payload {
no_cache: boolean | string

View File

@ -22,7 +22,7 @@ import { OB11Http, OB11HttpPost } from './connect/http'
import { OB11BaseEvent } from './event/OB11BaseEvent'
import { OB11Message } from './types'
import { OB11BaseMetaEvent } from './event/meta/OB11BaseMetaEvent'
import { postHttpEvent } from './helper/event-for-http'
import { postHttpEvent } from './helper/eventForHttp'
import { initActionMap } from './action'
import { llonebotError } from '../common/globalVars'
import { OB11GroupCardEvent } from './event/notice/OB11GroupCardEvent'

View File

@ -8,7 +8,7 @@ import { llonebotError, selfInfo } from '@/common/globalVars'
import { OB11Response } from '../action/OB11Response'
import { OB11Message } from '../types'
import { OB11BaseEvent } from '../event/OB11BaseEvent'
import { handleQuickOperation, QuickOperationEvent } from '../helper/quick-operation'
import { handleQuickOperation, QuickOperationEvent } from '../helper/quickOperation'
import { OB11HeartbeatEvent } from '../event/meta/OB11HeartbeatEvent'
type RegisterHandler = (res: Response, payload: any) => Promise<any>

View File

@ -18,7 +18,6 @@ import {
Peer,
GroupMember,
RawMessage,
SelfInfo,
Sex,
TipGroupElementType,
User,
@ -32,8 +31,7 @@ import { OB11GroupIncreaseEvent } from './event/notice/OB11GroupIncreaseEvent'
import { OB11GroupBanEvent } from './event/notice/OB11GroupBanEvent'
import { OB11GroupUploadNoticeEvent } from './event/notice/OB11GroupUploadNoticeEvent'
import { OB11GroupNoticeEvent } from './event/notice/OB11GroupNoticeEvent'
import { calcQQLevel } from '../common/utils/qqlevel'
import { isNull, sleep } from '../common/utils/helper'
import { calcQQLevel } from '../common/utils/misc'
import { getConfigUtil } from '../common/config'
import { OB11GroupTitleEvent } from './event/notice/OB11GroupTitleEvent'
import { OB11GroupCardEvent } from './event/notice/OB11GroupCardEvent'
@ -46,7 +44,7 @@ import { OB11GroupRecallNoticeEvent } from './event/notice/OB11GroupRecallNotice
import { OB11FriendPokeEvent, OB11GroupPokeEvent } from './event/notice/OB11PokeEvent'
import { OB11BaseNoticeEvent } from './event/notice/OB11BaseNoticeEvent'
import { OB11GroupEssenceEvent } from './event/notice/OB11GroupEssenceEvent'
import { omit } from 'cosmokit'
import { omit, isNullable } from 'cosmokit'
import { Context } from 'cordis'
import { selfInfo } from '@/common/globalVars'
@ -379,7 +377,7 @@ export namespace OB11Constructor {
// log("收到群提示消息", groupElement)
if (groupElement.type === TipGroupElementType.memberIncrease) {
ctx.logger.info('收到群成员增加消息', groupElement)
await sleep(1000)
await ctx.sleep(1000)
const member = await ctx.ntGroupApi.getGroupMember(msg.peerUid, groupElement.memberUid)
let memberUin = member?.uin
if (!memberUin) {
@ -598,7 +596,7 @@ export namespace OB11Constructor {
const title = json.items[3].txt
ctx.logger.info('收到群成员新头衔消息', json)
ctx.ntGroupApi.getGroupMember(msg.peerUid, memberUin).then(member => {
if (!isNull(member)) {
if (!isNullable(member)) {
member.memberSpecialTitle = title
}
})

View File

@ -0,0 +1,277 @@
import fs from 'node:fs'
import fsPromise from 'node:fs/promises'
import {
AtType,
ChatType,
GroupMemberRole,
SendMessageElement,
ElementType
} from '@/ntqqapi/types'
import {
OB11MessageData,
OB11MessageDataType,
OB11MessageFileBase,
OB11MessageMixType
} from '../types'
import { decodeCQCode } from '../cqcode'
import { Peer } from '@/ntqqapi/types/msg'
import { SendMsgElementConstructor } from '@/ntqqapi/constructor'
import { MessageUnique } from '@/common/utils/MessageUnique'
import { selfInfo } from '@/common/globalVars'
import { uri2local } from '@/common/utils'
import { Context } from 'cordis'
export async function createSendElements(
ctx: Context,
messageData: OB11MessageData[],
peer: Peer,
ignoreTypes: OB11MessageDataType[] = [],
) {
let sendElements: SendMessageElement[] = []
let deleteAfterSentFiles: string[] = []
for (let sendMsg of messageData) {
if (ignoreTypes.includes(sendMsg.type)) {
continue
}
switch (sendMsg.type) {
case OB11MessageDataType.text: {
const text = sendMsg.data?.text
if (text) {
sendElements.push(SendMsgElementConstructor.text(sendMsg.data!.text))
}
}
break
case OB11MessageDataType.at: {
if (!peer) {
continue
}
if (sendMsg.data?.qq) {
const atQQ = String(sendMsg.data.qq)
if (atQQ === 'all') {
// todo查询剩余的at全体次数
const groupCode = peer.peerUid
let remainAtAllCount = 1
let isAdmin: boolean = true
if (groupCode) {
try {
remainAtAllCount = (await ctx.ntGroupApi.getGroupAtAllRemainCount(groupCode)).atInfo
.RemainAtAllCountForUin
ctx.logger.info(`${groupCode}剩余at全体次数`, remainAtAllCount)
const self = await ctx.ntGroupApi.getGroupMember(groupCode, selfInfo.uin)
isAdmin = self?.role === GroupMemberRole.admin || self?.role === GroupMemberRole.owner
} catch (e) {
}
}
if (isAdmin && remainAtAllCount > 0) {
sendElements.push(SendMsgElementConstructor.at(atQQ, atQQ, AtType.atAll, '@全体成员'))
}
}
else if (peer.chatType === ChatType.group) {
const atMember = await ctx.ntGroupApi.getGroupMember(peer.peerUid, atQQ)
if (atMember) {
const display = `@${atMember.cardName || atMember.nick}`
sendElements.push(
SendMsgElementConstructor.at(atQQ, atMember.uid, AtType.atUser, display),
)
} else {
const atNmae = sendMsg.data?.name
const uid = await ctx.ntUserApi.getUidByUin(atQQ) || ''
const display = atNmae ? `@${atNmae}` : ''
sendElements.push(
SendMsgElementConstructor.at(atQQ, uid, AtType.atUser, display),
)
}
}
}
}
break
case OB11MessageDataType.reply: {
if (sendMsg.data?.id) {
const replyMsgId = await MessageUnique.getMsgIdAndPeerByShortId(+sendMsg.data.id)
if (!replyMsgId) {
ctx.logger.warn('回复消息不存在', replyMsgId)
continue
}
const replyMsg = (await ctx.ntMsgApi.getMsgsByMsgId(
replyMsgId.Peer,
[replyMsgId.MsgId!]
)).msgList[0]
if (replyMsg) {
sendElements.push(
SendMsgElementConstructor.reply(
replyMsg.msgSeq,
replyMsg.msgId,
replyMsg.senderUin!,
replyMsg.senderUin!,
),
)
}
}
}
break
case OB11MessageDataType.face: {
const faceId = sendMsg.data?.id
if (faceId) {
sendElements.push(SendMsgElementConstructor.face(parseInt(faceId)))
}
}
break
case OB11MessageDataType.mface: {
sendElements.push(
SendMsgElementConstructor.mface(
sendMsg.data.emoji_package_id,
sendMsg.data.emoji_id,
sendMsg.data.key,
sendMsg.data.summary,
),
)
}
break
case OB11MessageDataType.image: {
const res = await SendMsgElementConstructor.pic(
ctx,
(await handleOb11FileLikeMessage(ctx, sendMsg, { deleteAfterSentFiles })).path,
sendMsg.data.summary || '',
sendMsg.data.subType || 0
)
deleteAfterSentFiles.push(res.picElement.sourcePath)
sendElements.push(res)
}
break
case OB11MessageDataType.file: {
const { path, fileName } = await handleOb11FileLikeMessage(ctx, sendMsg, { deleteAfterSentFiles })
sendElements.push(await SendMsgElementConstructor.file(ctx, path, fileName))
}
break
case OB11MessageDataType.video: {
const { path, fileName } = await handleOb11FileLikeMessage(ctx, sendMsg, { deleteAfterSentFiles })
let thumb = sendMsg.data.thumb
if (thumb) {
const uri2LocalRes = await uri2local(thumb)
if (uri2LocalRes.success) thumb = uri2LocalRes.path
}
const res = await SendMsgElementConstructor.video(ctx, path, fileName, thumb)
deleteAfterSentFiles.push(res.videoElement.filePath)
sendElements.push(res)
}
break
case OB11MessageDataType.voice: {
const { path } = await handleOb11FileLikeMessage(ctx, sendMsg, { deleteAfterSentFiles })
sendElements.push(await SendMsgElementConstructor.ptt(ctx, path))
}
break
case OB11MessageDataType.json: {
sendElements.push(SendMsgElementConstructor.ark(sendMsg.data.data))
}
break
case OB11MessageDataType.poke: {
let qq = sendMsg.data?.qq || sendMsg.data?.id
}
break
case OB11MessageDataType.dice: {
const resultId = sendMsg.data?.result
sendElements.push(SendMsgElementConstructor.dice(resultId))
}
break
case OB11MessageDataType.RPS: {
const resultId = sendMsg.data?.result
sendElements.push(SendMsgElementConstructor.rps(resultId))
}
break
}
}
return {
sendElements,
deleteAfterSentFiles,
}
}
// forked from https://github.com/NapNeko/NapCatQQ/blob/6f6b258f22d7563f15d84e7172c4d4cbb547f47e/src/onebot11/action/msg/SendMsg/create-send-elements.ts#L26
async function handleOb11FileLikeMessage(
ctx: Context,
{ data: inputdata }: OB11MessageFileBase,
{ deleteAfterSentFiles }: { deleteAfterSentFiles: string[] },
) {
//有的奇怪的框架将url作为参数 而不是file 此时优先url 同时注意可能传入的是非file://开头的目录 By Mlikiowa
const {
path,
isLocal,
fileName,
errMsg,
success,
} = (await uri2local(inputdata?.url || inputdata.file))
if (!success) {
ctx.logger.error('文件下载失败', errMsg)
throw Error('文件下载失败' + errMsg)
}
if (!isLocal) { // 只删除http和base64转过来的文件
deleteAfterSentFiles.push(path)
}
return { path, fileName: inputdata.name || fileName }
}
export function convertMessage2List(message: OB11MessageMixType, autoEscape = false) {
if (typeof message === 'string') {
if (autoEscape === true) {
message = [
{
type: OB11MessageDataType.text,
data: {
text: message,
},
},
]
}
else {
message = decodeCQCode(message.toString())
}
}
else if (!Array.isArray(message)) {
message = [message]
}
return message
}
export async function sendMsg(
ctx: Context,
peer: Peer,
sendElements: SendMessageElement[],
deleteAfterSentFiles: string[],
waitComplete = true,
) {
if (!sendElements.length) {
throw '消息体无法解析,请检查是否发送了不支持的消息类型'
}
// 计算发送的文件大小
let totalSize = 0
for (const fileElement of sendElements) {
try {
if (fileElement.elementType === ElementType.PTT) {
totalSize += fs.statSync(fileElement.pttElement.filePath).size
}
if (fileElement.elementType === ElementType.FILE) {
totalSize += fs.statSync(fileElement.fileElement.filePath).size
}
if (fileElement.elementType === ElementType.VIDEO) {
totalSize += fs.statSync(fileElement.videoElement.filePath).size
}
if (fileElement.elementType === ElementType.PIC) {
totalSize += fs.statSync(fileElement.picElement.sourcePath).size
}
} catch (e) {
ctx.logger.warn('文件大小计算失败', e, fileElement)
}
}
//log('发送消息总大小', totalSize, 'bytes')
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
}

View File

@ -2,13 +2,12 @@ import { OB11Message, OB11MessageAt, OB11MessageData, OB11MessageDataType } from
import { OB11FriendRequestEvent } from '../event/request/OB11FriendRequest'
import { OB11GroupRequestEvent } from '../event/request/OB11GroupRequest'
import { ChatType, GroupRequestOperateTypes, Peer } from '@/ntqqapi/types'
import { convertMessage2List, createSendElements, sendMsg } from '../action/msg/SendMsg'
import { convertMessage2List, createSendElements, sendMsg } from '../helper/createMessage'
import { getConfigUtil } from '@/common/config'
import { MessageUnique } from '@/common/utils/MessageUnique'
import { isNullable } from 'cosmokit'
import { Context } from 'cordis'
interface QuickOperationPrivateMessage {
reply?: string
auto_escape?: boolean
@ -21,7 +20,6 @@ interface QuickOperationGroupMessage extends QuickOperationPrivateMessage {
kick?: boolean
ban?: boolean
ban_duration?: number
//
}
interface QuickOperationFriendRequest {

View File

@ -6,7 +6,6 @@
"strict": true,
"noImplicitAny": false,
"esModuleInterop": true,
"allowJs": true,
"allowSyntheticDefaultImports": true,
"experimentalDecorators": true,
"resolveJsonModule": true,