diff --git a/README.md b/README.md index 697bc4c8..5be03c70 100644 --- a/README.md +++ b/README.md @@ -26,7 +26,7 @@ | [Mrs4s/go-cqhttp](https://github.com/Mrs4s/go-cqhttp) | [MiraiGo](https://github.com/Mrs4s/MiraiGo) | Mrs4s | | [yyuueexxiinngg/cqhttp-mirai](https://github.com/yyuueexxiinngg/cqhttp-mirai) | [Mirai](https://github.com/mamoe/mirai) | yyuueexxiinngg | | [takayama-lily/onebot](https://github.com/takayama-lily/onebot) | [OICQ](https://github.com/takayama-lily/oicq) | takayama | - + > 如果您不知道什么是 [OneBot](https://github.com/howmanybots/onebot) 或不希望运行多个程序,还可以直接前往 [gocqzbp](https://github.com/FloatTech/gocqzbp) 的 [Release](https://github.com/FloatTech/gocqzbp/releases) 页面下载单一可执行文件或前往 [Packages](https://github.com/FloatTech/gocqzbp/pkgs/container/gocqzbp) 页面使用`docker`,运行后按提示登录即可。 @@ -403,6 +403,51 @@ print("run[CQ:image,file="+j["img"]+"]") - [x] 百度下[xxx] +
+ 百度内容审核 + + `import _ "github.com/FloatTech/ZeroBot-Plugin/plugin/baiduaudit"` + + - [x] 获取BDAkey + + - [x] 配置BDAKey [API Key] [Secret Key] + + - [x] 获取BDAkey + + - [x] [开启|关闭]内容审核 + + - [x] [开启|关闭]撤回提示 + + - [x] [开启|关闭]详细提示 + + - [x] [开启|关闭]撤回禁言 + + - [x] [开启|关闭]禁言累加 + + - [x] [开启|关闭]文本检测 + + - [x] [开启|关闭]图像检测 + + - [x] 设置最大禁言时间[分钟,默认:60,最大43200] + + - [x] 设置每次累加时间[分钟,默认:1] + + - [x] 设置撤回禁言时间[分钟,默认:1] + + - [x] 查看检测类型 + + - [x] 查看检测配置 + + - [x] 测试文本检测[文本内容] + + - [x] 测试图像检测[图片] + + - [x] 设置检测类型[类型编号] + + - [x] 设置不检测类型[类型编号] + + 检测类型编号列表:[1:违禁违规|2:文本色情|3:敏感信息|4:恶意推广|5:低俗辱骂|6:恶意推广-联系方式|7:恶意推广-软文推广] +
base64卦加解密 @@ -1245,9 +1290,7 @@ print("run[CQ:image,file="+j["img"]+"]") - [x] @Bot 任意文本(任意一句话回复) - [x] 设置回复模式[青云客 | 小爱] -
- ## 三种使用方法,推荐第一种 ### 1. 使用稳定版/测试版 (推荐) diff --git a/go.mod b/go.mod index 72863f00..79e98559 100644 --- a/go.mod +++ b/go.mod @@ -3,6 +3,7 @@ module github.com/FloatTech/ZeroBot-Plugin go 1.19 require ( + github.com/Baidu-AIP/golang-sdk v1.1.1 github.com/Coloured-glaze/gg v1.3.4 github.com/FloatTech/AnimeAPI v1.5.2-0.20221023084913-bd1ff35e91ed github.com/FloatTech/floatbox v0.0.0-20221011153549-68005767c531 diff --git a/go.sum b/go.sum index 0862375e..dffcc155 100644 --- a/go.sum +++ b/go.sum @@ -1,3 +1,5 @@ +github.com/Baidu-AIP/golang-sdk v1.1.1 h1:RQsAmgDSAkiq22I6n7XJ2t3afgzFeqjY46FGhvrx4cw= +github.com/Baidu-AIP/golang-sdk v1.1.1/go.mod h1:bXnGw7xPeKt8aF7UCELKrV6UZ/46spItONK1RQBQj1Y= github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= github.com/Coloured-glaze/gg v1.3.4 h1:l31zIF/HaVwkzjrj+A56RGQoSKyKuR1IWtIrqXGFStI= github.com/Coloured-glaze/gg v1.3.4/go.mod h1:Ih5NLNNDHOy3RJbB0EPqGTreIzq/H02TGThIagh8HJg= diff --git a/main.go b/main.go index 3f164925..cce18534 100644 --- a/main.go +++ b/main.go @@ -65,6 +65,7 @@ import ( _ "github.com/FloatTech/ZeroBot-Plugin/plugin/alipayvoice" // 支付宝到账语音 _ "github.com/FloatTech/ZeroBot-Plugin/plugin/b14" // base16384加解密 _ "github.com/FloatTech/ZeroBot-Plugin/plugin/baidu" // 百度一下 + _ "github.com/FloatTech/ZeroBot-Plugin/plugin/baiduaudit" // 百度内容审核 _ "github.com/FloatTech/ZeroBot-Plugin/plugin/base64gua" // base64卦加解密 _ "github.com/FloatTech/ZeroBot-Plugin/plugin/baseamasiro" // base天城文加解密 _ "github.com/FloatTech/ZeroBot-Plugin/plugin/bilibili" // b站相关 diff --git a/plugin/baiduaudit/audit.go b/plugin/baiduaudit/audit.go new file mode 100644 index 00000000..93004079 --- /dev/null +++ b/plugin/baiduaudit/audit.go @@ -0,0 +1,500 @@ +// Package baiduaudit 百度内容审核 +package baiduaudit + +import ( + "encoding/json" + "fmt" + "github.com/Baidu-AIP/golang-sdk/aip/censor" + "github.com/FloatTech/floatbox/binary" + "github.com/FloatTech/floatbox/file" + ctrl "github.com/FloatTech/zbpctrl" + "github.com/FloatTech/zbputils/control" + "github.com/FloatTech/zbputils/img/text" + zero "github.com/wdvxdr1123/ZeroBot" + "github.com/wdvxdr1123/ZeroBot/message" + "os" + "strconv" +) + +// 服务网址:https://console.bce.baidu.com/ai/?_=1665977657185#/ai/antiporn/overview/index +// 返回参数说明:https://cloud.baidu.com/doc/ANTIPORN/s/Nk3h6xbb2 +type baiduRes struct { + LogID int `json:"log_id"` // 请求唯一id + Conclusion string `json:"conclusion"` // 审核结果,可取值:合规、不合规、疑似、审核失败 + ConclusionType int `json:"conclusionType"` // 审核结果类型,可取值1.合规,2.不合规,3.疑似,4.审核失败 + Data []auditData `json:"data"` + ErrorCode int `json:"error_code"` // 错误提示码,失败才返回,成功不返回 + ErrorMsg string `json:"error_msg"` // 错误提示信息,失败才返回,成功不返回 +} + +type auditData struct { + Type int `json:"type"` // 审核主类型,11:百度官方违禁词库、12:文本反作弊、13:自定义文本黑名单、14:自定义文本白名单 + SubType int `json:"subType"` // 审核子类型,0:含多种类型,具体看官方链接,1:违禁违规、2:文本色情、3:敏感信息、4:恶意推广、5:低俗辱骂 6:恶意推广-联系方式、7:恶意推广-软文推广 + Conclusion string `json:"conclusion"` // 审核结果,可取值:合规、不合规、疑似、审核失败 + ConclusionType int `json:"conclusionType"` // 审核结果类型,可取值1.合规,2.不合规,3.疑似,4.审核失败 + Msg string `json:"msg"` // 不合规项描述信息 + Hits []hit `json:"hits"` +} // 不合规/疑似/命中白名单项详细信息。响应成功并且conclusion为疑似或不合规或命中白名单时才返回,响应失败或conclusion为合规且未命中白名单时不返回。 + +type hit struct { + DatasetName string `json:"datasetName"` // 违规项目所属数据集名称 + Words []string `json:"words"` // 送检文本命中词库的关键词(备注:建议参考新字段“wordHitPositions”,包含信息更丰富:关键词以及对应的位置及标签信息) + Probability float64 `json:"probability,omitempty"` // 不合规项置信度 +} // 送检文本违规原因的详细信息 +type keyConfig struct { + Key1 string `json:"key1"` // 百度云服务内容审核key存储 + Key2 string `json:"key2"` // 百度云服务内容审核key存储 + Groups map[int64]group `json:"groups"` // 群配置存储 +} + +type group struct { + Enable EnableMark // 是否启用内容审核 + TextAudit EnableMark // 文本检测 + ImageAudit EnableMark // 图像检测 + DMRemind EnableMark // 撤回提示 + MoreRemind EnableMark // 详细违规提示 + DMBAN EnableMark // 撤回后禁言 + BANTimeAddEnable EnableMark // 禁言累加 + BANTime int64 // 标准禁言时间,禁用累加,但开启禁言的的情况下采用该值 + MaxBANTimeAddRange int64 // 最大禁言时间累加范围,最高禁言时间 + BANTimeAddTime int64 // 禁言累加时间,该值是开启禁累加功能后,再次触发时,根据被禁次数X该值计算出的禁言时间 + WhiteListType [8]bool // 类型白名单,处于白名单类型的违规,不会被触发 0:含多种类型,具体看官方链接,1:违禁违规、2:文本色情、3:敏感信息、4:恶意推广、5:低俗辱骂 6:恶意推广-联系方式、7:恶意推广-软文推广 + AuditHistory map[int64]auditHistory // 被封禁用户列表 +} + +// EnableMark 启用:●,禁用:○ +type EnableMark bool + +// String 打印启用状态 +func (em EnableMark) String() string { + if em { + return "开启" + } + return "关闭" +} + +type auditHistory struct { + Count int64 `json:"key2"` // 被禁次数 + ResList []baiduRes `json:"reslist"` // 禁言原因 +} + +var bdcli *censor.ContentCensorClient // 百度云审核服务Client +var typetext = [8]string{ + 0: "默认违禁词库", + 1: "违禁违规", + 2: "文本色情", + 3: "敏感信息", + 4: "恶意推广", + 5: "低俗辱骂", + 6: "恶意推广-联系方式", + 7: "恶意推广-软文推广", +} // 文本类型 + +var ( + config keyConfig // 插件配置 + configinit bool // 配置初始化 + configpath string // 配置路径 +) + +func init() { + engine := control.Register("baiduaudit", &ctrl.Options[*zero.Ctx]{ + DisableOnDefault: false, + Help: "##该功能来自百度内容审核,需购买相关服务,并创建app##\n" + + "- 获取BDAKey\n" + + "- 配置BDAKey [API key] [Secret Key]\n" + + "- 开启/关闭内容审核\n" + + "- 开启/关闭撤回提示\n" + + "- 开启/关闭详细提示\n" + + "- 开启/关闭撤回禁言\n" + + "##禁言时间设置## 禁言时间计算方式为:禁言次数*每次禁言累加时间,当达到最大禁言时间时,再次触发按最大禁言时间计算\n" + + "- 开启/关闭禁言累加\n" + + "- 设置撤回禁言时间[分钟,默认:1]\n" + + "- 设置最大禁言时间[分钟,默认:60,最大43200]\n" + + "- 设置每次累加时间[分钟,默认:1]\n" + + "##检测类型设置## 类型编号列表:[1:违禁违规、2:文本色情、3:敏感信息、4:恶意推广、5:低俗辱骂 6:恶意推广-联系方式、7:恶意推广-软文推广]\n" + + "- 查看检测类型\n" + + "- 查看检测配置\n" + + "- 设置检测类型[类型编号]\n" + + "- 设置不检测类型[类型编号]\n" + + "- 开启/关闭文本检测\n" + + "- 开启/关闭图像检测\n" + + "##测试功能##\n" + + "- 测试文本检测[文本内容]\n" + + "- 测试图像检测[图片]\n", + PrivateDataFolder: "baiduaudit", + }) + configpath = engine.DataFolder() + "config.json" + loadConfig() + if configinit { + bdcli = censor.NewClient(config.Key1, config.Key2) + } + engine.OnFullMatch("获取BDAKey", zero.SuperUserPermission).SetBlock(true). + Handle(func(ctx *zero.Ctx) { + ctx.SendChain(message.Text("接口key创建网址:\n" + + "https://console.bce.baidu.com/ai/?_=1665977657185#/ai/antiporn/overview/index\n" + + "免费8w次数领取地址:\n" + + "https://console.bce.baidu.com/ai/?_=1665977657185#/ai/antiporn/overview/resource/getFree")) + }) + + engine.OnRegex("^查看检测(类型|配置)$", zero.AdminPermission, clientCheck).SetBlock(true). + Handle(func(ctx *zero.Ctx) { + // 获取群配置 + group := getGroup(ctx.Event.GroupID) + var msgs string + k1 := ctx.State["regex_matched"].([]string)[1] + if k1 == "类型" { + msgs += "本群检测类型:" + find := false + // 遍历群检测类型名单 + for i, v := range group.WhiteListType { + if !v { + find = true + msgs += fmt.Sprint("\n", i, ".", typetext[i]) + } + } + if !find { + msgs += "无" + } + + } else { + // 生成配置文本 + msgs = fmt.Sprintf("本群配置:\n"+ + "内容审核:%s\n"+ + "-文本:%s\n"+ + "-图像:%s\n"+ + "撤回提示:%s\n"+ + "-详细提示:%s\n"+ + "撤回禁言:%s\n"+ + "-禁言累加:%s\n"+ + "-撤回禁言时间:%v分钟\n"+ + "-每次累加时间:%v分钟\n"+ + "-最大禁言时间:%v分钟", group.Enable, group.TextAudit, group.ImageAudit, group.DMRemind, group.MoreRemind, group.DMBAN, group.BANTimeAddEnable, group.BANTime, group.BANTimeAddTime, group.MaxBANTimeAddRange) + } + b, err := text.RenderToBase64(msgs, text.FontFile, 300, 20) + if err != nil { + ctx.SendChain(message.Text("ERROR: ", err)) + return + } + ctx.SendChain(message.Image("base64://" + binary.BytesToString(b))) + }) + engine.OnRegex("^设置(不)?检测类型([01234567])$", zero.AdminPermission, clientCheck).SetBlock(true). + Handle(func(ctx *zero.Ctx) { + defer jsonSave(config, configpath) + k1 := ctx.State["regex_matched"].([]string)[1] + k2 := ctx.State["regex_matched"].([]string)[2] + group := getGroup(ctx.Event.GroupID) + inputType, _ := strconv.Atoi(k2) + if k1 == "不" { + group.WhiteListType[inputType] = true //不检测:则进入类型白名单 + } else { + group.WhiteListType[inputType] = false //检测:则退出白名单 + } + config.Groups[ctx.Event.GroupID] = group + ctx.SendChain(message.At(ctx.Event.UserID), message.Text(fmt.Sprintf("本群将%s检测%s类型内容", k1, typetext[inputType]))) + }) + engine.OnRegex("^设置(最大|每次|撤回)(累加|禁言)时间(\\d{1,5})$", zero.AdminPermission, clientCheck).SetBlock(true). + Handle(func(ctx *zero.Ctx) { + defer jsonSave(config, configpath) + k1 := ctx.State["regex_matched"].([]string)[1] + k3 := ctx.State["regex_matched"].([]string)[3] + group := getGroup(ctx.Event.GroupID) + time, _ := strconv.ParseInt(k1, 10, 64) + switch k1 { + case "最大": + group.MaxBANTimeAddRange = time + case "每次": + group.BANTimeAddTime = time + case "撤回": + group.BANTime = time + } + config.Groups[ctx.Event.GroupID] = group + ctx.SendChain(message.At(ctx.Event.UserID), message.Text(fmt.Sprintf("本群%s禁言累加时间已设置为%s", k3, k1))) + }) + engine.OnRegex("^(开启|关闭)(内容审核|撤回提示|撤回禁言|禁言累加|详细提示|文本检测|图像检测)$", zero.AdminPermission, clientCheck).SetBlock(true). + Handle(func(ctx *zero.Ctx) { + defer jsonSave(config, configpath) + k1 := ctx.State["regex_matched"].([]string)[1] + k2 := ctx.State["regex_matched"].([]string)[2] + isEnable := EnableMark(false) + group := getGroup(ctx.Event.GroupID) + if k1 == "开启" { + isEnable = true + } + switch k2 { + case "内容审核": + group.Enable = isEnable + case "撤回提示": + group.DMRemind = isEnable + case "撤回禁言": + group.DMBAN = isEnable + case "禁言累加": + group.BANTimeAddEnable = isEnable + case "详细提示": + group.MoreRemind = isEnable + case "文本检测": + group.TextAudit = isEnable + case "图像检测": + group.ImageAudit = isEnable + } + config.Groups[ctx.Event.GroupID] = group + ctx.SendChain(message.At(ctx.Event.UserID), message.Text(fmt.Sprintf("本群%s已%s", k2, k1))) + }) + engine.OnRegex(`^配置BDAKey\s*(.*)\s*(.*)$`, zero.SuperUserPermission).SetBlock(true). + Handle(func(ctx *zero.Ctx) { + k1 := ctx.State["regex_matched"].([]string)[1] + k2 := ctx.State["regex_matched"].([]string)[2] + bdcli = censor.NewClient(k1, k2) + config.Key1 = k1 + config.Key2 = k2 + if bdcli != nil { + jsonSave(config, configpath) + ctx.SendChain(message.Text("配置成功")) + } + }) + engine.OnMessage().SetBlock(false).Handle(func(ctx *zero.Ctx) { + group, ok := config.Groups[ctx.Event.GroupID] + // 如果没该配置,或者审核功能未开启直接跳过 + if !ok || !bool(group.Enable) { + return + } + for _, elem := range ctx.Event.Message { + switch elem.Type { + case "image": + if !group.ImageAudit { + return + } + res := bdcli.ImgCensorUrl(elem.Data["url"], nil) + bdres, err := jsonToBaiduRes(res) + if err != nil { + ctx.SendChain(message.Text("Error:", bdres.ErrorMsg, "(", bdres.ErrorCode, ")")) + return + } + banCheck(ctx, bdres) + + case "text": + if !group.TextAudit { + return + } + res := bdcli.TextCensor(elem.Data["text"]) + bdres, err := jsonToBaiduRes(res) + if err != nil { + ctx.SendChain(message.Text("Error:", bdres.ErrorMsg, "(", bdres.ErrorCode, ")")) + return + } + banCheck(ctx, bdres) + } + } + }) + engine.OnPrefix("文本检测", clientCheck).SetBlock(false). + Handle(func(ctx *zero.Ctx) { + if bdcli == nil { + ctx.SendChain(message.Text("Key未配置")) + return + } + args := ctx.ExtractPlainText() + res := bdcli.TextCensor(args) + bdres, err := jsonToBaiduRes(res) + if err != nil { + ctx.SendChain(message.Text("Error:", bdres.ErrorMsg, "(", bdres.ErrorCode, ")")) + return + } + group := getGroup(ctx.Event.GroupID) + ctx.SendChain(buildResp(bdres, group)...) + }) + engine.OnPrefix("^图像检测", clientCheck).SetBlock(false). + Handle(func(ctx *zero.Ctx) { + var urls []string + for _, elem := range ctx.Event.Message { + if elem.Type == "image" { + if elem.Data["url"] != "" { + urls = append(urls, elem.Data["url"]) + } + } + } + if len(urls) == 0 { + return + } + res := bdcli.ImgCensorUrl(urls[0], nil) + bdres, err := jsonToBaiduRes(res) + if err != nil { + ctx.SendChain(message.Text("Error:", bdres.ErrorMsg, "(", bdres.ErrorCode, ")")) + return + } + group := getGroup(ctx.Event.GroupID) + ctx.SendChain(buildResp(bdres, group)...) + + }) +} + +// 禁言检测 +func banCheck(ctx *zero.Ctx, bdres baiduRes) { + // 如果返回类型为2(不合规),0为合规,3为疑似 + if bdres.ConclusionType == 2 { + // 创建消息ID + mid := message.NewMessageIDFromInteger(ctx.Event.MessageID.(int64)) + // 获取群配置 + group := getGroup(ctx.Event.GroupID) + // 检测群配置里的不检测类型白名单,忽略掉不检测的违规类型 + for i, b := range group.WhiteListType { + if i == bdres.Data[0].SubType && b { + return + } + } + // 生成回复文本 + res := buildResp(bdres, group) + // 撤回消息 + ctx.DeleteMessage(mid) + // 查看是否启用撤回后禁言 + if group.DMBAN { + // 从历史违规记录中获取指定用户 + user := group.getUser(ctx.Event.UserID) + // 用户违规次数自增 + user.Count++ + // 用户违规原因记录 + user.ResList = append(user.ResList, bdres) + // 覆写该用户到群违规记录中 + group.AuditHistory[ctx.Event.UserID] = user + // 覆写该群信息 + config.Groups[ctx.Event.GroupID] = group + // 保存到json + jsonSave(config, configpath) + var bantime int64 + // 查看是否开启禁言累加功能,并计算禁言时间 + if group.BANTimeAddEnable { + bantime = user.Count * group.BANTimeAddTime * 60 + } else { + bantime = group.BANTime * 60 + } + //执行禁言 + ctx.SetGroupBan(ctx.Event.GroupID, ctx.Event.UserID, bantime) + } + //查看是否开启撤回提示 + if group.DMRemind { + res = append(res, message.At(ctx.Event.Sender.ID)) + ctx.SendChain(res...) + } + } +} + +// 获取群配置 +func getGroup(groupID int64) group { + g, ok := config.Groups[groupID] + if ok { + return g + } + if config.Groups == nil { + config.Groups = make(map[int64]group) + } + g = group{ + TextAudit: true, + ImageAudit: true, + BANTime: 1, + MaxBANTimeAddRange: 60, + BANTimeAddTime: 1, + WhiteListType: [8]bool{}, + AuditHistory: map[int64]auditHistory{}, + } + config.Groups[groupID] = g + return g +} + +// 从群历史违规记录中获取用户 +func (group *group) getUser(userID int64) auditHistory { + audit, ok := group.AuditHistory[userID] + if ok { + return audit + } + // 如果没有用户,则创建一个并返回 + if group.AuditHistory == nil { + group.AuditHistory = make(map[int64]auditHistory) + } + audit = auditHistory{0, []baiduRes{}} + group.AuditHistory[userID] = audit + return audit +} + +// 客户端是否初始化检测 +func clientCheck(ctx *zero.Ctx) bool { + if bdcli == nil { + ctx.SendChain(message.Text("Key未配置")) + return false + } + return true +} + +// 加载JSON配置文件 +func loadConfig() { + if file.IsExist(configpath) { + data, err := os.OpenFile(configpath, os.O_RDONLY, 0755) + if err != nil { + panic(err) + } + err = json.NewDecoder(data).Decode(&config) + if err != nil { + panic(err) + } + configinit = true + } else { + config = keyConfig{} + configinit = false + } +} + +// 保存配置文件 +func jsonSave(v keyConfig, path string) { + jsf, _ := os.OpenFile(path, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0666) + defer func(file *os.File) { + err := file.Close() + if err != nil { + fmt.Println(err) + } + }(jsf) // 结束时关闭句柄,释放资源 + err := json.NewEncoder(jsf).Encode(v) + if err != nil { + fmt.Println(err) + } +} + +// JSON反序列化 +func jsonToBaiduRes(resjson string) (baiduRes, error) { + var bdres baiduRes + err := json.Unmarshal(binary.StringToBytes(resjson), &bdres) + return bdres, err +} + +// 生成回复文本 +func buildResp(bdres baiduRes, group group) []message.MessageSegment { + // 建立消息段 + msgs := make([]message.MessageSegment, 0, 8) + // 生成简略审核结果回复 + msgs = append(msgs, message.Text(bdres.Conclusion, "\n")) + // 查看是否开启详细审核内容提示,并确定审核内容值为疑似,或者不合规 + if !group.MoreRemind { + return msgs + } + // 遍历返回的不合规数据,生成详细违规内容 + for i, datum := range bdres.Data { + msgs = append(msgs, message.Text("[", i, "]:", datum.Msg, "\n")) + // 检查命中词条是否大于0 + if len(datum.Hits) == 0 { + return msgs + } + // 遍历打印命中的违规词条 + for _, hit := range datum.Hits { + if len(datum.Hits) == 0 { + return msgs + } + msgs = append(msgs, message.Text("(")) + for i4, i3 := range hit.Words { + // 检查是否是最后一个要打印的词条,如果是则不加上逗号 + if i4 != len(hit.Words)-1 { + msgs = append(msgs, message.Text(i3, ",")) + } else { + msgs = append(msgs, message.Text(i3)) + } + } + msgs = append(msgs, message.Text(")")) + } + } + return msgs +}