diff --git a/cmd/root.go b/cmd/root.go index 3dd8113..5304020 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -18,7 +18,6 @@ package cmd import ( - "github.com/Unkn0wnCat/matrix-veles/internal/config" "github.com/spf13/viper" "log" "os" @@ -49,13 +48,16 @@ func Execute() { func init() { rootCmd.PersistentFlags().StringVar(&cfgFile, "config", "", "config file (default is ./config.yaml)") - viper.SetDefault("bot.homeserver", "https://matrix.org") + viper.SetDefault("bot.homeserver", "matrix.org") + viper.SetDefault("bot.homeserver_url", "https://matrix.org") viper.SetDefault("bot.username", "") viper.SetDefault("bot.password", "") viper.SetDefault("bot.accessKey", "") - viper.SetDefault("bot.rooms", config.RoomConfigTree{}) viper.SetDefault("bot.mongo.uri", "mongodb://localhost:27017") viper.SetDefault("bot.mongo.database", "veles") + viper.SetDefault("bot.mongo.collection.entries", "entries") + viper.SetDefault("bot.mongo.collection.lists", "lists") + viper.SetDefault("bot.mongo.collection.rooms", "rooms") cobra.OnInitialize(loadConfig) } diff --git a/internal/bot/getState.go b/internal/bot/getState.go new file mode 100644 index 0000000..6624816 --- /dev/null +++ b/internal/bot/getState.go @@ -0,0 +1,80 @@ +package bot + +import ( + "encoding/json" + "fmt" + "maunium.net/go/mautrix" + "maunium.net/go/mautrix/event" + "maunium.net/go/mautrix/id" +) + +type StateEventPL struct { + Type string `json:"type"` + Sender string `json:"sender"` + RoomID string `json:"room_id"` + EventID string `json:"event_id"` + OriginServerTS int64 `json:"origin_server_ts"` + Content StateEventPLContent `json:"content"` + Unsigned struct { + Age int `json:"age"` + } `json:"unsigned"` +} + +type StateEventPLContent struct { + Ban int `json:"ban"` + Events map[string]int `json:"events"` + EventsDefault int `json:"events_default"` + Invite int `json:"invite"` + Kick int `json:"kick"` + Notifications map[string]int `json:"notifications"` + Redact int `json:"redact"` + StateDefault int `json:"state_default"` + Users map[string]int `json:"users"` + UsersDefault int `json:"users_default"` +} + +func GetRoomState(matrixClient *mautrix.Client, roomId id.RoomID) (*StateEventPLContent, error) { + url := matrixClient.BuildURL("rooms", roomId.String(), "state") + res, err := matrixClient.MakeRequest("GET", url, nil, nil) + if err != nil { + return nil, fmt.Errorf("ERROR: Could request room state - %v", err) + } + + var stateEvents []StateEventPL + + err = json.Unmarshal(res, &stateEvents) + if err != nil { + return nil, fmt.Errorf("ERROR: Could parse room state - %v", err) + } + + var plEventContent StateEventPLContent + + found := false + + for _, e2 := range stateEvents { + if e2.Type != event.StatePowerLevels.Type { + continue + } + + found = true + plEventContent = e2.Content + } + + if !found { + return nil, fmt.Errorf("ERROR: Could find room power level - %v", err) + } + + if plEventContent.Events == nil { + plEventContent.Events = make(map[string]int) + } + + if plEventContent.Notifications == nil { + plEventContent.Notifications = make(map[string]int) + } + + if plEventContent.Users == nil { + plEventContent.Users = make(map[string]int) + } + + return &plEventContent, nil +} diff --git a/internal/bot/handleHashing.go b/internal/bot/handleHashing.go index 5c7b5b2..e9947c8 100644 --- a/internal/bot/handleHashing.go +++ b/internal/bot/handleHashing.go @@ -15,7 +15,6 @@ import ( "log" "maunium.net/go/mautrix" "maunium.net/go/mautrix/event" - "maunium.net/go/mautrix/id" ) func handleHashing(content *event.MessageEventContent, evt *event.Event, matrixClient *mautrix.Client) { @@ -86,6 +85,36 @@ func handleHashing(content *event.MessageEventContent, evt *event.Event, matrixC matrixClient.SendNotice(evt.RoomID, fmt.Sprintf("DEBUG:\n%s", buf.String())) } + if roomConfig.HashChecker.SubscribedLists == nil { + log.Printf("Room %s is not subscribed to any lists!", roomConfig.RoomID) + return // Not subscribed to any lists + } + + subMap := make(map[string]bool) + + for _, listId := range roomConfig.HashChecker.SubscribedLists { + subMap[listId.Hex()] = true + } + + found := false + log.Printf("%v", subMap) + + for _, listId := range hashObj.PartOf { + _, ok := subMap[listId.Hex()] + + if ok { + found = true + break + } + } + + if !found { + log.Printf("Room %s is not subscribed to any lists of hashobj %s!", roomConfig.RoomID, hashObj.ID.Hex()) + return // Not subscribed + } + + log.Printf("Illegal content detected in room %s!", roomConfig.RoomID) + handleIllegalContent(evt, matrixClient, hashObj, roomConfig) } @@ -97,78 +126,6 @@ func redactMessage(evt *event.Event, matrixClient *mautrix.Client, hashObj *mode } } -type StateEventPL struct { - Type string `json:"type"` - Sender string `json:"sender"` - RoomID string `json:"room_id"` - EventID string `json:"event_id"` - OriginServerTS int64 `json:"origin_server_ts"` - Content StateEventPLContent `json:"content"` - Unsigned struct { - Age int `json:"age"` - } `json:"unsigned"` -} - -type StateEventPLContent struct { - Ban int `json:"ban"` - Events map[string]int `json:"events"` - EventsDefault int `json:"events_default"` - Invite int `json:"invite"` - Kick int `json:"kick"` - Notifications map[string]int `json:"notifications"` - Redact int `json:"redact"` - StateDefault int `json:"state_default"` - Users map[string]int `json:"users"` - UsersDefault int `json:"users_default"` -} - -func GetRoomState(matrixClient *mautrix.Client, roomId id.RoomID) (*StateEventPLContent, error) { - url := matrixClient.BuildURL("rooms", roomId.String(), "state") - log.Println(url) - res, err := matrixClient.MakeRequest("GET", url, nil, nil) - if err != nil { - return nil, fmt.Errorf("ERROR: Could request room state - %v", err) - } - - var stateEvents []StateEventPL - - err = json.Unmarshal(res, &stateEvents) - if err != nil { - return nil, fmt.Errorf("ERROR: Could parse room state - %v", err) - } - - var plEventContent StateEventPLContent - - found := false - - for _, e2 := range stateEvents { - if e2.Type != event.StatePowerLevels.Type { - continue - } - - found = true - plEventContent = e2.Content - } - - if !found { - return nil, fmt.Errorf("ERROR: Could find room power level - %v", err) - } - - if plEventContent.Events == nil { - plEventContent.Events = make(map[string]int) - } - - if plEventContent.Notifications == nil { - plEventContent.Notifications = make(map[string]int) - } - - if plEventContent.Users == nil { - plEventContent.Users = make(map[string]int) - } - - return &plEventContent, nil -} - func muteUser(evt *event.Event, matrixClient *mautrix.Client, hashObj *model.DBEntry) { plEventContent, err := GetRoomState(matrixClient, evt.RoomID) if err != nil { @@ -186,6 +143,15 @@ func muteUser(evt *event.Event, matrixClient *mautrix.Client, hashObj *model.DBE } +func banUser(evt *event.Event, matrixClient *mautrix.Client, hashObj *model.DBEntry) { + req := mautrix.ReqBanUser{ + Reason: fmt.Sprintf("Veles has detected an hash-map-match! Tags: %s, ID: %s", hashObj.Tags, hashObj.ID.Hex()), + UserID: evt.Sender, + } + + matrixClient.BanUser(evt.RoomID, &req) +} + func postNotice(evt *event.Event, matrixClient *mautrix.Client, hashObj *model.DBEntry, roomConfig config.RoomConfig) { local, server, err := evt.Sender.Parse() if err != nil { @@ -198,18 +164,39 @@ If you believe this action was an accident, please contact an room administrator } +/*func msgMods(evt *event.Event, matrixClient *mautrix.Client, hashObj *model.DBEntry, roomConfig config.RoomConfig) { + local, server, err := evt.Sender.Parse() + if err != nil { + return + } + SendAlert(matrixClient, evt.RoomID.String(), fmt.Sprintf( + `Veles Triggered: The message by %s (on %s) was flagged for containing material used by spammers or trolls! (Reference: %s)`, local, server, hashObj.ID.Hex())) +}*/ + func handleIllegalContent(evt *event.Event, matrixClient *mautrix.Client, hashObj *model.DBEntry, roomConfig config.RoomConfig) { - switch roomConfig.HashCheckMode { + switch roomConfig.HashChecker.HashCheckMode { case 0: + postNotice(evt, matrixClient, hashObj, roomConfig) break case 1: + redactMessage(evt, matrixClient, hashObj) + if roomConfig.HashChecker.NoticeToChat { + postNotice(evt, matrixClient, hashObj, roomConfig) + } break case 2: muteUser(evt, matrixClient, hashObj) redactMessage(evt, matrixClient, hashObj) - postNotice(evt, matrixClient, hashObj, roomConfig) + if roomConfig.HashChecker.NoticeToChat { + postNotice(evt, matrixClient, hashObj, roomConfig) + } break case 3: + banUser(evt, matrixClient, hashObj) + redactMessage(evt, matrixClient, hashObj) + if roomConfig.HashChecker.NoticeToChat { + postNotice(evt, matrixClient, hashObj, roomConfig) + } break } diff --git a/internal/bot/sendAlert.go b/internal/bot/sendAlert.go new file mode 100644 index 0000000..e443ee2 --- /dev/null +++ b/internal/bot/sendAlert.go @@ -0,0 +1,47 @@ +package bot + +/*func SendAlert(matrixClient *mautrix.Client, room string, message string) { + roomConfig, err := config.GetRoomConfigByRoomID(room) + if err != nil { + log.Printf("Failed to get room config - %v", err) + return + } + + if roomConfig.AlertChannel == nil { + roomPLState, err := GetRoomState(matrixClient, id.RoomID(room)) + if err != nil { + log.Printf("Failed to get room power levels - %v", err) + return + } + + var mods []id.UserID + + for member, level := range roomPLState.Users { + if level >= roomConfig.HashChecker.NotificationPowerLevel { + mods = append(mods, id.UserID(member)) + } + } + + req := mautrix.ReqCreateRoom{ + Name: "Veles Alert Channel", + Topic: "Veles Alerts", + Invite: mods, + IsDirect: true, + Visibility: "private", + } + + resp, err := matrixClient.CreateRoom(&req) + if err != nil { + log.Printf("Failed to create alert room - %v", err) + return + } + + str := resp.RoomID.String() + + roomConfig.AlertChannel = &str + + config.SaveRoomConfig(roomConfig) + } + + matrixClient.SendNotice(id.RoomID(*roomConfig.AlertChannel), message) +}*/ diff --git a/internal/config/helpers.go b/internal/config/helpers.go index 640b4f6..de10ab2 100644 --- a/internal/config/helpers.go +++ b/internal/config/helpers.go @@ -42,7 +42,7 @@ func SetRoomConfigActive(id string, active bool) { roomConfigWg.Add(1) roomConfig := GetRoomConfig(id) - roomConfig.Active = true + roomConfig.Active = active err := SaveRoomConfig(&roomConfig) if err != nil { @@ -67,9 +67,9 @@ func GetRoomConfig(id string) RoomConfig { // RoomConfigInitialUpdate updates all RoomConfig entries to set activity and create blank configs func RoomConfigInitialUpdate(ids []id.RoomID) { - db := db.DbClient.Database(viper.GetString("bot.mongo.database")) + database := db.DbClient.Database(viper.GetString("bot.mongo.database")) - cursor, err := db.Collection("rooms").Find(context.TODO(), bson.D{}, nil) + cursor, err := database.Collection("rooms").Find(context.TODO(), bson.D{}, nil) if err != nil { log.Panicf("Error querying room configs: %v", err) } @@ -105,7 +105,8 @@ func AddRoomConfig(id string) RoomConfig { roomConfigWg.Wait() roomConfigWg.Add(1) - config := RoomConfig{ID: primitive.NewObjectID(), Active: true, RoomID: id} + config := GetDefaultRoomConfig() + config.RoomID = id err := SaveRoomConfig(&config) if err != nil { @@ -119,26 +120,26 @@ func AddRoomConfig(id string) RoomConfig { } func SaveRoomConfig(roomConfig *RoomConfig) error { - db := db.DbClient.Database(viper.GetString("bot.mongo.database")) + database := db.DbClient.Database(viper.GetString("bot.mongo.database")) opts := options.Replace().SetUpsert(true) filter := bson.D{{"room_id", roomConfig.RoomID}} - _, err := db.Collection("rooms").ReplaceOne(context.TODO(), filter, roomConfig, opts) + _, err := database.Collection(viper.GetString("bot.mongo.collection.rooms")).ReplaceOne(context.TODO(), filter, roomConfig, opts) return err } func GetRoomConfigByRoomID(id string) (*RoomConfig, error) { - db := db.DbClient.Database(viper.GetString("bot.mongo.database")) + database := db.DbClient.Database(viper.GetString("bot.mongo.database")) - res := db.Collection("rooms").FindOne(context.TODO(), bson.D{{"room_id", id}}) + res := database.Collection(viper.GetString("bot.mongo.collection.rooms")).FindOne(context.TODO(), bson.D{{"room_id", id}}) if res.Err() != nil { return nil, res.Err() } - object := RoomConfig{} + object := GetDefaultRoomConfig() err := res.Decode(&object) if err != nil { @@ -147,3 +148,18 @@ func GetRoomConfigByRoomID(id string) (*RoomConfig, error) { return &object, nil } + +func GetDefaultRoomConfig() RoomConfig { + return RoomConfig{ + ID: primitive.NewObjectID(), + Active: true, + RoomID: "", + Debug: false, + AdminPowerLevel: 100, + HashChecker: HashCheckerConfig{ + NoticeToChat: true, + NotificationPowerLevel: 50, + HashCheckMode: 1, + }, + } +} diff --git a/internal/config/structs.go b/internal/config/structs.go index 641b338..888359f 100644 --- a/internal/config/structs.go +++ b/internal/config/structs.go @@ -35,14 +35,28 @@ type RoomConfig struct { // Debug specifies if the bot shall run in dry run mode Debug bool `yaml:"debug" bson:"debug"` + AlertChannel *string `bson:"alert_channel"` + + AdminPowerLevel int `bson:"admin_power_level"` + + HashChecker HashCheckerConfig `bson:"hash_checker"` +} + +type HashCheckerConfig struct { + NoticeToChat bool `bson:"chat_notice"` + + NotificationPowerLevel int `yaml:"notification_level" bson:"notification_level"` + /* HashCheckMode specifies the mode the bot should operate under in this room HashCheck-Modes: - 0. Silent Mode (Don't do anything) - 1. Notify Mode (Message Room Admins & Mods) - 2. Mute Mode (Remove message, notify admin & mute user) - 3. Ban Mode (Remove message, notify admin & ban user) + 0. Notice Mode (Post notice) + 1. Delete Mode (Remove message, post notice) + 2. Mute Mode (Remove message, post notice & mute user) + 3. Ban Mode (Remove message, post notice & ban user) */ HashCheckMode uint8 `yaml:"mode" bson:"hash_check_mode"` + + SubscribedLists []*primitive.ObjectID `bson:"subscribed_lists" json:"subscribed_lists"` } diff --git a/internal/db/db.go b/internal/db/db.go index 2711261..9321219 100644 --- a/internal/db/db.go +++ b/internal/db/db.go @@ -5,6 +5,7 @@ import ( "github.com/Unkn0wnCat/matrix-veles/internal/db/model" "github.com/spf13/viper" "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/bson/primitive" "go.mongodb.org/mongo-driver/mongo" "go.mongodb.org/mongo-driver/mongo/options" "log" @@ -37,15 +38,15 @@ func SaveEntry(entry *model.DBEntry) error { filter := bson.D{{"_id", entry.ID}} - _, err := db.Collection("entries").ReplaceOne(context.TODO(), filter, entry, opts) + _, err := db.Collection(viper.GetString("bot.mongo.collection.entries")).ReplaceOne(context.TODO(), filter, entry, opts) return err } -func GetEntryByHash(hash string) (*model.DBEntry, error) { +func GetEntryByID(id primitive.ObjectID) (*model.DBEntry, error) { db := DbClient.Database(viper.GetString("bot.mongo.database")) - res := db.Collection("entries").FindOne(context.TODO(), bson.D{{"hash_value", hash}}) + res := db.Collection(viper.GetString("bot.mongo.collection.entries")).FindOne(context.TODO(), bson.D{{"_id", id}}) if res.Err() != nil { return nil, res.Err() } @@ -59,3 +60,51 @@ func GetEntryByHash(hash string) (*model.DBEntry, error) { return &object, nil } + +func GetEntryByHash(hash string) (*model.DBEntry, error) { + db := DbClient.Database(viper.GetString("bot.mongo.database")) + + res := db.Collection(viper.GetString("bot.mongo.collection.entries")).FindOne(context.TODO(), bson.D{{"hash_value", hash}}) + if res.Err() != nil { + return nil, res.Err() + } + + object := model.DBEntry{} + + err := res.Decode(&object) + if err != nil { + return nil, err + } + + return &object, nil +} + +func SaveList(list *model.DBHashList) error { + db := DbClient.Database(viper.GetString("bot.mongo.database")) + + opts := options.Replace().SetUpsert(true) + + filter := bson.D{{"_id", list.ID}} + + _, err := db.Collection(viper.GetString("bot.mongo.collection.lists")).ReplaceOne(context.TODO(), filter, list, opts) + + return err +} + +func GetListByID(id primitive.ObjectID) (*model.DBHashList, error) { + db := DbClient.Database(viper.GetString("bot.mongo.database")) + + res := db.Collection(viper.GetString("bot.mongo.collection.lists")).FindOne(context.TODO(), bson.D{{"_id", id}}) + if res.Err() != nil { + return nil, res.Err() + } + + object := model.DBHashList{} + + err := res.Decode(&object) + if err != nil { + return nil, err + } + + return &object, nil +} diff --git a/internal/db/model/comment.go b/internal/db/model/comment.go new file mode 100644 index 0000000..87c9efb --- /dev/null +++ b/internal/db/model/comment.go @@ -0,0 +1,8 @@ +package model + +import "go.mongodb.org/mongo-driver/bson/primitive" + +type DBComment struct { + CommentedBy *primitive.ObjectID `bson:"commented_by" json:"commented_by"` + Content string `bson:"content" json:"content"` +} diff --git a/internal/db/model/entry.go b/internal/db/model/entry.go index 1b70fdb..b89b2a5 100644 --- a/internal/db/model/entry.go +++ b/internal/db/model/entry.go @@ -6,16 +6,12 @@ import ( ) type DBEntry struct { - ID primitive.ObjectID `bson:"_id" json:"id"` - Tags []string `bson:"tags" json:"tags"` - HashValue string `bson:"hash_value" json:"hash"` - FileURL string `bson:"file_url" json:"file_url"` - Timestamp time.Time `bson:"timestamp" json:"timestamp"` - AddedBy *primitive.ObjectID `bson:"added_by" json:"added_by"` - Comments []*DBEntryComment `bson:"comments" json:"comments"` -} - -type DBEntryComment struct { - CommentedBy *primitive.ObjectID `bson:"commented_by" json:"commented_by"` - Content string `bson:"content" json:"content"` + ID primitive.ObjectID `bson:"_id" json:"id"` + Tags []string `bson:"tags" json:"tags"` + PartOf []*primitive.ObjectID `bson:"part_of" json:"part_of"` + HashValue string `bson:"hash_value" json:"hash"` + FileURL string `bson:"file_url" json:"file_url"` + Timestamp time.Time `bson:"timestamp" json:"timestamp"` + AddedBy *primitive.ObjectID `bson:"added_by" json:"added_by"` + Comments []*DBComment `bson:"comments" json:"comments"` } diff --git a/internal/db/model/list.go b/internal/db/model/list.go new file mode 100644 index 0000000..b280cda --- /dev/null +++ b/internal/db/model/list.go @@ -0,0 +1,10 @@ +package model + +import "go.mongodb.org/mongo-driver/bson/primitive" + +type DBHashList struct { + ID primitive.ObjectID `bson:"_id" json:"id"` + Tags []string `bson:"tags" json:"tags"` + Comments []*DBComment `bson:"comments" json:"comments"` + Maintainers []*primitive.ObjectID `bson:"maintainers" json:"maintainers"` +} diff --git a/internal/db/model/user.go b/internal/db/model/user.go new file mode 100644 index 0000000..b7785ae --- /dev/null +++ b/internal/db/model/user.go @@ -0,0 +1,38 @@ +package model + +import ( + "encoding/base64" + "golang.org/x/crypto/bcrypt" +) + +type DBUser struct { + Username string `bson:"username" json:"username"` + HashedPassword string `bson:"password" json:"password"` + + Password *string `bson:"-" json:"-"` +} + +func (usr *DBUser) HashPassword() error { + if usr.Password == nil { + return nil + } + + hash, err := bcrypt.GenerateFromPassword([]byte(*usr.Password), 14) + if err != nil { + return err + } + + usr.HashedPassword = base64.StdEncoding.EncodeToString(hash) + return nil +} + +func (usr *DBUser) CheckPassword(password string) error { + hash, err := base64.StdEncoding.DecodeString(usr.HashedPassword) + if err != nil { + return err + } + + err = bcrypt.CompareHashAndPassword(hash, []byte(password)) + + return err +}