Browse Source

Add 'api' package and move HTTP API requests there

This will make docs easier in addition to allowing the 'database' package
not circular-dep.
pull/98/head
Kegan Dougal 8 years ago
parent
commit
295c9bbb4b
  1. 57
      src/github.com/matrix-org/go-neb/api/api.go
  2. 9
      src/github.com/matrix-org/go-neb/clients/clients.go
  3. 87
      src/github.com/matrix-org/go-neb/config.go
  4. 11
      src/github.com/matrix-org/go-neb/database/db.go
  5. 11
      src/github.com/matrix-org/go-neb/database/schema.go
  6. 80
      src/github.com/matrix-org/go-neb/goneb.go
  7. 22
      src/github.com/matrix-org/go-neb/handlers.go
  8. 22
      src/github.com/matrix-org/go-neb/types/types.go

57
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
}

9
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

87
src/github.com/matrix-org/go-neb/config.go

@ -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
}

11
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 {

11
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 {

80
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)

22
src/github.com/matrix-org/go-neb/api.go → 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}
}

22
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

Loading…
Cancel
Save