diff --git a/apps/auth-set.js b/apps/auth-set.js new file mode 100644 index 0000000..1f82a6a --- /dev/null +++ b/apps/auth-set.js @@ -0,0 +1,95 @@ +import configControl from '../lib/config/configControl.js'; + +export class carbonAuthSetting extends plugin { + constructor() { + super({ + name: 'carbonAuth-setting', + dsc: '手性碳验证设置', + event: 'message.group', + priority: -1000, + rule: [ + { reg: '^#开启验证$', fnc: 'enableAuth' }, + { reg: '^#关闭验证$', fnc: 'disableAuth' }, + { reg: '^#切换验证模式$', fnc: 'switchMode' }, + { reg: '^#设置验证(提示|困难)模式(开启|关闭)$', fnc: 'setCarbonMode' }, + { reg: '^#设置验证次数(\\d+)$', fnc: 'setFrequency' }, + { reg: '^#设置撤回(开启|关闭)$', fnc: 'setRecall' }, + ], + }); + } + + //获取奇妙的配置 + async _getCfg(e) { + const cfg = (await configControl.get('auth')) || {}; + const groupCfg = cfg.groups[e.group_id] || JSON.parse(JSON.stringify(cfg.default)); + return { cfg, groupCfg }; + } + + //保存奇妙的配置 + async _saveCfg(e, cfg, groupCfg) { + cfg.groups[e.group_id] = groupCfg; + await configControl.set('auth', cfg); + } + + //在制定群开启验证 + async enableAuth(e) { + if (!(e.sender.role === 'owner' || e.sender.role === 'admin' || e.isMaster)) + return e.reply('只有群主或管理员可以设置验证..', true); + const { cfg, groupCfg } = await this._getCfg(e); + groupCfg.enable = true; + await this._saveCfg(e, cfg, groupCfg); + return e.reply('本群已开启入群验证,验证模式为数字验证..', true); + } + + async disableAuth(e) { + if (!(e.sender.role === 'owner' || e.sender.role === 'admin' || e.isMaster)) + return e.reply('只有群主或管理员可以设置验证..', true); + const { cfg, groupCfg } = await this._getCfg(e); + groupCfg.enable = false; + await this._saveCfg(e, cfg, groupCfg); + return e.reply('已关闭本群新人验证..', true); + } + + async switchMode(e) { + if (!(e.sender.role === 'owner' || e.sender.role === 'admin' || e.isMaster)) + return e.reply('只有群主或管理员可以设置验证..', true); + const { cfg, groupCfg } = await this._getCfg(e); + groupCfg.carbon.enable = !groupCfg.carbon.enable; + await this._saveCfg(e, cfg, groupCfg); + return e.reply( + groupCfg.carbon.enable ? '已切换为手性碳验证模式..' : '已切换为数字验证模式..', + true + ); + } + + async setCarbonMode(e) { + if (!(e.sender.role === 'owner' || e.sender.role === 'admin' || e.isMaster)) + return e.reply('只有群主或管理员可以设置验证..', true); + const [, type, state] = e.msg.match(/^#设置验证(提示|困难)模式(开启|关闭)$/); + const { cfg, groupCfg } = await this._getCfg(e); + if (type === '提示') groupCfg.carbon.hint = state === '开启'; + if (type === '困难') groupCfg.carbon['hard-mode'] = state === '开启'; + await this._saveCfg(e, cfg, groupCfg); + return e.reply(`已${state}手性碳${type}模式..`, true); + } + + async setFrequency(e) { + if (!(e.sender.role === 'owner' || e.sender.role === 'admin' || e.isMaster)) + return e.reply('只有群主或管理员可以设置验证..', true); + const [, num] = e.msg.match(/^#设置验证次数(\d+)$/); + const { cfg, groupCfg } = await this._getCfg(e); + groupCfg.frequency = parseInt(num); + await this._saveCfg(e, cfg, groupCfg); + return e.reply(`已将最大尝试次数设置为 ${num}..`, true); + } + + async setRecall(e) { + if (!(e.sender.role === 'owner' || e.sender.role === 'admin' || e.isMaster)) + return e.reply('只有群主或管理员可以设置验证..', true); + const [, state] = e.msg.match(/^#设置撤回(开启|关闭)$/); + const { cfg, groupCfg } = await this._getCfg(e); + groupCfg.recall = state === '开启'; + await this._saveCfg(e, cfg, groupCfg); + return e.reply(`已${state}错误回答自动撤回功能..`, true); + } +} diff --git a/apps/auth.js b/apps/auth.js new file mode 100644 index 0000000..236092a --- /dev/null +++ b/apps/auth.js @@ -0,0 +1,215 @@ +import configControl from '../lib/config/configControl.js'; +import axios from 'axios'; +import tools from '../components/tool.js'; +import Group from '../lib/yunzai/group.js'; +import Message from '../lib/yunzai/message.js'; + +export class CarbonAuth extends plugin { + constructor() { + super({ + name: 'carbon-auth', + dsc: '手性碳验证', + event: 'message.group', + priority: -114514, + rule: [ + { reg: '^#绕过验证([\\s\\S]*)?$', fnc: 'cmdBypass' }, + { reg: '^#重新验证([\\s\\S]*)?$', fnc: 'cmdRevalidate' }, + ], + }); + this.pending = new Map(); + + //答案监听 + Bot.on?.('message.group', async (e) => { + const key = `${e.group_id}_${e.user_id}`; + //logger.info(key); + const session = this.pending.get(key); + if (!session) return; + session.tries++; + const { type, answer, tries, cfg } = session; + + const pass = async () => { + this.pending.delete(key); + const redisKey = `Yz:pendingWelcome:${e.group_id}:${e.user_id}`; + const cached = await redis.get(redisKey); + if (cached) { + try { + const msgList = JSON.parse(cached); + await e.reply(msgList); + } finally { + await redis.del(redisKey); + } + } else { + return await e.reply('验证通过,欢迎加入本群~', true); + } + }; + + if (type === 'math') { + const msgStr = (e.message || []) + .filter((m) => m.type === 'text') + .map((m) => m.text) + .join('') + .trim(); + const num = parseInt(msgStr, 10); + if (!isNaN(num) && num === answer) return pass(); + if (tries >= cfg.frequency) { + this.pending.delete(key); + if (cfg.recall) await Message.deleteMsg(e, e.message_id); + e.reply([segment.at(e.user_id), '验证失败,你错太多次辣!'], true); + return await Group.groupKick(e, e.user_id, e.group_id, false); + } + if (cfg.recall) await Message.deleteMsg(e, e.message_id); + return e.reply( + [segment.at(e.user_id), `回答错了呢,你还有${cfg.frequency - tries}次机会,再试试看?`], + true + ); + } + + if (type === 'carbon') { + const msgStr = (e.message || []) + .filter((m) => m.type === 'text') + .map((m) => m.text) + .join(''); + const msgRegions = msgStr + .toUpperCase() + .replace(/,/g, ',') + .split(',') + .map((s) => s.trim()) + .filter(Boolean); + + const rightRegions = answer.map((r) => r.toUpperCase()); + + let correct; + if (cfg.carbon['hard-mode']) { + correct = rightRegions.every((r) => msgRegions.includes(r)); + } else { + correct = rightRegions.some((r) => msgRegions.includes(r)); + } + + if (correct) return pass(); + if (tries >= cfg.frequency) { + if (cfg.recall) await Message.deleteMsg(e, e.message_id); + this.pending.delete(key); + e.reply([segment.at(e.user_id), '验证失败,你错太多次辣!'], true); + return await Group.groupKick(e, e.user_id, e.group_id, false); + } + if (cfg.recall) await Message.deleteMsg(e, e.message_id); + return e.reply( + [segment.at(e.user_id), `回答错了呢,你还有${cfg.frequency - tries}次机会,再试试看?`], + true + ); + } + }); + + //主动退群 + Bot.on?.('notice.group.decrease', async (e) => { + const key = `${e.group_id}_${e.user_id}`; + if (this.pending.has(key)) { + this.pending.delete(key); + logger.mark(`[crystelf-plugin] 用户 ${e.user_id} 主动退群,验证流程结束..`); + e.reply('害,怎么跑路了'); + } + }); + + //加群事件 + Bot.on?.('notice.group.increase', async (e) => { + if (e.isMaster) return true; + await this.auth(e, e.group_id, e.user_id); + }); + } + + /** + * 验证 + * @param e 事件 + * @param group_id 群号 + * @param user_id 带验证用户id + * @returns {Promise<*>} + */ + async auth(e, group_id, user_id) { + const cfg = await configControl.get('auth'); + if (!cfg) return; + const groupCfg = cfg.groups[group_id] || cfg.default; + if (!groupCfg.enable) return; + const key = `${group_id}_${user_id}`; + + if (groupCfg.carbon.enable) { + try { + const res = await axios.post(`${cfg.url}/captcha/chiralCarbon/getChiralCarbonCaptcha`, { + answer: true, + hint: groupCfg.carbon.hint, + }); + if (!res.data?.data?.data) return e.reply('获取验证图失败,请稍后重试..'); + const { base64, regions } = res.data.data.data; + const regionCount = regions.length; + this.pending.set(key, { type: 'carbon', answer: regions, tries: 0, cfg: groupCfg }); + e.reply([ + segment.at(user_id), + segment.image(base64), + `上图中有一块或多块区域含有手性碳原子\n为了加入本群,你需要在${groupCfg.timeout}秒内正确找出${groupCfg.carbon['hard-mode'] ? '全部含有手性碳的区域' : '其中任意一块包含手性碳的区域'}\n回答的话,直接回复区域代号即可,多个区域用逗号隔开\n提示一下,本图共有${regionCount}块手性碳区域噢..`, + ]); + } catch (err) { + logger.error('[crystelf-plugin] 请求手性碳验证API失败..', err); + } + } else { + await tools.sleep(500); + const a = Math.floor(Math.random() * 100); + const b = Math.floor(Math.random() * 100); + const op = Math.random() > 0.5 ? '+' : '-'; + const ans = op === '+' ? a + b : a - b; + this.pending.set(key, { type: 'math', answer: ans, tries: 0, cfg: groupCfg }); + e.reply([segment.at(user_id), `请在${groupCfg.timeout}秒内发送${a} ${op} ${b}的计算结果..`]); + } + + if (groupCfg.timeout > 60) { + setTimeout( + async () => { + if (this.pending.has(key)) { + await e.reply([segment.at(user_id), `小朋友,你还有1分钟的时间完成验证噢~`]); + } + }, + (groupCfg.timeout - 60) * 1000 + ); + } + + setTimeout(async () => { + if (this.pending.has(key)) { + this.pending.delete(key); + await e.reply([segment.at(user_id), `小朋友,验证超时啦!请重新申请入群~`]); + await Group.groupKick(e, e.user_id, e.group_id, false); + } + }, groupCfg.timeout * 1000); + } + + async cmdBypass(e) { + if (!(e.sender && (e.sender.role === 'owner' || e.sender.role === 'admin' || e.isMaster))) { + return e.reply('只有群主或管理员可以使用此命令..', true); + } + const atElem = (e.message || []).find((m) => m.type === 'at'); + if (!atElem || !atElem.qq) return e.reply('你想绕过谁?', true); + const targetId = Number(atElem.qq); + const groupId = e.group_id; + const key = `${groupId}_${targetId}`; + if (this.pending.has(key)) this.pending.delete(key); + const redisKey = `Yz:pendingWelcome:${groupId}:${targetId}`; + const cached = await redis.get(redisKey); + if (cached) { + try { + const msgList = JSON.parse(cached); + await e.reply(msgList); + } finally { + await redis.del(redisKey); + } + } else { + return await e.reply([segment.at(targetId), '欢迎加入本群~'], true); + } + } + + async cmdRevalidate(e) { + if (!(e.sender && (e.sender.role === 'owner' || e.sender.role === 'admin' || e.isMaster))) { + return e.reply('只有群主或管理员可以使用此命令..', true); + } + let atElem = (e.message || []).find((m) => m.type === 'at'); + if (!atElem || !atElem.qq) return e.reply('你要验证谁?', true); + const targetId = Number(atElem.qq); + await this.auth(e, e.group_id, targetId); + } +} diff --git a/apps/poke.js b/apps/poke.js index 97c9fd7..be018c4 100644 --- a/apps/poke.js +++ b/apps/poke.js @@ -3,6 +3,7 @@ import tool from '../components/tool.js'; import axios from 'axios'; import configControl from '../lib/config/configControl.js'; import ConfigControl from '../lib/config/configControl.js'; +import Group from '../lib/yunzai/group.js'; export default class ChuochuoPlugin extends plugin { constructor() { @@ -51,13 +52,7 @@ async function pokeMaster(e) { async function masterPoke(e) { logger.info(`跟主人一起戳!`); - if (e.target_id !== e.uin) { - await e.bot.sendApi('group_poke', { - group_id: e.group_id, - user_id: e.target_id, - }); - } - return true; + if (e.target_id !== e.uin) await Group.groupPoke(e, e.target_id, e.group_id); } async function handleBotPoke(e) { @@ -75,10 +70,11 @@ async function handleBotPoke(e) { await e.reply(res.data.data, false, 110); if (Math.random() < replyPoke) { await tool.sleep(1000); - await e.bot.sendApi('group_poke', { group_id: e.group_id, user_id: e.operator_id }); + return await Group.groupPoke(e, e.operator_id, e.group_id); } + return true; } else { - await e.reply( + return await e.reply( `戳一戳出错了!${configControl.get('profile')?.nickName}不知道该说啥好了..`, false, { recallMsg: 60 } @@ -86,7 +82,7 @@ async function handleBotPoke(e) { } } catch (err) { logger.error('戳一戳请求失败', err); - await e.reply( + return await e.reply( `戳一戳出错了!${configControl.get('profile')?.nickName}不知道该说啥好了..`, false, { recallMsg: 60 } diff --git a/apps/welcome.js b/apps/welcome.js index 32341bf..a5f8a69 100644 --- a/apps/welcome.js +++ b/apps/welcome.js @@ -1,4 +1,5 @@ import configControl from '../lib/config/configControl.js'; +import tools from '../components/tool.js'; export class welcomeNewcomer extends plugin { constructor() { @@ -10,25 +11,31 @@ export class welcomeNewcomer extends plugin { }); } - /** - * 新人入群欢迎 - * @returns {Promise} - */ async accept(e) { try { + await tools.sleep(600); if (e.user_id === e.self_id) return; const groupId = e.group_id; const cdKey = `Yz:newcomers:${groupId}`; if (await redis.get(cdKey)) return; await redis.set(cdKey, '1', { EX: 30 }); - const allCfg = configControl.get('newcomer') || {}; - const cfg = allCfg[groupId] || {}; + const newcomerCfg = (await configControl.get('newcomer')) || {}; + const welcomeCfg = newcomerCfg[groupId] || {}; + const authCfg = await configControl.get('auth'); + const groupAuthCfg = authCfg?.groups?.[groupId] || authCfg?.default || {}; const msgList = [segment.at(e.user_id)]; - if (cfg.text) msgList.push(cfg.text); - if (cfg.image) msgList.push(segment.image(cfg.image)); - if (!cfg.text && !cfg.image) msgList.push('欢迎新人~!'); + if (welcomeCfg.text) msgList.push(welcomeCfg.text); + if (welcomeCfg.image) msgList.push(segment.image(welcomeCfg.image)); + if (!welcomeCfg.text && !welcomeCfg.image) msgList.push('欢迎新人~!'); + if (groupAuthCfg?.enable) { + // 缓存欢迎消息 + const redisKey = `Yz:pendingWelcome:${groupId}:${e.user_id}`; + await redis.set(redisKey, JSON.stringify(msgList), { EX: 300 }); + return; + } + // 未开启验证 await e.reply(msgList); - } catch (e) { + } catch (err) { return e.reply('加群欢迎出现错误,请重新设置加群欢迎', true); } } diff --git a/config/auth.json b/config/auth.json new file mode 100644 index 0000000..07b01ef --- /dev/null +++ b/config/auth.json @@ -0,0 +1,16 @@ +{ + "url": "https://carbon.crystelf.top", + "default": { + "enable": false, + "carbon": { + "enable": false, + "hint": true, + "hard-mode": false + }, + "timeout": 180, + "recall": true, + "frequency": 5 + }, + "groups": { + } +} \ No newline at end of file diff --git a/config/config.json b/config/config.json index d52606c..553240b 100644 --- a/config/config.json +++ b/config/config.json @@ -2,7 +2,6 @@ "debug": true, "core": true, "maxFeed": 10, - "adapter": "lgr", "poke": true, "60s": true, "fanqie": true, diff --git a/lib/yunzai/group.js b/lib/yunzai/group.js index 5a96950..ea80c94 100644 --- a/lib/yunzai/group.js +++ b/lib/yunzai/group.js @@ -1,16 +1,32 @@ -import ConfigControl from '../config/configControl.js'; +const Group = { + /** + * 群戳一戳 + * @param e + * @param user_id 被戳的用户 + * @param group_id 群号 + * @returns {Promise<*>} + */ + async groupPoke(e, user_id, group_id) { + return await e.bot.sendApi('group_poke', { + group_id: group_id, + user_id: user_id, + }); + }, -class NapcatGroup {} -class LgrGroup {} - -async function getGroupAdapter() { - const adapter = (await ConfigControl.get('config'))?.adapter; - if (!adapter || adapter === 'nc' || adapter === 'napcat') { - return new NapcatGroup(); - } else if (adapter === 'lgr' || adapter === 'lagrange') { - return new LgrGroup(); - } - return new NapcatGroup(); -} - -export default await getGroupAdapter(); + /** + * 群踢人 + * @param e + * @param user_id 要踢的人 + * @param group_id 群号 + * @param ban 是否允许再次加群 + * @returns {Promise<*>} + */ + async groupKick(e, user_id, group_id, ban) { + return await e.bot.sendApi('set_group_kick', { + user_id: user_id, + group_id: group_id, + reject_add_request: ban, + }); + }, +}; +export default Group; diff --git a/lib/yunzai/message.js b/lib/yunzai/message.js index 798e1dd..2801d1d 100644 --- a/lib/yunzai/message.js +++ b/lib/yunzai/message.js @@ -1,16 +1,14 @@ -import ConfigControl from '../config/configControl.js'; - -class NapcatMessage {} -class LgrMessage {} - -async function getMessageAdapter() { - const adapter = (await ConfigControl.get('config'))?.adapter; - if (!adapter || adapter === 'nc' || adapter === 'napcat') { - return new NapcatMessage(); - } else if (adapter === 'lgr' || adapter === 'lagrange') { - return new LgrMessage(); - } - return new NapcatMessage(); -} - -export default await getMessageAdapter(); +const Message = { + /** + * 群撤回消息 + * @param e + * @param message_id 消息id + * @returns {Promise<*>} + */ + async deleteMsg(e, message_id) { + return await e.bot.sendApi('delete_msg', { + message_id: message_id, + }); + }, +}; +export default Message; diff --git a/lib/yunzai/self.js b/lib/yunzai/self.js index 8cca86a..7e2d6d0 100644 --- a/lib/yunzai/self.js +++ b/lib/yunzai/self.js @@ -1,16 +1,3 @@ -import ConfigControl from '../config/configControl.js'; +const Self = {}; -class NapcatSelf {} -class LgrSelf {} - -async function getSelfAdapter() { - const adapter = (await ConfigControl.get('config'))?.adapter; - if (!adapter || adapter === 'nc' || adapter === 'napcat') { - return new NapcatSelf(); - } else if (adapter === 'lgr' || adapter === 'lagrange') { - return new LgrSelf(); - } - return new NapcatSelf(); -} - -export default await getSelfAdapter(); +export default Self;