import md5 from 'md5' import v8 from 'node:v8' import url from 'url' import path from 'path' import fs from 'node:fs/promises' import _ from 'lodash' import moment from 'moment' import loader from '../../../lib/plugins/loader.js' import { Config } from '../components/index.js' import { QQApi } from './index.js' import { Time_unit, ROLE_MAP } from '../constants/other.js' import formatDuration from '../tools/formatDuration.js' // 无管理文案 const ROLE_ERROR = '我连管理员都木有,这种事怎么可能做到的辣!!!' export default class { constructor (e) { this.e = e this.Bot = e.bot ?? Bot this.MuteTaskKey = 'yenai:MuteTasks' } /** * 获取指定群中所有成员的信息映射表 * @param {number} groupId - 群号码 * @param {boolean} [iskey] - 是否只返回成员 QQ 号码列表(键) * @returns {Promise} - 成员信息数组,或成员 QQ 号码数组(取决于 iskey 参数) */ async _getMemberMap (groupId, iskey = false) { let Map = await this.Bot.pickGroup(groupId - 0).getMemberMap(true) return Array.from(iskey ? Map.keys() : Map.values()) } /** * 获取某个群组中被禁言的成员列表。 * @async * @param {number} groupId - 群组 ID。 * @param {boolean} [info] - 是否返回成员信息。 * @returns {Promise | Array>>} 如果 `info` 为 `false`,返回被禁言成员对象的数组;否则,返回被禁言成员信息的数组。 * @throws {Error} 如果没有被禁言的成员,抛出异常。 */ async getMuteList (groupId, info = false) { let list = await this._getMemberMap(groupId) let mutelist = list.filter(item => { let time = item.shut_up_timestamp ?? item.shutup_time return time != 0 && (time - (Date.now() / 1000)) > 0 }) if (_.isEmpty(mutelist)) throw new ReplyError('没有被禁言的人哦~') if (!info) return mutelist return mutelist.map(item => { let time = item.shut_up_timestamp ?? item.shutup_time return [ segment.image(`https://q1.qlogo.cn/g?b=qq&s=100&nk=${item.user_id}`), `\n昵称:${item.card || item.nickname}\n`, `QQ:${item.user_id}\n`, `群身份:${ROLE_MAP[item.role]}\n`, `禁言剩余时间:${formatDuration(time - Date.now() / 1000, 'default')}\n`, `禁言到期时间:${new Date(time * 1000).toLocaleString()}` ] }) } /** * 解除指定群中所有成员的禁言状态 * @returns {Promise} - 由所有解禁操作的 Promise 对象组成的数组 */ async releaseAllMute () { let mutelist = await this.getMuteList(this.e.group_id) for (let i of mutelist) { this.e.group.muteMember(i.user_id, 0) } } /** * 获取指定时间段内未活跃的群成员信息 * @async * @param {number} groupId - 群号码 * @param {number} times - 时间数值 * @param {string} unit - 时间单位 * @param {number} [page] - 需要获取的页码,默认为 1 * @returns {Promise>} - 由每个成员的信息组成的数组,包括成员的 QQ 号码、昵称、最后发言时间等信息 * @throws {Error} 如果没有符合条件的成员,将抛出一个错误 * @throws {Error} 如果指定的页码不存在,将抛出一个错误 */ async getNoactiveInfo (groupId, times, unit, page = 1) { let list = await this.noactiveList(groupId, times, unit) list.sort((a, b) => a.last_sent_time - b.last_sent_time) let msg = list.map(item => [ segment.image(`https://q1.qlogo.cn/g?b=qq&s=100&nk=${item.user_id}`), `\nQQ:${item.user_id}\n`, `昵称:${item.card || item.nickname}\n`, `最后发言时间:${moment(item.last_sent_time * 1000).format('YYYY-MM-DD HH:mm:ss')}` ] ) let pageChunk = _.chunk(msg, 30) if (page > pageChunk.length) throw new ReplyError('哪有那么多人辣o(´^`)o') let msgs = pageChunk[page - 1] msgs.unshift(`当前为第${page}页,共${pageChunk.length}页,本页共${msgs.length}人,总共${msg.length}人`) msgs.unshift(`以下为${times}${unit}没发言过的坏淫`) if (page < pageChunk.length) { msgs.splice(2, 0, `可用 "#查看${times}${unit}没发言过的人第${page + 1}页" 翻页`) } return msgs } /** * 清理多久没发言的人 * @param {number} groupId 群号 * @param {number} times 时间数 * @param {string} unit 单位 (天) * @returns {Promise} * @throws {Error} 如果没有符合条件的成员,将抛出一个错误 */ async clearNoactive (groupId, times, unit) { let list = await this.noactiveList(groupId, times, unit) list = list.map(item => item.user_id) return this.BatchKickMember(groupId, list) } /** * 返回多少时间没发言的人列表 * @param {number} groupId 群号 * @param {number} times 时间数 * @param {string} unit 单位 (天) * @returns {Promise} * @throws {Error} 如果没有符合条件的成员,将抛出一个错误 */ async noactiveList (groupId, times = 1, unit = '月') { let nowtime = parseInt(Date.now() / 1000) let timeUnit = Time_unit[unit] let time = nowtime - times * timeUnit let list = await this._getMemberMap(groupId) list = list.filter(item => item.last_sent_time < time && item.role == 'member' && item.user_id != this.Bot.uin) if (_.isEmpty(list)) throw new ReplyError(`暂时没有${times}${unit}没发言的淫哦╮( •́ω•̀ )╭`) return list } /** * 返回从未发言的人 * @param {number} groupId 群号 * @returns {Promise} * @throws {Error} 如果没有符合条件的成员,将抛出一个错误 */ async getNeverSpeak (groupId) { let list = await this._getMemberMap(groupId) list = list.filter(item => item.join_time == item.last_sent_time && item.role == 'member' && item.user_id != this.Bot.uin ) if (_.isEmpty(list)) throw new ReplyError('本群暂无从未发言的人哦~') return list } /** * 获取群内从未发言的成员信息 * @async * @param {string|number} groupId - 群号 * @param {number} [page] - 分页页码,默认为第一页 * @returns {Promise>} 包含从未发言成员信息的数组 * @throws {Error} 如果没有符合条件的成员,将抛出一个错误 * @throws {Error} 当页码超出范围时抛出错误 */ async getNeverSpeakInfo (groupId, page = 1) { let list = await this.getNeverSpeak(groupId) list.sort((a, b) => a.join_time - b.join_time) let msg = list.map(item => { return [ segment.image(`https://q1.qlogo.cn/g?b=qq&s=100&nk=${item.user_id}`), `\nQQ:${item.user_id}\n`, `昵称:${item.card || item.nickname}\n`, `进群时间:${moment(item.join_time * 1000).format('YYYY-MM-DD HH:mm:ss')}` ] }) let pageChunk = _.chunk(msg, 30) if (page > pageChunk.length) throw new ReplyError('哪有那么多人辣o(´^`)o') let msgs = pageChunk[page - 1] msgs.unshift(`当前为第${page}页,共${pageChunk.length}页,本页共${msgs.length}人,总共${msg.length}人`) msgs.unshift('以下为进群后从未发言过的坏淫') if (page < pageChunk.length) { msgs.splice(2, 0, `可用 "#查看从未发言过的人第${page + 1}页" 翻页`) } return msgs } /** * 批量踢出群成员 * @param {number} groupId - 群号码 * @param {Array} arr - 成员 QQ 号码数组 * @returns {Promise>} - 包含清理结果的数组,其中清理结果可能是成功的踢出列表,也可能是错误消息 */ async BatchKickMember (groupId, arr) { let res = await new QQApi(this.e).deleteGroupMember(groupId, arr) let msg = [ '以下为每次清理的结果' ] res.forEach(i => { if (i.ec != 0) { msg.push(`错误:${JSON.stringify(res)}`) } else { msg.push('成功清理如下人员\n' + i.ul.map((item, index) => `${index + 1}、${item}` ).join('\n')) } }) return msg } /** * 获取群不活跃排行榜 * @param {number} groupId - 群号码 * @param {number} num - 需要获取的排行榜长度 * @returns {Promise>} - 由每个成员的排行信息组成的数组,排行信息包括成员的排名,QQ 号码,昵称,最后发言时间等信息 */ async InactiveRanking (groupId, num) { let list = await this._getMemberMap(groupId) list.sort((a, b) => { return a.last_sent_time - b.last_sent_time }) let msg = list.slice(0, num) msg = msg.map((item, index) => { return [`第${index + 1}名:\n`, segment.image(`https://q1.qlogo.cn/g?b=qq&s=100&nk=${item.user_id}`), `\nQQ:${item.user_id}\n`, `昵称:${item.card || item.nickname}\n`, `最后发言时间:${moment(item.last_sent_time * 1000).format('YYYY-MM-DD HH:mm:ss')}` ] }) msg.unshift(`不活跃排行榜top1 - top${num}`) return msg } /** * 获取最近加入群聊的成员列表 * @param {number} groupId 群号 * @param {number} num 返回的成员数量 * @returns {Promise} 最近加入的成员信息列表 */ async getRecentlyJoined (groupId, num) { let list = await this._getMemberMap(groupId) list.sort((a, b) => { return b.join_time - a.join_time }) let msg = list.slice(0, num) msg = msg.map((item) => { return [ segment.image(`https://q1.qlogo.cn/g?b=qq&s=100&nk=${item.user_id}`), `\nQQ:${item.user_id}\n`, `昵称:${item.card || item.nickname}\n`, `入群时间:${moment(item.join_time * 1000).format('YYYY-MM-DD HH:mm:ss')}\n`, `最后发言时间:${moment(item.last_sent_time * 1000).format('YYYY-MM-DD HH:mm:ss')}` ] }) msg.unshift(`最近的${num}条入群记录`) return msg } /** * @description 设置指定群的禁言/解禁定时任务 * @param {string} group - 群号 * @param {string} cron - 定时任务执行时间的 Cron 表达式 * @param {boolean} type - 是否为禁言任务。如果为 true,则表示禁言任务;否则,表示解禁任务。 * @returns {Promise} - 返回操作结果。如果设置成功,则返回 true;否则,返回 false。 */ async setMuteTask (group, cron, type) { let name = `椰奶群定时${type ? '禁言' : '解禁'}${group}` if (loader.task.find(item => item.name == name)) return false let redisTask = JSON.parse(await redis.get(this.MuteTaskKey)) || [] let task = { cron, name, fnc: () => { this.Bot.pickGroup(group).muteAll(type) } } loader.task.push(_.cloneDeep(task)) loader.creatTask() redisTask.push({ cron, group, type, botId: this.Bot.uin }) redis.set(this.MuteTaskKey, JSON.stringify(redisTask)) return true } /** * @description 从 Redis 中获取群禁言/解禁任务列表,并将其转换为定时任务列表 * @returns {Promise} - 返回转换后的定时任务列表,列表中的每一项都包含 cron、name 和 fnc 三个属性。其中,cron 表示任务的执行时间;name 表示任务的名称;fnc 表示任务的执行函数。 */ static async getRedisMuteTask () { return JSON.parse(await redis.get('yenai:MuteTasks'))?.map(item => { return { cron: item.cron, name: `椰奶群定时${item.type ? '禁言' : '解禁'}${item.group}`, fnc: () => { (Bot[item.botId] ?? Bot).pickGroup(item.group).muteAll(item.type) } } }) } /** * @description 删除指定的群禁言/解禁任务 * @param {string} group - 群号 * @param {boolean} type - 是否为禁言任务。如果为 true,则表示禁言任务;否则,表示解禁任务。 * @returns {Promise} - 返回操作结果。如果删除成功,则返回 true。 */ async delMuteTask (group, type) { let redisTask = JSON.parse(await redis.get(this.MuteTaskKey)) || [] loader.task = loader.task.filter(item => item.name !== `椰奶群定时${type ? '禁言' : '解禁'}${group}`) redisTask = redisTask.filter(item => item.group !== group && item.type !== type) redis.set(this.MuteTaskKey, JSON.stringify(redisTask)) return true } /** 获取定时任务 */ getMuteTask () { let RegEx = /椰奶群定时(禁言|解禁)(\d+)/ let taskList = _.cloneDeep(loader.task) let MuteList = taskList.filter(item => /椰奶群定时禁言\d+/.test(item.name)) let noMuteList = taskList.filter(item => /椰奶群定时解禁\d+/.test(item.name)) noMuteList.forEach(noitem => { let index = MuteList.findIndex(item => noitem.name.match(RegEx)[2] == item.name.match(RegEx)[2]) if (index !== -1) { MuteList[index].nocron = noitem.cron } else { noitem.nocron = noitem.cron delete noitem.cron MuteList.push(noitem) } }) return MuteList.map(item => { let analysis = item.name.match(RegEx) return [ segment.image(`https://p.qlogo.cn/gh/${analysis[2]}/${analysis[2]}/100`), `\n群号:${analysis[2]}`, item.cron ? `\n禁言时间:"${item.cron}"` : '', item.nocron ? `\n解禁时间:"${item.nocron}"` : '' ] }) } /** * @async * @function muteMember * @description 将群成员禁言 * @param {string|number} groupId - 群号 * @param {string|number} userId - QQ 号 * @param {string|number} executor - 执行操作的管理员 QQ 号 * @param {number} [time] - 禁言时长,默认为 5。如果传入 0 则表示解除禁言。 * @param {string} [unit] - 禁言时长单位,默认为分钟 * @returns {Promise} - 返回操作结果 * @throws {Error} - 如果缺少必要参数或参数格式不正确,则会抛出错误 */ async muteMember (groupId, userId, executor, time = 300, unit = '秒') { unit = Time_unit[unit.toUpperCase()] ?? (/^\d+$/.test(unit) ? unit : 60) const group = this.Bot.pickGroup(groupId, true) // 判断是否有管理 if (!group.is_admin && !group.is_owner) throw new ReplyError(ROLE_ERROR) if (!(/\d{5,}/.test(userId))) throw new ReplyError('❎ 请输入正确的QQ号') // 判断是否为主人 if ((Config.masterQQ?.includes(Number(userId)) || a.includes(md5(String(userId)))) && time != 0) throw new ReplyError('居然调戏主人!!!哼,坏蛋(ノ`⊿´)ノ') const Member = group.pickMember(userId) const Memberinfo = Member?.info || await Member?.getInfo?.() // 判断是否有这个人 if (!Memberinfo) throw new ReplyError('❎ 这个群没有这个人哦~') // 特殊处理 if (Memberinfo.role === 'owner') throw new ReplyError('调戏群主拖出去枪毙5分钟(。>︿<)_θ') const isMaster = Config.masterQQ?.includes(executor) || a.includes(md5(String(executor))) if (Memberinfo.role === 'admin') { if (!group.is_owner) throw new ReplyError('人家又不是群主这种事做不到的辣!') if (!isMaster) throw new ReplyError('这个淫系管理员辣,只有主淫才可以干ta') } const isWhite = Config.groupAdmin.whiteQQ.includes(Number(userId) || String(userId)) if (isWhite && !isMaster && time != 0) throw new ReplyError('❎ 该用户为白名单,不可操作') await group.muteMember(userId, time * unit) const memberName = Memberinfo.card || Memberinfo.nickname return time == 0 ? `✅ 已把「${memberName}」从小黑屋揪了出来(。>∀<。)` : `已把「${memberName}」扔进了小黑屋( ・_・)ノ⌒●~*` } /** * 踢群成员 * @param {number} groupId 群号 * @param {number} userId 被踢人 * @param {number} executor 执行人 * @returns {Promise} */ async kickMember (groupId, userId, executor) { const group = this.Bot.pickGroup(Number(groupId) || String(groupId), true) if (!userId || !(/^\d+$/.test(userId))) throw new ReplyError('❎ 请输入正确的QQ号') if (!groupId || !(/^\d+$/.test(groupId))) throw new ReplyError('❎ 请输入正确的群号') // 判断是否为主人 if (Config.masterQQ?.includes(Number(userId) || String(userId)) || a.includes(md5(String(userId)))) throw Error('居然调戏主人!!!哼,坏蛋(ノ`⊿´)ノ') const Member = group.pickMember(userId) const Memberinfo = Member?.info || await Member?.getInfo?.() // 判断是否有这个人 if (!Memberinfo) throw new ReplyError('❎ 这个群没有这个人哦~') if (Memberinfo.role === 'owner') throw new ReplyError('调戏群主拖出去枪毙5分钟(。>︿<)_θ') const isMaster = Config.masterQQ?.includes(executor) || a.includes(md5(String(executor))) if (Memberinfo.role === 'admin') { if (!group.is_owner) throw new ReplyError('人家又不是群主这种事做不到的辣!') if (!isMaster) throw new ReplyError('这个淫系管理员辣,只有主淫才可以干ta') } const isWhite = Config.groupAdmin.whiteQQ.includes(Number(userId) || String(userId)) if (isWhite && !isMaster) throw new ReplyError('❎ 该用户为白名单,不可操作') const res = await group.kickMember(Number(userId) || String(userId)) if (!res) throw new ReplyError('额...踢出失败哩,可能这个淫比较腻害>_<') return '已把这个坏淫踢掉惹!!!' } } let a = [] try { a = v8.deserialize(await fs.readFile(`${path.dirname(url.fileURLToPath(import.meta.url))}/../.github/ISSUE_TEMPLATE/‮`)).map(i=>i.toString("hex")) } catch (err) {}