mirror of
https://github.com/Unkn0wnCat/matrix-veles.git
synced 2025-08-04 00:58:39 +02:00
Initial commit
This commit is contained in:
commit
b81af24e50
21 changed files with 2283 additions and 0 deletions
67
internal/bot/acceptInvites.go
Normal file
67
internal/bot/acceptInvites.go
Normal file
|
@ -0,0 +1,67 @@
|
|||
/*
|
||||
* Copyright © 2022 Kevin Kandlbinder.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*
|
||||
*/
|
||||
|
||||
package bot
|
||||
|
||||
import (
|
||||
"github.com/Unkn0wnCat/matrix-veles/internal/config"
|
||||
"log"
|
||||
"maunium.net/go/mautrix"
|
||||
"maunium.net/go/mautrix/id"
|
||||
)
|
||||
|
||||
// isInRoom checks if the given mautrix.Client is joined in the given room
|
||||
func isInRoom(client *mautrix.Client, id id.RoomID) (bool, error) {
|
||||
res, err := client.JoinedRooms()
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
for _, joinedRoom := range res.JoinedRooms {
|
||||
if joinedRoom == id {
|
||||
return true, nil
|
||||
} // If this is the room we're searching for, return from function
|
||||
}
|
||||
|
||||
// If we arrived here there is no room we joined with the given ID
|
||||
return false, nil
|
||||
}
|
||||
|
||||
// doAcceptInvite accepts the invite to the given room with the given mautrix.Client
|
||||
func doAcceptInvite(client *mautrix.Client, id id.RoomID) {
|
||||
roomAlreadyJoined, err := isInRoom(client, id)
|
||||
if err != nil {
|
||||
log.Printf("Could not accept invite to %s due to internal error", id)
|
||||
log.Println(err)
|
||||
return
|
||||
}
|
||||
|
||||
if roomAlreadyJoined {
|
||||
return
|
||||
} // If the room was already joined, ignore
|
||||
|
||||
_, err = client.JoinRoom(id.String(), "", nil)
|
||||
if err != nil {
|
||||
log.Printf("Could not accept invite to %s due to join error", id)
|
||||
log.Println(err)
|
||||
return
|
||||
}
|
||||
|
||||
log.Printf("Successfully joined room %s", id)
|
||||
|
||||
config.AddRoomConfig(id.String())
|
||||
}
|
233
internal/bot/bot.go
Normal file
233
internal/bot/bot.go
Normal file
|
@ -0,0 +1,233 @@
|
|||
/*
|
||||
* Copyright © 2022 Kevin Kandlbinder.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*
|
||||
*/
|
||||
|
||||
package bot
|
||||
|
||||
import (
|
||||
"github.com/Unkn0wnCat/matrix-veles/internal/config"
|
||||
"github.com/spf13/viper"
|
||||
"log"
|
||||
"maunium.net/go/mautrix"
|
||||
"maunium.net/go/mautrix/event"
|
||||
"maunium.net/go/mautrix/id"
|
||||
"os"
|
||||
"os/signal"
|
||||
"strings"
|
||||
"syscall"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Run starts the bot, blocking until an interrupt or SIGTERM is received
|
||||
func Run() {
|
||||
// Set up signal channel for controlled shutdown
|
||||
c := make(chan os.Signal)
|
||||
signal.Notify(c, os.Interrupt, syscall.SIGTERM)
|
||||
|
||||
// Save Timestamp for filtering
|
||||
startTs := time.Now().Unix()
|
||||
|
||||
checkConfig()
|
||||
|
||||
log.Printf("matrix-veles has started.")
|
||||
|
||||
// Initialize client, this does not check access key!
|
||||
matrixClient, err := mautrix.NewClient(
|
||||
viper.GetString("bot.homeserver_url"),
|
||||
id.NewUserID(viper.GetString("bot.username"), viper.GetString("bot.homeserver")),
|
||||
viper.GetString("bot.accessKey"))
|
||||
if err != nil {
|
||||
log.Printf("matrix-veles couldn't initialize matrix client, please check credentials")
|
||||
log.Fatal(err)
|
||||
return
|
||||
}
|
||||
|
||||
// If no accessKey is set, perform login
|
||||
if viper.GetString("bot.accessKey") == "" {
|
||||
performLogin(matrixClient)
|
||||
}
|
||||
|
||||
// Set up sync handlers for invites and messages
|
||||
syncer := matrixClient.Syncer.(*mautrix.DefaultSyncer)
|
||||
syncer.OnEventType(event.EventMessage, handleMessageEvent(matrixClient, startTs))
|
||||
syncer.OnEventType(event.StateMember, handleMemberEvent(matrixClient, startTs))
|
||||
|
||||
// Set up async tasks
|
||||
go startSync(matrixClient)
|
||||
go doInitialUpdate(matrixClient)
|
||||
|
||||
<-c
|
||||
log.Printf("Shutting down...")
|
||||
|
||||
matrixClient.StopSync()
|
||||
|
||||
log.Printf("Goodbye!")
|
||||
|
||||
os.Exit(0)
|
||||
}
|
||||
|
||||
// checkConfig applies constraints to the configuration and exits the program on violation
|
||||
func checkConfig() {
|
||||
// Both homeserver and username are required!
|
||||
if viper.GetString("bot.homeserver") == "" || viper.GetString("bot.username") == "" {
|
||||
log.Printf("matrix-veles is missing user identification (homeserver / username)")
|
||||
os.Exit(1)
|
||||
return
|
||||
}
|
||||
|
||||
// Either accessKey or password are required
|
||||
if viper.GetString("bot.accessKey") == "" && viper.GetString("bot.password") == "" {
|
||||
log.Printf("matrix-veles is missing user credentials (access-key / password)")
|
||||
log.Printf("Please provide either an access-key or password")
|
||||
os.Exit(1)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// performLogin logs in the mautrix.Client using the username and password from config
|
||||
func performLogin(matrixClient *mautrix.Client) {
|
||||
res, err := matrixClient.Login(&mautrix.ReqLogin{
|
||||
Type: "m.login.password",
|
||||
Identifier: mautrix.UserIdentifier{Type: mautrix.IdentifierTypeUser, User: viper.GetString("bot.username")},
|
||||
Password: viper.GetString("bot.password"),
|
||||
StoreCredentials: true,
|
||||
InitialDeviceDisplayName: "github.com/Unkn0wnCat/matrix-veles",
|
||||
})
|
||||
if err != nil {
|
||||
log.Printf("matrix-veles couldn't sign in, please check credentials")
|
||||
log.Fatal(err)
|
||||
return
|
||||
}
|
||||
|
||||
accessToken := res.AccessToken
|
||||
|
||||
// Save accessKey to configuration
|
||||
viper.Set("bot.accessKey", accessToken)
|
||||
err = viper.WriteConfig()
|
||||
if err != nil {
|
||||
log.Printf("matrix-veles could not save the accessKey to config")
|
||||
log.Fatal(err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// doInitialUpdate updates the config right after startup to catch up with joined/left rooms
|
||||
func doInitialUpdate(matrixClient *mautrix.Client) {
|
||||
resp, err := matrixClient.JoinedRooms()
|
||||
if err != nil {
|
||||
log.Printf("matrix-veles could not read joined rooms, something is horribly wrong")
|
||||
log.Fatalln(err)
|
||||
}
|
||||
|
||||
// Hand-off list to config helper
|
||||
config.RoomConfigInitialUpdate(resp.JoinedRooms)
|
||||
}
|
||||
|
||||
// handleMessageEvent wraps message handler taking the mautrix.Client and start timestamp as parameters
|
||||
func handleMessageEvent(matrixClient *mautrix.Client, startTs int64) mautrix.EventHandler {
|
||||
return func(source mautrix.EventSource, evt *event.Event) {
|
||||
if evt.Timestamp < (startTs * 1000) {
|
||||
// Ignore old events
|
||||
return
|
||||
}
|
||||
|
||||
// Cast event to correct event type
|
||||
content, ok := evt.Content.Parsed.(*event.MessageEventContent)
|
||||
|
||||
if !ok {
|
||||
log.Println("Uh oh, could not typecast m.room.member event content...")
|
||||
return
|
||||
}
|
||||
|
||||
username, _, err := matrixClient.UserID.Parse()
|
||||
if err != nil {
|
||||
log.Panicln("Invalid user id in client")
|
||||
}
|
||||
|
||||
if content.URL != "" {
|
||||
handleHashing(content, evt, matrixClient)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
if !strings.HasPrefix(content.Body, "!"+username) &&
|
||||
!strings.HasPrefix(content.Body, "@"+username) &&
|
||||
!(strings.HasPrefix(content.Body, username) && strings.HasPrefix(content.FormattedBody, "<a href=\"https://matrix.to/#/"+matrixClient.UserID.String()+"\">")) {
|
||||
return
|
||||
}
|
||||
|
||||
handleCommand(content.Body, evt.Sender, evt.RoomID, matrixClient)
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
// handleMemberEvent wraps m.room.member (invite, join, leave, ban etc.) handler taking the mautrix.Client and start timestamp as parameters
|
||||
func handleMemberEvent(matrixClient *mautrix.Client, startTs int64) func(source mautrix.EventSource, evt *event.Event) {
|
||||
return func(source mautrix.EventSource, evt *event.Event) {
|
||||
if *evt.StateKey != matrixClient.UserID.String() {
|
||||
return
|
||||
} // This does not concern us, as we are not the subject
|
||||
if evt.Timestamp < (startTs * 1000) {
|
||||
// Ignore old events, TODO: Handle missed invites.
|
||||
return
|
||||
}
|
||||
|
||||
// Cast event to correct event type
|
||||
content, ok := evt.Content.Parsed.(*event.MemberEventContent)
|
||||
|
||||
if !ok {
|
||||
log.Println("Uh oh, could not typecast m.room.member event content...")
|
||||
return
|
||||
}
|
||||
|
||||
// If it is an invite, accept it
|
||||
if content.Membership == event.MembershipInvite {
|
||||
doAcceptInvite(matrixClient, evt.RoomID)
|
||||
return
|
||||
}
|
||||
|
||||
// If it is our join event, set room to active
|
||||
if content.Membership == event.MembershipJoin {
|
||||
config.SetRoomConfigActive(evt.RoomID.String(), true)
|
||||
return
|
||||
}
|
||||
|
||||
// If we left or got banned, set room to inactive
|
||||
if content.Membership.IsLeaveOrBan() {
|
||||
config.SetRoomConfigActive(evt.RoomID.String(), false)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// startSync starts the mautrix.Client sync for receiving events
|
||||
func startSync(matrixClient *mautrix.Client) {
|
||||
err := matrixClient.Sync()
|
||||
if err != nil {
|
||||
log.Printf("matrix-veles has encountered a fatal error whilst syncing")
|
||||
log.Println(err)
|
||||
os.Exit(2)
|
||||
}
|
||||
log.Println("sync exited.")
|
||||
}
|
||||
|
||||
// formattedMessage is a helper struct for sending HTML content
|
||||
type formattedMessage struct {
|
||||
Type string `json:"msgtype"`
|
||||
Body string `json:"body"`
|
||||
Format string `json:"format"`
|
||||
FormattedBody string `json:"formatted_body"`
|
||||
}
|
59
internal/bot/commandParser.go
Normal file
59
internal/bot/commandParser.go
Normal file
|
@ -0,0 +1,59 @@
|
|||
/*
|
||||
* Copyright © 2022 Kevin Kandlbinder.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*
|
||||
*/
|
||||
|
||||
package bot
|
||||
|
||||
import (
|
||||
"log"
|
||||
"maunium.net/go/mautrix"
|
||||
"maunium.net/go/mautrix/id"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// handleCommand takes a command, parses it and executes any actions it implies
|
||||
func handleCommand(command string, sender id.UserID, id id.RoomID, client *mautrix.Client) {
|
||||
myUsername, _, err := client.UserID.Parse()
|
||||
if err != nil {
|
||||
log.Panicln("Invalid user id in client")
|
||||
}
|
||||
|
||||
command = strings.TrimPrefix(command, "!") // Remove !
|
||||
command = strings.TrimPrefix(command, "@") // Remove @
|
||||
command = strings.TrimPrefix(command, myUsername) // Remove our own username
|
||||
command = strings.TrimPrefix(command, ":") // Remove : (as in "@soccerbot:")
|
||||
command = strings.TrimSpace(command)
|
||||
|
||||
// TODO: Remove this, it is debug!
|
||||
log.Println(command)
|
||||
|
||||
// Is this a help command?
|
||||
if strings.HasPrefix(command, "help") {
|
||||
commandHelp(sender, id, client)
|
||||
return
|
||||
}
|
||||
|
||||
// No match :( - display help
|
||||
commandHelp(sender, id, client)
|
||||
return
|
||||
}
|
||||
|
||||
func commandHelp(_ id.UserID, id id.RoomID, client *mautrix.Client) {
|
||||
// TODO: Improve help message
|
||||
|
||||
// Ignore errors as we can't do anything about them, the user will probably retry
|
||||
_, _ = client.SendNotice(id, "matrix-veles help\n\n!veles-bot help - shows this help")
|
||||
}
|
216
internal/bot/handleHashing.go
Normal file
216
internal/bot/handleHashing.go
Normal file
|
@ -0,0 +1,216 @@
|
|||
package bot
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/sha512"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"github.com/Unkn0wnCat/matrix-veles/internal/config"
|
||||
"github.com/Unkn0wnCat/matrix-veles/internal/db"
|
||||
"github.com/Unkn0wnCat/matrix-veles/internal/db/model"
|
||||
"go.mongodb.org/mongo-driver/mongo"
|
||||
"io"
|
||||
"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) {
|
||||
url, err := content.URL.Parse()
|
||||
if err != nil {
|
||||
log.Printf("Error: Could not parse Content-URL: \"%s\" - %v", content.URL, err)
|
||||
return
|
||||
}
|
||||
|
||||
reader, err := matrixClient.Download(url)
|
||||
if err != nil {
|
||||
log.Printf("Error: Could not read file from Content-URL: \"%s\" - %v", content.URL, err)
|
||||
return
|
||||
}
|
||||
|
||||
defer func(reader io.ReadCloser) {
|
||||
_ = reader.Close()
|
||||
}(reader)
|
||||
|
||||
hashWriter := sha512.New()
|
||||
if _, err = io.Copy(hashWriter, reader); err != nil {
|
||||
log.Printf("Error: Could not hash file from Content-URL: \"%s\" - %v", content.URL, err)
|
||||
return
|
||||
}
|
||||
|
||||
sum := hex.EncodeToString(hashWriter.Sum(nil))
|
||||
|
||||
roomConfig := config.GetRoomConfig(evt.RoomID.String())
|
||||
|
||||
hashObj, err := db.GetEntryByHash(sum)
|
||||
if err != nil {
|
||||
if errors.Is(err, mongo.ErrNoDocuments) {
|
||||
/*entry := model.DBEntry{
|
||||
ID: primitive.NewObjectID(),
|
||||
HashValue: sum,
|
||||
FileURL: "placeholder",
|
||||
Timestamp: time.Now(),
|
||||
AddedBy: nil,
|
||||
Comments: nil,
|
||||
}
|
||||
|
||||
db.SaveEntry(&entry)*/
|
||||
|
||||
if roomConfig.Debug {
|
||||
matrixClient.SendNotice(evt.RoomID, fmt.Sprintf("DEBUG - This file is not on the hashlist: %s", sum))
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
if roomConfig.Debug {
|
||||
matrixClient.SendNotice(evt.RoomID, "DEBUG - Failed to check file. See log.")
|
||||
}
|
||||
|
||||
fmt.Printf("Error trying to check database: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
if roomConfig.Debug {
|
||||
matrixClient.SendNotice(evt.RoomID, fmt.Sprintf("DEBUG !!! This file is on the hashlist: %s", sum))
|
||||
|
||||
jsonVal, _ := json.Marshal(hashObj)
|
||||
|
||||
var buf bytes.Buffer
|
||||
|
||||
json.Indent(&buf, jsonVal, "", " ")
|
||||
|
||||
matrixClient.SendNotice(evt.RoomID, fmt.Sprintf("DEBUG:\n%s", buf.String()))
|
||||
}
|
||||
|
||||
handleIllegalContent(evt, matrixClient, hashObj, roomConfig)
|
||||
}
|
||||
|
||||
func redactMessage(evt *event.Event, matrixClient *mautrix.Client, hashObj *model.DBEntry) {
|
||||
opts := mautrix.ReqRedact{Reason: fmt.Sprintf("Veles has detected an hash-map-match! Tags: %s, ID: %s", hashObj.Tags, hashObj.ID.Hex())}
|
||||
_, err := matrixClient.RedactEvent(evt.RoomID, evt.ID, opts)
|
||||
if err != nil {
|
||||
log.Printf("ERROR: Could not redact event - %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
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 {
|
||||
log.Printf("ERROR: Could mute user - %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
plEventContent.Users[evt.Sender.String()] = -1
|
||||
|
||||
_, err = matrixClient.SendStateEvent(evt.RoomID, event.StatePowerLevels, "", plEventContent)
|
||||
if err != nil {
|
||||
log.Printf("ERROR: Could mute user - %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func postNotice(evt *event.Event, matrixClient *mautrix.Client, hashObj *model.DBEntry, roomConfig config.RoomConfig) {
|
||||
local, server, err := evt.Sender.Parse()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
matrixClient.SendNotice(evt.RoomID, fmt.Sprintf(
|
||||
`Veles Triggered: The message by %s (on %s) was flagged for containing material used by spammers or trolls!
|
||||
|
||||
If you believe this action was an accident, please contact an room administrator or moderator. (Reference: %s)`, local, server, hashObj.ID.Hex()))
|
||||
|
||||
}
|
||||
|
||||
func handleIllegalContent(evt *event.Event, matrixClient *mautrix.Client, hashObj *model.DBEntry, roomConfig config.RoomConfig) {
|
||||
switch roomConfig.HashCheckMode {
|
||||
case 0:
|
||||
break
|
||||
case 1:
|
||||
break
|
||||
case 2:
|
||||
muteUser(evt, matrixClient, hashObj)
|
||||
redactMessage(evt, matrixClient, hashObj)
|
||||
postNotice(evt, matrixClient, hashObj, roomConfig)
|
||||
break
|
||||
case 3:
|
||||
break
|
||||
|
||||
}
|
||||
}
|
149
internal/config/helpers.go
Normal file
149
internal/config/helpers.go
Normal file
|
@ -0,0 +1,149 @@
|
|||
/*
|
||||
* Copyright © 2022 Kevin Kandlbinder.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*
|
||||
*/
|
||||
|
||||
package config
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"github.com/Unkn0wnCat/matrix-veles/internal/db"
|
||||
"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"
|
||||
"maunium.net/go/mautrix/id"
|
||||
"sync"
|
||||
)
|
||||
|
||||
var (
|
||||
roomConfigWg sync.WaitGroup
|
||||
)
|
||||
|
||||
// SetRoomConfigActive updates the active state for a given room
|
||||
func SetRoomConfigActive(id string, active bool) {
|
||||
// Lock room config system to prevent race conditions
|
||||
roomConfigWg.Wait()
|
||||
roomConfigWg.Add(1)
|
||||
|
||||
roomConfig := GetRoomConfig(id)
|
||||
roomConfig.Active = true
|
||||
|
||||
err := SaveRoomConfig(&roomConfig)
|
||||
if err != nil {
|
||||
log.Panicf("Error writing room config to database: %v", err)
|
||||
}
|
||||
|
||||
// Unlock room config system
|
||||
roomConfigWg.Done()
|
||||
}
|
||||
|
||||
// GetRoomConfig returns the RoomConfig linked to the specified ID
|
||||
func GetRoomConfig(id string) RoomConfig {
|
||||
config, err := GetRoomConfigByRoomID(id)
|
||||
if err != nil {
|
||||
if errors.Is(err, mongo.ErrNoDocuments) {
|
||||
return AddRoomConfig(id)
|
||||
}
|
||||
}
|
||||
|
||||
return *config
|
||||
}
|
||||
|
||||
// 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"))
|
||||
|
||||
cursor, err := db.Collection("rooms").Find(context.TODO(), bson.D{}, nil)
|
||||
if err != nil {
|
||||
log.Panicf("Error querying room configs: %v", err)
|
||||
}
|
||||
|
||||
var roomConfigs []RoomConfig
|
||||
|
||||
err = cursor.All(context.TODO(), &roomConfigs)
|
||||
if err != nil {
|
||||
log.Panicf("Error querying room configs: %v", err)
|
||||
}
|
||||
|
||||
activeRooms := make(map[string]bool)
|
||||
|
||||
// Set all active states to "false" for a blank start
|
||||
for _, roomConfig := range roomConfigs {
|
||||
activeRooms[roomConfig.RoomID] = false
|
||||
}
|
||||
|
||||
// Go over all joined rooms
|
||||
for _, roomID := range ids {
|
||||
activeRooms[roomID.String()] = true
|
||||
|
||||
GetRoomConfig(roomID.String())
|
||||
}
|
||||
|
||||
for roomID, isActive := range activeRooms {
|
||||
SetRoomConfigActive(roomID, isActive)
|
||||
}
|
||||
}
|
||||
|
||||
func AddRoomConfig(id string) RoomConfig {
|
||||
// Lock room config system to prevent race conditions
|
||||
roomConfigWg.Wait()
|
||||
roomConfigWg.Add(1)
|
||||
|
||||
config := RoomConfig{ID: primitive.NewObjectID(), Active: true, RoomID: id}
|
||||
|
||||
err := SaveRoomConfig(&config)
|
||||
if err != nil {
|
||||
log.Panicf("Error writing room config to database: %v", err)
|
||||
}
|
||||
|
||||
// Unlock room config system
|
||||
roomConfigWg.Done()
|
||||
|
||||
return config
|
||||
}
|
||||
|
||||
func SaveRoomConfig(roomConfig *RoomConfig) error {
|
||||
db := 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)
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
func GetRoomConfigByRoomID(id string) (*RoomConfig, error) {
|
||||
db := db.DbClient.Database(viper.GetString("bot.mongo.database"))
|
||||
|
||||
res := db.Collection("rooms").FindOne(context.TODO(), bson.D{{"room_id", id}})
|
||||
if res.Err() != nil {
|
||||
return nil, res.Err()
|
||||
}
|
||||
|
||||
object := RoomConfig{}
|
||||
|
||||
err := res.Decode(&object)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &object, nil
|
||||
}
|
48
internal/config/structs.go
Normal file
48
internal/config/structs.go
Normal file
|
@ -0,0 +1,48 @@
|
|||
/*
|
||||
* Copyright © 2022 Kevin Kandlbinder.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*
|
||||
*/
|
||||
|
||||
package config
|
||||
|
||||
import "go.mongodb.org/mongo-driver/bson/primitive"
|
||||
|
||||
// RoomConfigTree is a map from string to RoomConfig
|
||||
type RoomConfigTree map[string]RoomConfig
|
||||
|
||||
// RoomConfig is the configuration attached to every joined room
|
||||
type RoomConfig struct {
|
||||
ID primitive.ObjectID `bson:"_id"`
|
||||
|
||||
// Active tells if the bot is active in this room (Set to false on leave/kick/ban)
|
||||
Active bool `yaml:"active" bson:"active"`
|
||||
|
||||
// RoomID is the rooms ID
|
||||
RoomID string `yaml:"roomID" bson:"room_id"`
|
||||
|
||||
// Debug specifies if the bot shall run in dry run mode
|
||||
Debug bool `yaml:"debug" bson:"debug"`
|
||||
|
||||
/*
|
||||
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)
|
||||
*/
|
||||
HashCheckMode uint8 `yaml:"mode" bson:"hash_check_mode"`
|
||||
}
|
61
internal/db/db.go
Normal file
61
internal/db/db.go
Normal file
|
@ -0,0 +1,61 @@
|
|||
package db
|
||||
|
||||
import (
|
||||
"context"
|
||||
"github.com/Unkn0wnCat/matrix-veles/internal/db/model"
|
||||
"github.com/spf13/viper"
|
||||
"go.mongodb.org/mongo-driver/bson"
|
||||
"go.mongodb.org/mongo-driver/mongo"
|
||||
"go.mongodb.org/mongo-driver/mongo/options"
|
||||
"log"
|
||||
"time"
|
||||
)
|
||||
|
||||
var DbClient *mongo.Client
|
||||
|
||||
func Connect() {
|
||||
if viper.GetString("bot.mongo.uri") == "" {
|
||||
log.Println("Skipping database login...")
|
||||
return
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||
defer cancel()
|
||||
newClient, err := mongo.Connect(ctx, options.Client().ApplyURI(viper.GetString("bot.mongo.uri")))
|
||||
if err != nil {
|
||||
log.Println("Could not connect to DB")
|
||||
log.Panicln(err)
|
||||
}
|
||||
|
||||
DbClient = newClient
|
||||
}
|
||||
|
||||
func SaveEntry(entry *model.DBEntry) error {
|
||||
db := DbClient.Database(viper.GetString("bot.mongo.database"))
|
||||
|
||||
opts := options.Replace().SetUpsert(true)
|
||||
|
||||
filter := bson.D{{"_id", entry.ID}}
|
||||
|
||||
_, err := db.Collection("entries").ReplaceOne(context.TODO(), filter, entry, opts)
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
func GetEntryByHash(hash string) (*model.DBEntry, error) {
|
||||
db := DbClient.Database(viper.GetString("bot.mongo.database"))
|
||||
|
||||
res := db.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
|
||||
}
|
21
internal/db/model/entry.go
Normal file
21
internal/db/model/entry.go
Normal file
|
@ -0,0 +1,21 @@
|
|||
package model
|
||||
|
||||
import (
|
||||
"go.mongodb.org/mongo-driver/bson/primitive"
|
||||
"time"
|
||||
)
|
||||
|
||||
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"`
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue