Initial implementation of on-enter ban

This commit is contained in:
eleksir
2023-09-02 10:22:39 -07:00
parent 0e9fa089c0
commit 59b92f450d
6 changed files with 115 additions and 26 deletions

View File

@@ -61,3 +61,8 @@ Cуществует несколько видов пинга:
------------------------------------------------------------------------------------------------------------------------
Причину бана можно только включить или выключить в конфиге на per-chat основе. Фразу настроить невозможно. (не хотелось
реализовывать парсинг темплейтов, как минимум даты-времени)
Парсинг списка и бан здобеев на моменте входа в комнату
------------------------------------------------------------------------------------------------------------------------
Бан реализован только для real jid-ов, но не для nick-ов. Пока непонятно, как изящно хранить и то и другое.

View File

@@ -55,25 +55,6 @@ make
помещает этот jid в список изгнанных из комнаты (бан-лист конкретной комнаты, в которой присутствует бот) и выгоняет
этого jid-а.
### В чём тонкость?
Ровно в том, что для исполнения действия, боту надо заметить presence участника, а presence посылается
при изменении состояния клиента. То есть, если jid уже был в комнате на момент захода бота в неё, то его не забанят
(во всяком случае моментально). Самый простой способ поместить таких персонажей в бан - просто кикнуть их. Ихний
affiliation сменится на none, бот заметит presence и, если jid подпадает под правила чёрного списка, то отправит этот
jid в бан.
Но, простите, ведь "по правилам", при заходе клиента в комнату боту присылается presence всех участников, чтобы он мог
построить локальный ростер комнаты и начать отслеживать изменения состояния участников конференции. Почему бы в этот
момент не банить злодеев?
Да, так делать можно, однако пока этого нет. Дело в том, что presence самого себя участнику приходит последним (после
presence-ов тех, кто находится в комнате на момент появления в ней бота), и, согласно этому событию, можно считать, что
клиент зашёл в комнату успешно и формально *только после этого*, клиент может что-то пытаться делать в этой комнате.
Соответственно, чтобы кого-то банить исходя из списка presense-ов, который прилетает на входе, необходимо реализовать
"одноразовую" очередь тех, кого имеет смысл забанить и разгрести её "когда будет можно". Эта задача факультативная,
возможно, она будет решена потом.
## Как работает белый список?
Белый список - это список jid-ов, которых банить нельзя ни при каких обстоятельствах. В нём всегда неявно присутствует

View File

@@ -3,6 +3,7 @@ package main
import (
"encoding/xml"
"fmt"
"sort"
"strings"
"time"
@@ -488,6 +489,9 @@ func parseEvent(e interface{}) { //nolint:maintidx,gocognit,gocyclo
// клиента забанят из-за спама "вошёл-вышел".
// Однако, это не значит, что такую ситуацию мы не должны корректным образом обрабатывать.
if v.Type == "unavailable" {
// Считаем, что мы больше не в комнате, поэтому не знаем, кто там есть
roomPresencesJid.Delete(v.From)
log.Error("Presence notification - looks like another instance of client leaves room")
if slices.Contains(roomsConnected, v.From) {
@@ -499,20 +503,72 @@ func parseEvent(e interface{}) { //nolint:maintidx,gocognit,gocyclo
log.Errorf(spew.Sdump(v))
}
} else {
log.Infof("Presence notification, Type: %s, From: %s, To: %s Show: %s, Status: %s, Affiliation: %s, Role: %s, JID: %s",
v.Type, v.From, v.To, v.Show, v.Status, v.Affiliation, v.Role, v.JID)
log.Infof(
"Presence notification, Type: %s, From: %s, To: %s Show: %s, Status: %s, Affiliation: %s, Role: %s, JID: %s",
v.Type, v.From, v.To, v.Show, v.Status, v.Affiliation, v.Role, v.JID,
)
room := strings.SplitN(v.From, "/", 2)[0]
nick := strings.SplitN(v.From, "/", 2)[1]
if nick == "" {
log.Infof("Presence stanza contains incorrect from attribute: %s", v.From)
return
}
// Это наш собственный Presence
if v.Show == "" && v.Status == "" {
if from := strings.Split(v.From, "/"); from[1] == config.Jabber.Nick {
roomsConnected = append(roomsConnected, from[0])
if nick == config.Jabber.Nick {
roomsConnected = append(roomsConnected, room)
// На всякий случай дедуплицируем список комнат, к которым мы заджойнились.
sort.Strings(roomsConnected)
slices.Compact(roomsConnected)
}
}
if err := buny(v); err != nil {
gTomb.Kill(err)
switch v.Role {
// Участник ушёл
case "none":
if namesInterface, present := roomPresencesJid.Get(room); present {
var newNames []string
names := interfaceToStringSlice(namesInterface)
return
for _, name := range names {
if name != v.JID {
newNames = append(newNames, name)
}
}
if len(newNames) == 0 {
roomPresencesJid.Delete(room)
} else {
roomPresencesJid.Set(room, newNames)
}
}
// Участник пришёл
default:
var newNames []string
if namesInterface, present := roomPresencesJid.Get(room); present {
names := interfaceToStringSlice(namesInterface)
names = append(names, v.JID)
roomPresencesJid.Set(room, names)
} else {
newNames = append(newNames, v.JID)
roomPresencesJid.Set(room, newNames)
}
}
// Проверяем, а не злодей ли зашёл? Сделать это мы можем, только если мы находимся в комнате.
// По правилам, мы можем что-то делать, только после того, как нам прилетит наш собственный presence, это
// значит, что мы вошли в комнату.
if slices.Contains(roomsConnected, room) {
if err := buny(v); err != nil {
gTomb.Kill(err)
return
}
}
}

View File

@@ -66,4 +66,7 @@ var serverPingTimestampRx int64
// Объектик для хранения стейта утилизатора горутинок.
var gTomb tomb.Tomb
// sync.Map-ка со списком участников конференций.
var roomPresencesJid *Collection
/* vim: set ft=go noet ai ts=4 sw=4 sts=4: */

43
lib.go
View File

@@ -6,6 +6,7 @@ import (
"math/rand"
"os"
"path/filepath"
"reflect"
"syscall"
"time"
@@ -580,6 +581,30 @@ func joinMuc(room string) {
log.Infof("Joined to MUC: %s", room)
go RotateStatus(room)
// Время прoверить участников на предмет злобности
namesInterface, present := roomPresencesJid.Get(room)
// Если room есть в списке presence-ов, то фигачим. room там должен быть, просто обязан.
if present {
for _, name := range interfaceToStringSlice(namesInterface) {
var v xmpp.Presence
v.From = name
v.To = fmt.Sprintf("%s/%s", room, config.Jabber.Nick)
v.ID = "fake_id"
v.JID = v.From
v.Affiliation = "member"
v.Role = "participant"
v.Priority = ""
v.Show = ""
v.Status = ""
// Оно там внутри всё само обработает, если вдруг возникнет wire error, то зарекконетится.
_ = buny(v)
}
}
// TODO: тоже самое и для nick-ов
}
// probeServerLiveness проверяет живость соединения с сервером. Для многих серверов обязательная штука, без которой
@@ -822,4 +847,22 @@ func randomPhrase(list []string) string {
return phrase
}
// interfaceToStringSlice превращает данный интерфейс в слайс строк.
// Если может, конечно :)
func interfaceToStringSlice(iface interface{}) []string {
var mySlice []string
// А теперь мы начинаем дурдом, нам надо превратить ёбанный interface{} в []string
// Поскольку interface{} может быть чем угодно, перестрахуемся
if reflect.TypeOf(iface).Kind() == reflect.Slice {
shit := reflect.ValueOf(iface)
for i := 0; i < shit.Len(); i++ {
mySlice = append(mySlice, fmt.Sprint(shit.Index(i)))
}
}
return mySlice
}
/* vim: set ft=go noet ai ts=4 sw=4 sts=4: */

View File

@@ -128,6 +128,7 @@ func myLoop() {
mucCapsList = NewCollection()
serverPingTimestampTx = 0
serverPingTimestampRx = 0
roomPresencesJid = NewCollection()
// Установим коннект
establishConnection()