aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorAlex Auvolat <alex@adnab.me>2020-02-26 22:49:27 +0100
committerAlex Auvolat <alex@adnab.me>2020-02-26 22:49:27 +0100
commit8a5ed3f507d37c52e2a68a23ced6942cc752221d (patch)
tree14e85d2f6031d6b38ad34bb7b360df3918c1c9e4
parent775fc7b2172a632587e82cd44b9d7400ca4f4f74 (diff)
downloadeasybridge-8a5ed3f507d37c52e2a68a23ced6942cc752221d.tar.gz
easybridge-8a5ed3f507d37c52e2a68a23ced6942cc752221d.zip
Initial ability to configure accounts from web interface
-rw-r--r--account.go22
-rw-r--r--connector/config.go21
-rw-r--r--connector/irc/config.go32
-rw-r--r--connector/irc/irc.go24
-rw-r--r--connector/mattermost/config.go52
-rw-r--r--connector/mattermost/mattermost.go6
-rw-r--r--connector/xmpp/config.go38
-rw-r--r--connector/xmpp/xmpp.go13
-rw-r--r--templates/config.html71
-rw-r--r--templates/home.html43
-rw-r--r--web.go139
11 files changed, 422 insertions, 39 deletions
diff --git a/account.go b/account.go
index 6785fb7..0d7f94c 100644
--- a/account.go
+++ b/account.go
@@ -34,6 +34,9 @@ func SetAccount(mxid string, name string, protocol string, config map[string]str
accounts := registeredAccounts[mxid]
if prev_acct, ok := accounts[name]; ok {
+ prev_acct.Conn.Close()
+ prev_acct.JoinedRooms = map[RoomID]bool{}
+
if protocol != prev_acct.Protocol {
return fmt.Errorf("Wrong protocol")
}
@@ -112,6 +115,8 @@ func RemoveAccount(mxUser string, name string) {
}
}
+// ----
+
func SaveDbAccounts(mxid string, key *[32]byte) {
accountsLock.Lock()
defer accountsLock.Unlock()
@@ -130,6 +135,23 @@ func SaveDbAccounts(mxid string, key *[32]byte) {
}
}
+func LoadDbAccounts(mxid string, key *[32]byte) {
+ var allAccounts []DbAccountConfig
+ db.Where(&DbAccountConfig{MxUserID: mxid}).Find(&allAccounts)
+ for _, acct := range allAccounts {
+ config, err := decryptAccountConfig(acct.Config, key)
+ if err != nil {
+ ezbrSystemSendf("Could not decrypt stored configuration for account %s", acct.Name)
+ continue
+ }
+
+ err = SetAccount(mxid, acct.Name, acct.Protocol, config)
+ if err != nil {
+ ezbrSystemSendf("Could not setup account %s: %s", acct.Name, err.Error())
+ }
+ }
+}
+
// ----
func (a *Account) ezbrMessagef(format string, args ...interface{}) {
diff --git a/connector/config.go b/connector/config.go
index e0fcf17..97e4556 100644
--- a/connector/config.go
+++ b/connector/config.go
@@ -43,3 +43,24 @@ func (c Configuration) GetBool(k string, deflt ...bool) (bool, error) {
}
return false, fmt.Errorf("Missing configuration key: %s", k)
}
+
+// ----
+
+type ConfigSchema []*ConfigEntry
+
+type ConfigEntry struct {
+ Name string
+ Description string
+ Default string
+ FixedValue string
+ Required bool
+ IsPassword bool
+ IsNumeric bool
+ IsBoolean bool
+}
+
+var Protocols = map[string]ConfigSchema{}
+
+func Register(name string, schema ConfigSchema) {
+ Protocols[name] = schema
+}
diff --git a/connector/irc/config.go b/connector/irc/config.go
new file mode 100644
index 0000000..26d9a63
--- /dev/null
+++ b/connector/irc/config.go
@@ -0,0 +1,32 @@
+package irc
+
+import (
+ . "git.deuxfleurs.fr/Deuxfleurs/easybridge/connector"
+)
+
+func init() {
+ Register("irc", ConfigSchema{
+ &ConfigEntry{
+ Name: "nick",
+ Description: "Nickname",
+ Required: true,
+ },
+ &ConfigEntry{
+ Name: "server",
+ Description: "Server",
+ Required: true,
+ },
+ &ConfigEntry{
+ Name: "port",
+ Description: "Port",
+ IsNumeric: true,
+ Default: "6667",
+ },
+ &ConfigEntry{
+ Name: "ssl",
+ Description: "Use SSL",
+ IsBoolean: true,
+ Default: "false",
+ },
+ })
+}
diff --git a/connector/irc/irc.go b/connector/irc/irc.go
index 2ed3923..d69884e 100644
--- a/connector/irc/irc.go
+++ b/connector/irc/irc.go
@@ -127,6 +127,10 @@ func (irc *IRC) SetUserInfo(info *UserInfo) error {
}
func (irc *IRC) SetRoomInfo(roomId RoomID, info *RoomInfo) error {
+ if irc.conn == nil {
+ return fmt.Errorf("Not connected")
+ }
+
ch, err := irc.checkRoomId(roomId)
if err != nil {
return err
@@ -145,6 +149,10 @@ func (irc *IRC) SetRoomInfo(roomId RoomID, info *RoomInfo) error {
}
func (irc *IRC) Join(roomId RoomID) error {
+ if irc.conn == nil {
+ return fmt.Errorf("Not connected")
+ }
+
ch, err := irc.checkRoomId(roomId)
if err != nil {
return err
@@ -155,6 +163,10 @@ func (irc *IRC) Join(roomId RoomID) error {
}
func (irc *IRC) Invite(userId UserID, roomId RoomID) error {
+ if irc.conn == nil {
+ return fmt.Errorf("Not connected")
+ }
+
who, err := irc.checkUserId(userId)
if err != nil {
return err
@@ -174,6 +186,10 @@ func (irc *IRC) Invite(userId UserID, roomId RoomID) error {
}
func (irc *IRC) Leave(roomId RoomID) {
+ if irc.conn == nil {
+ return
+ }
+
ch, err := irc.checkRoomId(roomId)
if err != nil {
return
@@ -183,6 +199,10 @@ func (irc *IRC) Leave(roomId RoomID) {
}
func (irc *IRC) Send(event *Event) error {
+ if irc.conn == nil {
+ return fmt.Errorf("Not connected")
+ }
+
// Workaround girc bug
if event.Text[0] == ':' {
event.Text = " " + event.Text
@@ -231,7 +251,9 @@ func (irc *IRC) Send(event *Event) error {
func (irc *IRC) Close() {
conn := irc.conn
irc.conn = nil
- conn.Close()
+ if conn != nil {
+ conn.Close()
+ }
}
func (irc *IRC) connectLoop(c *girc.Client) {
diff --git a/connector/mattermost/config.go b/connector/mattermost/config.go
new file mode 100644
index 0000000..b7c4ba8
--- /dev/null
+++ b/connector/mattermost/config.go
@@ -0,0 +1,52 @@
+package mattermost
+
+import (
+ . "git.deuxfleurs.fr/Deuxfleurs/easybridge/connector"
+)
+
+func init() {
+ Register("mattermost", ConfigSchema{
+ &ConfigEntry{
+ Name: "server",
+ Description: "Server",
+ Required: true,
+ },
+ &ConfigEntry{
+ Name: "username",
+ Description: "Username",
+ Required: true,
+ },
+ &ConfigEntry{
+ Name: "password",
+ Description: "Password",
+ IsPassword: true,
+ },
+ &ConfigEntry{
+ Name: "token",
+ Description: "Authentification token (replaces password if set)",
+ },
+ &ConfigEntry{
+ Name: "teams",
+ Description: "Comma-separated list of teams to follow",
+ Required: true,
+ },
+ &ConfigEntry{
+ Name: "no_tls",
+ Description: "Disable SSL/TLS",
+ IsBoolean: true,
+ Default: "false",
+ },
+ &ConfigEntry{
+ Name: "initial_backlog",
+ Description: "Maximum number of messages to load when joining a channel",
+ IsNumeric: true,
+ Default: "1000",
+ },
+ &ConfigEntry{
+ Name: "initial_members",
+ Description: "Maximum number of members to load when joining a channel",
+ IsNumeric: true,
+ Default: "100",
+ },
+ })
+}
diff --git a/connector/mattermost/mattermost.go b/connector/mattermost/mattermost.go
index 0b863fb..e3a6429 100644
--- a/connector/mattermost/mattermost.go
+++ b/connector/mattermost/mattermost.go
@@ -69,7 +69,7 @@ func (mm *Mattermost) Configure(c Configuration) error {
return err
}
- mm.initial_members, err = c.GetInt("initial_members", 1000)
+ mm.initial_members, err = c.GetInt("initial_members", 100)
if err != nil {
return err
}
@@ -312,7 +312,9 @@ func (mm *Mattermost) Send(event *Event) error {
}
func (mm *Mattermost) Close() {
- mm.conn.WsQuit = true
+ if mm.conn != nil {
+ mm.conn.WsQuit = true
+ }
if mm.handlerStopChan != nil {
mm.handlerStopChan <- true
mm.handlerStopChan = nil
diff --git a/connector/xmpp/config.go b/connector/xmpp/config.go
new file mode 100644
index 0000000..6fd5f9b
--- /dev/null
+++ b/connector/xmpp/config.go
@@ -0,0 +1,38 @@
+package xmpp
+
+import (
+ . "git.deuxfleurs.fr/Deuxfleurs/easybridge/connector"
+)
+
+func init() {
+ Register("xmpp", ConfigSchema{
+ &ConfigEntry{
+ Name: "jid",
+ Description: "JID",
+ Required: true,
+ },
+ &ConfigEntry{
+ Name: "password",
+ Description: "Password",
+ Required: true,
+ IsPassword: true,
+ },
+ &ConfigEntry{
+ Name: "nickname",
+ Description: "Nickname in MUCs",
+ Required: true,
+ },
+ &ConfigEntry{
+ Name: "port",
+ Description: "Port",
+ IsNumeric: true,
+ Default: "6667",
+ },
+ &ConfigEntry{
+ Name: "ssl",
+ Description: "Use SSL",
+ IsBoolean: true,
+ Default: "true",
+ },
+ })
+}
diff --git a/connector/xmpp/xmpp.go b/connector/xmpp/xmpp.go
index 698016f..f1a75b2 100644
--- a/connector/xmpp/xmpp.go
+++ b/connector/xmpp/xmpp.go
@@ -55,11 +55,6 @@ func (xm *XMPP) Configure(c Configuration) error {
// Parse and validate configuration
var err error
- xm.server, err = c.GetString("server")
- if err != nil {
- return err
- }
-
xm.port, err = c.GetInt("port", 5222)
if err != nil {
return err
@@ -78,9 +73,7 @@ func (xm *XMPP) Configure(c Configuration) error {
if len(jid_parts) != 2 {
return fmt.Errorf("Invalid JID: %s", xm.jid)
}
- if jid_parts[1] != xm.server {
- return fmt.Errorf("JID %s not on server %s", xm.jid, xm.server)
- }
+ xm.server = jid_parts[1]
xm.jid_localpart = jid_parts[0]
xm.nickname, _ = c.GetString("nickname", xm.jid_localpart)
@@ -353,7 +346,9 @@ func (xm *XMPP) Send(event *Event) error {
}
func (xm *XMPP) Close() {
- xm.conn.Close()
+ if xm.conn != nil {
+ xm.conn.Close()
+ }
xm.conn = nil
xm.connectorLoopNum += 1
}
diff --git a/templates/config.html b/templates/config.html
new file mode 100644
index 0000000..2d64444
--- /dev/null
+++ b/templates/config.html
@@ -0,0 +1,71 @@
+{{define "title"}}Account configuration |{{end}}
+
+{{define "body"}}
+<div class="d-flex">
+ <h4>Configure account</h4>
+ <a class="ml-auto btn btn-info" href="/">Go back</a>
+</div>
+
+{{if .ErrorMessage}}
+ <div class="alert alert-danger mt-4">An error occurred.
+ <div style="font-size: 0.8em">{{ .ErrorMessage }}</div>
+ </div>
+{{end}}
+
+<form method="POST" class="mt-4">
+ <div class="form-group">
+ <label for="name">Account name:</label>
+ <input type="text" {{if .NameEditable}}{{else}}disabled="disabled"{{end}} id="name" name="name" class="form-control" value="{{ .Name }}" />
+ {{if .InvalidName}}
+ <div class="alert alert-warning">Invalid name (must not be empty)</div>
+ {{end}}
+ </div>
+ <div class="form-group">
+ <label>Protocol:</label>
+ <input type="text" disabled="disabled" class="form-control" value="{{ .Protocol }}" />
+ </div>
+ {{$config := .Config}}
+ {{$errors := .Errors}}
+ {{range $i, $schema := .Schema}}
+ <div class="form-group">
+ <label for="{{$schema.Name}}">{{$schema.Description}}</label>
+ {{if $schema.FixedValue}}
+ <input type="text"
+ disabled="disabled"
+ class="form-control"
+ name="{{$schema.Name}}"
+ id="{{$schema.Name}}"
+ value="{{index $config $schema.Name}}" />
+ {{else if $schema.IsBoolean}}
+ {{$value := index $config $schema.Name}}
+ <label for="{{$schema.Name}}-true">
+ <input type="radio" name="{{$schema.Name}}" id="{{$schema.Name}}-true" value="true" {{if eq $value "true"}}checked="checked"{{end}} />
+ Yes
+ </label>
+ <label for="{{$schema.Name}}-false">
+ <input type="radio" name="{{$schema.Name}}" id="{{$schema.Name}}-false" value="false" {{if eq $value "false"}}checked="checked"{{end}} />
+ No
+ </label>
+ {{else if $schema.IsPassword}}
+ <input type="password"
+ class="form-control"
+ name="{{$schema.Name}}"
+ id="{{$schema.Name}}"
+ value="{{index $config $schema.Name}}" />
+ {{else}}
+ <input type="text"
+ class="form-control"
+ name="{{$schema.Name}}"
+ id="{{$schema.Name}}"
+ value="{{index $config $schema.Name}}" />
+ {{end}}
+ {{$error := index $errors $schema.Name}}
+ {{if $error}}
+ <div class="alert alert-warning mt-2">{{$error}}</div>
+ {{end}}
+ </div>
+ {{end}}
+ <button type="submit" class="btn btn-primary">Save configuration</button>
+</form>
+
+{{end}}
diff --git a/templates/home.html b/templates/home.html
index 40a0e5c..da3e478 100644
--- a/templates/home.html
+++ b/templates/home.html
@@ -8,23 +8,34 @@
<a class="ml-auto btn btn-sm btn-dark" href="/logout">Log out</a>
</div>
-<table class="table mt-4">
- <thead>
- <tr>
- <th>Account name</th>
- <th>Protocol</th>
- <th></th>
- </tr>
- </thead>
- <tbody>
- {{range $name, $acc := .Accounts}}
+{{ if .Accounts }}
+ <table class="table mt-4">
+ <thead>
<tr>
- <td>{{ $name }}</td>
- <td>{{ $acc.Protocol }}</td>
- <td>Modifier etc</td>
+ <th>Account name</th>
+ <th>Protocol</th>
+ <th></th>
</tr>
- {{end}}
- </tbody>
-</table>
+ </thead>
+ <tbody>
+ {{range $i, $acc := .Accounts}}
+ <tr>
+ <td>{{ $acc.AccountName }}</td>
+ <td>{{ $acc.Protocol }}</td>
+ <td>
+ <a class="btn btn-sm btn-primary" href="/edit/{{ $acc.AccountName }}">Modify</a>
+ <a class="btn btn-sm btn-danger ml-4" href="/delete/{{ $acc.AccountName }}">Delete</a>
+ </td>
+ </tr>
+ {{end}}
+ </tbody>
+ </table>
+{{end}}
+
+<h5 class="mt-4">Add account</h5>
+
+<a class="btn btn-sm btn-dark" href="/add/irc">IRC</a>
+<a class="btn btn-sm btn-warning ml-4" href="/add/xmpp">XMPP</a>
+<a class="btn btn-sm btn-info ml-4" href="/add/mattermost">Mattermost</a>
{{end}}
diff --git a/web.go b/web.go
index 74dd1f8..83d3283 100644
--- a/web.go
+++ b/web.go
@@ -5,12 +5,14 @@ import (
"html/template"
"log"
"net/http"
+ "strconv"
"strings"
"github.com/gorilla/mux"
"github.com/gorilla/sessions"
"golang.org/x/crypto/argon2"
+ "git.deuxfleurs.fr/Deuxfleurs/easybridge/connector"
"git.deuxfleurs.fr/Deuxfleurs/easybridge/mxlib"
)
@@ -30,6 +32,9 @@ func StartWeb() {
r := mux.NewRouter()
r.HandleFunc("/", handleHome)
r.HandleFunc("/logout", handleLogout)
+ r.HandleFunc("/add/{protocol}", handleAdd)
+ r.HandleFunc("/edit/{account}", handleEdit)
+ r.HandleFunc("/delete/{account}", handleDelete)
staticfiles := http.FileServer(http.Dir("static"))
r.Handle("/static/{file:.*}", http.StripPrefix("/static/", staticfiles))
@@ -178,19 +183,131 @@ func handleLogin(w http.ResponseWriter, r *http.Request) *LoginInfo {
}
}
-func LoadDbAccounts(mxid string, key *[32]byte) {
- var allAccounts []DbAccountConfig
- db.Where(&DbAccountConfig{MxUserID: mxid}).Find(&allAccounts)
- for _, acct := range allAccounts {
- config, err := decryptAccountConfig(acct.Config, key)
- if err != nil {
- ezbrSystemSendf("Could not decrypt stored configuration for account %s", acct.Name)
- continue
+// ----
+
+func handleAdd(w http.ResponseWriter, r *http.Request) {
+ login := checkLogin(w, r)
+ if login == nil {
+ return
+ }
+
+ protocol := mux.Vars(r)["protocol"]
+
+ configForm(w, r, login, "", protocol, map[string]string{})
+}
+
+func handleEdit(w http.ResponseWriter, r *http.Request) {
+ login := checkLogin(w, r)
+ if login == nil {
+ return
+ }
+
+ account := mux.Vars(r)["account"]
+ acct := FindAccount(login.MxId, account)
+ if acct == nil {
+ http.Error(w, "No such account", http.StatusNotFound)
+ return
+ }
+
+ configForm(w, r, login, account, acct.Protocol, acct.Config)
+}
+
+type ConfigFormData struct {
+ ErrorMessage string
+
+ Name string
+ NameEditable bool
+ InvalidName bool
+
+ Protocol string
+
+ Config map[string]string
+ Errors map[string]string
+ Schema connector.ConfigSchema
+}
+
+func configForm(w http.ResponseWriter, r *http.Request,
+ login *LoginInfo, name string, protocol string,
+ prevConfig map[string]string) {
+ templateConfig := template.Must(template.ParseFiles("templates/layout.html", "templates/config.html"))
+
+ data := &ConfigFormData{
+ Name: name,
+ NameEditable: (name == ""),
+ Protocol: protocol,
+ Config: map[string]string{},
+ Errors: map[string]string{},
+ Schema: connector.Protocols[protocol],
+ }
+ for k, v := range prevConfig {
+ data.Config[k] = v
+ }
+ for _, sch := range data.Schema {
+ if _, ok := data.Config[sch.Name]; !ok && sch.Default != "" {
+ data.Config[sch.Name] = sch.Default
}
+ }
- err = SetAccount(mxid, acct.Name, acct.Protocol, config)
- if err != nil {
- ezbrSystemSendf("Could not setup account %s: %s", acct.Name, err.Error())
+ if r.Method == "POST" {
+ ok := true
+ r.ParseForm()
+
+ if data.NameEditable {
+ data.Name = strings.Join(r.Form["name"], "")
+ if data.Name == "" {
+ ok = false
+ data.InvalidName = true
+ }
+ }
+
+ for _, schema := range data.Schema {
+ field := schema.Name
+ data.Config[field] = strings.Join(r.Form[field], "")
+ if data.Config[field] == "" {
+ if schema.Required {
+ ok = false
+ data.Errors[field] = "This field is required"
+ }
+ } else if schema.FixedValue != "" {
+ if data.Config[field] != schema.FixedValue {
+ ok = false
+ data.Errors[field] = "This field must be equal to " + schema.FixedValue
+ }
+ } else if schema.IsBoolean {
+ if data.Config[field] != "false" && data.Config[field] != "true" {
+ ok = false
+ data.Errors[field] = "This field must be 'true' or 'false'"
+ }
+ } else if schema.IsNumeric {
+ _, err := strconv.Atoi(data.Config[field])
+ if err != nil {
+ ok = false
+ data.Errors[field] = "This field must be a valid number"
+ }
+ }
+ }
+
+ if ok {
+ var entry DbAccountConfig
+ db.Where(&DbAccountConfig{
+ MxUserID: login.MxId,
+ Name: data.Name,
+ }).Assign(&DbAccountConfig{
+ Protocol: protocol,
+ Config: encryptAccountConfig(data.Config, userKeys[login.MxId]),
+ }).FirstOrCreate(&entry)
+
+ err := SetAccount(login.MxId, data.Name, protocol, data.Config)
+ if err == nil {
+ http.Redirect(w, r, "/", http.StatusFound)
+ return
+ }
+ data.ErrorMessage = err.Error()
}
}
+
+ templateConfig.Execute(w, data)
+}
+
+func handleDelete(w http.ResponseWriter, r *http.Request) {
}