ZeroBot-Plugin/plugin/baiduaudit/audit.go
GenesisAN df8a320fbe
fix baiduaudit的正则错误 (#495)
修复一个正则错误,导致不能正确解析第二个ApiKey的问题
2022-11-12 11:16:31 +08:00

503 lines
17 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// Package baiduaudit 百度内容审核
package baiduaudit
import (
"encoding/json"
"fmt"
"os"
"strconv"
"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"
)
// 服务网址: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,
Brief: "百度内容审核",
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
}