package jabber import ( "buny-jabber-bot/internal/log" "encoding/json" "fmt" "regexp" "slices" "strings" "github.com/davecgh/go-spew/spew" "github.com/xmppo/go-xmpp" ) // BunyPresense производит проверку по бело-чёрным спискам. Если presence пришёл от злодея (из чёрного списка), то // отправляет его в бан. func (j *Jabber) BunyPresense(v xmpp.Presence) error { //nolint:gocognit,gocyclo var err error // Если у presence-а есть JID и presence из одной из комнат, в которой мы есть и если его домен в чёрном // списке, заносим пидора в список outcast-ов. if v.JID != "" { // На всякий случай: себя никогда не баним, явным образом. if v.JID == j.Talk.JID() { return err } room := strings.SplitN(v.From, "/", 2)[0] if room == "" { msg := "We got empty room field in presence event, which kinda strange: " + spew.Sdump(v) for str := range strings.SplitSeq(msg, "\n") { log.Info(str) } return err } goodJid := strings.SplitN(v.JID, "/", 2)[0] // Обрабатываем правила белого списка. for _, good := range j.WhiteList.Whitelist { // Глобальный белый список. if good.RoomName == "" { if slices.Contains(good.Jid, goodJid) { return err } } // Белый список для конкретных каналов. if good.RoomName == room { if slices.Contains(good.Jid, goodJid) { return err } } } id, err := j.QuerySoftwareVersion(v.From) if err != nil { err := fmt.Errorf( "unable to query user software and version: jid=%s id=%s, err=%w", v.JID, id, err, ) j.GTomb.Kill(err) return err } evilJid := strings.SplitN(v.JID, "/", 2)[0] evilNick := "" evilNicks := strings.SplitN(v.From, "/", 2) // Давайте-ка похэндлим нонсенс - когда у нас в строке нету разделителя. if len(evilNicks) > 1 { evilNick = evilNicks[1] } // Обрабатываем правила чёрного списка. for _, cRoom := range j.RoomsConnected { if cRoom == room { for _, bEntry := range j.BlackList.Blacklist { // Обработаем правила глобального чёрного списка. if bEntry.RoomName == "" { for _, jidRegexp := range bEntry.JidRe { if jidRegexp == "" { continue } re, err := regexp.Compile(jidRegexp) if err != nil { log.Errorf("Incorrect regexp in global blacklist: %s, skipping", jidRegexp) continue } log.Debugf("Checking jid %s vs global blacklist regex %s", v.JID, jidRegexp) if re.MatchString(v.JID) { log.Warnf( "Hammer falls on %s (%s): jid matches with global blacklist entry: %s", v.From, evilJid, jidRegexp, ) id, err := j.Squash(room, evilJid, bEntry.ReasonEnable, v.Type) if err != nil { err := fmt.Errorf( "unable to ban user: id=%s, err=%w", id, err, ) j.GTomb.Kill(err) } return err } } for _, nickRegexp := range bEntry.NickRe { if nickRegexp == "" { continue } re, err := regexp.Compile(nickRegexp) if err != nil { log.Errorf("Incorrect regexp in global blacklist: %s, skipping", nickRegexp) continue } log.Debugf("Checking nick %s vs global blacklist regex %s", evilNick, nickRegexp) if re.MatchString(evilNick) { log.Warnf( "Hammer falls on %s (%s): nick matches with global blacklist entry: %s", v.From, evilJid, nickRegexp, ) // Баним именно jid. id, err := j.Squash(room, evilJid, bEntry.ReasonEnable, v.Type) if err != nil { err := fmt.Errorf( "unable to ban user: id=%s, err=%w", id, err, ) j.GTomb.Kill(err) } return err } } continue } // Обработаем правила конкретного канала, комнаты конференции. if bEntry.RoomName == room { for _, jidRegexp := range bEntry.JidRe { if jidRegexp == "" { continue } re, err := regexp.Compile(jidRegexp) if err != nil { log.Errorf("Incorrect regexp in room %s blacklist: %s, skipping", room, jidRegexp) continue } log.Debugf("Checking jid %s vs room %s blacklist regex %s", v.JID, room, jidRegexp) if re.MatchString(v.JID) { log.Warnf( "Hammer falls on %s (%s): jid matches with room blacklist entry: %s", v.From, evilJid, jidRegexp, ) id, err := j.Squash(room, evilJid, bEntry.ReasonEnable, v.Type) if err != nil { err := fmt.Errorf( "unable to ban user: id=%s, err=%w", id, err, ) j.GTomb.Kill(err) } return err } } if bEntry.RoomName == room { for _, nickRegexp := range bEntry.NickRe { if nickRegexp == "" { continue } re, err := regexp.Compile(nickRegexp) if err != nil { log.Errorf("Incorrect regexp in room %s blacklist: %s, skipping", room, nickRegexp) continue } log.Debugf("Checking nick %s vs room %s blacklist regex %s", evilJid, room, nickRegexp) if re.MatchString(evilNick) { log.Warnf( "Hammer falls on %s (%s): nick matches with room blacklist entry: %s", v.From, evilJid, nickRegexp, ) // Баним именно jid. id, err := j.Squash(room, evilJid, bEntry.ReasonEnable, v.Type) if err != nil { err := fmt.Errorf( "unable to ban user: id=%s, err=%w", id, err, ) j.GTomb.Kill(err) } return err } } } continue } } } } } return err } // BunyChat производит проверку сообщений участников чата по списку забаненных фраз и в случае нахождения запрещённого // шаблона банит участника чата. func (j *Jabber) BunyChat(v xmpp.Chat) error { var ( room = (strings.SplitN(v.Remote, "/", 2))[0] err error ) // Действовать мы можем только в рамках тех комнат, где явно присуствуем. for _, cRoom := range j.RoomsConnected { if cRoom == room { // Перебирём правила чёрных списков. for _, bEntry := range j.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) { realJID := j.GetRealJIDfromNick(v.Remote) log.Warnf( "Hammer falls on %s (%s): phrase matches with global blacklist entry: %s vs %s", v.Remote, realJID, v.Text, phraseRegexp, ) if id, err := j.Squash(room, realJID, bEntry.ReasonEnable, v.Type); err != nil { err := fmt.Errorf( "unable to ban user: id=%s, err=%w", id, err, ) j.GTomb.Kill(err) } 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) { realJID := j.GetRealJIDfromNick(v.Remote) log.Warnf( "Hammer falls on %s (%s): phrase matches with room blacklist entry: %s vs %s", v.Remote, realJID, v.Text, phraseRegexp, ) if id, err := j.Squash(room, realJID, bEntry.ReasonEnable, v.Type); err != nil { err := fmt.Errorf( "unable to ban user: id=%s, err=%w", id, err, ) j.GTomb.Kill(err) } return err } } } } // Если включено, проверяем фразу на КАПС. for _, channel := range j.C.Jabber.Channels { if channel.Name == room && channel.AllCaps.Enabled { // Нормализуем строку и вырежем из неё пробелы. normPhrase := strings.ReplaceAll(nString(v.Text), " ", "") // Проверяем согласно тому, что длина фразы более чем сколько-то символов. if len(normPhrase) >= channel.AllCaps.MinLength { normPhraseUpper := strings.ReplaceAll(nStringUpper(v.Text), " ", "") if normPhrase == normPhraseUpper { realJID := j.GetRealJIDfromNick(v.Remote) id, err := j.Squash(room, realJID, false, v.Type) if err != nil { err := fmt.Errorf( "unable to ban user: id=%s, err=%w", id, err, ) j.GTomb.Kill(err) } } } } } break } } return err } // BunySoftwareVersion производит проверку по чёрному списку версий и названий клинтского ПО. func (j *Jabber) BunySoftwareVersion(v xmpp.IQ, ver IqResultSoftwareVersion) error { var err error if v.From != "" { // На всякий случай: себя никогда не баним, явным образом. if v.From == j.Talk.JID() { log.Debugf("Skipping useragent blacklist check for myself") return err } room := strings.SplitN(v.From, "/", 2)[0] if presenceJSONInterface, present := j.RoomPresences.Get(room); present { presenceJSON, ok := presenceJSONInterface.([]byte) if !ok { log.Errorf("Looks like we have corrupted entry in presence db") return nil } var presences []xmpp.Presence if err := json.Unmarshal(presenceJSON, &presences); err != nil { log.Errorf("Looks like we have corrupted entry in presence db") return nil //nolint: nilerr // Это не критическая ошибка, её не надо возвращать. } for _, p := range presences { //nolint: gocritic // Развели тут, блять, страну советов, напиши что надо сделать кодом, сука. // Если находим, что у нас уже есть клиент с таким же From, то есть полным nick-ом (для groupchat) // пропускаем его. if p.From != v.From { log.Debugf("Skip check for %s because it is not %s", p.From, v.From) continue } if j.C.Debug.PresenceDB { log.Debugf("Found jid of %s in presence db: %s", v.From, p.JID) } if p.Affiliation == "admin" || p.Affiliation == "owner" { log.Infof("Skipping useragent blacklist check for %s because his affiliation is %s", v.From, p.Affiliation) continue } // На всякий случай. if p.Role == "moderator" { log.Infof("Skipping useragent blacklist check for %s because his role is %s", v.From, p.Role) continue } log.Debug("Useragent is not owner or moderator, proceeding with check...") // Lookup in global blacklist. log.Debug("Searching our room in connected room list...") for _, cRoom := range j.RoomsConnected { if cRoom == room { log.Debug("Found our room in list of connected rooms") for _, bEntry := range j.BlackList.Blacklist { // Обработаем правила глобального чёрного списка. if bEntry.RoomName == "" { log.Debugf("Check in global blacklist") log.Debugf("We have useragent struct with Name=%s, Version=%s, OS=%s", ver.Name, ver.Version, ver.Os) for _, useragent := range bEntry.UserAgent { log.Debugf("We have useragent blacklist entry Name=%s, Version=%s, OS=%s", useragent.Name, useragent.Version, useragent.Os) switch { case useragent.Name != "" && useragent.Version != "" && useragent.Os != "": if ver.Name == useragent.Name && ver.Version == useragent.Version && ver.Os == useragent.Os { log.Warnf("Hammer falls on %s (%s): Software Name, Version and OS match entry in blacklist", p.From, p.JID) id, err := j.Squash(room, p.JID, bEntry.ReasonEnable, v.Type) if err != nil { err := fmt.Errorf( "unable to ban user: id=%s, err=%w", id, err, ) j.GTomb.Kill(err) } return err } case useragent.Name != "" && useragent.Version != "" && useragent.Os == "": if ver.Name == useragent.Name && ver.Version == useragent.Version { log.Warnf("Hammer falls on %s (%s): Software Name and Version match entry in blacklist", p.From, p.JID) id, err := j.Squash(room, p.JID, bEntry.ReasonEnable, v.Type) if err != nil { err := fmt.Errorf( "unable to ban user: id=%s, err=%w", id, err, ) j.GTomb.Kill(err) } return err } case useragent.Name != "" && useragent.Version == "" && useragent.Os == "": if ver.Name == useragent.Name { log.Warnf("Hammer falls on %s (%s): Software Name match entry in blacklist", p.From, p.JID) id, err := j.Squash(room, p.JID, bEntry.ReasonEnable, v.Type) if err != nil { err := fmt.Errorf( "unable to ban user: id=%s, err=%w", id, err, ) j.GTomb.Kill(err) } return err } case useragent.Name == "" && useragent.Version != "" && useragent.Os == "": if ver.Version == useragent.Version { log.Warnf("Hammer falls on %s (%s): Software Version match entry in blacklist", p.From, p.JID) id, err := j.Squash(room, p.JID, bEntry.ReasonEnable, v.Type) if err != nil { err := fmt.Errorf( "unable to ban user: id=%s, err=%w", id, err, ) j.GTomb.Kill(err) } return err } } } log.Debug("No matches in global software versions blacklist") } if bEntry.RoomName == room { log.Debug("Found blacklist entries for room") for _, useragent := range bEntry.UserAgent { log.Debugf("We have useragent blacklist entry Name=%s, Version=%s, OS=%s", useragent.Name, useragent.Version, useragent.Os) switch { case useragent.Name != "" && useragent.Version != "" && useragent.Os != "": if ver.Name == useragent.Name && ver.Version == useragent.Version && ver.Os == useragent.Os { log.Warnf("Hammer falls on %s (%s): Software Name, Version and OS match entry in blacklist", p.From, p.JID) id, err := j.Squash(room, p.JID, bEntry.ReasonEnable, v.Type) if err != nil { err := fmt.Errorf( "unable to ban user: id=%s, err=%w", id, err, ) j.GTomb.Kill(err) } return err } case useragent.Name != "" && useragent.Version != "" && useragent.Os == "": if ver.Name == useragent.Name && ver.Version == useragent.Version { log.Warnf("Hammer falls on %s (%s): Software Name and Version match entry in blacklist", p.From, p.JID) id, err := j.Squash(room, p.JID, bEntry.ReasonEnable, v.Type) if err != nil { err := fmt.Errorf( "unable to ban user: id=%s, err=%w", id, err, ) j.GTomb.Kill(err) } return err } case useragent.Name != "" && useragent.Version == "" && useragent.Os == "": if ver.Name == useragent.Name { log.Warnf("Hammer falls on %s (%s): Software Name match entry in blacklist", p.From, p.JID) id, err := j.Squash(room, p.JID, bEntry.ReasonEnable, v.Type) if err != nil { err := fmt.Errorf( "unable to ban user: id=%s, err=%w", id, err, ) j.GTomb.Kill(err) } return err } case useragent.Name == "" && useragent.Version != "" && useragent.Os == "": if ver.Version == useragent.Version { log.Warnf("Hammer falls on %s (%s): Software Version match entry in blacklist", p.From, p.JID) id, err := j.Squash(room, p.JID, bEntry.ReasonEnable, v.Type) if err != nil { err := fmt.Errorf( "unable to ban user: id=%s, err=%w", id, err, ) j.GTomb.Kill(err) } return err } } } log.Debugf("No matches in software versions blacklist for %s", room) } } } } } } } return err } /* vim: set ft=go noet ai ts=4 sw=4 sts=4: */