diff --git a/.gitignore b/.gitignore index cbb5beb..60bb304 100644 --- a/.gitignore +++ b/.gitignore @@ -4,4 +4,5 @@ runtime/ dist/ config.toml db/geecaptcha.db -captcha-bot \ No newline at end of file +captcha-bot +dict/dec_* \ No newline at end of file diff --git a/README.md b/README.md index bc0ea6e..d858506 100644 --- a/README.md +++ b/README.md @@ -32,14 +32,14 @@ Telegram Api文档:[Telegram Api](https://core.telegram.org/bots/api) 下载: ```shell # 下载项目 -git clone https://github.com/assimon/captcha-bot && cd captcha-bot && cp .env.example .env +git clone https://github.com/assimon/captcha-bot && cd captcha-bot ``` 编译: ```shell # 编译 -go build -o cbot +go build -o captcha-bot # 给予执行权限 -chmod +x ./cbot +chmod +x ./captcha-bot ``` 配置: ```shell @@ -48,9 +48,9 @@ cp .example.config.toml config.toml 执行: ```shell # 调试启动 -./cbot +./captcha-bot # nohup -nohup ./cbot >> run.log 2>&1 & +nohup ./captcha-bot >> run.log 2>&1 & ``` ### 二、下载已经编译好的二进制程序 @@ -58,22 +58,34 @@ nohup ./cbot >> run.log 2>&1 & 进入打包好的版本列表,下载程序:[https://github.com/assimon/captcha-bot/releases](https://github.com/assimon/captcha-bot/releases) 配置: ```shell -cp .env.example .env +cp .example.config.toml config.toml ``` 运行: ```shell # linux # 调试启动 ./captcha-bot -# nohup 常驻启动 # windows captcha-bot.exe ``` -## 配置: -请将项目目录下`.env.example`文件重命名为`.env`, 然后对`.env`文件进行编辑即可! -里面的配置项有详细的注释。 +### 三、机器人命令 +``` +/ping #存活检测,机器人若正常将返回"pong" +# 广告相关 +/add_ad #新增一条广告,格式:广告标题|跳转链接|到期时间(带时分秒)|权重(倒序,值越大越靠前),例如:/add_ad 📢广告招租|https://google.com|2099-01-01 00:00:00|100 +/all_ad #查看所有广告 +/del_ad #删除一条广告,例如:/del_ad 1(删除id为1的广告) +``` + +### 四、敏感词词库使用 +在项目`dict`文件夹提供了一些敏感词库,用于机器人反垃圾功能。由于不可描述原因词库不能`明文`放置于项目仓库。 +如需使用,请使用`openssl`命令进行解密,且文件名必须以`dec_`开头,否则无法正常加载! +例如: +```shell +openssl enc -d -aes256 -pass pass:captcha-bot -in dict/enc_dc1.txt -out dict/dec_dc1.txt +``` ## 预览 ![禁言.png](https://i.loli.net/2021/09/27/dZQSFKmI23nbXhN.png) diff --git a/bootstrap/bootstrap.go b/bootstrap/bootstrap.go index 989092c..efa138e 100644 --- a/bootstrap/bootstrap.go +++ b/bootstrap/bootstrap.go @@ -5,6 +5,7 @@ import ( "github.com/assimon/captcha-bot/util/config" "github.com/assimon/captcha-bot/util/log" "github.com/assimon/captcha-bot/util/orm" + "github.com/assimon/captcha-bot/util/sensitiveword" "os" "os/signal" "syscall" @@ -15,6 +16,7 @@ func Start() { config.InitConfig() log.InitLog() orm.InitDb() + sensitiveword.InitSensitiveWord() // 机器人启动 go func() { defer func() { diff --git a/dict/enc_dc1.txt b/dict/enc_dc1.txt new file mode 100644 index 0000000..c34fabe Binary files /dev/null and b/dict/enc_dc1.txt differ diff --git a/dict/enc_dc2.txt b/dict/enc_dc2.txt new file mode 100644 index 0000000..dd2f941 Binary files /dev/null and b/dict/enc_dc2.txt differ diff --git a/example.config.toml b/example.config.toml index 5511bef..998e290 100644 --- a/example.config.toml +++ b/example.config.toml @@ -1,14 +1,14 @@ #系统设置 [system] -join_hint_after_del_time=60 #加群验证提示消息多久删除,秒 -captcha_timeout=120 #验证超时时间,秒 -runtime_path="/runtime" +join_hint_after_del_time=60 # 加群验证提示消息多久删除,秒 +captcha_timeout=120 # 验证超时时间,秒 +runtime_path="/runtime" # 缓存目录 #telegram 配置 [telegram] -bot_token="" -api_proxy="" -manage_users=[] +bot_token="" # 机器人apitoken +api_proxy="" # telegram api代理,仅大陆地区服务器需要 +manage_users=[] # 超级管理员userid数组,以英文逗号分割,例如123,456,789 #日志配置 [log] @@ -18,6 +18,12 @@ max_backups=3 #消息模板 [message] -join_hint="欢迎 @%s 加入 %s !\n\n⚠本群已开启新成员验证功能,未通过验证的用户无法发言!\n\n⏱本条消息 %d 秒后自动删除\n\n👇点击下方按钮自助解除禁言" +join_hint="欢迎 [%s](%s) 加入 %s\n\n⚠️本群已开启新成员验证功能,未通过验证的用户无法发言 \n\n⏱本条消息 %d 秒后自动删除\n\n👇点击下方按钮自助解除禁言" captcha_image="欢迎您加入[%s]!\n\n⚠本群已开启新成员验证功能。\n\n👆为了证明您不是机器人,请发送以上图片验证码内容\n\n🤖机器人将自动验证您发送的验证码内容是否正确\n\n⏱本条验证消息有效期[%d]秒" -verification_complete="恭喜您成功通过[🤖人机验证],系统已为您解除禁言限制。\n\n如若还是无法发言,请重启telegram客户端" \ No newline at end of file +verification_complete="恭喜您成功通过[🤖人机验证],系统已为您解除禁言限制。\n\n如若还是无法发言,请重启telegram客户端" +block_hint="\\#封禁预警\n[%s](%s) 请注意,您的消息中含有部分违禁词 \n⚠️您已被系统判断为高风险用户,已被封禁\n系统已向超管发送预警信息,若由超管判定为误杀,会及时将您解除封禁。\n您的违禁词包含:%s" + +#广告阻止 +[adblock] +number_of_forbidden_words=2 # 违禁词判定个数,如果一句话中出现的违禁词为该设置个数,则判断为违禁 +block_time=-1 # 阻止时间,单位:秒。如果为-1,则代表永久封禁 diff --git a/go.mod b/go.mod index de805ae..4cbeb06 100644 --- a/go.mod +++ b/go.mod @@ -18,6 +18,7 @@ require ( github.com/fsnotify/fsnotify v1.5.4 // indirect github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 // indirect github.com/hashicorp/hcl v1.0.0 // indirect + github.com/importcjj/sensitive v0.0.0-20200106142752-42d1c505be7b // indirect github.com/jinzhu/inflection v1.0.0 // indirect github.com/jinzhu/now v1.1.5 // indirect github.com/magiconair/properties v1.8.6 // indirect diff --git a/go.sum b/go.sum index 4770484..cbe63e2 100644 --- a/go.sum +++ b/go.sum @@ -134,6 +134,8 @@ github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= +github.com/importcjj/sensitive v0.0.0-20200106142752-42d1c505be7b h1:9hudrgWUhyfR4FRMOfL9KB1uYw48DUdHkkgr9ODOw7Y= +github.com/importcjj/sensitive v0.0.0-20200106142752-42d1c505be7b/go.mod h1:zLVdX6Ed2SvCbEamKmve16U0E03UkdJo4ls1TBfmc8Q= github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E= github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc= github.com/jinzhu/now v1.1.4/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= diff --git a/service/AdvertiseService.go b/service/AdvertiseService.go index 1d904bf..01cf02e 100644 --- a/service/AdvertiseService.go +++ b/service/AdvertiseService.go @@ -6,21 +6,24 @@ import ( "github.com/golang-module/carbon/v2" ) +// AddAdvertiseService 新增广告 func AddAdvertiseService(advertise model.Advertise) (err error) { return orm.Gdb.Model(&advertise).Create(&advertise).Error } +// AllAdvertiseService 加载所有广告 func AllAdvertiseService() (advertises []model.Advertise, err error) { err = orm.Gdb.Model(&advertises).Find(&advertises).Error return } +// GetEfficientAdvertiseService 加载正在生效的广告 func GetEfficientAdvertiseService() (advertises []model.Advertise, err error) { - nowTime := carbon.Now().Timestamp() - err = orm.Gdb.Model(&advertises).Where("validity_period > ?", nowTime).Order("sort desc").Find(&advertises).Error + err = orm.Gdb.Model(&advertises).Where("validity_period > ?", carbon.Now().Timestamp()).Order("sort desc").Find(&advertises).Error return } +// DeleteAdvertiseService 删除一条广告 func DeleteAdvertiseService(id int64) (err error) { return orm.Gdb.Where("id = ?", id).Delete(&model.Advertise{}).Error } diff --git a/telegram/handle.go b/telegram/handle.go index 83738c1..a52bb80 100644 --- a/telegram/handle.go +++ b/telegram/handle.go @@ -7,6 +7,7 @@ import ( "github.com/assimon/captcha-bot/util/captcha" "github.com/assimon/captcha-bot/util/config" "github.com/assimon/captcha-bot/util/log" + "github.com/assimon/captcha-bot/util/sensitiveword" "github.com/golang-module/carbon/v2" uuid "github.com/satori/go.uuid" tb "gopkg.in/telebot.v3" @@ -25,6 +26,7 @@ var ( var ( captchaMessageMenu = &tb.ReplyMarkup{ResizeKeyboard: true} + manslaughterMenu = &tb.ReplyMarkup{ResizeKeyboard: true} ) var ( @@ -101,7 +103,7 @@ func StartCaptcha(c tb.Context) error { userCaptchaCodeKey := strconv.FormatInt(userId, 10) gUserCaptchaCodeTable.Set(userCaptchaCodeKey, userCaptchaCodeVal) time.AfterFunc(time.Duration(config.SystemC.CaptchaTimeout)*time.Second, func() { - os.Remove(imgUrl) + _ = os.Remove(imgUrl) gMessageTokenMap.Delete(chatToken) gUserCaptchaCodeTable.Del(userCaptchaCodeKey) err = Bot.Delete(botMsg) @@ -114,10 +116,73 @@ func StartCaptcha(c tb.Context) error { // OnTextMessage 文本消息 func OnTextMessage(c tb.Context) error { - // 不是私聊 - if !c.Message().Private() { + // 私聊走入群验证操作 + if c.Message().Private() { + return VerificationProcess(c) + } + // 否则走广告阻止监听 + return AdBlock(c) +} + +// AdBlock 广告阻止 +func AdBlock(c tb.Context) error { + userId := c.Message().Sender.ID + userLink := fmt.Sprintf("tg://user?id=%d", c.Message().Sender.ID) + userNickname := c.Message().Sender.LastName + c.Message().Sender.FirstName + messageText := c.Message().Text + // 管理员 放行任何操作 + if isManage(c.Chat(), userId) { return nil } + dict := sensitiveword.Filter.FindAll(messageText) + if len(dict) <= 0 || len(dict) < config.AdBlockC.NumberOfForbiddenWords { + return nil + } + // ban user + restrictedUntil := config.AdBlockC.BlockTime + if restrictedUntil <= 0 { + restrictedUntil = tb.Forever() + } + err := Bot.Restrict(c.Chat(), &tb.ChatMember{ + Rights: tb.NoRights(), + User: c.Message().Sender, + RestrictedUntil: restrictedUntil, + }) + if err != nil { + log.Sugar.Error("[AdBlock] ban user err:", err) + return err + } + blockMessage := fmt.Sprintf(config.MessageC.BlockHint, + userNickname, + userLink, + strings.Join(dict, ",")) + manslaughterBtn := manslaughterMenu.Data("👮🏻管理员解封", strconv.FormatInt(userId, 10)) + manslaughterMenu.Inline(manslaughterMenu.Row(manslaughterBtn)) + Bot.Handle(&manslaughterBtn, func(c tb.Context) error { + if err = Bot.Delete(c.Message()); err != nil { + log.Sugar.Error("[AdBlock] delete adblock message err:", err) + return err + } + // 解禁用户 + err = Bot.Restrict(c.Chat(), &tb.ChatMember{ + User: &tb.User{ID: userId}, + Rights: tb.NoRestrictions(), + }) + if err != nil { + log.Sugar.Error("[AdBlock] unban user err:", err) + return err + } + return c.Send(fmt.Sprintf("管理员已解除对用户:[%s](%s) 的封禁", userNickname, userLink), tb.ModeMarkdownV2) + }, isManageMiddleware) + if err = c.Reply(blockMessage, manslaughterMenu, tb.ModeMarkdownV2); err != nil { + log.Sugar.Error("[AdBlock] reply message err:", err) + return err + } + return c.Delete() +} + +// VerificationProcess 验证处理 +func VerificationProcess(c tb.Context) error { userIdStr := strconv.FormatInt(c.Sender().ID, 10) captchaCode := gUserCaptchaCodeTable.Get(userIdStr) if captchaCode == nil || captchaCode.UserId != c.Sender().ID { @@ -140,17 +205,20 @@ func OnTextMessage(c tb.Context) error { gUserCaptchaCodeTable.Del(userIdStr) gUserCaptchaPendingTable.Del(fmt.Sprintf("%d|%d", captchaCode.PendingMessage.ID, captchaCode.PendingMessage.Chat.ID)) //删除验证消息 - Bot.Delete(captchaCode.CaptchaMessage) - Bot.Delete(captchaCode.PendingMessage) + if err = Bot.Delete(captchaCode.CaptchaMessage); err != nil { + log.Sugar.Error("[OnTextMessage] delete captcha message err:", err) + } + if err = Bot.Delete(captchaCode.PendingMessage); err != nil { + log.Sugar.Error("[OnTextMessage] delete pending message err:", err) + } return c.Send(config.MessageC.VerificationComplete) - } // UserJoinGroup 用户加群事件 func UserJoinGroup(c tb.Context) error { var err error - err = c.Delete() - if err != nil { + + if err = c.Delete(); err != nil { log.Sugar.Error("[UserJoinGroup] delete join message err:", err) } // 如果是管理员邀请的,直接通过 @@ -166,10 +234,14 @@ func UserJoinGroup(c tb.Context) error { if err != nil { log.Sugar.Error("[UserJoinGroup] ban user err:", err) } - joinMessage := fmt.Sprintf(config.MessageC.JoinHint, c.Message().UserJoined.Username, c.Chat().Title, config.SystemC.JoinHintAfterDelTime) + userLink := fmt.Sprintf("tg://user?id=%d", c.Message().UserJoined.ID) + joinMessage := fmt.Sprintf(config.MessageC.JoinHint, + c.Message().UserJoined.LastName+c.Message().UserJoined.FirstName, + userLink, + c.Chat().Title, + config.SystemC.JoinHintAfterDelTime) chatToken := uuid.NewV4().String() doCaptchaBtn := joinMessageMenu.URL("👉🏻点我开始人机验证🤖", fmt.Sprintf("https://t.me/%s?start=%s", Bot.Me.Username, chatToken)) - joinMessageMenu.Inline( joinMessageMenu.Row(doCaptchaBtn), joinMessageMenu.Row(manageBanBtn, managePassBtn), @@ -191,7 +263,7 @@ func UserJoinGroup(c tb.Context) error { if err != nil { log.Sugar.Error("[UserJoinGroup] add captcha record err:", err) } - captchaMessage, err := Bot.Send(c.Chat(), joinMessage, joinMessageMenu) + captchaMessage, err := Bot.Send(c.Chat(), joinMessage, joinMessageMenu, tb.ModeMarkdownV2) if err != nil { log.Sugar.Error("[UserJoinGroup] send join hint message err:", err) } @@ -206,8 +278,7 @@ func UserJoinGroup(c tb.Context) error { captchaDataKey := fmt.Sprintf("%d|%d", captchaMessage.ID, c.Chat().ID) gUserCaptchaPendingTable.Set(captchaDataKey, captchaDataVal) time.AfterFunc(time.Duration(config.SystemC.JoinHintAfterDelTime)*time.Second, func() { - err = Bot.Delete(captchaMessage) - if err != nil { + if err = Bot.Delete(captchaMessage); err != nil { log.Sugar.Error("[UserJoinGroup] delete join hint message err:", err) } }) @@ -279,7 +350,7 @@ func refreshCaptcha() func(c tb.Context) error { } captchaCode.Code = code gUserCaptchaCodeTable.Set(userIdStr, captchaCode) - os.Remove(imgUrl) + _ = os.Remove(imgUrl) return c.Respond(&tb.CallbackResponse{ Text: "验证码已刷新~", }) @@ -307,7 +378,9 @@ func AddAd(c tb.Context) error { if err != nil { return c.Send("新增广告失败:" + err.Error()) } - c.Send("新增广告成功") + if err = c.Send("新增广告成功"); err != nil { + log.Sugar.Error("[AddAd] send success message err:", err) + } return AllAd(c) } @@ -339,10 +412,11 @@ func DelAd(c tb.Context) error { if err != nil { return c.Send(err.Error()) } - err = service.DeleteAdvertiseService(id) - if err != nil { + if err = service.DeleteAdvertiseService(id); err != nil { return c.Send(err.Error()) } - c.Send("广告删除成功!") + if err = c.Send("广告删除成功!"); err != nil { + log.Sugar.Error("[DelAd] send success message err:", err) + } return AllAd(c) } diff --git a/telegram/root.go b/telegram/root.go index ca7f7d8..37d30b7 100644 --- a/telegram/root.go +++ b/telegram/root.go @@ -41,11 +41,12 @@ func RegisterHandle() { Bot.Handle(START_CMD, StartCaptcha) Bot.Handle(tb.OnUserJoined, UserJoinGroup) Bot.Handle(tb.OnText, OnTextMessage) - Bot.Handle(&manageBanBtn, ManageBan(), isManageMiddleware) - Bot.Handle(&managePassBtn, ManagePass(), isManageMiddleware) Bot.Handle(tb.OnUserLeft, func(c tb.Context) error { return c.Delete() }) + // 按钮点击事件 + Bot.Handle(&manageBanBtn, ManageBan(), isManageMiddleware) + Bot.Handle(&managePassBtn, ManagePass(), isManageMiddleware) // 广告 Bot.Handle(ADD_AD, AddAd, isRootMiddleware) Bot.Handle(ALL_AD, AllAd, isRootMiddleware) diff --git a/util/config/config.go b/util/config/config.go index 9979eb0..9f83256 100644 --- a/util/config/config.go +++ b/util/config/config.go @@ -36,10 +36,18 @@ type Message struct { JoinHint string `mapstructure:"join_hint"` CaptchaImage string `mapstructure:"captcha_image"` VerificationComplete string `mapstructure:"verification_complete"` + BlockHint string `mapstructure:"block_hint"` } var MessageC Message +type AdBlock struct { + NumberOfForbiddenWords int `mapstructure:"number_of_forbidden_words"` + BlockTime int64 `mapstructure:"block_time"` +} + +var AdBlockC AdBlock + // InitConfig 配置加载 func InitConfig() { path, err := os.Getwd() @@ -68,4 +76,8 @@ func InitConfig() { if err != nil { log.Fatal("load config log err:", err) } + err = viper.UnmarshalKey("adblock", &AdBlockC) + if err != nil { + log.Fatal("load adblock log err:", err) + } } diff --git a/util/sensitiveword/sensitiveword.go b/util/sensitiveword/sensitiveword.go new file mode 100644 index 0000000..4340eee --- /dev/null +++ b/util/sensitiveword/sensitiveword.go @@ -0,0 +1,32 @@ +package sensitiveword + +import ( + "github.com/assimon/captcha-bot/util/config" + "github.com/importcjj/sensitive" + "io/ioutil" + "log" + "strings" +) + +var Filter *sensitive.Filter + +// InitSensitiveWord 加载敏感词库 +func InitSensitiveWord() { + sensitiveWordPath := config.AppPath + "/dict/" + Filter = sensitive.New() + files, err := ioutil.ReadDir(sensitiveWordPath) + if err != nil { + log.Fatalln("[InitSensitiveWord] load dict err:", err) + } + for _, file := range files { + // 文件名必须是已解密文件 + if !strings.Contains(file.Name(), "dec_") { + continue + } + sensitiveFile := sensitiveWordPath + file.Name() + err = Filter.LoadWordDict(sensitiveFile) + if err != nil { + log.Fatalln("[InitSensitiveWord] load sensitive file err:", err, ", file:", sensitiveFile) + } + } +}