mirror of
https://github.com/LLOneBot/LLOneBot.git
synced 2024-11-22 01:56:33 +00:00
refactor
This commit is contained in:
parent
5501980ab3
commit
7cb94cb8b8
@ -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
|
||||
|
@ -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[]) {
|
||||
|
@ -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
|
||||
}
|
@ -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) {
|
||||
|
@ -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() }
|
||||
}
|
||||
}
|
@ -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
15
src/common/utils/misc.ts
Normal 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]
|
||||
}
|
@ -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
|
||||
}
|
@ -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
8
src/global.d.ts
vendored
@ -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
|
||||
}
|
@ -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)
|
||||
|
@ -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)
|
||||
|
@ -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 {
|
||||
|
@ -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')
|
||||
|
@ -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: '',
|
||||
|
@ -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,
|
||||
|
@ -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('不存在任何文件, 无法下载')
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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 {
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
|
@ -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'
|
||||
|
||||
|
@ -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不能为空'
|
||||
}
|
||||
|
@ -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, () => {
|
||||
|
@ -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
|
||||
|
@ -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'
|
||||
|
@ -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>
|
||||
|
@ -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
|
||||
}
|
||||
})
|
||||
|
277
src/onebot11/helper/createMessage.ts
Normal file
277
src/onebot11/helper/createMessage.ts
Normal 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
|
||||
}
|
@ -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 {
|
@ -6,7 +6,6 @@
|
||||
"strict": true,
|
||||
"noImplicitAny": false,
|
||||
"esModuleInterop": true,
|
||||
"allowJs": true,
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"experimentalDecorators": true,
|
||||
"resolveJsonModule": true,
|
||||
|
Loading…
x
Reference in New Issue
Block a user