Implement basic matrix client

This commit is contained in:
Luca 2022-07-24 02:40:44 +02:00
parent 8dd0a1f3be
commit f8479ed7b6
11 changed files with 583 additions and 14 deletions

View File

@ -8,6 +8,8 @@ steps:
- name: build
image: golang
commands:
- apt-get update
- apt-get install -y libolm-dev
- go build -ldflags="-s -w" -o matrix-pretix .
- name: release

13
go.mod
View File

@ -2,9 +2,20 @@ module git.luj0ga.de/franconian/matrix-pretix
go 1.18
require maunium.net/go/mautrix v0.11.0
require (
github.com/mattn/go-sqlite3 v1.14.13
maunium.net/go/mautrix v0.11.0
)
require (
github.com/gorilla/mux v1.8.0 // indirect
github.com/gorilla/websocket v1.5.0 // indirect
github.com/tidwall/gjson v1.14.1 // indirect
github.com/tidwall/match v1.1.1 // indirect
github.com/tidwall/pretty v1.2.0 // indirect
github.com/tidwall/sjson v1.2.4 // indirect
golang.org/x/crypto v0.0.0-20220513210258-46612604a0f9 // indirect
golang.org/x/net v0.0.0-20220513224357-95641704303c // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect
maunium.net/go/maulogger/v2 v2.3.2 // indirect
)

21
go.sum
View File

@ -1,10 +1,31 @@
github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8=
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.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc=
github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/mattn/go-sqlite3 v1.14.13 h1:1tj15ngiFfcZzii7yd82foL+ks+ouQcj8j/TPq3fk1I=
github.com/mattn/go-sqlite3 v1.14.13/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/stretchr/testify v1.7.1 h1:5TQK59W5E3v0r2duFAb7P95B6hEeOyEnHRa8MjYSMTY=
github.com/tidwall/gjson v1.12.1/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
github.com/tidwall/gjson v1.14.1 h1:iymTbGkQBhveq21bEvAQ81I0LEBork8BFe1CUZXdyuo=
github.com/tidwall/gjson v1.14.1/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA=
github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM=
github.com/tidwall/pretty v1.2.0 h1:RWIZEg2iJ8/g6fDDYzMpobmaoGh5OLl4AXtGUGPcqCs=
github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
github.com/tidwall/sjson v1.2.4 h1:cuiLzLnaMeBhRmEv00Lpk3tkYrcxpmbU81tAY4Dw0tc=
github.com/tidwall/sjson v1.2.4/go.mod h1:098SZ494YoMWPmMO6ct4dcFnqxwj9r/gF0Etp19pSNM=
golang.org/x/crypto v0.0.0-20220513210258-46612604a0f9 h1:NUzdAbFtCJSXU20AOXgeqaUwg8Ypg4MPYmL+d+rsB5c=
golang.org/x/crypto v0.0.0-20220513210258-46612604a0f9/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/net v0.0.0-20220513224357-95641704303c h1:nF9mHSvoKBLkQNQhJZNsc66z2UzAMUbLGjC95CF3pU0=
golang.org/x/net v0.0.0-20220513224357-95641704303c/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo=
maunium.net/go/maulogger/v2 v2.3.2 h1:1XmIYmMd3PoQfp9J+PaHhpt80zpfmMqaShzUTC7FwY0=
maunium.net/go/maulogger/v2 v2.3.2/go.mod h1:TYWy7wKwz/tIXTpsx8G3mZseIRiC5DoMxSZazOHy68A=
maunium.net/go/mautrix v0.11.0 h1:B1FBHcvE4Mud+AC+zgNQQOw0JxSVrt40watCejhVA7w=
maunium.net/go/mautrix v0.11.0/go.mod h1:K29EcHwsNg6r7fMfwvi0GHQ9o5wSjqB9+Q8RjCIQEjA=

View File

@ -6,13 +6,27 @@ import (
)
type Config struct {
DB DatabaseConfig
Matrix MatrixConfig
Server ServerConfig
}
type DatabaseConfig struct {
Filename string
}
type MatrixConfig struct {
AllowedRooms []string
DisplayName string
LogLevel uint
HomeserverURL string
UserIdentifier string
Password string
PickleKey string
}
type ServerConfig struct {
ListenAddress string
}
func ParseFromFile(path string) (config *Config, err error) {

View File

@ -0,0 +1,15 @@
package database
import (
"database/sql"
_ "github.com/mattn/go-sqlite3"
"git.luj0ga.de/franconian/matrix-pretix/internal/config"
)
const DBDriverName = "sqlite3"
func Open(config *config.DatabaseConfig) (*sql.DB, error) {
return sql.Open(DBDriverName, config.Filename)
}

View File

@ -1,29 +1,166 @@
package matrix
import (
"context"
"database/sql"
"log"
"sync"
"git.luj0ga.de/franconian/matrix-pretix/internal/config"
"git.luj0ga.de/franconian/matrix-pretix/internal/database"
"maunium.net/go/mautrix"
"maunium.net/go/mautrix/crypto"
"maunium.net/go/mautrix/event"
"maunium.net/go/mautrix/id"
)
func NewClient(config *config.MatrixConfig) (*mautrix.Client, error) {
type Client struct {
client *mautrix.Client
config *config.MatrixConfig
db *sql.DB
deviceID id.DeviceID
olmMachine *crypto.OlmMachine
store *sqlStore
syncer *mautrix.DefaultSyncer
userID id.UserID
}
func NewClient(config *config.MatrixConfig, db *sql.DB) (*Client, error) {
client, err := mautrix.NewClient(config.HomeserverURL, "", "")
if err != nil {
return nil, err
}
_, err = client.Login(&mautrix.ReqLogin{
Type: mautrix.AuthTypePassword,
Identifier: mautrix.UserIdentifier{
Type: mautrix.IdentifierTypeUser,
User: config.UserIdentifier,
},
Password: config.Password,
StoreCredentials: true,
})
syncer := mautrix.NewDefaultSyncer()
client.Syncer = syncer
store := &sqlStore{db}
client.Store = store
err = store.CreateTables()
if err != nil {
return nil, err
}
return client, nil
userID, err := makeUserID(config.UserIdentifier, config.HomeserverURL)
if err != nil {
return nil, err
}
deviceID := loadDeviceID(db, userID)
return &Client{client, config, db, deviceID, nil, store, syncer, userID}, nil
}
func (c *Client) Login() error {
c.syncer.OnEventType(event.StateMember, c.handleMemberEvent)
_, err := c.client.Login(&mautrix.ReqLogin{
Type: mautrix.AuthTypePassword,
Identifier: mautrix.UserIdentifier{
Type: mautrix.IdentifierTypeUser,
User: c.config.UserIdentifier,
},
Password: c.config.Password,
DeviceID: c.deviceID,
InitialDeviceDisplayName: c.config.DisplayName,
StoreCredentials: true,
})
if err != nil {
return err
}
return nil
}
func (c *Client) Encrypt() error {
sqlCryptoStore := crypto.NewSQLCryptoStore(
c.db,
database.DBDriverName,
c.userID.String(),
c.deviceID,
[]byte(c.config.PickleKey),
logger{},
)
err := sqlCryptoStore.CreateTables()
if err != nil {
return err
}
c.olmMachine = crypto.NewOlmMachine(c.client, &logger{}, sqlCryptoStore, c.store)
err = c.olmMachine.Load()
if err != nil {
return err
}
c.syncer.OnSync(c.olmMachine.ProcessSyncResponse)
c.syncer.OnEventType(event.StateEncryption, c.handleEncryptionEvent)
return nil
}
func (c *Client) Sync(ctx context.Context, wg *sync.WaitGroup) {
wg.Add(1)
defer wg.Done()
for {
select {
case <-ctx.Done():
return
default:
err := c.client.SyncWithContext(ctx)
if err != nil && err != ctx.Err() {
log.Print(err)
}
}
}
}
func (c *Client) handleMemberEvent(source mautrix.EventSource, evt *event.Event) {
if c.olmMachine != nil {
c.olmMachine.HandleMemberEvent(evt)
}
c.store.SetMembership(evt.RoomID, evt.GetStateKey(), evt.Content.AsMember().Membership)
if evt.GetStateKey() == c.userID.String() && evt.Content.AsMember().Membership == event.MembershipInvite {
allowed := false
for _, room := range c.config.AllowedRooms {
if room == evt.RoomID.String() {
allowed = true
break
}
}
if allowed {
_, err := c.client.JoinRoomByID(evt.RoomID)
if err != nil {
log.Print(err)
}
} else {
_, err := c.client.LeaveRoom(evt.RoomID)
if err != nil {
log.Print(err)
}
}
}
}
func (c *Client) handleEncryptionEvent(source mautrix.EventSource, evt *event.Event) {
c.store.SetEncryptionEvent(evt.RoomID, evt.Content.AsEncryption())
}
func loadDeviceID(db *sql.DB, accountID id.UserID) (deviceID id.DeviceID) {
row := db.QueryRow("SELECT device_id FROM crypto_account WHERE account_id = ?;", accountID)
err := row.Scan(&deviceID)
if err != nil {
return ""
}
return deviceID
}

38
internal/matrix/logger.go Normal file
View File

@ -0,0 +1,38 @@
package matrix
import (
"fmt"
"log"
)
type logger struct{
level uint
}
func (l logger) Error(message string, args ...interface{}) {
if l.level > 0 {
logLevel("error", message, args...)
}
}
func (l logger) Warn(message string, args ...interface{}) {
if l.level > 1 {
logLevel("warning", message, args...)
}
}
func (l logger) Debug(message string, args ...interface{}) {
if l.level > 2 {
logLevel("debug", message, args...)
}
}
func (l logger) Trace(message string, args ...interface{}) {
if l.level > 3 {
logLevel("trace", message, args...)
}
}
func logLevel(level, message string, args ...interface{}) {
log.Print("[", level, "] ", fmt.Sprintf(message, args...))
}

188
internal/matrix/store.go Normal file
View File

@ -0,0 +1,188 @@
package matrix
import (
"database/sql"
"encoding/json"
"log"
"maunium.net/go/mautrix"
"maunium.net/go/mautrix/event"
"maunium.net/go/mautrix/id"
)
type sqlStore struct {
db *sql.DB
}
func (s sqlStore) CreateTables() error {
tables := []string{
`CREATE TABLE IF NOT EXISTS filter_ids (
user_id TEXT PRIMARY KEY ON CONFLICT REPLACE,
filter_id TEXT NOT NULL
);
`,
`CREATE TABLE IF NOT EXISTS next_batch_tokens (
user_id TEXT PRIMARY KEY ON CONFLICT REPLACE,
next_batch_token TEXT NOT NULL
);
`,
`CREATE TABLE IF NOT EXISTS rooms (
room_id TEXT PRIMARY KEY ON CONFLICT REPLACE,
encryption_event TEXT
);
`,
`CREATE TABLE IF NOT EXISTS room_members (
room_id TEXT,
user_id TEXT,
PRIMARY KEY (room_id, user_id)
);
`,
}
tx, err := s.db.Begin()
if err != nil {
return err
}
for _, table := range tables {
_, err := tx.Exec(table)
if err != nil {
if err := tx.Rollback(); err != nil {
log.Print(err)
}
return err
}
}
err = tx.Commit()
if err != nil {
return err
}
return nil
}
func (s sqlStore) SaveFilterID(userID id.UserID, filterID string) {
_, err := s.db.Exec("INSERT INTO filter_ids VALUES(?, ?);", userID, filterID)
if err != nil {
log.Print(err)
}
}
func (s sqlStore) LoadFilterID(userID id.UserID) (filterID string) {
row := s.db.QueryRow("SELECT filter_id FROM filter_ids WHERE user_id = ?;", userID)
err := row.Scan(&filterID)
if err != nil {
return ""
}
return filterID
}
func (s sqlStore) SaveNextBatch(userID id.UserID, nextBatchToken string) {
_, err := s.db.Exec("INSERT INTO next_batch_tokens VALUES(?, ?);", userID, nextBatchToken)
if err != nil {
log.Print(err)
}
}
func (s sqlStore) LoadNextBatch(userID id.UserID) (nextBatchToken string) {
row := s.db.QueryRow("SELECT next_batch_token FROM next_batch_tokens WHERE user_id = ?;", userID)
err := row.Scan(&nextBatchToken)
if err != nil {
return ""
}
return nextBatchToken
}
func (s sqlStore) SaveRoom(room *mautrix.Room) {
}
func (s sqlStore) LoadRoom(roomID id.RoomID) *mautrix.Room {
return mautrix.NewRoom(roomID)
}
func (s sqlStore) IsEncrypted(roomID id.RoomID) (isEncrypted bool) {
row := s.db.QueryRow("SELECT encryption_event NOT NULL FROM rooms WHERE room_id = ?;", roomID)
err := row.Scan(&isEncrypted)
if err != nil {
return false
}
return isEncrypted
}
func (s sqlStore) GetEncryptionEvent(roomID id.RoomID) (encryptionEvent *event.EncryptionEventContent) {
row := s.db.QueryRow("SELECT encryption_event FROM rooms WHERE room_id = ?;", roomID)
var data []byte
if err := row.Scan(&data); err != nil {
return nil
}
err := json.Unmarshal(data, encryptionEvent)
if err != nil {
return nil
}
return encryptionEvent
}
func (s sqlStore) FindSharedRooms(userID id.UserID) (sharedRooms []id.RoomID) {
rows, err := s.db.Query("SELECT rooms.room_id FROM rooms, (SELECT room_id FROM room_members GROUP BY room_id HAVING COUNT(*) > 1) shared_rooms WHERE shared_rooms.room_id = rooms.room_id AND encryption_event NOT NULL;")
if err != nil {
return nil
}
defer rows.Close()
for rows.Next() {
var roomID string
if err := rows.Scan(&roomID); err != nil {
continue
}
sharedRooms = append(sharedRooms, id.RoomID(roomID))
}
if rows.Err() != nil {
return nil
}
return sharedRooms
}
func (s sqlStore) SetMembership(roomID id.RoomID, userID string, membership event.Membership) {
if membership.IsInviteOrJoin() {
_, err := s.db.Exec("INSERT INTO room_members VALUES(?, ?);", roomID, userID)
if err != nil {
log.Print(err)
}
} else if membership.IsLeaveOrBan() {
_, err := s.db.Exec("DELETE FROM room_members WHERE room_id = ? AND user_id = ?;", roomID, userID)
if err != nil {
log.Print(err)
}
}
}
func (s sqlStore) SetEncryptionEvent(roomID id.RoomID, encryptionEvent *event.EncryptionEventContent) {
var data []byte
if encryptionEvent != nil {
var err error
data, err = json.Marshal(encryptionEvent)
if err != nil {
log.Print(err)
return
}
}
_, err := s.db.Exec("INSERT INTO rooms VALUES(?, ?);", roomID, data)
if err != nil {
log.Print(err)
}
}

View File

@ -0,0 +1,20 @@
package matrix
import "maunium.net/go/mautrix/id"
func makeUserID(userIdentifier, homeserverURL string) (id.UserID, error) {
userID := id.UserID(userIdentifier)
localpart, _, err := userID.Parse()
if err != nil {
userID = id.NewUserID(userIdentifier, homeserverURL)
if _, _, err := userID.ParseAndValidate(); err != nil {
return "", err
}
} else if err := id.ValidateUserLocalpart(localpart); err != nil {
return "", err
} else if len(userID) > id.UserIDMaxLength {
return "", id.ErrUserIDTooLong
}
return userID, nil
}

75
internal/pretix/server.go Normal file
View File

@ -0,0 +1,75 @@
package pretix
import (
"context"
"encoding/json"
"io"
"log"
"net/http"
"sync"
"git.luj0ga.de/franconian/matrix-pretix/internal/config"
"git.luj0ga.de/franconian/matrix-pretix/internal/matrix"
)
type Server struct {
client http.Client
matrix *matrix.Client
}
func NewServer(matrix *matrix.Client) *Server {
return &Server{
matrix: matrix,
}
}
func (s Server) ListenAndServe(config *config.ServerConfig, ctx context.Context, wg *sync.WaitGroup) error {
http.HandleFunc("/", http.NotFound)
http.HandleFunc("/order_placed", s.orderPlaced)
server := http.Server{
Addr: config.ListenAddress,
}
go func() {
wg.Add(1)
defer wg.Done()
<-ctx.Done()
err := server.Shutdown(context.Background())
if err != nil {
log.Print(err)
}
}()
err := server.ListenAndServe()
if err != http.ErrServerClosed {
return err
}
return nil
}
func (s Server) orderPlaced(w http.ResponseWriter, r *http.Request) {
if r.Header.Get("Content-Type") != "application/json" {
writeStatus(w, http.StatusBadRequest)
return
}
decoder := json.NewDecoder(r.Body)
var data map[string]interface{}
err := decoder.Decode(&data)
if err != nil {
writeStatus(w, http.StatusBadRequest)
return
}
log.Print(data)
}
func writeStatus(w http.ResponseWriter, code int) {
w.WriteHeader(code)
io.WriteString(w, http.StatusText(code))
}

52
main.go
View File

@ -1,11 +1,17 @@
package main
import (
"context"
"log"
"os"
"os/signal"
"sync"
"syscall"
"git.luj0ga.de/franconian/matrix-pretix/internal/config"
"git.luj0ga.de/franconian/matrix-pretix/internal/database"
"git.luj0ga.de/franconian/matrix-pretix/internal/matrix"
"git.luj0ga.de/franconian/matrix-pretix/internal/pretix"
)
func main() {
@ -18,9 +24,51 @@ func main() {
log.Fatal(err)
}
client, err := matrix.NewClient(&config.Matrix)
db, err := database.Open(&config.DB)
if err != nil {
log.Fatal(err)
}
_ = client
defer db.Close()
client, err := matrix.NewClient(&config.Matrix, db)
if err != nil {
log.Fatal(err)
}
err = client.Login()
if err != nil {
log.Fatal(err)
}
err = client.Encrypt()
if err != nil {
log.Fatal(err)
}
ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
defer stop()
var wg sync.WaitGroup
go func() {
wg.Add(1)
defer wg.Done()
<-ctx.Done()
stop()
}()
go client.Sync(ctx, &wg)
server := pretix.NewServer(client)
err = server.ListenAndServe(&config.Server, ctx, &wg)
if err != nil {
stop()
wg.Wait()
log.Fatal(err)
}
wg.Wait()
log.Print("done")
}