Miao-Yunzai/apps/add.ts

995 lines
22 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import {
createWriteStream,
existsSync,
mkdirSync,
readFileSync,
readdirSync,
unlink,
writeFileSync
} from 'node:fs'
import lodash from 'lodash'
import fetch from 'node-fetch'
import moment from 'moment'
import { pipeline } from 'stream'
import { promisify } from 'util'
import { ConfigController as cfg } from 'yunzai/config'
import { Plugin } from 'yunzai/core'
import { makeForwardMsg } from 'yunzai/core'
const textArr = {}
/**
* tudo
*/
export class add extends Plugin {
path = './data/textJson/'
facePath = './data/face/'
isGlobal = false
keyWord = null
/**
*
*/
constructor() {
/**
name: '添加表情',
dsc: '添加表情,文字等',
*/
super()
this.priority = 50000
this.rule = [
{
reg: /^#(全局)?添加(.*)/,
fnc: this.add.name
},
{
reg: /^#(全局)?删除(.*)/,
fnc: this.del.name
},
{
reg: /(.*)/,
fnc: this.getText.name,
log: false
},
{
reg: /^#+(全局)?(?:查看|查询)(?:表情|词条)(.+)$/,
fnc: this.faceDetail.name
},
{
reg: /#(全局)?(表情|词条)(.*)/,
fnc: this.list.name
}
]
}
/**
*
*/
async accept() {
/** 处理消息 */
if (
this.e.atBot &&
this.e.msg &&
this.e?.msg.includes('添加') &&
!this.e?.msg.includes('#')
) {
this.e.msg = '#' + this.e.msg
}
}
/**
*
*/
async init() {
if (!existsSync(this.path)) {
mkdirSync(this.path)
}
if (!existsSync(this.facePath)) {
mkdirSync(this.facePath)
}
}
/**
*
*/
get grpKey() {
return `Yz:group_id:${this.e.user_id}`
}
/**
*
*/
async add() {
this.isGlobal = this.e?.msg.includes('全局')
await this.getGroupId()
if (!this.group_id) {
this.e.reply('请先在群内触发表情,确定添加的群')
return
}
this.initTextArr()
if (!this.checkAuth()) return
if (!this.checkKeyWord()) return
if (await this.singleAdd()) return
/** 获取关键词 */
this.getKeyWord()
if (!this.keyWord) {
this.e.reply('添加错误:没有关键词')
return
}
if (/uid/i.test(this.keyWord)) {
this.e.reply('请勿添加特殊关键词')
return
}
this.setContext('addContext')
await this.e.reply('请发送添加内容', false, { at: true })
}
/**
*
*/
async getGroupId() {
/** 添加全局表情存入到机器人qq文件中 */
if (this.isGlobal) {
this.group_id = this.e.bot.uin
return this.e.bot.uin
}
if (this.e.isGroup) {
this.group_id = this.e.group_id
redis.setEx(this.grpKey, 3600 * 24 * 30, String(this.group_id))
return this.group_id
}
// redis获取
let groupId = await redis.get(this.grpKey)
if (groupId) {
this.group_id = Number(groupId)
return this.group_id
}
return false
}
/**
*
*/
checkAuth() {
if (this.e.isMaster) return true
let groupCfg = cfg.getGroup(this.group_id)
if (groupCfg.imgAddLimit == 2) {
this.e.reply('暂无权限,只有主人才能操作')
return false
}
if (groupCfg.imgAddLimit == 1) {
if (!this.e.bot.gml.has(this.group_id)) {
return false
}
if (!this.e.bot.gml.get(this.group_id).get(this.e.user_id)) {
return false
}
if (!this.e.member.is_admin) {
this.e.reply('暂无权限,只有管理员才能操作')
return false
}
}
if (!this.e.isGroup && groupCfg.addPrivate != 1) {
this.e.reply('禁止私聊添加')
return false
}
return true
}
/**
*
*/
checkKeyWord() {
if (this.e.img && this.e.img.length > 1) {
this.e.reply('添加错误:只能发送一个表情当关键词')
return false
}
if (this.e.at) {
let at = lodash.filter(this.e.message, o => {
return o.type == 'at' && o.qq != this.e.bot.uin
})
if (at.length > 1) {
this.e.reply('添加错误:只能@一个人当关键词')
return false
}
}
if (this.e.img && this.e.at) {
this.e.reply('添加错误:没有关键词')
return false
}
return true
}
/**
* 单独添加
* @returns
*/
async singleAdd() {
if (this.e.message.length != 2) return false
let msg = lodash.keyBy(this.e.message, 'type')
if (!this.e.msg || !msg.image) return false
// #全局添加文字+表情包,无法正确添加到全局路径
this.e.isGlobal = this.isGlobal
let keyWord = this.e.msg.replace(/#||图片|表情|添加|全局/g, '').trim()
if (!keyWord) return false
this.keyWord = this.trimAlias(keyWord)
this.e.keyWord = this.keyWord
if (this.e.msg.includes('添加图片')) {
this.e.addImg = true
}
this.e.message = [msg.image]
await this.addContext()
return true
}
/**
* 获取添加关键词
*/
getKeyWord() {
this.e.isGlobal = this.e.msg.includes('全局')
this.keyWord = this.e
.toString()
.trim()
/** 过滤#添加 */
.replace(/#||图片|表情|添加|删除|全局/g, '')
/** 过滤@ */
.replace(new RegExp('{at:' + this.e.bot.uin + '}', 'g'), '')
.trim()
this.keyWord = this.trimAlias(this.keyWord)
this.e.keyWord = this.keyWord
if (this.e.msg.includes('添加图片')) {
this.e.addImg = true
}
}
/**
* 过滤别名
* @param msg
* @returns
*/
trimAlias(msg) {
let groupCfg = cfg.getGroup(this.group_id)
let alias = groupCfg.botAlias
if (!Array.isArray(alias)) {
alias = [alias]
}
for (let name of alias) {
if (msg.startsWith(name)) {
msg = lodash.trimStart(msg, name).trim()
}
}
return msg
}
/**
* 添加内容
* @returns
*/
async addContext() {
this.isGlobal = this.e.isGlobal || this.getContext()?.addContext?.isGlobal
await this.getGroupId()
/** 关键词 */
let keyWord = this.keyWord || this.getContext()?.addContext?.keyWord
let addImg = this.e.addImg || this.getContext()?.addContext?.addImg
/** 添加内容 */
let message = this.e.message
let retMsg = this.getRetMsg()
this.finish('addContext')
for (let i in message) {
if (message[i].type == 'at') {
if (message[i].qq == this.e.bot.uin) {
this.e.reply('添加内容不能@机器人!')
return
}
}
if (message[i].type == 'file') {
this.e.reply('添加错误:禁止添加文件')
return
}
// 保存用户信息用于追溯添加者
message[i].from_user = {
card: this.e.sender.card,
nickname: this.e.sender.nickname,
user_id: this.e.sender.user_id
}
}
if (message.length == 1 && message[0].type == 'image') {
let local = await this.saveImg(message[0].url, keyWord)
if (!local) return
message[0].local = local
message[0].asface = true
if (addImg) message[0].asface = false
}
if (!textArr[this.group_id]) textArr[this.group_id] = new Map()
/** 支持单个关键词添加多个 */
let text = textArr[this.group_id].get(keyWord)
if (text) {
text.push(message)
textArr[this.group_id].set(keyWord, text)
} else {
text = [message]
textArr[this.group_id].set(keyWord, text)
}
if (text.length > 1 && retMsg[0].type != 'image') {
retMsg.push(String(text.length))
}
retMsg.unshift('添加成功:')
this.saveJson()
this.e.reply(retMsg)
}
/**
* 添加成功回复消息
* @returns
*/
getRetMsg() {
const retMsg = this.getContext()
let msg = ''
if (retMsg?.addContext?.message) {
msg = retMsg.addContext.message
for (let i in msg) {
if (msg[i].type == 'text' && msg[i].text.includes('添加')) {
msg[i].text = this.trimAlias(msg[i].text)
msg[i].text = msg[i].text
.trim()
.replace(/#||图片|表情|添加|全局/g, '')
if (!msg[i].text) delete msg[i]
continue
}
if (msg[i].type == 'at') {
if (msg[i].qq == this.e.bot.uin) {
delete msg[i]
continue
} else {
msg[i].text = ''
}
}
}
}
if (!msg && this.keyWord) {
msg = [this.keyWord]
}
return lodash.compact(msg)
}
/**
*
*/
saveJson() {
let obj = {}
for (let [k, v] of textArr[this.group_id]) {
obj[k] = v
}
writeFileSync(
`${this.path}${this.group_id}.json`,
JSON.stringify(obj, '', '\t')
)
}
/**
*
*/
saveGlobalJson() {
let obj = {}
for (let [k, v] of textArr[this.e.bot.uin]) {
obj[k] = v
}
writeFileSync(
`${this.path}${this.e.bot.uin}.json`,
JSON.stringify(obj, '', '\t')
)
}
/**
*
* @param url
* @param keyWord
* @returns
*/
async saveImg(url, keyWord) {
let groupCfg = cfg.getGroup(this.group_id)
let savePath = `${this.facePath}${this.group_id}/`
if (!existsSync(savePath)) {
mkdirSync(savePath)
}
const response = await fetch(url)
keyWord = keyWord.replace(/\.|\\|\/|:|\*|\?|<|>|\|"/g, '_')
if (!response.ok) {
this.e.reply('添加图片下载失败。。')
return false
}
let imgSize = (response.headers.get('size') / 1024 / 1024).toFixed(2)
if (imgSize > 1024 * 1024 * groupCfg.imgMaxSize) {
this.e.reply(`添加失败:表情太大了,${imgSize}m`)
return false
}
let type = response.headers.get('content-type').split('/')[1]
if (type == 'jpeg') type = 'jpg'
if (existsSync(`${savePath}${keyWord}.${type}`)) {
keyWord = `${keyWord}_${moment().format('X')}`
}
savePath = `${savePath}${keyWord}.${type}`
const streamPipeline = promisify(pipeline)
await streamPipeline(response.body, createWriteStream(savePath))
return savePath
}
/**
*
* @returns
*/
async getText() {
if (!this.e.message) return false
this.isGlobal = false
await this.getGroupId()
if (!this.group_id) return false
this.initTextArr()
this.initGlobalTextArr()
let keyWord = this.e
.toString()
.replace(/#|/g, '')
.replace(`{at:${this.e.bot.uin}}`, '')
.trim()
keyWord = this.trimAlias(keyWord)
let num = 0
if (isNaN(keyWord)) {
num = keyWord.trim().match(/[0-9]+$/)?.[0]
if (
!isNaN(num) &&
!textArr[this.group_id].has(keyWord) &&
!textArr[this.e.bot.uin].has(keyWord)
) {
keyWord = lodash.trimEnd(keyWord, num).trim()
num--
}
}
let msg = textArr[this.group_id].get(keyWord) || []
let globalMsg = textArr[this.e.bot.uin].get(keyWord) || []
if (lodash.isEmpty(msg) && lodash.isEmpty(globalMsg)) return false
msg = [...msg, ...globalMsg]
/** 如果只有一个则不随机 */
if (num >= 0 && msg.length === 1) {
msg = msg[num]
} else {
/** 随机获取一个 */
num = lodash.random(0, msg.length - 1)
msg = msg[num]
}
if (msg[0] && msg[0].local) {
if (existsSync(msg[0].local)) {
let tmp = segment.image(msg[0].local)
tmp.asface = msg[0].asface
msg = tmp
} else {
// this.e.reply(`表情已删除:${keyWord}`)
return
}
}
if (Array.isArray(msg)) {
msg.forEach(m => {
/** 去除回复@@ */
if (m?.type == 'at') {
delete m.text
}
})
}
logger.mark(`[发送表情]${this.e.logText} ${keyWord}`)
let ret = await this.e.reply(msg)
if (!ret) {
this.expiredMsg(keyWord, num)
}
return true
}
/**
*
* @param keyWord
* @param num
*/
expiredMsg(keyWord, num) {
logger.mark(`[发送表情]${this.e.logText} ${keyWord} 表情已过期失效`)
let arr = textArr[this.group_id].get(keyWord)
arr.splice(num, 1)
if (arr.length <= 0) {
textArr[this.group_id].delete(keyWord)
} else {
textArr[this.group_id].set(keyWord, arr)
}
this.saveJson()
}
/**
* 初始化已添加内容
* @returns
*/
initTextArr() {
if (textArr[this.group_id]) return
textArr[this.group_id] = new Map()
let path = `${this.path}${this.group_id}.json`
if (!existsSync(path)) {
return
}
try {
let text = JSON.parse(readFileSync(path, 'utf8'))
for (let i in text) {
if (text[i][0] && !Array.isArray(text[i][0])) {
text[i] = [text[i]]
}
textArr[this.group_id].set(String(i), text[i])
}
} catch (error) {
logger.error(`json格式错误${path}`)
delete textArr[this.group_id]
return false
}
/** 加载表情 */
let facePath = `${this.facePath}${this.group_id}`
if (existsSync(facePath)) {
const files = readdirSync(`${this.facePath}${this.group_id}`).filter(
file => /\.(jpeg|jpg|png|gif)$/g.test(file)
)
for (let val of files) {
let tmp = val.split('.')
tmp[0] = tmp[0].replace(/_[0-9]{10}$/, '')
if (/at|image/g.test(val)) continue
if (textArr[this.group_id].has(tmp[0])) continue
textArr[this.group_id].set(tmp[0], [
[
{
local: `${facePath}/${val}`,
asface: true
}
]
])
}
this.saveJson()
} else {
mkdirSync(facePath)
}
}
/**
* 初始化全局已添加内容
* @returns
*/
initGlobalTextArr() {
if (textArr[this.e.bot.uin]) return
textArr[this.e.bot.uin] = new Map()
let globalPath = `${this.path}${this.e.bot.uin}.json`
if (!existsSync(globalPath)) {
return
}
try {
let text = JSON.parse(readFileSync(globalPath, 'utf8'))
for (let i in text) {
if (text[i][0] && !Array.isArray(text[i][0])) {
text[i] = [text[i]]
}
textArr[this.e.bot.uin].set(String(i), text[i])
}
} catch (error) {
logger.error(`json格式错误${globalPath}`)
delete textArr[this.e.bot.uin]
return false
}
/** 加载表情 */
let globalFacePath = `${this.facePath}${this.e.bot.uin}`
if (existsSync(globalFacePath)) {
const files = fs
.readdirSync(`${this.facePath}${this.e.bot.uin}`)
.filter(file => /\.(jpeg|jpg|png|gif)$/g.test(file))
for (let val of files) {
let tmp = val.split('.')
tmp[0] = tmp[0].replace(/_[0-9]{10}$/, '')
if (/at|image/g.test(val)) continue
if (textArr[this.e.bot.uin].has(tmp[0])) continue
textArr[this.e.bot.uin].set(tmp[0], [
[
{
local: `${globalFacePath}/${val}`,
asface: true
}
]
])
}
this.saveGlobalJson()
} else {
mkdirSync(globalFacePath)
}
}
/**
*
* @returns
*/
async del() {
this.isGlobal = this.e?.msg.includes('全局')
await this.getGroupId()
if (!this.group_id) return false
if (!this.checkAuth()) return
this.initTextArr()
let keyWord = this.e
.toString()
.replace(/#||图片|表情|删除|全部|全局/g, '')
keyWord = this.trimAlias(keyWord)
let num = false
let index = 0
if (isNaN(keyWord)) {
num = keyWord.charAt(keyWord.length - 1)
if (!isNaN(num) && !textArr[this.group_id].has(keyWord)) {
keyWord = lodash.trimEnd(keyWord, num).trim()
index = num - 1
} else {
num = false
}
}
let arr = textArr[this.group_id].get(keyWord)
if (!arr) {
// await this.e.reply(`暂无此表情:${keyWord}`)
return false
}
let tmp = []
if (num) {
if (!arr[index]) {
// await this.e.reply(`暂无此表情:${keyWord}${num}`)
return false
}
tmp = arr[index]
arr.splice(index, 1)
if (arr.length <= 0) {
textArr[this.group_id].delete(keyWord)
} else {
textArr[this.group_id].set(keyWord, arr)
}
} else {
if (this.e.msg.includes('删除全部')) {
tmp = arr
arr = []
} else {
tmp = arr.pop()
}
if (arr.length <= 0) {
textArr[this.group_id].delete(keyWord)
} else {
textArr[this.group_id].set(keyWord, arr)
}
}
if (!num) num = ''
let retMsg = [{ type: 'text', text: '删除成功:' }]
for (let msg of this.e.message) {
if (msg.type == 'text') {
msg.text = msg.text.replace(/#||图片|表情|删除|全部|全局/g, '')
if (!msg.text) continue
}
retMsg.push(msg)
}
if (num > 0) {
retMsg.push({ type: 'text', text: num })
}
await this.e.reply(retMsg)
/** 删除图片 */
tmp.forEach(item => {
let img = item
if (Array.isArray(item)) {
img = item[0]
}
if (img.local) {
unlink(img.local, () => {})
}
})
this.saveJson()
}
/**
*
*/
async list() {
this.isGlobal = this.e?.msg.includes('全局')
let page = 1
let pageSize = 100
let type = 'list'
await this.getGroupId()
if (!this.group_id) return false
this.initTextArr()
let search = this.e.msg.replace(/#||表情|词条|全局/g, '')
if (search.includes('列表')) {
page = search.replace(/列表/g, '') || 1
} else {
type = 'search'
}
let list = textArr[this.group_id]
if (lodash.isEmpty(list)) {
await this.e.reply('暂无表情')
return
}
let arr = []
for (let [k, v] of textArr[this.group_id]) {
if (type == 'list') {
arr.push({ key: k, val: v, num: arr.length + 1 })
} else if (k.includes(search)) {
/** 搜索表情 */
arr.push({ key: k, val: v, num: arr.length + 1 })
}
}
let count = arr.length
arr = arr.reverse()
if (type == 'list') {
arr = this.pagination(page, pageSize, arr)
}
if (lodash.isEmpty(arr)) {
return
}
let msg = [],
result = [],
num = 0
for (let i in arr) {
if (num >= page * pageSize) break
let keyWord = await this.keyWordTran(arr[i].key)
if (!keyWord) continue
if (Array.isArray(keyWord)) {
keyWord.unshift(`${num + 1}`)
// keyWord.push('\n')
keyWord.push(v => msg.push(v))
} else if (keyWord.type) {
msg.push(`\n${num + 1}`, keyWord)
} else {
msg.push(`${num + 1}`, keyWord)
}
num++
}
/** 数组分段 */
for (const i in msg) {
result.push([msg[i]])
}
/** 计算页数 */
let book = count / pageSize
if (book % 1 === 0) {
book = result
} else {
book = Math.floor(book) + 1
}
if (type == 'list' && msg.length >= pageSize) {
result.push(`更多内容请翻页查看\n如#表情列表${Number(page) + 1}`)
}
let title = `表情列表,第${page}页,共${count}条,共${book}`
if (type == 'search') {
title = `表情${search}${count}`
}
let forwardMsg = await makeForwardMsg(this.e, [title, ...result], title)
this.e.reply(forwardMsg)
}
/**
*
*/
pagination(pageNo, pageSize, array) {
let offset = (pageNo - 1) * pageSize
return offset + pageSize >= array.length
? array.slice(offset, array.length)
: array.slice(offset, offset + pageSize)
}
/**
* 关键词转换成可发送消息
* @param msg
* @returns
*/
async keyWordTran(msg) {
/** 图片 */
if (msg.includes('{image')) {
let tmp = msg.split('{image')
if (tmp.length > 2) return false
let md5 = tmp[1].replace(/}|_|:/g, '')
msg = segment.image(`http://gchat.qpic.cn/gchatpic_new/0/0-0-${md5}/0`)
msg.asface = true
} else if (msg.includes('{at:')) {
let tmp = msg.match(/{at:(.+?)}/g)
for (let qq of tmp) {
qq = qq.match(/[1-9][0-9]{4,14}/g)[0]
let member = await await this.e.bot
.getGroupMemberInfo(this.group_id, Number(qq))
.catch(() => {})
let name = member?.card ?? member?.nickname
if (!name) continue
msg = msg.replace(`{at:${qq}}`, `@${name}`)
}
} else if (msg.includes('{face')) {
let tmp = msg.match(/{face(:|_)(.+?)}/g)
if (!tmp) return msg
msg = []
for (let face of tmp) {
let id = face.match(/\d+/g)
msg.push(segment.face(id))
}
}
return msg
}
/**
*
* @returns
*/
async faceDetail() {
if (!this.e.message) return false
this.isGlobal = false
await this.getGroupId()
if (!this.group_id) return false
let faceDetailReg = /^#+(全局)?(?:查看|查询)(?:表情|词条)(.+)$/
let regGroup = faceDetailReg.exec(this.e.msg)
let keyWord
if (regGroup[1]) {
this.isGlobal = true
}
keyWord = regGroup[2].trim()
if (keyWord === '') return
this.initTextArr()
this.initGlobalTextArr()
let faces = textArr[this.group_id].get(keyWord) || []
let globalfaces = textArr[this.e.bot.uin].get(keyWord) || []
faces = [...faces, ...globalfaces]
if (lodash.isEmpty(faces)) {
await this.e.reply(`表情${keyWord}不存在`)
return
}
// process faces into replyArr in type:
let replyArr = []
for (let i = 0; i < faces.length; i++) {
let face = faces[i]
let faceItem = face[0]
let fromUser = faceItem?.from_user
if (fromUser) {
fromUser = `添加者: ${fromUser.card}(${fromUser.nickname})[${fromUser.user_id}]`
} else {
fromUser = '未知'
}
let faceContent
console.log(faceItem)
if (faceItem.type === 'image') {
// face is an image
let tmp = segment.image(faceItem.local)
tmp.asface = faceItem.asface
faceContent = tmp
replyArr.push(`${i + 1}${fromUser}`)
replyArr.push(faceContent)
} else {
faceContent = `${faceItem.text}`
replyArr.push(`${i + 1}${fromUser}: ` + faceContent)
}
}
if (lodash.isEmpty(replyArr)) {
return
}
let forwardMsg = await makeForwardMsg(
this.e,
replyArr,
`表情${keyWord}详情`
)
this.e.reply(forwardMsg)
}
}