Initial ability to configure accounts from web interface
This commit is contained in:
parent
775fc7b217
commit
8a5ed3f507
11 changed files with 423 additions and 40 deletions
22
account.go
22
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{}) {
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
32
connector/irc/config.go
Normal file
32
connector/irc/config.go
Normal file
|
@ -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",
|
||||
},
|
||||
})
|
||||
}
|
|
@ -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) {
|
||||
|
|
52
connector/mattermost/config.go
Normal file
52
connector/mattermost/config.go
Normal file
|
@ -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",
|
||||
},
|
||||
})
|
||||
}
|
|
@ -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
|
||||
|
|
38
connector/xmpp/config.go
Normal file
38
connector/xmpp/config.go
Normal file
|
@ -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",
|
||||
},
|
||||
})
|
||||
}
|
|
@ -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
|
||||
}
|
||||
|
|
71
templates/config.html
Normal file
71
templates/config.html
Normal file
|
@ -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}}
|
|
@ -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}}
|
||||
|
|
141
web.go
141
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
|
||||
}
|
||||
// ----
|
||||
|
||||
err = SetAccount(mxid, acct.Name, acct.Protocol, config)
|
||||
if err != nil {
|
||||
ezbrSystemSendf("Could not setup account %s: %s", acct.Name, err.Error())
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
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) {
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue