diff --git a/src/github.com/matrix-org/go-neb/api/api.go b/src/github.com/matrix-org/go-neb/api/api.go new file mode 100644 index 0000000..ed97a70 --- /dev/null +++ b/src/github.com/matrix-org/go-neb/api/api.go @@ -0,0 +1,57 @@ +package api + +import ( + "encoding/json" + "errors" + "net/url" +) + +type ConfigureAuthRealmRequest struct { + ID string + Type string + Config json.RawMessage +} + +type ConfigureServiceRequest struct { + ID string + Type string + UserID string + Config json.RawMessage +} + +// A ClientConfig is the configuration for a matrix client for a bot to use. +type ClientConfig struct { + UserID string // The matrix UserId to connect with. + HomeserverURL string // A URL with the host and port of the matrix server. E.g. https://matrix.org:8448 + AccessToken string // The matrix access token to authenticate the requests with. + Sync bool // True to start a sync stream for this user + AutoJoinRooms bool // True to automatically join all rooms for this user + DisplayName string // The display name to set for the matrix client +} + +// SessionRequest are usually multi-stage things so this type only exists for the config form +type SessionRequest struct { + SessionID string + RealmID string + UserID string + Config json.RawMessage +} + +// ConfigFile represents config.sample.yaml +type ConfigFile struct { + Clients []ClientConfig + Realms []ConfigureAuthRealmRequest + Services []ConfigureServiceRequest + Sessions []SessionRequest +} + +// Check that the client has the correct fields. +func (c *ClientConfig) Check() error { + if c.UserID == "" || c.HomeserverURL == "" || c.AccessToken == "" { + return errors.New(`Must supply a "UserID", a "HomeserverURL", and an "AccessToken"`) + } + if _, err := url.Parse(c.HomeserverURL); err != nil { + return err + } + return nil +} diff --git a/src/github.com/matrix-org/go-neb/clients/clients.go b/src/github.com/matrix-org/go-neb/clients/clients.go index caab1f4..c6fa205 100644 --- a/src/github.com/matrix-org/go-neb/clients/clients.go +++ b/src/github.com/matrix-org/go-neb/clients/clients.go @@ -2,6 +2,7 @@ package clients import ( log "github.com/Sirupsen/logrus" + "github.com/matrix-org/go-neb/api" "github.com/matrix-org/go-neb/database" "github.com/matrix-org/go-neb/matrix" "github.com/matrix-org/go-neb/plugin" @@ -64,7 +65,7 @@ func (c *Clients) Client(userID string) (*matrix.Client, error) { } // Update updates the config for a matrix client -func (c *Clients) Update(config types.ClientConfig) (types.ClientConfig, error) { +func (c *Clients) Update(config api.ClientConfig) (api.ClientConfig, error) { _, old, err := c.updateClientInDB(config) return old.config, err } @@ -86,7 +87,7 @@ func (c *Clients) Start() error { } type clientEntry struct { - config types.ClientConfig + config api.ClientConfig client *matrix.Client } @@ -123,7 +124,7 @@ func (c *Clients) loadClientFromDB(userID string) (entry clientEntry, err error) return } -func (c *Clients) updateClientInDB(newConfig types.ClientConfig) (new clientEntry, old clientEntry, err error) { +func (c *Clients) updateClientInDB(newConfig api.ClientConfig) (new clientEntry, old clientEntry, err error) { c.dbMutex.Lock() defer c.dbMutex.Unlock() @@ -231,7 +232,7 @@ func (c *Clients) onRoomMemberEvent(client *matrix.Client, event *matrix.Event) } } -func (c *Clients) newClient(config types.ClientConfig) (*matrix.Client, error) { +func (c *Clients) newClient(config api.ClientConfig) (*matrix.Client, error) { homeserverURL, err := url.Parse(config.HomeserverURL) if err != nil { return nil, err diff --git a/src/github.com/matrix-org/go-neb/config.go b/src/github.com/matrix-org/go-neb/config.go deleted file mode 100644 index f63bae0..0000000 --- a/src/github.com/matrix-org/go-neb/config.go +++ /dev/null @@ -1,87 +0,0 @@ -package main - -import ( - "encoding/json" - "fmt" - log "github.com/Sirupsen/logrus" - "github.com/matrix-org/go-neb/database" - "github.com/matrix-org/go-neb/types" - "gopkg.in/yaml.v2" - "io/ioutil" -) - -type configFile struct { - Clients []types.ClientConfig - Realms []configureAuthRealmRequest - // Sessions []sessionConfig `yaml:"sessions"` - // Services []serviceConfig -} - -func loadFromConfig(db *database.ServiceDB, configFilePath string) (*configFile, error) { - // ::Horrible hacks ahead:: - // The config is represented as YAML, and we want to convert that into NEB types. - // However, NEB types make liberal use of json.RawMessage which the YAML parser - // doesn't like. We can't implement MarshalYAML/UnmarshalYAML as a custom type easily - // because YAML is insane and supports numbers as keys. The YAML parser therefore has the - // generic form of map[interface{}]interface{} - but the JSON parser doesn't know - // how to parse that. - // - // The hack that follows gets around this by type asserting all parsed YAML keys as - // strings then re-encoding/decoding as JSON. That is: - // YAML bytes -> map[interface]interface -> map[string]interface -> JSON bytes -> NEB types - - // Convert to YAML bytes - contents, err := ioutil.ReadFile(configFilePath) - if err != nil { - return nil, err - } - - // Convert to map[interface]interface - var cfg map[interface{}]interface{} - if err = yaml.Unmarshal(contents, &cfg); err != nil { - return nil, fmt.Errorf("Failed to unmarshal YAML: %s", err) - } - - // Convert to map[string]interface - dict := convertKeysToStrings(cfg) - - // Convert to JSON bytes - b, err := json.Marshal(dict) - if err != nil { - return nil, fmt.Errorf("Failed to marshal config as JSON: %s", err) - } - - // Finally, Convert to NEB types - var c configFile - if err := json.Unmarshal(b, &c); err != nil { - return nil, fmt.Errorf("Failed to convert to config file: %s", err) - } - log.Print(c.Realms) - - // sanity check (at least 1 client and 1 service) - if len(c.Clients) == 0 { - return nil, fmt.Errorf("At least 1 client and 1 service must be specified") - } - - return &c, nil -} - -func convertKeysToStrings(iface interface{}) interface{} { - obj, isObj := iface.(map[interface{}]interface{}) - if isObj { - strObj := make(map[string]interface{}) - for k, v := range obj { - strObj[k.(string)] = convertKeysToStrings(v) // handle nested objects - } - return strObj - } - - arr, isArr := iface.([]interface{}) - if isArr { - for i := range arr { - arr[i] = convertKeysToStrings(arr[i]) // handle nested objects - } - return arr - } - return iface // base type like string or number -} diff --git a/src/github.com/matrix-org/go-neb/database/db.go b/src/github.com/matrix-org/go-neb/database/db.go index 1aaf932..5b8dad6 100644 --- a/src/github.com/matrix-org/go-neb/database/db.go +++ b/src/github.com/matrix-org/go-neb/database/db.go @@ -2,6 +2,7 @@ package database import ( "database/sql" + "github.com/matrix-org/go-neb/api" "github.com/matrix-org/go-neb/types" "time" ) @@ -48,7 +49,7 @@ func Open(databaseType, databaseURL string) (serviceDB *ServiceDB, err error) { // StoreMatrixClientConfig stores the Matrix client config for a bot service. // If a config already exists then it will be updated, otherwise a new config // will be inserted. The previous config is returned. -func (d *ServiceDB) StoreMatrixClientConfig(config types.ClientConfig) (oldConfig types.ClientConfig, err error) { +func (d *ServiceDB) StoreMatrixClientConfig(config api.ClientConfig) (oldConfig api.ClientConfig, err error) { err = runTransaction(d.db, func(txn *sql.Tx) error { oldConfig, err = selectMatrixClientConfigTxn(txn, config.UserID) now := time.Now() @@ -64,7 +65,7 @@ func (d *ServiceDB) StoreMatrixClientConfig(config types.ClientConfig) (oldConfi } // LoadMatrixClientConfigs loads all Matrix client configs from the database. -func (d *ServiceDB) LoadMatrixClientConfigs() (configs []types.ClientConfig, err error) { +func (d *ServiceDB) LoadMatrixClientConfigs() (configs []api.ClientConfig, err error) { err = runTransaction(d.db, func(txn *sql.Tx) error { configs, err = selectMatrixClientConfigsTxn(txn) return err @@ -74,7 +75,7 @@ func (d *ServiceDB) LoadMatrixClientConfigs() (configs []types.ClientConfig, err // LoadMatrixClientConfig loads a Matrix client config from the database. // Returns sql.ErrNoRows if the client isn't in the database. -func (d *ServiceDB) LoadMatrixClientConfig(userID string) (config types.ClientConfig, err error) { +func (d *ServiceDB) LoadMatrixClientConfig(userID string) (config api.ClientConfig, err error) { err = runTransaction(d.db, func(txn *sql.Tx) error { config, err = selectMatrixClientConfigTxn(txn, userID) return err @@ -272,6 +273,10 @@ func (d *ServiceDB) StoreBotOptions(opts types.BotOptions) (oldOpts types.BotOpt return } +func (d *ServiceDB) InsertFromConfig(cfg *api.ConfigFile) error { + return nil +} + func runTransaction(db *sql.DB, fn func(txn *sql.Tx) error) (err error) { txn, err := db.Begin() if err != nil { diff --git a/src/github.com/matrix-org/go-neb/database/schema.go b/src/github.com/matrix-org/go-neb/database/schema.go index d8ae5cd..7e3a30f 100644 --- a/src/github.com/matrix-org/go-neb/database/schema.go +++ b/src/github.com/matrix-org/go-neb/database/schema.go @@ -4,6 +4,7 @@ import ( "database/sql" "encoding/json" "fmt" + "github.com/matrix-org/go-neb/api" "github.com/matrix-org/go-neb/types" "time" ) @@ -64,7 +65,7 @@ const selectMatrixClientConfigSQL = ` SELECT client_json FROM matrix_clients WHERE user_id = $1 ` -func selectMatrixClientConfigTxn(txn *sql.Tx, userID string) (config types.ClientConfig, err error) { +func selectMatrixClientConfigTxn(txn *sql.Tx, userID string) (config api.ClientConfig, err error) { var configJSON []byte err = txn.QueryRow(selectMatrixClientConfigSQL, userID).Scan(&configJSON) if err != nil { @@ -78,14 +79,14 @@ const selectMatrixClientConfigsSQL = ` SELECT client_json FROM matrix_clients ` -func selectMatrixClientConfigsTxn(txn *sql.Tx) (configs []types.ClientConfig, err error) { +func selectMatrixClientConfigsTxn(txn *sql.Tx) (configs []api.ClientConfig, err error) { rows, err := txn.Query(selectMatrixClientConfigsSQL) if err != nil { return } defer rows.Close() for rows.Next() { - var config types.ClientConfig + var config api.ClientConfig var configJSON []byte if err = rows.Scan(&configJSON); err != nil { return @@ -104,7 +105,7 @@ INSERT INTO matrix_clients( ) VALUES ($1, $2, '', $3, $4) ` -func insertMatrixClientConfigTxn(txn *sql.Tx, now time.Time, config types.ClientConfig) error { +func insertMatrixClientConfigTxn(txn *sql.Tx, now time.Time, config api.ClientConfig) error { t := now.UnixNano() / 1000000 configJSON, err := json.Marshal(&config) if err != nil { @@ -119,7 +120,7 @@ UPDATE matrix_clients SET client_json = $1, time_updated_ms = $2 WHERE user_id = $3 ` -func updateMatrixClientConfigTxn(txn *sql.Tx, now time.Time, config types.ClientConfig) error { +func updateMatrixClientConfigTxn(txn *sql.Tx, now time.Time, config api.ClientConfig) error { t := now.UnixNano() / 1000000 configJSON, err := json.Marshal(&config) if err != nil { diff --git a/src/github.com/matrix-org/go-neb/goneb.go b/src/github.com/matrix-org/go-neb/goneb.go index d8a3ad4..21f5803 100644 --- a/src/github.com/matrix-org/go-neb/goneb.go +++ b/src/github.com/matrix-org/go-neb/goneb.go @@ -1,8 +1,11 @@ package main import ( + "encoding/json" + "fmt" log "github.com/Sirupsen/logrus" "github.com/matrix-org/dugong" + "github.com/matrix-org/go-neb/api" "github.com/matrix-org/go-neb/clients" "github.com/matrix-org/go-neb/database" _ "github.com/matrix-org/go-neb/metrics" @@ -19,12 +22,83 @@ import ( "github.com/matrix-org/go-neb/types" _ "github.com/mattn/go-sqlite3" "github.com/prometheus/client_golang/prometheus" + "gopkg.in/yaml.v2" + "io/ioutil" "net/http" _ "net/http/pprof" "os" "path/filepath" ) +// loadFromConfig loads a config file and returns a ConfigFile +func loadFromConfig(db *database.ServiceDB, configFilePath string) (*api.ConfigFile, error) { + // ::Horrible hacks ahead:: + // The config is represented as YAML, and we want to convert that into NEB types. + // However, NEB types make liberal use of json.RawMessage which the YAML parser + // doesn't like. We can't implement MarshalYAML/UnmarshalYAML as a custom type easily + // because YAML is insane and supports numbers as keys. The YAML parser therefore has the + // generic form of map[interface{}]interface{} - but the JSON parser doesn't know + // how to parse that. + // + // The hack that follows gets around this by type asserting all parsed YAML keys as + // strings then re-encoding/decoding as JSON. That is: + // YAML bytes -> map[interface]interface -> map[string]interface -> JSON bytes -> NEB types + + // Convert to YAML bytes + contents, err := ioutil.ReadFile(configFilePath) + if err != nil { + return nil, err + } + + // Convert to map[interface]interface + var cfg map[interface{}]interface{} + if err = yaml.Unmarshal(contents, &cfg); err != nil { + return nil, fmt.Errorf("Failed to unmarshal YAML: %s", err) + } + + // Convert to map[string]interface + dict := convertKeysToStrings(cfg) + + // Convert to JSON bytes + b, err := json.Marshal(dict) + if err != nil { + return nil, fmt.Errorf("Failed to marshal config as JSON: %s", err) + } + + // Finally, Convert to NEB types + var c api.ConfigFile + if err := json.Unmarshal(b, &c); err != nil { + return nil, fmt.Errorf("Failed to convert to config file: %s", err) + } + + // sanity check (at least 1 client and 1 service) + if len(c.Clients) == 0 || len(c.Services) == 0 { + return nil, fmt.Errorf("At least 1 client and 1 service must be specified") + } + + return &c, nil +} + +func convertKeysToStrings(iface interface{}) interface{} { + obj, isObj := iface.(map[interface{}]interface{}) + if isObj { + strObj := make(map[string]interface{}) + for k, v := range obj { + strObj[k.(string)] = convertKeysToStrings(v) // handle nested objects + } + return strObj + } + + arr, isArr := iface.([]interface{}) + if isArr { + for i := range arr { + arr[i] = convertKeysToStrings(arr[i]) // handle nested objects + } + return arr + } + return iface // base type like string or number +} + func main() { bindAddress := os.Getenv("BIND_ADDRESS") databaseType := os.Getenv("DATABASE_TYPE") @@ -63,11 +137,13 @@ func main() { database.SetServiceDB(db) if configYAML != "" { - var cfg *configFile + var cfg *api.ConfigFile if cfg, err = loadFromConfig(db, configYAML); err != nil { log.WithError(err).WithField("config_file", configYAML).Panic("Failed to load config file") } - log.Info(cfg) + if err := db.InsertFromConfig(cfg); err != nil { + log.WithError(err).Panic("Failed to persist config data into in-memory DB") + } } clients := clients.New(db) diff --git a/src/github.com/matrix-org/go-neb/api.go b/src/github.com/matrix-org/go-neb/handlers.go similarity index 97% rename from src/github.com/matrix-org/go-neb/api.go rename to src/github.com/matrix-org/go-neb/handlers.go index 37f13c9..e89fac5 100644 --- a/src/github.com/matrix-org/go-neb/api.go +++ b/src/github.com/matrix-org/go-neb/handlers.go @@ -5,6 +5,7 @@ import ( "encoding/base64" "encoding/json" log "github.com/Sirupsen/logrus" + "github.com/matrix-org/go-neb/api" "github.com/matrix-org/go-neb/clients" "github.com/matrix-org/go-neb/database" "github.com/matrix-org/go-neb/errors" @@ -132,17 +133,11 @@ type configureAuthRealmHandler struct { db *database.ServiceDB } -type configureAuthRealmRequest struct { - ID string - Type string - Config json.RawMessage -} - func (h *configureAuthRealmHandler) OnIncomingRequest(req *http.Request) (interface{}, *errors.HTTPError) { if req.Method != "POST" { return nil, &errors.HTTPError{nil, "Unsupported Method", 405} } - var body configureAuthRealmRequest + var body api.ConfigureAuthRealmRequest if err := json.NewDecoder(req.Body).Decode(&body); err != nil { return nil, &errors.HTTPError{err, "Error parsing request JSON", 400} } @@ -225,7 +220,7 @@ func (s *configureClientHandler) OnIncomingRequest(req *http.Request) (interface return nil, &errors.HTTPError{nil, "Unsupported Method", 405} } - var body types.ClientConfig + var body api.ClientConfig if err := json.NewDecoder(req.Body).Decode(&body); err != nil { return nil, &errors.HTTPError{err, "Error parsing request JSON", 400} } @@ -240,8 +235,8 @@ func (s *configureClientHandler) OnIncomingRequest(req *http.Request) (interface } return &struct { - OldClient types.ClientConfig - NewClient types.ClientConfig + OldClient api.ClientConfig + NewClient api.ClientConfig }{oldClient, body}, nil } @@ -336,12 +331,7 @@ func (s *configureServiceHandler) OnIncomingRequest(req *http.Request) (interfac } func (s *configureServiceHandler) createService(req *http.Request) (types.Service, *errors.HTTPError) { - var body struct { - ID string - Type string - UserID string - Config json.RawMessage - } + var body api.ConfigureServiceRequest if err := json.NewDecoder(req.Body).Decode(&body); err != nil { return nil, &errors.HTTPError{err, "Error parsing request JSON", 400} } diff --git a/src/github.com/matrix-org/go-neb/types/types.go b/src/github.com/matrix-org/go-neb/types/types.go index fa446a0..ed1ae38 100644 --- a/src/github.com/matrix-org/go-neb/types/types.go +++ b/src/github.com/matrix-org/go-neb/types/types.go @@ -7,32 +7,10 @@ import ( "github.com/matrix-org/go-neb/matrix" "github.com/matrix-org/go-neb/plugin" "net/http" - "net/url" "strings" "time" ) -// A ClientConfig is the configuration for a matrix client for a bot to use. -type ClientConfig struct { - UserID string `yaml:"user_id"` // The matrix UserId to connect with. - HomeserverURL string `yaml:"homeserver_url"` // A URL with the host and port of the matrix server. E.g. https://matrix.org:8448 - AccessToken string `yaml:"access_token"` // The matrix access token to authenticate the requests with. - Sync bool `yaml:"sync"` // True to start a sync stream for this user - AutoJoinRooms bool `yaml:"auto_join_rooms"` // True to automatically join all rooms for this user - DisplayName string `yaml:"display_name"` // The display name to set for the matrix client -} - -// Check that the client has the correct fields. -func (c *ClientConfig) Check() error { - if c.UserID == "" || c.HomeserverURL == "" || c.AccessToken == "" { - return errors.New(`Must supply a "UserID", a "HomeserverURL", and an "AccessToken"`) - } - if _, err := url.Parse(c.HomeserverURL); err != nil { - return err - } - return nil -} - // BotOptions for a given bot user in a given room type BotOptions struct { RoomID string