koishi-plugin-smmcat-delsetu
Version:
481 lines (474 loc) • 18.3 kB
JavaScript
var __create = Object.create;
var __defProp = Object.defineProperty;
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
var __getOwnPropNames = Object.getOwnPropertyNames;
var __getProtoOf = Object.getPrototypeOf;
var __hasOwnProp = Object.prototype.hasOwnProperty;
var __name = (target, value) => __defProp(target, "name", { value, configurable: true });
var __export = (target, all) => {
for (var name2 in all)
__defProp(target, name2, { get: all[name2], enumerable: true });
};
var __copyProps = (to, from, except, desc) => {
if (from && typeof from === "object" || typeof from === "function") {
for (let key of __getOwnPropNames(from))
if (!__hasOwnProp.call(to, key) && key !== except)
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
}
return to;
};
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
// If the importer is in node compatibility mode or this is not an ESM
// file that has been converted to a CommonJS file using a Babel-
// compatible transform (i.e. "__esModule" has not been set), then set
// "default" to the CommonJS "module.exports" for node compatibility.
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
mod
));
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
// src/index.ts
var src_exports = {};
__export(src_exports, {
Config: () => Config,
apply: () => apply,
name: () => name,
usage: () => usage
});
module.exports = __toCommonJS(src_exports);
var import_koishi = require("koishi");
var import_path = __toESM(require("path"));
var import_fs = __toESM(require("fs"));
var import_crypto = __toESM(require("crypto"));
var import_node_stream = __toESM(require("node:stream"));
var name = "smmcat-delsetu";
var Config = import_koishi.Schema.object({
useFree: import_koishi.Schema.boolean().default(false).description("无appid&key,使用免费的nsfw接口 (开启后不支持filter选项)"),
maxSEX: import_koishi.Schema.number().default(0.8).description("免费的nsfw接口配置: 最色评级 (允许禁言)"),
SEX: import_koishi.Schema.number().default(0.7).description("免费的nsfw接口配置: 色评级"),
poorSEX: import_koishi.Schema.number().default(0.6).description("免费的nsfw接口配置: 一般色评级"),
Appid: import_koishi.Schema.string().description("Api-Appid [加群申请](https://qm.qq.com/q/Ghom0pXQYK)"),
key: import_koishi.Schema.string().description("密钥"),
detectionGoal: import_koishi.Schema.array(String).role("table").description("要检测的目标群"),
filter: import_koishi.Schema.array(String).role("table").default([
"ACGPorn",
"ButtocksExposed",
"WomenSexyChest",
"WomenSexy",
"ACGSexy",
"SexualGoods",
"Porn",
"PornSum",
"Sexy"
]).description("要检测的词条"),
isAllGoal: import_koishi.Schema.boolean().default(false).description("监控所有群 (慎用)"),
useTempByMd5: import_koishi.Schema.boolean().default(true).description("开启Md5校验缓存(节约接口调用并提高效率)"),
dontAsk: import_koishi.Schema.boolean().default(true).description("处理后不做任何通知"),
isDel: import_koishi.Schema.boolean().default(true).description("是否撤回"),
shutMouth: import_koishi.Schema.boolean().default(true).description("是否禁言"),
muteTime: import_koishi.Schema.number().default(6e4).description("禁言时长(毫秒)"),
deBug: import_koishi.Schema.boolean().default(false).description("查看日志报错"),
allAsk: import_koishi.Schema.boolean().default(false).description("每条图片均提示"),
isDownload: import_koishi.Schema.boolean().default(false).description("违规图片自动下载到本地"),
downloadPath: import_koishi.Schema.string().default("./data/delsetu/").description("图片保存位置")
});
var usage = `
词条信息:
|输入词|描述|
|:-|:-|
|ACGPorn|动漫色情|
|ButtocksExposed|臀部暴露|
|WomenSexyChest|性感女人|
|WomenSexy|性暗示、性感|
|ACGSexy|动漫性暗示|
|SexualGoods|性用品展示|
|Porn|色情|
|PornSum|色情描写|
|Sexy|性感|
|Gamble|赌博|
|Terror|恐怖|
|WomenSexyLeg|女性性感的腿|
从 1.7 版本开始,提供免费的 nsfw 接口。供用户选择:
***
**nsfwjs** 免费,低效率,容易误检测
**腾讯云不良内容识别** 收费,高精度,低延迟
***
反色图插件由于作者资源刷爆,从新版本开始需要收费使用。每次调用均需携带签名。
费用预计一张 \`0.0020\` 元
`;
function apply(ctx, config) {
const logs = {};
const freeApi = {
async checkImgToDo(session, next) {
if (!config.isAllGoal && !config.detectionGoal.includes(session.guildId)) {
return next();
}
const img = import_koishi.h.select(session.elements, "img");
if (!img.length) {
return next();
}
const imgUrl = img.map((item) => {
return item.attrs.src;
});
const result = await this.auditPic(imgUrl);
if (result.code) {
config.isDel && session.bot.deleteMessage(session.channelId, session.messageId);
config.shutMouth && session.bot.muteGuildMember(session.guildId, session.userId, config.muteTime);
config.isDownload && !result.repeat && downloadImage(imgUrl);
}
const msg = result.data.filter((item) => item.msg).map((item) => item.msg).join(",");
session.send(msg);
},
async auditPic(imgList) {
try {
const temp = { code: false, msg: "", data: [] };
let index = 0;
for (const img of imgList) {
try {
index++;
config.deBug && console.log(`检测第${index}个图`);
let md5 = "";
if (config.useTempByMd5) {
md5 = await freeTool.getPicMd5(img);
const cache = await freeTool.checkPicRepeatDyMD5(md5);
if (cache != null) {
temp.data.push({ ...cache, repeat: true, msg: `[图${index}] 已经检测过,不做描述` });
continue;
}
}
const result = await ctx.http.post("https://tools.mgtv100.com/external/v1/pear/pornImage", { img_url: img });
let code = false;
let msg = "";
if (Number(result.data.data.hentai) > config.maxSEX) {
temp.code = true;
code = true;
msg = `[图${index}] 太sao了!`;
} else if (Number(result.data.data.hentai) > config.SEX) {
temp.code = true;
code = true;
msg = `[图${index}] 好se`;
} else if (Number(result.data.data.hentai) > config.poorSEX) {
config.allAsk && (msg = `[图${index}] 一般`);
} else {
config.allAsk && (msg = `[图${index}] 健全`);
}
config.deBug && console.log(result.data.data);
config.deBug && console.log(`结果:${msg}`);
temp.data.push({ code, msg, data: result.data.data });
if (config.useTempByMd5) {
freeTool.putPicMd5TempData(md5, { code, msg, data: result.data.data });
}
if (Number(result.data.data.hentai) > 0.8) break;
} catch (error) {
break;
}
}
return temp;
} catch (error) {
console.log(error);
return { code: true, msg: "未知错误", data: [] };
}
}
};
async function downloadImage(imageUrl, upath = import_path.default.join(ctx.baseDir, config.downloadPath)) {
if (!import_fs.default.existsSync(upath)) {
import_fs.default.mkdirSync(upath, { recursive: true });
}
const timestamp = (/* @__PURE__ */ new Date()).getTime();
const imagePath = import_path.default.join(upath, `${timestamp}.jpg`);
const response = await ctx.http.get(imageUrl, { responseType: "stream" });
const writer = import_fs.default.createWriteStream(imagePath);
const responseNodeStream = import_node_stream.default.Readable.fromWeb(response);
responseNodeStream.pipe(writer);
await new Promise((resolve, reject) => {
writer.on("finish", () => {
config.deBug && console.log(`下载完成,文件路径 ${imagePath}`);
resolve;
});
writer.on("error", reject);
});
}
__name(downloadImage, "downloadImage");
function checkIsPass(dictList) {
config.deBug && console.log(dictList.some((item) => {
return dictMsg.includes(item.Scene);
}));
return dictList.some((item) => {
return dictMsg.includes(item.Scene);
});
}
__name(checkIsPass, "checkIsPass");
const dictMsg = config.filter;
const mapMsg = {
"ACGPorn": "动漫色情",
"ButtocksExposed": "臀部暴露",
"WomenSexyChest": "性感女人",
"WomenSexy": "性暗示、性感",
"ACGSexy": "动漫性暗示",
"SexualGoods": "性用品展示",
"Porn": "色情",
"PornSum": "色情描写",
"Sexy": "性感",
"Gamble": "赌博",
"Terror": "恐怖",
"WomenSexyLeg": "女性性感的腿"
};
async function checkImgToDo(session, next) {
if (!config.isAllGoal && !config.detectionGoal.includes(session.guildId)) {
return next();
}
const img = import_koishi.h.select(session.elements, "img");
if (!img.length) {
return next();
}
const imgUrl = img.map((item) => {
return item.attrs.src;
});
try {
let result = null;
let isPass = true;
if (imgUrl.length == 1) {
let md5Code = "";
let tempData = null;
let repeat = false;
if (config.useTempByMd5) {
md5Code = await tool.getPicMd5(imgUrl[0]);
tempData = tool.checkPicRepeatDyMD5(md5Code);
}
if (!tempData) {
result = await ctx.http.post(`https://tools.mgtv100.com/external/v1/qcloud_content_audit`, {
audit_type: "image",
audit_content: imgUrl[0]
}, {
headers: getSignature()
});
config.useTempByMd5 && tool.putPicMd5TempData(md5Code, result);
} else {
repeat = true;
result = tempData;
}
config.deBug && console.log(result);
const label = result.data.LabelResults?.map((item) => {
if (item.Suggestion != "Pass") {
return { Scene: item.Scene, Score: item.Score };
} else {
return null;
}
}).filter((item) => item !== null);
config.deBug && console.log(label);
const type = checkIsPass(label);
if (type || config.allAsk) {
isPass = !type;
await markMassage(isPass, label, session, imgUrl[0], repeat);
}
} else {
let repeat = false;
const eventList = imgUrl.map((item) => {
return new Promise(async (resolve, reject) => {
try {
let md5Code = "";
let tempData = null;
if (config.useTempByMd5) {
md5Code = await tool.getPicMd5(item);
tempData = tool.checkPicRepeatDyMD5(md5Code);
}
if (!tempData) {
result = await ctx.http.post(`https://tools.mgtv100.com/external/v1/qcloud_content_audit`, {
audit_type: "image",
audit_content: item
}, {
headers: getSignature()
});
config.useTempByMd5 && tool.putPicMd5TempData(md5Code, result);
resolve([result, item]);
} else {
repeat = true;
resolve([tempData, item]);
}
} catch (error) {
config.deBug && console.log(error);
resolve({});
}
});
});
for (let i = 0; i < eventList.length; i++) {
try {
const res = await eventList[i];
result = res[0];
config.deBug && console.log("查找问题图片" + result.data.Suggestion);
const label = result.data.LabelResults?.map((item) => {
if (item.Suggestion != "Pass") {
return { Scene: item.Scene, Score: item.Score };
} else {
return null;
}
}).filter((item) => item !== null);
config.deBug && console.log(label);
const type = checkIsPass(label);
if (type || config.allAsk) {
result = result;
isPass = !type;
config.deBug && console.log("找到问题,跳出循环");
await markMassage(isPass, label, session, res[1], repeat);
return;
}
} catch (error) {
config.deBug && console.log(error);
}
}
}
} catch (error) {
config.deBug && console.log(error);
}
async function markMassage(isPass, label, session2, imgurl, repeat) {
if (!isPass) {
config.isDel && session2.bot.deleteMessage(session2.channelId, session2.messageId);
config.shutMouth && session2.bot.muteGuildMember(session2.guildId, session2.userId, config.muteTime);
config.isDownload && !repeat && downloadImage(imgUrl);
}
config.deBug && console.log(JSON.stringify(label));
!config.dontAsk && session2.send(
import_koishi.h.at(session2.userId) + `状态:${isPass ? "通过" : "不通过"}
信息:` + (label.length ? label.map((item) => mapMsg[item.Scene]).join("、") : "无") + (label.length ? `
得分:` + label.map((item) => item.Score).join("、") : "")
);
}
__name(markMassage, "markMassage");
}
__name(checkImgToDo, "checkImgToDo");
ctx.on("ready", () => {
getAppkeyResidualCredit();
});
async function getAppkeyResidualCredit() {
const res = await ctx.http.post("https://tools.mgtv100.com/external/v1/qcloud_content_audit/search", {
app_id: config.Appid,
secret_key: config.key
});
const result = res.data;
return `当前密钥剩余可用次数:${result.available_count - result.request_count}`;
}
__name(getAppkeyResidualCredit, "getAppkeyResidualCredit");
function getSignature() {
function generateHmacSha256(key2, data) {
const hmac = import_crypto.default.createHmac("sha256", key2);
hmac.update(data);
const hash = hmac.digest("hex");
return hash;
}
__name(generateHmacSha256, "generateHmacSha256");
const apiId = config.Appid;
const key = config.key;
const time = Math.floor(+/* @__PURE__ */ new Date() / 1e3);
const queryKey = {
"Api-Appid": apiId,
"Api-Nonce-Str": "123456",
"Api-Timestamp": time,
"key": key
};
const ascllSortMap = Object.keys(queryKey).sort();
const strKey = ascllSortMap.map((item) => {
return `${item}=${queryKey[item]}`;
}).join("&");
config.deBug && console.log(strKey);
const keyData = generateHmacSha256(key, strKey).toUpperCase();
config.deBug && console.log(keyData);
return {
"Api-Appid": apiId,
"Api-Nonce-Str": "123456",
"Api-Timestamp": time,
"Api-Sign": keyData
};
}
__name(getSignature, "getSignature");
const freeTool = {
md5temp: {},
md5Len: [],
async getPicMd5(imageUrl) {
const response = await ctx.http.get(imageUrl, { responseType: "arraybuffer" });
const hash = import_crypto.default.createHash("md5");
const buffer = hash.update(Buffer.from(response));
return buffer.digest("hex");
},
/** 校验图片是否存在MD5缓存 */
checkPicRepeatDyMD5(md5Data) {
if (this.md5temp[md5Data]) {
config.deBug && console.log("存在重复图片 返回缓存");
return this.md5temp[md5Data];
}
return null;
},
/** 将返回的结果存进MD5缓存 */
putPicMd5TempData(md5Data, result) {
if (!this.md5temp[md5Data]) {
config.deBug && console.log("新图 开始存入缓存");
this.md5temp[md5Data] = result;
this.tempFullToDelect();
config.deBug && console.log("新图 完成存入缓存");
}
},
// 超过约束的数量自动清理
tempFullToDelect() {
if (this.md5Len.length > 300) {
config.deBug && console.log("缓存超过约束长度,执行清理");
const delMd5 = this.md5Len.shift();
delete this.md5temp[delMd5];
}
}
};
const tool = {
md5temp: {},
md5Len: [],
async getPicMd5(imageUrl) {
const response = await ctx.http.get(imageUrl, { responseType: "arraybuffer" });
const hash = import_crypto.default.createHash("md5");
const buffer = hash.update(Buffer.from(response));
return buffer.digest("hex");
},
// 校验图片是否存在MD5缓存
checkPicRepeatDyMD5(md5Data) {
if (this.md5temp[md5Data]) {
config.deBug && console.log("存在重复图片 返回缓存");
return this.md5temp[md5Data];
}
return null;
},
// 将返回的结果存进MD5缓存
putPicMd5TempData(md5Data, result) {
if (!this.md5temp[md5Data]) {
config.deBug && console.log("新图 开始存入缓存");
this.md5temp[md5Data] = result;
this.tempFullToDelect();
config.deBug && console.log("新图 完成存入缓存");
}
},
// 超过约束的数量自动清理
tempFullToDelect() {
if (this.md5Len.length > 300) {
config.deBug && console.log("缓存超过约束长度,执行清理");
const delMd5 = this.md5Len.shift();
delete this.md5temp[delMd5];
}
}
};
ctx.command("查询额度").action(async ({ session }) => {
return await getAppkeyResidualCredit();
});
ctx.middleware(async (session, next) => {
if (!session.guildId) {
return next();
}
if (config.useFree) {
freeApi.checkImgToDo(session, next);
} else {
checkImgToDo(session, next);
}
return next();
});
}
__name(apply, "apply");
// Annotate the CommonJS export names for ESM import in node:
0 && (module.exports = {
Config,
apply,
name,
usage
});