update: 修改

This commit is contained in:
ningmengchongshui 2024-06-11 21:03:42 +08:00
commit bc9cf29004
18 changed files with 2063 additions and 0 deletions

12
README.md Normal file
View File

@ -0,0 +1,12 @@
# System-Plugin
Miao-Yunzai V4 插件开发示例
## 使用教程
- 安装源码
```sh
git clone --depth=1 -b dev https://github.com/yoimiya-kokomi/Miao-Yunzai.git
```

18
apps.ts Normal file
View File

@ -0,0 +1,18 @@
/**
* ***********
*
* *****
*/
export * from './apps/add'
export * from './apps/disFriPoke'
export * from './apps/disablePrivate'
export * from './apps/friend'
export * from './apps/invite'
export * from './apps/quit'
export * from './apps/restart'
export * from './apps/sendLog'
export * from './apps/status'
export * from './apps/update'
export * from './apps/example2'
export * from './apps/newcomer'
export * from './apps/outNotice'

966
apps/add.ts Normal file
View File

@ -0,0 +1,966 @@
import fs from 'node:fs'
import lodash from 'lodash'
import { pipeline } from 'stream'
import { promisify } from 'util'
import fetch from 'node-fetch'
import moment from 'moment'
import { ConfigController as cfg } from '#miao/config'
import { plugin } from '#miao/core'
import * as common from '#miao/core'
const textArr = {}
export class add extends plugin {
/**
*
*/
path = './data/textJson/'
/**
*
*/
facePath = './data/face/'
/**
*
*/
isGlobal = false
/**
*
*/
constructor() {
/**
name: '添加表情',
dsc: '添加表情,文字等',
*/
super({
event: 'message',
priority: 50000,
});
/**
* rule
*/
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 init() {
if (!fs.existsSync(this.path)) {
fs.mkdirSync(this.path)
}
if (!fs.existsSync(this.facePath)) {
fs.mkdirSync(this.facePath)
}
}
/**
*
*/
async accept() {
/** 处理消息 */
if (this.e.atBot && this.e.msg && this.e?.msg.includes('添加') && !this.e?.msg.includes('#')) {
this.e.msg = '#' + this.e.msg
}
}
/**
*
*/
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 = 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() {
let 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
}
fs.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;
}
fs.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 (!fs.existsSync(savePath)) {
fs.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 (fs.existsSync(`${savePath}${keyWord}.${type}`)) {
keyWord = `${keyWord}_${moment().format('X')}`
}
savePath = `${savePath}${keyWord}.${type}`
const streamPipeline = promisify(pipeline)
await streamPipeline(response.body, fs.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 (fs.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 (!fs.existsSync(path)) {
return
}
try {
let text = JSON.parse(fs.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 (fs.existsSync(facePath)) {
const files = fs.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 {
fs.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 (!fs.existsSync(globalPath)) {
return;
}
try {
let text = JSON.parse(fs.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 (fs.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 {
fs.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) {
fs.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 common.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 common.makeForwardMsg(this.e, replyArr, `表情${keyWord}详情`)
this.e.reply(forwardMsg)
}
}

25
apps/disFriPoke.ts Normal file
View File

@ -0,0 +1,25 @@
import { ConfigController as cfg } from '#miao/config'
import { plugin } from '#miao/core'
/**
*
*/
export class disFriPoke extends plugin {
constructor() {
super({
name: '禁止私聊',
dsc: '对私聊禁用做处理当开启私聊禁用时只接收cookie以及抽卡链接',
event: 'notice.friend.poke'
})
this.priority = 0
}
async accept() {
if (!cfg.other?.disablePrivate) return
if (this.e.isMaster) return
this.e.reply(cfg.other.disableMsg)
return 'return'
}
}

79
apps/disablePrivate.ts Normal file
View File

@ -0,0 +1,79 @@
import { ConfigController as cfg } from '#miao/config'
import { plugin } from '#miao/core'
/**
*
*/
export class disPri extends plugin {
/**
*
*/
constructor() {
/**
name: '禁止私聊',
dsc: '对私聊禁用做处理当开启私聊禁用时只接收cookie以及抽卡链接',
*/
super({
event: 'message.private'
})
/**
*
*/
this.priority = 0
}
/**
*
* @returns
*/
async accept() {
if (!cfg.other?.disablePrivate) return
if (this.e.isMaster) return
/** 发送日志文件xlsxjson */
if (this.e.file) {
if (!/(.*)\.txt|xlsx|json/ig.test(this.e.file?.name)) {
this.sendTips()
return 'return'
} else {
return false
}
}
/** 绑定ck抽卡链接 */
let wordReg = /(.*)(ltoken|_MHYUUID|authkey=)(.*)|导出记录(json)*|(记录|安卓|苹果|ck|cookie|体力)帮助|^帮助$|^#*(删除|我的)ck$|^#(我的)?(uid|UID)[0-9]{0,2}$/g
/** 自定义通行字符 */
let disableAdopt = cfg.other?.disableAdopt
if (!Array.isArray(disableAdopt)) {
disableAdopt = []
}
disableAdopt = disableAdopt.filter(str => str != null && str !== '');
let disableReg = `(.*)(${disableAdopt.join('|')})(.*)`
if (this.e.raw_message) {
if (!new RegExp(wordReg).test(this.e.raw_message) && (disableAdopt.length === 0 || !new RegExp(disableReg).test(this.e.raw_message))) {
this.sendTips()
return 'return'
}
}
}
/**
*
* @returns
*/
async sendTips() {
/** 冷却cd 10s */
let cd = 10
if (this.e.user_id == cfg.qq) return
/** cd */
let key = `Yz:disablePrivate:${this.e.user_id}`
if (await redis.get(key)) return
this.e.reply(cfg.other.disableMsg)
redis.setEx(key, cd, '1')
}
}

40
apps/example2.ts Normal file
View File

@ -0,0 +1,40 @@
import { plugin } from '#miao/core'
/**
*
*/
export class example2 extends plugin {
constructor () {
/**
name: '复读',
dsc: '复读用户发送的内容,然后撤回',
*/
super({
event: 'message',
priority: 5000,
rule: [
{
reg: '^#复读$',
fnc: 'repeat'
}
]
})
}
/**
*
*/
async repeat () {
/** 设置上下文后续接收到内容会执行doRep方法 */
this.setContext('doRep')
/** 回复 */
await this.reply('请发送要复读的内容', false, { at: true })
}
/**
*
*/
doRep () {
/** 复读内容 */
this.reply(this.e.message, false, { recallMsg: 5 })
/** 结束上下文 */
this.finish('doRep')
}
}

40
apps/friend.ts Normal file
View File

@ -0,0 +1,40 @@
import { ConfigController as cfg } from '#miao/config'
import { sleep } from '#miao/utils'
import { plugin } from '#miao/core'
/**
*
*/
export class friend extends plugin {
/**
*
*/
constructor() {
/**
*
name: 'autoFriend',
dsc: '自动同意好友',
*/
super({
event: 'request.friend'
})
}
/**
*
*/
async accept() {
/**
*
*/
if (this.e.sub_type == 'add' || this.e.sub_type == 'single') {
/**
*
*/
if (cfg.other.autoFriend == 1) {
logger.mark(`[自动同意][添加好友] ${this.e.user_id}`)
await sleep(2000)
this.e.approve(true)
}
}
}
}

36
apps/invite.ts Normal file
View File

@ -0,0 +1,36 @@
import { ConfigController as cfg } from '#miao/config'
import { plugin } from '#miao/core'
/**
*
*/
export class invite extends plugin {
/**
*
*/
constructor() {
/**
*
name: 'invite',
dsc: '主人邀请自动进群',
*/
super({
event: 'request.group.invite'
})
}
/**
*
* @returns
*/
async accept() {
if (!cfg.masterQQ || !cfg.masterQQ.includes(String(this.e.user_id))) {
logger.mark(`[邀请加群]${this.e.group_name}${this.e.group_id}`)
return
}
logger.mark(`[主人邀请加群]${this.e.group_name}${this.e.group_id}`)
this.e.approve(true)
this.e.bot.sendPrivateMsg(this.e.user_id, `已同意加群:${this.e.group_name}`).catch((err) => {
logger.error(err)
})
}
}

42
apps/newcomer.ts Normal file
View File

@ -0,0 +1,42 @@
import { plugin, segment } from '#miao/core'
/**
*
*/
export class newcomer extends plugin {
/**
*
*/
constructor() {
/**
name: '欢迎新人',
dsc: '新人入群欢迎',
*/
super({
event: 'notice.group.increase',
priority: 5000
})
}
/**
*
* @returns
*/
async accept() {
/** 定义入群欢迎内容 */
let msg = '欢迎新人!'
/** 冷却cd 30s */
let cd = 30
if (this.e.user_id == this.e.bot.uin) return
/** cd */
let key = `Yz:newcomers:${this.e.group_id}`
if (await redis.get(key)) return
redis.set(key, '1', { EX: cd })
/** 回复 */
await this.reply([
segment.at(this.e.user_id),
// segment.image(),
msg
])
}
}

31
apps/outNotice.ts Normal file
View File

@ -0,0 +1,31 @@
import { plugin } from '#miao/core'
export class outNotice extends plugin {
tips = '退群了'
constructor() {
/**
name: '退群通知',
dsc: 'xx退群了',
*/
super({
event: 'notice.group.decrease'
})
}
/**
*
* @returns
*/
async accept() {
if (this.e.user_id == this.e.bot.uin) return
let name = null, msg = null
if (this.e.member) {
name = this.e.member.card || this.e.member.nickname
}
if (name) {
msg = `${name}(${this.e.user_id}) ${this.tips}`
} else {
msg = `${this.e.user_id} ${this.tips}`
}
logger.mark(`[退出通知]${this.e.logText} ${msg}`)
await this.reply(msg)
}
}

66
apps/quit.ts Normal file
View File

@ -0,0 +1,66 @@
import { ConfigController as cfg } from '#miao/config'
import { plugin } from '#miao/core'
/**
*
*/
export class quit extends plugin {
/**
*
*/
constructor() {
/**
name: 'notice',
dsc: '自动退群',
*/
super({
event: 'notice.group.increase'
})
}
/**
*
* @returns
*/
async accept() {
if (this.e.user_id != this.e.bot.uin) return
/**
*
*/
let other = cfg.other
/**
*
*/
if (other.autoQuit <= 0) return
/**
* 退
*/
let gl = await this.e.group.getMemberMap()
for (let qq of cfg.masterQQ) {
if (gl.has(Number(qq))) {
logger.mark(`[主人拉群] ${this.e.group_id}`)
return
}
}
/**
* 退
*/
if (Array.from(gl).length <= other.autoQuit && !this.e.group.is_owner) {
/**
*
*/
await this.e.reply('禁止拉群,已自动退出')
/**
*
*/
logger.mark(`[自动退群] ${this.e.group_id}`)
/**
*
*/
setTimeout(() => {
/**
*
*/
this.e.group.quit()
}, 2000)
}
}
}

187
apps/restart.ts Normal file
View File

@ -0,0 +1,187 @@
import { plugin } from '#miao/core'
import fetch from 'node-fetch'
import net from 'net'
import fs from 'fs'
import YAML from 'yaml'
import { exec } from 'child_process'
/**
*
* @param port
* @returns
*/
const isPortTaken = async (port) => {
return new Promise((resolve) => {
const tester = net.createServer()
.once('error', () => resolve(true))
.once('listening', () => tester.once('close', () => resolve(false)).close())
.listen(port);
});
};
/**
*
*/
export class Restart extends plugin {
key = 'Yz:restart'
/**
*
* @param e
*/
constructor() {
/**
name: '重启',
dsc: '#重启',
*/
super({
event: 'message',
priority: 10,
rule: [{
reg: '^#重启$',
fnc: 'restart',
permission: 'master'
}, {
reg: '^#(停机|关机)$',
fnc: 'stop',
permission: 'master'
}]
})
}
async init() {
let restart = await redis.get(this.key)
if (restart) {
restart = JSON.parse(restart)
const uin = restart?.uin || Bot.uin
let time = restart.time || new Date().getTime()
time = (new Date().getTime() - time) / 1000
let msg = `重启成功:耗时${time.toFixed(2)}`
try {
if (restart.isGroup) {
Bot[uin].pickGroup(restart.id).sendMsg(msg)
} else {
Bot[uin].pickUser(restart.id).sendMsg(msg)
}
} catch (error) {
/** 不发了,发不出去... */
logger.debug(error)
}
redis.del(this.key)
}
}
async restart() {
let restart_port
try {
restart_port = YAML.parse(fs.readFileSync(`./config/config/bot.yaml`, `utf-8`))
restart_port = restart_port.restart_port || 27881
} catch { }
await this.e.reply('开始执行重启,请稍等...')
logger.mark(`${this.e.logFnc} 开始执行重启,请稍等...`)
let data = JSON.stringify({
uin: this.e?.self_id || this.e.bot.uin,
isGroup: !!this.e.isGroup,
id: this.e.isGroup ? this.e.group_id : this.e.user_id,
time: new Date().getTime()
})
let npm = await this.checkPnpm()
await redis.set(this.key, data, { EX: 120 })
if (await isPortTaken(restart_port || 27881)) {
try {
let result = await fetch(`http://localhost:${restart_port || 27881}/restart`)
result = await result.text()
if (result !== `OK`) {
redis.del(this.key)
this.e.reply(`操作失败!`)
logger.error(`重启失败`)
}
} catch (error) {
redis.del(this.key)
this.e.reply(`操作失败!\n${error}`)
}
} else {
try {
let cm = `${npm} start`
if (process.argv[1].includes('pm2')) {
cm = `${npm} run restart`
}
exec(cm, { windowsHide: true }, (error, stdout, stderr) => {
if (error) {
redis.del(this.key)
this.e.reply(`操作失败!\n${error.stack}`)
logger.error(`重启失败\n${error.stack}`)
} else if (stdout) {
logger.mark('重启成功,运行已由前台转为后台')
logger.mark(`查看日志请用命令:${npm} run log`)
logger.mark(`停止后台运行命令:${npm} stop`)
process.exit()
}
})
} catch (error) {
redis.del(this.key)
let e = error.stack ?? error
this.e.reply(`操作失败!\n${e}`)
}
}
return true
}
async checkPnpm() {
let npm = 'npm'
let ret = await this.execSync('pnpm -v')
if (ret.stdout) npm = 'pnpm'
return npm
}
async execSync(cmd) {
return new Promise((resolve, reject) => {
exec(cmd, { windowsHide: true }, (error, stdout, stderr) => {
resolve({ error, stdout, stderr })
})
})
}
async stop() {
let restart_port
try {
restart_port = YAML.parse(fs.readFileSync(`./config/config/bot.yaml`, `utf-8`))
restart_port = restart_port.restart_port || 27881
} catch { }
if (await isPortTaken(restart_port || 27881)) {
try {
logger.mark('关机成功,已停止运行')
await this.e.reply(`关机成功,已停止运行`)
await fetch(`http://localhost:${restart_port || 27881}/exit`)
return
} catch (error) {
this.e.reply(`操作失败!\n${error}`)
logger.error(`关机失败\n${error}`)
}
}
if (!process.argv[1].includes('pm2')) {
logger.mark('关机成功,已停止运行')
await this.e.reply('关机成功,已停止运行')
process.exit()
}
logger.mark('关机成功,已停止运行')
await this.e.reply('关机成功,已停止运行')
let npm = await this.checkPnpm()
exec(`${npm} stop`, { windowsHide: true }, (error, stdout, stderr) => {
if (error) {
this.e.reply(`操作失败!\n${error.stack}`)
logger.error(`关机失败\n${error.stack}`)
}
})
}
}

91
apps/sendLog.ts Normal file
View File

@ -0,0 +1,91 @@
import { plugin } from '#miao/core'
import {makeForwardMsg} from '#miao/core'
import fs from "node:fs"
import lodash from "lodash"
import moment from "moment"
/**
*
*/
export class sendLog extends plugin {
lineNum = 100
maxNum = 1000
errFile = "logs/error.log"
logFile = `logs/command.${moment().format("YYYY-MM-DD")}.log`
constructor() {
/**
name: "发送日志",
dsc: "发送最近100条运行日志",
*
*/
super({
event: "message",
rule: [
{
reg: "^#(运行|错误)*日志[0-9]*(.*)",
fnc: "sendLog",
permission: "master"
}
]
})
}
/**
*
* @returns
*/
async sendLog() {
let lineNum = this.e.msg.match(/\d+/g)
if (lineNum) {
this.lineNum = lineNum[0]
} else {
this.keyWord = this.e.msg.replace(/#|运行|错误|日志|\d/g, "")
}
let logFile = this.logFile
let type = "运行"
if (this.e.msg.includes("错误")) {
logFile = this.errFile
type = "错误"
}
if (this.keyWord) type = this.keyWord
const log = this.getLog(logFile)
if (lodash.isEmpty(log))
return this.reply(`暂无相关日志:${type}`)
return this.reply(await makeForwardMsg(this.e, [log.join("\n")], `最近${log.length}${type}日志`))
}
/**
*
* @param logFile
* @returns
*/
getLog(logFile) {
let log = fs.readFileSync(logFile, { encoding: "utf-8" })
log = log.split("\n")
if (this.keyWord) {
for (const i in log)
if (!log[i].includes(this.keyWord))
delete log[i]
} else {
log = lodash.slice(log, (Number(this.lineNum) + 1) * -1)
}
log = log.reverse()
const tmp = []
for (let i of log) {
if (!i) continue
if (this.keyWord && tmp.length >= this.maxNum) return
/* eslint-disable no-control-regex */
i = i.replace(/\x1b[[0-9;]*m/g, "")
i = i.replace(/\r|\n/, "")
tmp.push(i)
}
return tmp
}
}

141
apps/status.ts Normal file
View File

@ -0,0 +1,141 @@
import { ConfigController as cfg } from '#miao/config'
import moment from 'moment'
import { plugin } from '#miao/core'
/**
*
*/
export class status extends plugin {
/**
name: '其他功能',
dsc: '#状态',
*/
constructor() {
super({
event: 'message',
rule: [
{
reg: '^#状态$',
fnc: 'status'
}
]
})
}
/**
*
* @returns
*/
async status() {
if (this.e.isMaster) return this.statusMaster()
if (!this.e.isGroup) {
this.reply('请群聊查看')
return
}
return this.statusGroup()
}
async statusMaster() {
let runTime = moment().diff(moment.unix(this.e.bot.stat.start_time), 'seconds')
let Day = Math.floor(runTime / 3600 / 24)
let Hour = Math.floor((runTime / 3600) % 24)
let Min = Math.floor((runTime / 60) % 60)
if (Day > 0) {
runTime = `${Day}${Hour}小时${Min}分钟`
} else {
runTime = `${Hour}小时${Min}分钟`
}
let format = (bytes) => {
return (bytes / 1024 / 1024).toFixed(2) + 'MB'
}
let msg = '-------状态-------'
msg += `\n运行时间${runTime}`
msg += `\n内存使用${format(process.memoryUsage().rss)}`
msg += `\n当前版本v${cfg.package.version}`
msg += '\n-------累计-------'
msg += await this.getCount()
await this.reply(msg)
}
async statusGroup() {
let msg = '-------状态-------'
msg += await this.getCount(this.e.group_id)
await this.reply(msg)
}
async getCount(groupId = '') {
this.date = moment().format('MMDD')
this.month = Number(moment().month()) + 1
this.key = 'Yz:count:'
if (groupId) {
this.key += `group:${groupId}:`
}
this.msgKey = {
day: `${this.key}sendMsg:day:`,
month: `${this.key}sendMsg:month:`
}
this.screenshotKey = {
day: `${this.key}screenshot:day:`,
month: `${this.key}screenshot:month:`
}
let week = {
msg: 0,
screenshot: 0
}
for (let i = 0; i <= 6; i++) {
let date = moment().startOf('week').add(i, 'days').format('MMDD')
week.msg += Number(await redis.get(`${this.msgKey.day}${date}`)) ?? 0
week.screenshot += Number(await redis.get(`${this.screenshotKey.day}${date}`)) ?? 0
}
let count = {
total: {
msg: await redis.get(`${this.key}sendMsg:total`) || 0,
screenshot: await redis.get(`${this.key}screenshot:total`) || 0
},
today: {
msg: await redis.get(`${this.msgKey.day}${this.date}`) || 0,
screenshot: await redis.get(`${this.screenshotKey.day}${this.date}`) || 0
},
week,
month: {
msg: await redis.get(`${this.msgKey.month}${this.month}`) || 0,
screenshot: await redis.get(`${this.screenshotKey.month}${this.month}`) || 0
}
}
let msg = ''
if (groupId) {
msg = `\n发送消息${count.today.msg}`
msg += `\n生成图片${count.today.screenshot}`
} else {
msg = `\n发送消息${count.total.msg}`
msg += `\n生成图片${count.total.screenshot}`
}
if (count.month.msg > 200) {
msg += '\n-------本周-------'
msg += `\n发送消息${count.week.msg}`
msg += `\n生成图片${count.week.screenshot}`
}
if (moment().format('D') >= 8 && count.month.msg > 400) {
msg += '\n-------本月-------'
msg += `\n发送消息${count.month.msg}`
msg += `\n生成图片${count.month.screenshot}`
}
return msg
}
}

266
apps/update.ts Normal file
View File

@ -0,0 +1,266 @@
import { makeForwardMsg, plugin } from '#miao/core'
import lodash from 'lodash'
import fs from 'node:fs'
import { Restart } from './restart.js'
import {} from '#miao/core'
import { sleep } from '#miao/utils'
import { exec, execSync } from 'child_process'
import { BOT_NAME } from '#miao/config'
let uping = false
export class update extends plugin {
typeName = BOT_NAME
messages = []
constructor() {
/**
name: '更新',
dsc: '#更新 #强制更新',
*/
super({
event: 'message',
priority: 4000,
rule: [
{
reg: '^#更新日志',
fnc: 'updateLog'
},
{
reg: '^#(强制)?更新',
fnc: 'update'
},
{
reg: '^#(静默)?全部(强制)?更新$',
fnc: 'updateAll',
permission: 'master'
}
]
})
}
async update() {
if (!this.e.isMaster) return false
if (uping) return this.reply('已有命令更新中..请勿重复操作')
if (/详细|详情|面板|面版/.test(this.e.msg)) return false
/** 获取插件 */
let plugin = this.getPlugin()
if (plugin === false) return false
/** 执行更新 */
if (plugin === '') {
await this.runUpdate('')
await sleep(1000)
plugin = this.getPlugin('miao-plugin')
await this.runUpdate(plugin)
} else {
await this.runUpdate(plugin)
}
/** 是否需要重启 */
if (this.isUp) {
// await this.reply('即将执行重启,以应用更新')
setTimeout(() => this.restart(), 2000)
}
}
getPlugin(plugin = '') {
if (!plugin) {
plugin = this.e.msg.replace(/#(强制)?更新(日志)?/, '')
if (!plugin) return ''
}
if (!fs.existsSync(`plugins/${plugin}/.git`)) return false
this.typeName = plugin
return plugin
}
async execSync(cmd) {
return new Promise((resolve, reject) => {
exec(cmd, { windowsHide: true }, (error, stdout, stderr) => {
resolve({ error, stdout, stderr })
})
})
}
async runUpdate(plugin = '') {
this.isNowUp = false
let cm = 'git pull --no-rebase'
let type = '更新'
if (this.e.msg.includes('强制')) {
type = '强制更新'
cm = `git reset --hard && git pull --rebase --allow-unrelated-histories`
}
if (plugin) cm = `cd "plugins/${plugin}" && ${cm}`
this.oldCommitId = await this.getcommitId(plugin)
logger.mark(`${this.e.logFnc} 开始${type}${this.typeName}`)
await this.reply(`开始${type} ${this.typeName}`)
uping = true
const ret = await this.execSync(cm)
uping = false
if (ret.error) {
logger.mark(`${this.e.logFnc} 更新失败:${this.typeName}`)
this.gitErr(ret.error, ret.stdout)
return false
}
const time = await this.getTime(plugin)
if (/Already up|已经是最新/g.test(ret.stdout)) {
await this.reply(`${this.typeName} 已是最新\n最后更新时间${time}`)
} else {
await this.reply(`${this.typeName} 更新成功\n更新时间${time}`)
this.isUp = true
await this.reply(await this.getLog(plugin))
}
logger.mark(`${this.e.logFnc} 最后更新时间:${time}`)
return true
}
async getcommitId(plugin = '') {
let cm = 'git rev-parse --short HEAD'
if (plugin) cm = `cd "plugins/${plugin}" && ${cm}`
const commitId = await execSync(cm, { encoding: 'utf-8' })
return lodash.trim(commitId)
}
async getTime(plugin = '') {
let cm = 'git log -1 --pretty=%cd --date=format:"%F %T"'
if (plugin) cm = `cd "plugins/${plugin}" && ${cm}`
let time = ''
try {
time = await execSync(cm, { encoding: 'utf-8' })
time = lodash.trim(time)
} catch (error) {
logger.error(error.toString())
time = '获取时间失败'
}
return time
}
async gitErr(err, stdout) {
const msg = '更新失败!'
const errMsg = err.toString()
stdout = stdout.toString()
if (errMsg.includes('Timed out')) {
const remote = errMsg.match(/'(.+?)'/g)[0].replace(/'/g, '')
return this.reply(`${msg}\n连接超时${remote}`)
}
if (/Failed to connect|unable to access/g.test(errMsg)) {
const remote = errMsg.match(/'(.+?)'/g)[0].replace(/'/g, '')
return this.reply(`${msg}\n连接失败${remote}`)
}
if (errMsg.includes('be overwritten by merge')) {
return this.reply(`${msg}\n存在冲突\n${errMsg}\n请解决冲突后再更新或者执行#强制更新,放弃本地修改`)
}
if (stdout.includes('CONFLICT')) {
return this.reply(`${msg}\n存在冲突\n${errMsg}${stdout}\n请解决冲突后再更新或者执行#强制更新,放弃本地修改`)
}
return this.reply([errMsg, stdout])
}
async updateAll() {
const dirs = fs.readdirSync('./plugins/')
const originalReply = this.reply
const testReg = /^#静默全部(强制)?更新$/.test(this.e.msg)
if (testReg) {
await this.reply(`开始执行静默全部更新,请稍等...`)
this.reply = (message) => {
this.messages.push(message)
}
}
await this.runUpdate()
for (let plu of dirs) {
plu = this.getPlugin(plu)
if (plu === false) continue
await sleep(1500)
await this.runUpdate(plu)
}
if (testReg) {
await this.reply(await makeForwardMsg(this.e, this.messages))
}
if (this.isUp) {
// await this.reply('即将执行重启,以应用更新')
setTimeout(() => this.restart(), 2000)
}
this.reply = originalReply
}
restart() {
new Restart(this.e).restart()
}
async getLog(plugin = '') {
let cm = 'git log -100 --pretty="%h||[%cd] %s" --date=format:"%F %T"'
if (plugin) cm = `cd "plugins/${plugin}" && ${cm}`
let logAll
try {
logAll = await execSync(cm, { encoding: 'utf-8' })
} catch (error) {
logger.error(error.toString())
await this.reply(error.toString())
}
if (!logAll) return false
logAll = logAll.trim().split('\n')
let log = []
for (let str of logAll) {
str = str.split('||')
if (str[0] == this.oldCommitId) break
if (str[1].includes('Merge branch')) continue
log.push(str[1])
}
let line = log.length
log = log.join('\n\n')
if (log.length <= 0) return ''
let end = ''
try {
cm = 'git config -l'
if (plugin) cm = `cd "plugins/${plugin}" && ${cm}`
end = await execSync(cm, { encoding: 'utf-8' })
end = end.match(/remote\..*\.url=.+/g).join('\n\n').replace(/remote\..*\.url=/g, '').replace(/\/\/([^@]+)@/, '//')
} catch (error) {
logger.error(error.toString())
await this.reply(error.toString())
}
return makeForwardMsg(this.e, [log, end], `${plugin || 'Miao-Yunzai'} 更新日志,共${line}`)
}
async updateLog() {
const plugin = this.getPlugin()
if (plugin === false) return false
return this.reply(await this.getLog(plugin))
}
}

10
deploy.sh Normal file
View File

@ -0,0 +1,10 @@
#!/usr/bin/env sh
# 确保脚本抛出遇到的错误
set -e
git init
git add -A
git commit -m 'update: 修改'
git push -f git@github.com:yoimiya-kokomi/Miao-Yunzai.git master:system

1
index.ts Normal file
View File

@ -0,0 +1 @@
export * as apps from './apps.js'

12
package.json Normal file
View File

@ -0,0 +1,12 @@
{
"name": "system-plugin",
"version": "1.0.0-rc.0",
"author": "Yoimiya-Kokomi, Le-niao",
"description": "QQ Group Bot",
"main": "./index.js",
"private":true,
"type": "module",
"scripts": {},
"dependencies": {},
"devDependencies": {}
}