Create auth API & comment code

This commit is contained in:
Kevin Kandlbinder 2022-02-23 18:58:46 +01:00
parent 1b15b12859
commit c8d1c33cb4
17 changed files with 475 additions and 101 deletions

View file

@ -58,6 +58,9 @@ func init() {
viper.SetDefault("bot.mongo.collection.entries", "entries")
viper.SetDefault("bot.mongo.collection.lists", "lists")
viper.SetDefault("bot.mongo.collection.rooms", "rooms")
viper.SetDefault("bot.mongo.collection.users", "users")
viper.SetDefault("bot.web.listen", "127.0.0.1:8123")
viper.SetDefault("bot.web.secret", "hunter2")
cobra.OnInitialize(loadConfig)
}
@ -75,4 +78,8 @@ func loadConfig() {
if err := viper.ReadInConfig(); err == nil {
log.Println("Using config file:", viper.ConfigFileUsed())
}
if viper.GetString("bot.web.secret") == "hunter2" {
log.Println("Web secret is not set! YOUR INSTALLATION IS INSECURE!")
}
}

View file

@ -20,6 +20,7 @@ package cmd
import (
"github.com/Unkn0wnCat/matrix-veles/internal/bot"
"github.com/Unkn0wnCat/matrix-veles/internal/db"
"github.com/Unkn0wnCat/matrix-veles/internal/web"
"github.com/spf13/cobra"
)
@ -33,6 +34,7 @@ var runCmd = &cobra.Command{
The bot will log in to the homeserver and start posting updates to subscribed channels.`,
Run: func(cmd *cobra.Command, args []string) {
db.Connect()
go web.StartServer()
bot.Run()
},
}

7
go.mod
View file

@ -3,11 +3,12 @@ module github.com/Unkn0wnCat/matrix-veles
go 1.16
require (
github.com/gomarkdown/markdown v0.0.0-20210514010506-3b9f47219fe7
github.com/golang-jwt/jwt/v4 v4.3.0
github.com/gorilla/mux v1.8.0
github.com/spf13/cobra v1.3.0
github.com/spf13/viper v1.10.1
go.mongodb.org/mongo-driver v1.8.3 // indirect
go.mongodb.org/mongo-driver v1.8.3
golang.org/x/crypto v0.0.0-20210817164053-32db794688a5
golang.org/x/sys v0.0.0-20220114195835-da31bd327af9 // indirect
golang.org/x/text v0.3.7
maunium.net/go/mautrix v0.9.14
)

9
go.sum
View file

@ -133,6 +133,8 @@ github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/me
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
github.com/golang-jwt/jwt/v4 v4.3.0 h1:kHL1vqdqWNfATmA0FNMdmZNMyZI1U6O31X4rlIPoBog=
github.com/golang-jwt/jwt/v4 v4.3.0/go.mod h1:/xlHOz8bRuivTWchD4jCa+NbatV+wEUSzwAxVc6locg=
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
@ -167,8 +169,6 @@ github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiu
github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
github.com/golang/snappy v0.0.3 h1:fHPg5GQYlCeLIPB9BZqMVR5nR9A+IM5zcgeTdjMYmLA=
github.com/golang/snappy v0.0.3/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
github.com/gomarkdown/markdown v0.0.0-20210514010506-3b9f47219fe7 h1:oKYOfNR7Hp6XpZ4JqolL5u642Js5Z0n7psPVl+S5heo=
github.com/gomarkdown/markdown v0.0.0-20210514010506-3b9f47219fe7/go.mod h1:aii0r/K0ZnHv7G0KF7xy1v0A7s2Ljrb5byB7MO5p6TU=
github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
@ -182,6 +182,7 @@ github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/
github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.6 h1:BKbKCqvP6I+rmFHt06ZmyQtvB8xAkWdhFyr0ZUNZcxQ=
github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=
@ -208,6 +209,7 @@ github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+
github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk=
github.com/googleapis/gax-go/v2 v2.1.0/go.mod h1:Q3nei7sK6ybPYH7twZdmQpAd1MKb7pfu6SK+H1/DsU0=
github.com/googleapis/gax-go/v2 v2.1.1/go.mod h1:hddJymUZASv3XPyGkUpKj8pPO47Rmb0eJc8R6ouapiM=
github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI=
github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So=
github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw=
@ -372,6 +374,7 @@ github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69
github.com/tidwall/gjson v1.6.8/go.mod h1:zeFuBCIqD4sN/gmqBzZ4j7Jd6UcA2Fc56x7QFsv+8fI=
github.com/tidwall/match v1.0.3/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM=
github.com/tidwall/pretty v1.0.0/go.mod h1:XNkn88O1ChpSDQmQeStsy+sBenx6DDtFZJxhVysOjyk=
github.com/tidwall/pretty v1.0.2 h1:Z7S3cePv9Jwm1KwS0513MRaoUe3S01WPbLNV40pwWZU=
github.com/tidwall/pretty v1.0.2/go.mod h1:XNkn88O1ChpSDQmQeStsy+sBenx6DDtFZJxhVysOjyk=
github.com/tidwall/sjson v1.1.5/go.mod h1:VuJzsZnTowhSxWdOgsAnb886i4AjEyTkk7tNtsL7EYE=
github.com/tv42/httpunix v0.0.0-20150427012821-b75d8614f926/go.mod h1:9ESjWnEqriFuLhtthL60Sar/7RFoluCcXsuvEwTV5KM=
@ -404,7 +407,6 @@ go.opentelemetry.io/proto/otlp v0.7.0/go.mod h1:PqfVotwruBrMGOCsRd/89rSnXhoiJIqe
go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU=
go.uber.org/zap v1.17.0/go.mod h1:MXVU+bhUf/A7Xi2HNOnopQOrmycQ5Ih87HtOu4q5SSo=
golang.org/dl v0.0.0-20190829154251-82a15e2f2ead/go.mod h1:IUMfjQLJQd4UTqG1Z90tenwKoCX93Gn3MAQJMOSBsDQ=
golang.org/x/crypto v0.0.0-20170930174604-9419663f5a44/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20181029021203-45a5f77698d3/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
@ -671,6 +673,7 @@ golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE=
google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M=

View file

@ -158,19 +158,20 @@ func handleMessageEvent(matrixClient *mautrix.Client, startTs int64) mautrix.Eve
}
if content.URL != "" {
handleHashing(content, evt, matrixClient)
// This has an attachment!
handleHashing(content, evt, matrixClient) // -> handleHashing.go
return
}
// No attachment, is this a command?
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)
// It is a command!
handleCommand(content.Body, evt.Sender, evt.RoomID, matrixClient) // -> commandParser.go
}
}

View file

@ -8,19 +8,19 @@ import (
"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"`
type StateEventPowerLevel 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 StateEventPowerLevelContent `json:"content"`
Unsigned struct {
Age int `json:"age"`
} `json:"unsigned"`
}
type StateEventPLContent struct {
type StateEventPowerLevelContent struct {
Ban int `json:"ban"`
Events map[string]int `json:"events"`
EventsDefault int `json:"events_default"`
@ -33,45 +33,51 @@ type StateEventPLContent struct {
UsersDefault int `json:"users_default"`
}
func GetRoomState(matrixClient *mautrix.Client, roomId id.RoomID) (*StateEventPLContent, error) {
// GetRoomPowerLevelState returns the rooms current power levels from the state
func GetRoomPowerLevelState(matrixClient *mautrix.Client, roomId id.RoomID) (*StateEventPowerLevelContent, error) {
// https://matrix.example.com/_matrix/client/r0/rooms/<roomId.String()>/state
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
// res contains an array of state events
var stateEvents []StateEventPowerLevel
err = json.Unmarshal(res, &stateEvents)
if err != nil {
return nil, fmt.Errorf("ERROR: Could parse room state - %v", err)
}
var plEventContent StateEventPLContent
// plEventContent will hold the final event
var plEventContent StateEventPowerLevelContent
found := false
for _, e2 := range stateEvents {
if e2.Type != event.StatePowerLevels.Type {
continue
continue // If the current event is not of the power level, skip.
}
// This is what we're looking for!
found = true
plEventContent = e2.Content
break
}
if !found {
return nil, fmt.Errorf("ERROR: Could find room power level - %v", err)
}
// The following handle cases in which empty lists may have been parsed as nil
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)
}

View file

@ -17,6 +17,7 @@ import (
"maunium.net/go/mautrix/event"
)
// handleHashing hashes and checks a message, taking configured actions on match
func handleHashing(content *event.MessageEventContent, evt *event.Event, matrixClient *mautrix.Client) {
url, err := content.URL.Parse()
if err != nil {
@ -30,9 +31,7 @@ func handleHashing(content *event.MessageEventContent, evt *event.Event, matrixC
return
}
defer func(reader io.ReadCloser) {
_ = reader.Close()
}(reader)
defer func(reader io.ReadCloser) { _ = reader.Close() }(reader)
hashWriter := sha512.New()
if _, err = io.Copy(hashWriter, reader); err != nil {
@ -42,52 +41,51 @@ func handleHashing(content *event.MessageEventContent, evt *event.Event, matrixC
sum := hex.EncodeToString(hashWriter.Sum(nil))
// Fetch room configuration for adjusting behaviour
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()))
matrixClient.SendNotice(evt.RoomID, fmt.Sprintf("DEBUG:\n%s", makeFancyJSON(jsonVal)))
}
if !checkSubscription(&roomConfig, hashObj) {
return
}
log.Printf("Illegal content detected in room %s!", roomConfig.RoomID)
handleIllegalContent(evt, matrixClient, hashObj, roomConfig)
}
// makeFancyJSON formats / indents a JSON string
func makeFancyJSON(input []byte) string {
var buf bytes.Buffer
json.Indent(&buf, input, "", " ")
return buf.String()
}
// checkSubscription checks if the room is subscribed to one of hashObjs lists
func checkSubscription(roomConfig *config.RoomConfig, hashObj *model.DBEntry) bool {
if roomConfig.HashChecker.SubscribedLists == nil {
log.Printf("Room %s is not subscribed to any lists!", roomConfig.RoomID)
return // Not subscribed to any lists
return false // Not subscribed to any lists
}
subMap := make(map[string]bool)
@ -110,14 +108,42 @@ func handleHashing(content *event.MessageEventContent, evt *event.Event, matrixC
if !found {
log.Printf("Room %s is not subscribed to any lists of hashobj %s!", roomConfig.RoomID, hashObj.ID.Hex())
return // Not subscribed
return false // Not subscribed
}
log.Printf("Illegal content detected in room %s!", roomConfig.RoomID)
handleIllegalContent(evt, matrixClient, hashObj, roomConfig)
return true
}
// handleIllegalContent is called when a hash-match is found to take configured actions
func handleIllegalContent(evt *event.Event, matrixClient *mautrix.Client, hashObj *model.DBEntry, roomConfig config.RoomConfig) {
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)
redactMessage(evt, matrixClient, hashObj)
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
}
}
// redactMessage deletes the message sent in the given event
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)
@ -126,8 +152,9 @@ func redactMessage(evt *event.Event, matrixClient *mautrix.Client, hashObj *mode
}
}
func muteUser(evt *event.Event, matrixClient *mautrix.Client, hashObj *model.DBEntry) {
plEventContent, err := GetRoomState(matrixClient, evt.RoomID)
// muteUser sets a users power-level to -1 to prevent them from sending messages
func muteUser(evt *event.Event, matrixClient *mautrix.Client) {
plEventContent, err := GetRoomPowerLevelState(matrixClient, evt.RoomID)
if err != nil {
log.Printf("ERROR: Could mute user - %v", err)
return
@ -140,9 +167,9 @@ func muteUser(evt *event.Event, matrixClient *mautrix.Client, hashObj *model.DBE
log.Printf("ERROR: Could mute user - %v", err)
return
}
}
// banUser bans the sender of an event from the room
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()),
@ -152,6 +179,7 @@ func banUser(evt *event.Event, matrixClient *mautrix.Client, hashObj *model.DBEn
matrixClient.BanUser(evt.RoomID, &req)
}
// postNotice posts a notice about the given event into its room
func postNotice(evt *event.Event, matrixClient *mautrix.Client, hashObj *model.DBEntry, roomConfig config.RoomConfig) {
local, server, err := evt.Sender.Parse()
if err != nil {
@ -172,32 +200,3 @@ If you believe this action was an accident, please contact an room administrator
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.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)
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
}
}

View file

@ -8,7 +8,7 @@ package bot
}
if roomConfig.AlertChannel == nil {
roomPLState, err := GetRoomState(matrixClient, id.RoomID(room))
roomPLState, err := GetRoomPowerLevelState(matrixClient, id.RoomID(room))
if err != nil {
log.Printf("Failed to get room power levels - %v", err)
return

View file

@ -35,16 +35,21 @@ type RoomConfig struct {
// Debug specifies if the bot shall run in dry run mode
Debug bool `yaml:"debug" bson:"debug"`
// AlertChannel is currently unused
AlertChannel *string `bson:"alert_channel"`
// AdminPowerLevel specifies the power-level a user has to have to manage the room
AdminPowerLevel int `bson:"admin_power_level"`
// HashChecker contains configuration specific to the hash-checker
HashChecker HashCheckerConfig `bson:"hash_checker"`
}
type HashCheckerConfig struct {
// NoticeToChat specifies weather or not to post a public notice to chat
NoticeToChat bool `bson:"chat_notice"`
// NotificationPowerLevel is currently unused
NotificationPowerLevel int `yaml:"notification_level" bson:"notification_level"`
/*
@ -58,5 +63,6 @@ type HashCheckerConfig struct {
*/
HashCheckMode uint8 `yaml:"mode" bson:"hash_check_mode"`
// SubscribedLists contains the lists this room is subscribed to
SubscribedLists []*primitive.ObjectID `bson:"subscribed_lists" json:"subscribed_lists"`
}

View file

@ -108,3 +108,51 @@ func GetListByID(id primitive.ObjectID) (*model.DBHashList, error) {
return &object, nil
}
func SaveUser(user *model.DBUser) error {
db := DbClient.Database(viper.GetString("bot.mongo.database"))
opts := options.Replace().SetUpsert(true)
filter := bson.D{{"_id", user.ID}}
_, err := db.Collection(viper.GetString("bot.mongo.collection.users")).ReplaceOne(context.TODO(), filter, user, opts)
return err
}
func GetUserByID(id primitive.ObjectID) (*model.DBUser, error) {
db := DbClient.Database(viper.GetString("bot.mongo.database"))
res := db.Collection(viper.GetString("bot.mongo.collection.users")).FindOne(context.TODO(), bson.D{{"_id", id}})
if res.Err() != nil {
return nil, res.Err()
}
object := model.DBUser{}
err := res.Decode(&object)
if err != nil {
return nil, err
}
return &object, nil
}
func GetUserByUsername(username string) (*model.DBUser, error) {
db := DbClient.Database(viper.GetString("bot.mongo.database"))
res := db.Collection(viper.GetString("bot.mongo.collection.users")).FindOne(context.TODO(), bson.D{{"username", username}})
if res.Err() != nil {
return nil, res.Err()
}
object := model.DBUser{}
err := res.Decode(&object)
if err != nil {
return nil, err
}
return &object, nil
}

View file

@ -3,6 +3,6 @@ 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"`
CommentedBy *primitive.ObjectID `bson:"commented_by" json:"commented_by"` // CommentedBy contains a reference to the user who commented
Content string `bson:"content" json:"content"` // Content is the body of the comment
}

View file

@ -7,11 +7,11 @@ import (
type DBEntry struct {
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"`
Tags []string `bson:"tags" json:"tags"` // Tags used for searching entries and ordering
PartOf []*primitive.ObjectID `bson:"part_of" json:"part_of"` // PartOf specifies the lists this entry is part of
HashValue string `bson:"hash_value" json:"hash"` // HashValue is the SHA512-hash of the file
FileURL string `bson:"file_url" json:"file_url"` // FileURL may be set to a file link
Timestamp time.Time `bson:"timestamp" json:"timestamp"` // Timestamp of when this entry was added
AddedBy *primitive.ObjectID `bson:"added_by" json:"added_by"` // AddedBy is a reference to the user who added this
Comments []*DBComment `bson:"comments" json:"comments"` // Comments regarding this entry
}

View file

@ -4,7 +4,7 @@ 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"`
Tags []string `bson:"tags" json:"tags"` // Tags of this list for discovery, and sorting
Comments []*DBComment `bson:"comments" json:"comments"` // Comments regarding this list
Maintainers []*primitive.ObjectID `bson:"maintainers" json:"maintainers"` // Maintainers contains references to the users who may edit this list
}

View file

@ -2,14 +2,20 @@ package model
import (
"encoding/base64"
"go.mongodb.org/mongo-driver/bson/primitive"
"golang.org/x/crypto/bcrypt"
)
type DBUser struct {
Username string `bson:"username" json:"username"`
HashedPassword string `bson:"password" json:"password"`
ID primitive.ObjectID `bson:"_id" json:"id"`
Password *string `bson:"-" json:"-"`
Username string `bson:"username" json:"username"` // Username is the username the user has
HashedPassword string `bson:"password" json:"password"` // HashedPassword contains the bcrypt-ed password
MatrixLinks []*string `bson:"matrix_links" json:"matrix_links"` // MatrixLinks is the matrix-users this user has verified ownership over
PendingMatrixLinks []*string `bson:"pending_matrix_links" json:"pending_matrix_links"` // PendingMatrixLinks is the matrix-users pending verification
Password *string `bson:"-" json:"-"` // Password may never be sent out!
}
func (usr *DBUser) HashPassword() error {
@ -23,6 +29,7 @@ func (usr *DBUser) HashPassword() error {
}
usr.HashedPassword = base64.StdEncoding.EncodeToString(hash)
usr.Password = nil
return nil
}

86
internal/web/api/api.go Normal file
View file

@ -0,0 +1,86 @@
package api
import (
"context"
"encoding/json"
"errors"
"github.com/gorilla/mux"
"net/http"
"strings"
)
func SetupAPI(router *mux.Router) {
router.NotFoundHandler = NotFoundHandler{}
router.MethodNotAllowedHandler = MethodNotAllowedHandler{}
router.Path("/auth/login").Methods("POST").HandlerFunc(apiHandleAuthLogin)
router.Path("/auth/register").Methods("POST").HandlerFunc(apiHandleAuthRegister)
bot := router.PathPrefix("/bot").Subrouter()
bot.Use(checkAuthMiddleware)
bot.Path("/test").HandlerFunc(func(writer http.ResponseWriter, request *http.Request) {
writer.WriteHeader(200)
claims := request.Context().Value("claims").(jwtClaims)
writer.Write([]byte(`hello ` + claims.Username))
})
}
func checkAuthMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(res http.ResponseWriter, req *http.Request) {
token := req.Header.Get("Authorization")
tokenSplit := strings.Split(token, " ")
if token == "" || len(tokenSplit) < 2 {
writeJSONError(res, http.StatusUnauthorized, errors.New("bearer token required"))
return
}
token = tokenSplit[1]
claims, _, err := parseToken(token)
if err != nil {
writeJSONError(res, http.StatusUnauthorized, errors.New("invalid token"))
return
}
ctx := context.WithValue(req.Context(), "claims", *claims)
req = req.WithContext(ctx)
next.ServeHTTP(res, req)
})
}
func writeJSONError(res http.ResponseWriter, statusCode int, err error) {
res.Header().Set("Content-Type", "application/json")
res.WriteHeader(statusCode)
enc, _ := json.Marshal(struct {
Error string `json:"error"`
ErrorCode int `json:"error_code"`
}{
Error: err.Error(),
ErrorCode: statusCode,
})
_, _ = res.Write(enc)
}
type NotFoundHandler struct{}
func (NotFoundHandler) ServeHTTP(res http.ResponseWriter, req *http.Request) {
res.Header().Set("Content-Type", "application/json")
res.WriteHeader(http.StatusNotFound)
_, _ = res.Write([]byte(`{"error": "not_found","error_code":404}`))
}
type MethodNotAllowedHandler struct{}
func (MethodNotAllowedHandler) ServeHTTP(res http.ResponseWriter, req *http.Request) {
res.Header().Set("Content-Type", "application/json")
res.WriteHeader(http.StatusMethodNotAllowed)
_, _ = res.Write([]byte(`{"error": "method_not_allowed","error_code":405}`))
}

173
internal/web/api/auth.go Normal file
View file

@ -0,0 +1,173 @@
package api
import (
"encoding/json"
"errors"
"github.com/Unkn0wnCat/matrix-veles/internal/db"
"github.com/Unkn0wnCat/matrix-veles/internal/db/model"
"github.com/golang-jwt/jwt/v4"
"github.com/spf13/viper"
"go.mongodb.org/mongo-driver/bson/primitive"
"go.mongodb.org/mongo-driver/mongo"
"net/http"
"time"
)
type apiAuthRequestBody struct {
Username string `json:"username"`
Password string `json:"password"`
}
type jwtClaims struct {
Username string `json:"username"`
jwt.RegisteredClaims
}
func parseToken(tokenString string) (*jwtClaims, *jwt.Token, error) {
claims := jwtClaims{}
jwtSigningKey := []byte(viper.GetString("bot.web.secret"))
token, err := jwt.ParseWithClaims(tokenString, &claims, func(token *jwt.Token) (interface{}, error) {
return jwtSigningKey, nil
})
if err != nil {
return nil, nil, err
}
return &claims, token, nil
}
func apiHandleAuthLogin(res http.ResponseWriter, req *http.Request) {
body := req.Body
bodyContent := apiAuthRequestBody{}
err := json.NewDecoder(body).Decode(&bodyContent)
if err != nil {
writeJSONError(res, http.StatusBadRequest, errors.New("malformed body"))
return
}
user, err := db.GetUserByUsername(bodyContent.Username)
if err != nil {
if errors.Is(err, mongo.ErrNoDocuments) {
writeJSONError(res, http.StatusUnauthorized, errors.New("invalid credentials"))
return
}
writeJSONError(res, http.StatusInternalServerError, errors.New("database error"))
return
}
err = user.CheckPassword(bodyContent.Password)
if err != nil {
writeJSONError(res, http.StatusUnauthorized, errors.New("invalid credentials"))
return
}
jwtSigningKey := []byte(viper.GetString("bot.web.secret"))
claims := jwtClaims{
Username: user.Username,
RegisteredClaims: jwt.RegisteredClaims{
ExpiresAt: jwt.NewNumericDate(time.Now().Add(time.Hour * 24 * 365 * 100)),
IssuedAt: jwt.NewNumericDate(time.Now()),
NotBefore: jwt.NewNumericDate(time.Now()),
Issuer: "veles-api",
Subject: user.ID.Hex(),
},
}
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
ss, err := token.SignedString(jwtSigningKey)
if err != nil {
writeJSONError(res, http.StatusInternalServerError, errors.New("unable to create token"))
return
}
res.Header().Set("Content-Type", "application/json")
res.WriteHeader(http.StatusOK)
enc, err := json.Marshal(struct {
Token string `json:"token"`
}{
Token: ss,
})
_, _ = res.Write(enc)
}
type apiAuthRegisterBody struct {
Username string `json:"username"`
Password string `json:"password"`
}
func apiHandleAuthRegister(res http.ResponseWriter, req *http.Request) {
body := req.Body
bodyContent := apiAuthRegisterBody{}
err := json.NewDecoder(body).Decode(&bodyContent)
if err != nil {
writeJSONError(res, http.StatusBadRequest, errors.New("malformed body"))
return
}
_, err = db.GetUserByUsername(bodyContent.Username)
if err == nil {
writeJSONError(res, http.StatusBadRequest, errors.New("username taken"))
return
}
if !errors.Is(err, mongo.ErrNoDocuments) {
writeJSONError(res, http.StatusInternalServerError, errors.New("database error"))
return
}
user := model.DBUser{
ID: primitive.NewObjectID(),
Username: bodyContent.Username,
Password: &bodyContent.Password,
}
err = user.HashPassword()
if err != nil {
writeJSONError(res, http.StatusInternalServerError, errors.New("unable to hash password"))
return
}
err = db.SaveUser(&user)
if err != nil {
writeJSONError(res, http.StatusInternalServerError, errors.New("database error"))
return
}
jwtSigningKey := viper.GetString("bot.web.secret")
claims := jwtClaims{
Username: user.Username,
RegisteredClaims: jwt.RegisteredClaims{
ExpiresAt: jwt.NewNumericDate(time.Now().Add(time.Hour * 24)),
IssuedAt: jwt.NewNumericDate(time.Now()),
NotBefore: jwt.NewNumericDate(time.Now()),
Issuer: "veles-api",
Subject: user.ID.Hex(),
},
}
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
ss, err := token.SignedString(jwtSigningKey)
if err != nil {
writeJSONError(res, http.StatusInternalServerError, errors.New("unable to create token"))
}
res.Header().Set("Content-Type", "application/json")
res.WriteHeader(http.StatusOK)
enc, err := json.Marshal(struct {
Token string `json:"token"`
}{
Token: ss,
})
_, _ = res.Write(enc)
}

35
internal/web/web.go Normal file
View file

@ -0,0 +1,35 @@
package web
import (
"github.com/Unkn0wnCat/matrix-veles/internal/web/api"
"github.com/gorilla/mux"
"github.com/spf13/viper"
"log"
"net/http"
"time"
)
func StartServer() {
r := mux.NewRouter()
r.HandleFunc("/", HomeHandler)
//r.PathPrefix("/static/").Handler(http.StripPrefix("/static/", http.FileServer(http.Dir("./static"))))
apiRouter := r.PathPrefix("/api").Subrouter()
api.SetupAPI(apiRouter)
srv := &http.Server{
Handler: r,
Addr: viper.GetString("bot.web.listen"),
WriteTimeout: 15 * time.Second,
ReadTimeout: 15 * time.Second,
}
log.Printf("Now serving web-interface on http://%s", viper.GetString("bot.web.listen"))
log.Fatal(srv.ListenAndServe())
}
func HomeHandler(writer http.ResponseWriter, request *http.Request) {
writer.WriteHeader(200)
writer.Write([]byte("hello world"))
}