diff --git a/plugin/baiduaudit/audit.go b/plugin/baiduaudit/audit.go index 5a801df0..6827ad7d 100644 --- a/plugin/baiduaudit/audit.go +++ b/plugin/baiduaudit/audit.go @@ -4,115 +4,51 @@ package baiduaudit import ( "encoding/json" "fmt" - "os" "strconv" + "strings" "github.com/Baidu-AIP/golang-sdk/aip/censor" + "github.com/sirupsen/logrus" + zero "github.com/wdvxdr1123/ZeroBot" + "github.com/wdvxdr1123/ZeroBot/message" + "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" ) -// 服务网址: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 // 配置路径 + bdcli *censor.ContentCensorClient // 百度云审核服务Client + txttyp = [...]string{ + 0: "默认违禁词库", + 1: "违禁违规", + 2: "文本色情", + 3: "敏感信息", + 4: "恶意推广", + 5: "低俗辱骂", + 6: "恶意推广-联系方式", + 7: "恶意推广-软文推广", + } // 文本类型 + config = newconfig() // 插件配置 ) func init() { engine := control.Register("baiduaudit", &ctrl.Options[*zero.Ctx]{ DisableOnDefault: false, Brief: "百度内容审核", - Help: "##该功能来自百度内容审核,需购买相关服务,并创建app##\n" + + Help: "##该功能来自百度内容审核, 需购买相关服务, 并创建app##\n" + "- 获取BDAKey\n" + "- 配置BDAKey [API key] [Secret Key]\n" + "- 开启/关闭内容审核\n" + "- 开启/关闭撤回提示\n" + "- 开启/关闭详细提示\n" + "- 开启/关闭撤回禁言\n" + - "##禁言时间设置## 禁言时间计算方式为:禁言次数*每次禁言累加时间,当达到最大禁言时间时,再次触发按最大禁言时间计算\n" + + "##禁言时间设置## 禁言时间计算方式为:禁言次数*每次禁言累加时间,当达到最大禁言时间时, 再次触发按最大禁言时间计算\n" + "- 开启/关闭禁言累加\n" + - "- 设置撤回禁言时间[分钟,默认:1]\n" + - "- 设置最大禁言时间[分钟,默认:60,最大43200]\n" + - "- 设置每次累加时间[分钟,默认:1]\n" + + "- 设置撤回禁言时间[分钟, 默认:1]\n" + + "- 设置最大禁言时间[分钟, 默认:60,最大43200]\n" + + "- 设置每次累加时间[分钟, 默认:1]\n" + "##检测类型设置## 类型编号列表:[1:违禁违规、2:文本色情、3:敏感信息、4:恶意推广、5:低俗辱骂 6:恶意推广-联系方式、7:恶意推广-软文推广]\n" + "- 查看检测类型\n" + "- 查看检测配置\n" + @@ -121,15 +57,19 @@ func init() { "- 开启/关闭文本检测\n" + "- 开启/关闭图像检测\n" + "##测试功能##\n" + - "- 测试文本检测[文本内容]\n" + - "- 测试图像检测[图片]\n", + "- ^文本检测[文本内容]\n" + + "- ^图像检测[图片]\n", PrivateDataFolder: "baiduaudit", }) - configpath = engine.DataFolder() + "config.json" - loadConfig() - if configinit { + + configpath := engine.DataFolder() + "config.json" + err := config.load(configpath) + if err != nil { + logrus.Warnln("[baiduaudit] 加载配置错误:", err) + } else if config.Key1 != "" && config.Key2 != "" { 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" + @@ -138,28 +78,33 @@ func init() { "https://console.bce.baidu.com/ai/?_=1665977657185#/ai/antiporn/overview/resource/getFree")) }) - engine.OnRegex("^查看检测(类型|配置)$", zero.AdminPermission, clientCheck).SetBlock(true). + engine.OnRegex("^查看检测(类型|配置)$", zero.AdminPermission, hasinit).SetBlock(true). Handle(func(ctx *zero.Ctx) { // 获取群配置 - group := getGroup(ctx.Event.GroupID) - var msgs string + group := config.groupof(ctx.Event.GroupID) + msg := "" k1 := ctx.State["regex_matched"].([]string)[1] if k1 == "类型" { - msgs += "本群检测类型:" - find := false + sb := strings.Builder{} + sb.WriteString("本群检测类型:") + found := false // 遍历群检测类型名单 - for i, v := range group.WhiteListType { + for i, v := range group.copyWhiteListType() { if !v { - find = true - msgs += fmt.Sprint("\n", i, ".", typetext[i]) + found = true + sb.WriteByte('\n') + sb.WriteString(strconv.Itoa(i)) + sb.WriteByte('.') + sb.WriteString(txttyp[i]) } } - if !find { - msgs += "无" + if !found { + sb.WriteString("无") } + msg = sb.String() } else { // 生成配置文本 - msgs = fmt.Sprintf("本群配置:\n"+ + msg = fmt.Sprintf("本群配置:\n"+ "内容审核:%s\n"+ "-文本:%s\n"+ "-图像:%s\n"+ @@ -171,138 +116,145 @@ func init() { "-每次累加时间:%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) + b, err := text.RenderToBase64(msg, 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). + + engine.OnRegex("^设置(不)?检测类型([0-7])$", zero.AdminPermission, hasinit).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) + group := config.groupof(ctx.Event.GroupID) inputType, _ := strconv.Atoi(k2) - if k1 == "不" { - group.WhiteListType[inputType] = true // 不检测:则进入类型白名单 - } else { - group.WhiteListType[inputType] = false // 检测:则退出白名单 + group.setWhiteListType(inputType, k1 == "不") + err := config.saveto(configpath) + if err != nil { + ctx.SendChain(message.Text("ERROR: ", err)) + return } - config.Groups[ctx.Event.GroupID] = group - ctx.SendChain(message.At(ctx.Event.UserID), message.Text(fmt.Sprintf("本群将%s检测%s类型内容", k1, typetext[inputType]))) + ctx.SendChain(message.At(ctx.Event.UserID), message.Text(fmt.Sprintf("本群将%s检测%s类型内容", k1, txttyp[inputType]))) }) - engine.OnRegex("^设置(最大|每次|撤回)(累加|禁言)时间(\\d{1,5})$", zero.AdminPermission, clientCheck).SetBlock(true). + + engine.OnRegex("^设置(最大|每次|撤回)(累加|禁言)时间(\\d{1,5})$", zero.AdminPermission, hasinit).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.groupof(ctx.Event.GroupID).set(func(g *group) { + switch k1 { + case "最大": + g.MaxBANTimeAddRange = time + case "每次": + g.BANTimeAddTime = time + case "撤回": + g.BANTime = time + } + }) + err := config.saveto(configpath) + if err != nil { + ctx.SendChain(message.Text("ERROR: ", err)) + return } - 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). + + engine.OnRegex("^(开启|关闭)(内容审核|撤回提示|撤回禁言|禁言累加|详细提示|文本检测|图像检测)$", zero.AdminPermission, hasinit).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 + isEnable := mark(k1 == "开启") + config.groupof(ctx.Event.GroupID).set(func(g *group) { + switch k2 { + case "内容审核": + g.Enable = isEnable + case "撤回提示": + g.DMRemind = isEnable + case "撤回禁言": + g.DMBAN = isEnable + case "禁言累加": + g.BANTimeAddEnable = isEnable + case "详细提示": + g.MoreRemind = isEnable + case "文本检测": + g.TextAudit = isEnable + case "图像检测": + g.ImageAudit = isEnable + } + }) + err := config.saveto(configpath) + if err != nil { + ctx.SendChain(message.Text("ERROR: ", err)) + return } - 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 + config.setkey(k1, k2) if bdcli != nil { - jsonSave(config, configpath) + err := config.saveto(configpath) + if err != nil { + ctx.SendChain(message.Text("ERROR: ", err)) + return + } 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) { + + engine.OnMessage(config.isgroupexist).SetBlock(false).Handle(func(ctx *zero.Ctx) { + group := config.groupof(ctx.Event.GroupID) + if !bool(group.Enable) { return } + var bdres baiduRes + var err error for _, elem := range ctx.Event.Message { switch elem.Type { case "image": - if !group.ImageAudit { - return + if !group.ImageAudit || elem.Data["url"] == "" { + continue } res := bdcli.ImgCensorUrl(elem.Data["url"], nil) - bdres, err := jsonToBaiduRes(res) + bdres, err = parse2BaiduRes(res) if err != nil { - ctx.SendChain(message.Text("Error:", bdres.ErrorMsg, "(", bdres.ErrorCode, ")")) - return + continue } - banCheck(ctx, bdres) - case "text": - if !group.TextAudit { - return + if !group.TextAudit || elem.Data["text"] == "" { + continue } - res := bdcli.TextCensor(elem.Data["text"]) - bdres, err := jsonToBaiduRes(res) + bdres, err = parse2BaiduRes(bdcli.TextCensor(elem.Data["text"])) if err != nil { - ctx.SendChain(message.Text("Error:", bdres.ErrorMsg, "(", bdres.ErrorCode, ")")) - return + continue } - banCheck(ctx, bdres) + default: + continue } } + bdres.audit(ctx, configpath) }) - engine.OnPrefix("文本检测", clientCheck).SetBlock(false). + + engine.OnPrefix("^文本检测", hasinit).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) + bdres, err := parse2BaiduRes(res) if err != nil { - ctx.SendChain(message.Text("Error:", bdres.ErrorMsg, "(", bdres.ErrorCode, ")")) + ctx.SendChain(message.Text("ERROR: ", bdres.ErrorMsg, "(", bdres.ErrorCode, ")")) return } - group := getGroup(ctx.Event.GroupID) - ctx.SendChain(buildResp(bdres, group)...) + ctx.Send(config.groupof(ctx.Event.GroupID).reply(&bdres)) }) - engine.OnPrefix("^图像检测", clientCheck).SetBlock(false). + + engine.OnPrefix("^图像检测", hasinit).SetBlock(false). Handle(func(ctx *zero.Ctx) { var urls []string for _, elem := range ctx.Event.Message { @@ -316,105 +268,17 @@ func init() { return } res := bdcli.ImgCensorUrl(urls[0], nil) - bdres, err := jsonToBaiduRes(res) + bdres, err := parse2BaiduRes(res) if err != nil { - ctx.SendChain(message.Text("Error:", bdres.ErrorMsg, "(", bdres.ErrorCode, ")")) + ctx.SendChain(message.Text("ERROR: ", bdres.ErrorMsg, "(", bdres.ErrorCode, ")")) return } - group := getGroup(ctx.Event.GroupID) - ctx.SendChain(buildResp(bdres, group)...) + ctx.Send(config.groupof(ctx.Event.GroupID).reply(&bdres)) }) } -// 禁言检测 -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 { +func hasinit(ctx *zero.Ctx) bool { if bdcli == nil { ctx.SendChain(message.Text("Key未配置")) return false @@ -422,74 +286,7 @@ func clientCheck(ctx *zero.Ctx) bool { 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, err := os.OpenFile(path, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0666) - if err != nil { - return - } - defer jsf.Close() - _ = json.NewEncoder(jsf).Encode(v) -} - -// 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 +func parse2BaiduRes(resjson string) (bdres baiduRes, err error) { + err = json.Unmarshal(binary.StringToBytes(resjson), &bdres) + return } diff --git a/plugin/baiduaudit/model.go b/plugin/baiduaudit/model.go new file mode 100644 index 00000000..43fdc99a --- /dev/null +++ b/plugin/baiduaudit/model.go @@ -0,0 +1,271 @@ +package baiduaudit + +import ( + "encoding/json" + "os" + "sync" + "sync/atomic" + + "github.com/FloatTech/floatbox/file" + zero "github.com/wdvxdr1123/ZeroBot" + "github.com/wdvxdr1123/ZeroBot/message" +) + +// 服务网址:https://console.bce.baidu.com/ai/?_=1665977657185#/ai/antiporn/overview/index +// 返回参数说明:https://cloud.baidu.com/doc/ANTIPORN/s/Nk3h6xbb2 +type baiduRes struct { + mu sync.Mutex `json:"-"` + // 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"` // 错误提示信息, 失败才返回, 成功不返回 +} + +// 禁言检测 +func (bdres *baiduRes) audit(ctx *zero.Ctx, configpath string) { + bdres.mu.Lock() + defer bdres.mu.Unlock() + // 如果返回类型为2(不合规), 0为合规, 3为疑似 + if bdres.ConclusionType != 2 { + return + } + // 创建消息ID + mid := message.NewMessageIDFromInteger(ctx.Event.MessageID.(int64)) + // 获取群配置 + group := config.groupof(ctx.Event.GroupID) + // 检测群配置里的不检测类型白名单, 忽略掉不检测的违规类型 + for i, b := range group.copyWhiteListType() { + if i == bdres.Data[0].SubType && b { + return + } + } + // 生成回复文本 + res := group.reply(bdres) + // 撤回消息 + ctx.DeleteMessage(mid) + // 查看是否启用撤回后禁言 + if group.DMBAN { + // 从历史违规记录中获取指定用户 + user := group.historyof(ctx.Event.UserID) + // 用户违规次数自增 + atomic.AddInt64(&user.Count, 1) + user.mu.Lock() + // 用户违规原因记录 + user.ResList = append(user.ResList, bdres) + user.mu.Unlock() + // 保存到json + err := config.saveto(configpath) + if err != nil { + ctx.SendChain(message.Text("ERROR: ", err)) + } + var bantime int64 + // 查看是否开启禁言累加功能, 并计算禁言时间 + if group.BANTimeAddEnable { + bantime = atomic.LoadInt64(&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.Send(res) + } +} + +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 auditHistory struct { + mu sync.Mutex `json:"-"` + Count int64 `json:"key2"` // 被禁次数 + ResList []*baiduRes `json:"reslist"` // 禁言原因 +} + +type hit struct { + // DatasetName string `json:"datasetName"` // 违规项目所属数据集名称 + Words []string `json:"words"` // 送检文本命中词库的关键词(备注:建议参考新字段“wordHitPositions”, 包含信息更丰富:关键词以及对应的位置及标签信息) + // Probability float64 `json:"probability,omitempty"` // 不合规项置信度 +} // 送检文本违规原因的详细信息 + +type keyConfig struct { + mu sync.Mutex `json:"-"` + Key1 string `json:"key1"` // 百度云服务内容审核key存储 + Key2 string `json:"key2"` // 百度云服务内容审核key存储 + Groups map[int64]*group `json:"groups"` // 群配置存储 +} + +func newconfig() (kc keyConfig) { + kc.Groups = make(map[int64]*group, 64) + return +} + +func (kc *keyConfig) setkey(k1, k2 string) { + kc.mu.Lock() + defer kc.mu.Unlock() + kc.Key1 = k1 + kc.Key2 = k2 +} + +// 加载JSON配置文件 +func (kc *keyConfig) load(filename string) error { + if file.IsNotExist(filename) { + return nil + } + f, err := os.Open(filename) + if err != nil { + return err + } + kc.mu.Lock() + defer kc.mu.Unlock() + return json.NewDecoder(f).Decode(kc) +} + +func (kc *keyConfig) isgroupexist(ctx *zero.Ctx) (ok bool) { + kc.mu.Lock() + defer kc.mu.Unlock() + _, ok = kc.Groups[ctx.Event.GroupID] + return +} + +// 获取群配置 +func (kc *keyConfig) groupof(groupID int64) *group { + kc.mu.Lock() + defer kc.mu.Unlock() + g, ok := kc.Groups[groupID] + if ok { + return g + } + g = &group{ + TextAudit: true, + ImageAudit: true, + BANTime: 1, + MaxBANTimeAddRange: 60, + BANTimeAddTime: 1, + AuditHistory: map[int64]*auditHistory{}, + } + kc.Groups[groupID] = g + return g +} + +// 保存配置文件 +func (kc *keyConfig) saveto(filename string) error { + kc.mu.Lock() + defer kc.mu.Unlock() + f, err := os.Create(filename) + if err != nil { + return err + } + defer f.Close() + return json.NewEncoder(f).Encode(kc) +} + +type group struct { + mu sync.Mutex + Enable mark // 是否启用内容审核 + TextAudit mark // 文本检测 + ImageAudit mark // 图像检测 + DMRemind mark // 撤回提示 + MoreRemind mark // 详细违规提示 + DMBAN mark // 撤回后禁言 + BANTimeAddEnable mark // 禁言累加 + BANTime int64 // 标准禁言时间, 禁用累加, 但开启禁言的的情况下采用该值 + MaxBANTimeAddRange int64 // 最大禁言时间累加范围, 最高禁言时间 + BANTimeAddTime int64 // 禁言累加时间, 该值是开启禁累加功能后, 再次触发时, 根据被禁次数X该值计算出的禁言时间 + WhiteListType [8]bool // 类型白名单, 处于白名单类型的违规, 不会被触发 0:含多种类型, 具体看官方链接, 1:违禁违规、2:文本色情、3:敏感信息、4:恶意推广、5:低俗辱骂 6:恶意推广-联系方式、7:恶意推广-软文推广 + AuditHistory map[int64]*auditHistory // 被封禁用户列表 +} + +func (g *group) set(f func(g *group)) { + g.mu.Lock() + f(g) + g.mu.Unlock() +} + +func (g *group) setWhiteListType(typ int, ok bool) { + g.mu.Lock() + defer g.mu.Unlock() + g.WhiteListType[typ] = ok +} + +func (g *group) copyWhiteListType() [8]bool { + g.mu.Lock() + defer g.mu.Unlock() + return g.WhiteListType +} + +// 从群历史违规记录中获取用户 +func (g *group) historyof(userID int64) *auditHistory { + g.mu.Lock() + defer g.mu.Unlock() + audit, ok := g.AuditHistory[userID] + if ok { + return audit + } + // 如果没有用户, 则创建一个并返回 + if g.AuditHistory == nil { + g.AuditHistory = make(map[int64]*auditHistory) + } + audit = &auditHistory{} + g.AuditHistory[userID] = audit + return audit +} + +// 生成回复文本 +func (g *group) reply(bdres *baiduRes) message.Message { + g.mu.Lock() + defer g.mu.Unlock() + // 建立消息段 + msgs := make([]message.MessageSegment, 0, 8) + // 生成简略审核结果回复 + msgs = append(msgs, message.Text(bdres.Conclusion, "\n")) + // 查看是否开启详细审核内容提示, 并确定审核内容值为疑似, 或者不合规 + if !g.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 +} + +type mark bool + +// String 打印启用状态 +func (em mark) String() string { + if em { + return "开启" + } + return "关闭" +} diff --git a/plugin/kfccrazythursday/kfccrazythursday.go b/plugin/kfccrazythursday/kfccrazythursday.go index b3789817..e7f071ff 100644 --- a/plugin/kfccrazythursday/kfccrazythursday.go +++ b/plugin/kfccrazythursday/kfccrazythursday.go @@ -23,7 +23,7 @@ func init() { engine.OnFullMatch("疯狂星期四").SetBlock(true).Handle(func(ctx *zero.Ctx) { data, err := web.GetData(crazyURL) if err != nil { - ctx.SendChain(message.Text("Error:", err)) + ctx.SendChain(message.Text("ERROR: ", err)) return } ctx.SendChain(message.Text(gjson.ParseBytes(data).Get("@this.0.content").String()))