diff --git a/Design.txt b/Design.txt index 4bbed4f..2b1975d 100644 --- a/Design.txt +++ b/Design.txt @@ -61,3 +61,9 @@ Cуществует несколько видов пинга: ------------------------------------------------------------------------------------------------------------------------ Причину бана можно только включить или выключить в конфиге на per-chat основе. Фразу настроить невозможно. (не хотелось реализовывать парсинг темплейтов, как минимум даты-времени) + + +Бан за фразы +------------------------------------------------------------------------------------------------------------------------ +Бот не читает историю, когда присоединяется к чатам, поэтому если его какое-то время не было в комнате и кто-то произнёс +непотребную фразу, ретроспективного бана не будет. diff --git a/README.md b/README.md index 0a5e532..0f416f0 100644 --- a/README.md +++ b/README.md @@ -12,8 +12,9 @@ без поддержки механизма start tls. Что-то в гошке или на стороне сервера при выборе StartTLS не работает. Имеет возможность заносить пользователей в бан-лист, согласно заданным в чёрном списке правилам: по совпадению с -регулярными выражениями в nick-е или jid-е злодея. Правила можно настроимть как глобально, для всех комнат, где -присутсвует бот, так и для каждой комнаты отдельно. При изменении списка правил бота надо перезапускать. +регулярными выражениями в nick-е или jid-е злодея, а также по регулярным выражениям характерных фраз. Правила можно +настроить как глобально, для всех комнат, где присутсвует бот, так и для каждой комнаты отдельно. При изменении списка +правил бота надо перезапускать. ## Что он не может? diff --git a/buny.go b/buny.go index 8edd27c..f03f197 100644 --- a/buny.go +++ b/buny.go @@ -11,9 +11,9 @@ import ( log "github.com/sirupsen/logrus" ) -// buny производит проверку по бело-чёрным спискам. Если presence пришёл от злодея (из чёрного списка), то отправляет -// его в бан. -func buny(v xmpp.Presence) error { //nolint:gocognit,gocyclo +// bunyPresense производит проверку по бело-чёрным спискам. Если presence пришёл от злодея (из чёрного списка), то +// отправляет его в бан. +func bunyPresense(v xmpp.Presence) error { //nolint:gocognit,gocyclo var err error // Если у presence-а есть JID и presence из одной из комнат, в которой мы есть и если его домен в чёрном @@ -217,6 +217,97 @@ func buny(v xmpp.Presence) error { //nolint:gocognit,gocyclo return err } +// bunyChat проиводит проверку сообщений участников чата по списку забаненных фраз и в случае нахождения запрещённого +// шаблона банит участника чата. +func bunyChat(v xmpp.Chat) error { + var ( + room = (strings.SplitN(v.Remote, "/", 2))[0] + // :wq + // nick = (strings.SplitN(v.Remote, "/", 2))[1] + err error + ) + + for _, cRoom := range roomsConnected { + if cRoom == room { + for _, bEntry := range blackList.Blacklist { + // Обработаем правила глобального чёрного списка + if bEntry.RoomName == "" { + for _, phraseRegexp := range bEntry.PhraseRe { + if phraseRegexp == "" { + continue + } + + re, err := regexp.Compile(phraseRegexp) + + if err != nil { + log.Errorf("Incorrect regexp in room %s blacklist: %s, skipping", room, phraseRegexp) + + continue + } + + log.Debugf("Checking phrase %s vs room %s blacklist regex %s", v.Text, room, phraseRegexp) + + if re.MatchString(v.Text) { + if id, err := squash(room, getRealJIDfromNick(v.Remote), bEntry.ReasonEnable, v.Type); err != nil { + err := fmt.Errorf( + "unable to ban user: id=%s, err=%w", + id, + err, + ) + + gTomb.Kill(err) + + continue + } + + return err + } + } + } + + // Обработаем правила чёрного списка для конкретной комнаты + if bEntry.RoomName == room { + for _, phraseRegexp := range bEntry.PhraseRe { + if phraseRegexp == "" { + continue + } + + re, err := regexp.Compile(phraseRegexp) + + if err != nil { + log.Errorf("Incorrect regexp in room %s blacklist: %s, skipping", room, phraseRegexp) + + continue + } + + log.Debugf("Checking phrase %s vs room %s blacklist regex %s", v.Text, room, phraseRegexp) + + if re.MatchString(v.Text) { + if id, err := squash(room, getRealJIDfromNick(v.Remote), bEntry.ReasonEnable, v.Type); err != nil { + err := fmt.Errorf( + "unable to ban user: id=%s, err=%w", + id, + err, + ) + + gTomb.Kill(err) + + continue + } + + return err + } + } + } + } + + break + } + } + + return err +} + // squash банит указанный jid в указанной комнате. // reasonEnable указывает, надо ли писать дату автобана в банлисте в поле reason (это единственная причина, в которую // умеет бот). diff --git a/commands.go b/commands.go index 1e1288d..eed136a 100644 --- a/commands.go +++ b/commands.go @@ -2,6 +2,7 @@ package main import ( "fmt" + "slices" "strings" "github.com/davecgh/go-spew/spew" @@ -128,6 +129,73 @@ func cmd(v xmpp.Chat) error { } } case "chat": + room := (strings.SplitN(v.Remote, "/", 2))[0] + + // Реагировать на приватную команду "из комнаты", только если бот в комнате + if slices.Contains(roomsConnected, room) { + var ( + chosenOneTalks = false + listsLoaded = true + ) + + // Реагировать на rehash только если собеседник является bot master-ом + realJID := getRealJIDfromNick(v.Remote) + + for _, master := range config.Jabber.BotMasters { + if (strings.SplitN(realJID, "/", 2))[0] == master { + chosenOneTalks = true + } + } + + if !chosenOneTalks { + log.Infof( + "Command %srehash given by non-bot_master user %s(%s), ignoring", + config.CSign, + realJID, + v.Remote, + ) + + return err + } + + if err := readWhitelist(); err != nil { + listsLoaded = false + + var msg xmpp.Chat + msg.Remote = v.Remote + msg.Type = v.Type + msg.Text = fmt.Sprint(err) + + if _, err := talk.Send(msg); err != nil { + return err + } + } + + if err := readBlacklist(); err != nil { + listsLoaded = false + + var msg xmpp.Chat + msg.Remote = v.Remote + msg.Type = v.Type + msg.Text = fmt.Sprint(err) + + if _, err := talk.Send(msg); err != nil { + return err + } + } + + if listsLoaded { + var msg xmpp.Chat + msg.Remote = v.Remote + msg.Type = v.Type + msg.Text = "Сделано" + + if _, err := talk.Send(msg); err != nil { + return err + } + } + } + default: log.Infof("Rehash command from outer space: %s", spew.Sdump(v)) } diff --git a/data/sample_blacklist.json b/data/sample_blacklist.json index 2ee8810..8a3ae29 100644 --- a/data/sample_blacklist.json +++ b/data/sample_blacklist.json @@ -3,9 +3,11 @@ { # Пустой список означает глобальные баны, которые применяются ко всем комнатам, где присутствует бот "room_name": "", + # Записывать ли в reason в банлисте комнаты сообщение вида autoban at yyyy.MM.dd hh:mm:ss # Если не задано, то false "reason_enable": false, + # Список регулярок JID-ов, которых надо банить "jid_re": [ "^[Mm]ary@server.tld/resource1$", @@ -13,11 +15,17 @@ "^(his|her)_cat@sweet.home/.*$", "^(her|his)_dog@.*\\.street/.*" ], + # Список регулярок nick-ов злодеев "nick_re": [ "^(Annoying_)?[F|f]ly$", "^Buzzing_Wasp$" - ] + ], + + # Список регулярок, запрещённых к упоминанию в чяте + "phrase_re": [ + "^Exterminate.$" + ] } ] } diff --git a/event_parser.go b/event_parser.go index c83a617..0222514 100644 --- a/event_parser.go +++ b/event_parser.go @@ -33,7 +33,7 @@ func parseEvent(e interface{}) { //nolint:maintidx,gocognit,gocyclo // Групповой чятик case "groupchat": log.Debugf("Message from public chat: %s", v.Text) - // dest := strings.SplitN(v.Remote, "/", 2)[0] + // room := strings.SplitN(v.Remote, "/", 2)[0] nick := strings.SplitN(v.Remote, "/", 2)[1] if nick == config.Jabber.Nick { @@ -48,6 +48,12 @@ func parseEvent(e interface{}) { //nolint:maintidx,gocognit,gocyclo return } + if err := bunyChat(v); err != nil { + gTomb.Kill(err) + + return + } + lastActivity = lastServerActivity if muc, _ := strings.CutSuffix(v.Remote, "/"); muc != "" { @@ -111,7 +117,7 @@ func parseEvent(e interface{}) { //nolint:maintidx,gocognit,gocyclo if id, err := talk.IqVersionResponse( v, - "buny-jabber-bot", + "bunyPresense-jabber-bot", "1.с_чем-то", "линупс", ); err != nil { @@ -592,7 +598,7 @@ func parseEvent(e interface{}) { //nolint:maintidx,gocognit,gocyclo // По правилам, мы можем что-то делать, только после того, как нам прилетит наш собственный presence, это // значит, что мы вошли в комнату. if slices.Contains(roomsConnected, room) { - if err := buny(v); err != nil { + if err := bunyPresense(v); err != nil { gTomb.Kill(err) return diff --git a/lib.go b/lib.go index a3bdb3d..22e9858 100644 --- a/lib.go +++ b/lib.go @@ -88,9 +88,9 @@ func readConfig() error { //nolint:gocognit,gocyclo configJSONPath := fmt.Sprintf("%s/data/config.json", filepath.Dir(executablePath)) locations := []string{ - "~/.buny-jabber-bot.json", - "~/buny-jabber-bot.json", - "/etc/buny-jabber-bot.json", + "~/.bunyPresense-jabber-bot.json", + "~/bunyPresense-jabber-bot.json", + "/etc/bunyPresense-jabber-bot.json", configJSONPath, } @@ -221,9 +221,9 @@ func readConfig() error { //nolint:gocognit,gocyclo } if sampleConfig.Jabber.Resource == "" { - sampleConfig.Jabber.Resource = "buny bot" + sampleConfig.Jabber.Resource = "bunyPresense bot" - log.Info("Jabber resource not defined in config, using buny bot") + log.Info("Jabber resource not defined in config, using bunyPresense bot") } if sampleConfig.Jabber.User == "" { @@ -309,9 +309,9 @@ func readWhitelist() error { whitelistJSONPath := fmt.Sprintf("%s/data/whitelist.json", filepath.Dir(executablePath)) locations := []string{ - "~/.buny-jabber-bot-whitelist.json", - "~/buny-jabber-bot-whitelist.json", - "/etc/buny-jabber-bot-whitelist.json", + "~/.bunyPresense-jabber-bot-whitelist.json", + "~/bunyPresense-jabber-bot-whitelist.json", + "/etc/bunyPresense-jabber-bot-whitelist.json", whitelistJSONPath, } @@ -405,9 +405,9 @@ func readBlacklist() error { whitelistJSONPath := fmt.Sprintf("%s/data/blacklist.json", filepath.Dir(executablePath)) locations := []string{ - "~/.buny-jabber-bot-blacklist.json", - "~/buny-jabber-bot-blacklist.json", - "/etc/buny-jabber-bot-blacklist.json", + "~/.bunyPresense-jabber-bot-blacklist.json", + "~/bunyPresense-jabber-bot-blacklist.json", + "/etc/bunyPresense-jabber-bot-blacklist.json", whitelistJSONPath, } @@ -618,7 +618,7 @@ func joinMuc(room string) { _ = json.Unmarshal([]byte(name), &v) log.Infof("Fake presence forged for %s just for on-enter check", name) // Оно там внутри всё само обработает, если вдруг возникнет wire error, то зарекконетится. - _ = buny(v) + _ = bunyPresense(v) } } } diff --git a/types.go b/types.go index b924c00..cf781cf 100644 --- a/types.go +++ b/types.go @@ -56,6 +56,7 @@ type myBlackList struct { ReasonEnable bool `json:"reason_enable,omitempty"` JidRe []string `json:"jid_re,omitempty"` NickRe []string `json:"nick_re,omitempty"` + PhraseRe []string `json:"phrase_re,omitempty"` } `json:"blacklist,omitempty"` }