Switch startup, config, commands and db migrations to mautrix-go systems

This commit is contained in:
Tulir Asokan
2022-05-22 22:16:42 +03:00
parent cf5384d908
commit 9f9f7ca4fd
74 changed files with 3470 additions and 5682 deletions

View File

@@ -1,85 +0,0 @@
package config
import (
as "maunium.net/go/mautrix/appservice"
)
type appservice struct {
Address string `yaml:"address"`
Hostname string `yaml:"hostname"`
Port uint16 `yaml:"port"`
ID string `yaml:"id"`
Bot bot `yaml:"bot"`
Provisioning provisioning `yaml:"provisioning"`
Database database `yaml:"database"`
EphemeralEvents bool `yaml:"ephemeral_events"`
ASToken string `yaml:"as_token"`
HSToken string `yaml:"hs_token"`
}
func (a *appservice) validate() error {
if a.ID == "" {
a.ID = "discord"
}
if a.Address == "" {
a.Address = "http://localhost:29350"
}
if a.Hostname == "" {
a.Hostname = "0.0.0.0"
}
if a.Port == 0 {
a.Port = 29350
}
if err := a.Database.validate(); err != nil {
return err
}
if err := a.Bot.validate(); err != nil {
return err
}
return nil
}
func (a *appservice) UnmarshalYAML(unmarshal func(interface{}) error) error {
type rawAppservice appservice
raw := rawAppservice{}
if err := unmarshal(&raw); err != nil {
return err
}
*a = appservice(raw)
return a.validate()
}
func (cfg *Config) CreateAppService() (*as.AppService, error) {
appservice := as.Create()
appservice.HomeserverURL = cfg.Homeserver.Address
appservice.HomeserverDomain = cfg.Homeserver.Domain
appservice.Host.Hostname = cfg.Appservice.Hostname
appservice.Host.Port = cfg.Appservice.Port
appservice.DefaultHTTPRetries = 4
reg, err := cfg.getRegistration()
if err != nil {
return nil, err
}
appservice.Registration = reg
return appservice, nil
}

View File

@@ -1,33 +0,0 @@
package config
type bot struct {
Username string `yaml:"username"`
Displayname string `yaml:"displayname"`
Avatar string `yaml:"avatar"`
}
func (b *bot) validate() error {
if b.Username == "" {
b.Username = "discordbot"
}
if b.Displayname == "" {
b.Displayname = "Discord Bridge Bot"
}
return nil
}
func (b *bot) UnmarshalYAML(unmarshal func(interface{}) error) error {
type rawBot bot
raw := rawBot{}
if err := unmarshal(&raw); err != nil {
return err
}
*b = bot(raw)
return b.validate()
}

View File

@@ -1,3 +1,19 @@
// mautrix-discord - A Matrix-Discord puppeting bridge.
// Copyright (C) 2022 Tulir Asokan
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
package config
import (
@@ -5,19 +21,19 @@ import (
"strings"
"text/template"
"maunium.net/go/mautrix/id"
"github.com/bwmarrin/discordgo"
"maunium.net/go/mautrix/bridge/bridgeconfig"
)
type bridge struct {
type BridgeConfig struct {
UsernameTemplate string `yaml:"username_template"`
DisplaynameTemplate string `yaml:"displayname_template"`
ChannelnameTemplate string `yaml:"channelname_template"`
CommandPrefix string `yaml:"command_prefix"`
ManagementRoomText managementRoomText `yaml:"management_root_text"`
ManagementRoomText bridgeconfig.ManagementRoomTexts `yaml:"management_room_text"`
PortalMessageBuffer int `yaml:"portal_message_buffer"`
@@ -30,127 +46,81 @@ type bridge struct {
DoublePuppetAllowDiscovery bool `yaml:"double_puppet_allow_discovery"`
LoginSharedSecretMap map[string]string `yaml:"login_shared_secret_map"`
Encryption encryption `yaml:"encryption"`
Encryption bridgeconfig.EncryptionConfig `yaml:"encryption"`
Provisioning struct {
Prefix string `yaml:"prefix"`
SharedSecret string `yaml:"shared_secret"`
} `yaml:"provisioning"`
Permissions bridgeconfig.PermissionConfig `yaml:"permissions"`
usernameTemplate *template.Template `yaml:"-"`
displaynameTemplate *template.Template `yaml:"-"`
channelnameTemplate *template.Template `yaml:"-"`
}
func (config *Config) CanAutoDoublePuppet(userID id.UserID) bool {
_, homeserver, _ := userID.Parse()
_, hasSecret := config.Bridge.LoginSharedSecretMap[homeserver]
type umBridgeConfig BridgeConfig
return hasSecret
}
func (b *bridge) validate() error {
var err error
if b.UsernameTemplate == "" {
b.UsernameTemplate = "discord_{{.}}"
}
b.usernameTemplate, err = template.New("username").Parse(b.UsernameTemplate)
func (bc *BridgeConfig) UnmarshalYAML(unmarshal func(interface{}) error) error {
err := unmarshal((*umBridgeConfig)(bc))
if err != nil {
return err
}
if b.DisplaynameTemplate == "" {
b.DisplaynameTemplate = "{{.Username}}#{{.Discriminator}} (D){{if .Bot}} (bot){{end}}"
bc.usernameTemplate, err = template.New("username").Parse(bc.UsernameTemplate)
if err != nil {
return err
} else if !strings.Contains(bc.FormatUsername("1234567890"), "1234567890") {
return fmt.Errorf("username template is missing user ID placeholder")
}
b.displaynameTemplate, err = template.New("displayname").Parse(b.DisplaynameTemplate)
bc.displaynameTemplate, err = template.New("displayname").Parse(bc.DisplaynameTemplate)
if err != nil {
return err
}
if b.ChannelnameTemplate == "" {
b.ChannelnameTemplate = "{{if .Guild}}{{.Guild}} - {{end}}{{if .Folder}}{{.Folder}} - {{end}}{{.Name}} (D)"
}
b.channelnameTemplate, err = template.New("channelname").Parse(b.ChannelnameTemplate)
bc.channelnameTemplate, err = template.New("channelname").Parse(bc.ChannelnameTemplate)
if err != nil {
return err
}
if b.PortalMessageBuffer <= 0 {
b.PortalMessageBuffer = 128
}
if b.CommandPrefix == "" {
b.CommandPrefix = "!dis"
}
if err := b.ManagementRoomText.validate(); err != nil {
return err
}
return nil
}
func (b *bridge) UnmarshalYAML(unmarshal func(interface{}) error) error {
type rawBridge bridge
var _ bridgeconfig.BridgeConfig = (*BridgeConfig)(nil)
// Set our defaults that aren't zero values.
raw := rawBridge{
SyncWithCustomPuppets: true,
DefaultBridgeReceipts: true,
DefaultBridgePresence: true,
}
err := unmarshal(&raw)
if err != nil {
return err
}
*b = bridge(raw)
return b.validate()
func (bc BridgeConfig) GetEncryptionConfig() bridgeconfig.EncryptionConfig {
return bc.Encryption
}
func (b bridge) FormatUsername(userid string) string {
func (bc BridgeConfig) GetCommandPrefix() string {
return bc.CommandPrefix
}
func (bc BridgeConfig) GetManagementRoomTexts() bridgeconfig.ManagementRoomTexts {
return bc.ManagementRoomText
}
func (bc BridgeConfig) FormatUsername(userid string) string {
var buffer strings.Builder
b.usernameTemplate.Execute(&buffer, userid)
_ = bc.usernameTemplate.Execute(&buffer, userid)
return buffer.String()
}
type simplfiedUser struct {
Username string
Discriminator string
Locale string
Verified bool
MFAEnabled bool
Bot bool
System bool
}
func (b bridge) FormatDisplayname(user *discordgo.User) string {
func (bc BridgeConfig) FormatDisplayname(user *discordgo.User) string {
var buffer strings.Builder
b.displaynameTemplate.Execute(&buffer, simplfiedUser{
Username: user.Username,
Discriminator: user.Discriminator,
Locale: user.Locale,
Verified: user.Verified,
MFAEnabled: user.MFAEnabled,
Bot: user.Bot,
System: user.System,
})
_ = bc.displaynameTemplate.Execute(&buffer, user)
return buffer.String()
}
type simplfiedChannel struct {
type wrappedChannel struct {
*discordgo.Channel
Guild string
Folder string
Name string
NSFW bool
}
func (b bridge) FormatChannelname(channel *discordgo.Channel, session *discordgo.Session) (string, error) {
func (bc BridgeConfig) FormatChannelname(channel *discordgo.Channel, session *discordgo.Session) (string, error) {
var buffer strings.Builder
var guildName, folderName string
@@ -171,18 +141,17 @@ func (b bridge) FormatChannelname(channel *discordgo.Channel, session *discordgo
if channel.Name == "" {
recipients := make([]string, len(channel.Recipients))
for idx, user := range channel.Recipients {
recipients[idx] = b.FormatDisplayname(user)
recipients[idx] = bc.FormatDisplayname(user)
}
return strings.Join(recipients, ", "), nil
}
}
b.channelnameTemplate.Execute(&buffer, simplfiedChannel{
Guild: guildName,
Folder: folderName,
Name: channel.Name,
NSFW: channel.NSFW,
_ = bc.channelnameTemplate.Execute(&buffer, wrappedChannel{
Channel: channel,
Guild: guildName,
Folder: folderName,
})
return buffer.String(), nil

View File

@@ -1,36 +0,0 @@
package config
import (
"fmt"
"os"
"go.mau.fi/mautrix-discord/globals"
)
type Cmd struct {
HomeserverAddress string `kong:"arg,help='The url to for the homeserver',required='1'"`
Domain string `kong:"arg,help='The domain for the homeserver',required='1'"`
Force bool `kong:"flag,help='Overwrite an existing configuration file if one already exists',short='f',default='0'"`
}
func (c *Cmd) Run(g *globals.Globals) error {
if _, err := os.Stat(g.Config); err == nil {
if c.Force == false {
return fmt.Errorf("file %q exists, use -f to overwrite", g.Config)
}
}
cfg := &Config{
Homeserver: homeserver{
Address: c.HomeserverAddress,
Domain: c.Domain,
},
}
if err := cfg.validate(); err != nil {
return err
}
return cfg.Save(g.Config)
}

View File

@@ -1,101 +1,35 @@
// mautrix-discord - A Matrix-Discord puppeting bridge.
// Copyright (C) 2022 Tulir Asokan
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
package config
import (
"fmt"
"io/ioutil"
"gopkg.in/yaml.v2"
"maunium.net/go/mautrix/bridge/bridgeconfig"
"maunium.net/go/mautrix/id"
)
type Config struct {
Homeserver homeserver `yaml:"homeserver"`
Appservice appservice `yaml:"appservice"`
Bridge bridge `yaml:"bridge"`
Logging logging `yaml:"logging"`
*bridgeconfig.BaseConfig `yaml:",inline"`
filename string `yaml:"-"`
Bridge BridgeConfig `yaml:"bridge"`
}
var configUpdated bool
func (config *Config) CanAutoDoublePuppet(userID id.UserID) bool {
_, homeserver, _ := userID.Parse()
_, hasSecret := config.Bridge.LoginSharedSecretMap[homeserver]
func (cfg *Config) validate() error {
if err := cfg.Homeserver.validate(); err != nil {
return err
}
if err := cfg.Appservice.validate(); err != nil {
return err
}
if err := cfg.Bridge.validate(); err != nil {
return err
}
if err := cfg.Logging.validate(); err != nil {
return err
}
if configUpdated {
return cfg.Save(cfg.filename)
}
return nil
}
func (cfg *Config) UnmarshalYAML(unmarshal func(interface{}) error) error {
type rawConfig Config
raw := rawConfig{
filename: cfg.filename,
}
if err := unmarshal(&raw); err != nil {
return err
}
*cfg = Config(raw)
return cfg.validate()
}
func FromBytes(filename string, data []byte) (*Config, error) {
cfg := Config{
filename: filename,
}
if err := yaml.Unmarshal(data, &cfg); err != nil {
return nil, err
}
if err := cfg.validate(); err != nil {
return nil, err
}
return &cfg, nil
}
func FromString(str string) (*Config, error) {
return FromBytes("", []byte(str))
}
func FromFile(filename string) (*Config, error) {
data, err := ioutil.ReadFile(filename)
if err != nil {
return nil, err
}
return FromBytes(filename, data)
}
func (cfg *Config) Save(filename string) error {
if filename == "" {
return fmt.Errorf("no filename specified yep")
}
data, err := yaml.Marshal(cfg)
if err != nil {
return err
}
return ioutil.WriteFile(filename, data, 0600)
return hasSecret
}

View File

@@ -1,58 +0,0 @@
package config
import (
log "maunium.net/go/maulogger/v2"
db "go.mau.fi/mautrix-discord/database"
)
type database struct {
Type string `yaml:"type"`
URI string `yaml:"uri"`
MaxOpenConns int `yaml:"max_open_conns"`
MaxIdleConns int `yaml:"max_idle_conns"`
}
func (d *database) validate() error {
if d.Type == "" {
d.Type = "sqlite3"
}
if d.URI == "" {
d.URI = "mautrix-discord.db"
}
if d.MaxOpenConns == 0 {
d.MaxOpenConns = 20
}
if d.MaxIdleConns == 0 {
d.MaxIdleConns = 2
}
return nil
}
func (d *database) UnmarshalYAML(unmarshal func(interface{}) error) error {
type rawDatabase database
raw := rawDatabase{}
if err := unmarshal(&raw); err != nil {
return err
}
*d = database(raw)
return d.validate()
}
func (c *Config) CreateDatabase(baseLog log.Logger) (*db.Database, error) {
return db.New(
c.Appservice.Database.Type,
c.Appservice.Database.URI,
c.Appservice.Database.MaxOpenConns,
c.Appservice.Database.MaxIdleConns,
baseLog,
)
}

View File

@@ -1,29 +0,0 @@
package config
type encryption struct {
Allow bool `yaml:"allow"`
Default bool `yaml:"default"`
KeySharing struct {
Allow bool `yaml:"allow"`
RequireCrossSigning bool `yaml:"require_cross_signing"`
RequireVerification bool `yaml:"require_verification"`
} `yaml:"key_sharing"`
}
func (e *encryption) validate() error {
return nil
}
func (e *encryption) UnmarshalYAML(unmarshal func(interface{}) error) error {
type rawEncryption encryption
raw := rawEncryption{}
if err := unmarshal(&raw); err != nil {
return err
}
*e = encryption(raw)
return e.validate()
}

View File

@@ -1,43 +0,0 @@
package config
import (
"errors"
)
var (
ErrHomeserverNoAddress = errors.New("no homeserver address specified")
ErrHomeserverNoDomain = errors.New("no homeserver domain specified")
)
type homeserver struct {
Address string `yaml:"address"`
Domain string `yaml:"domain"`
Asmux bool `yaml:"asmux"`
StatusEndpoint string `yaml:"status_endpoint"`
AsyncMedia bool `yaml:"async_media"`
}
func (h *homeserver) validate() error {
if h.Address == "" {
return ErrHomeserverNoAddress
}
if h.Domain == "" {
return ErrHomeserverNoDomain
}
return nil
}
func (h *homeserver) UnmarshalYAML(unmarshal func(interface{}) error) error {
type rawHomeserver homeserver
raw := rawHomeserver{}
if err := unmarshal(&raw); err != nil {
return err
}
*h = homeserver(raw)
return h.validate()
}

View File

@@ -1,89 +0,0 @@
package config
import (
"errors"
"strings"
"maunium.net/go/maulogger/v2"
as "maunium.net/go/mautrix/appservice"
)
type logging as.LogConfig
func (l *logging) validate() error {
if l.Directory == "" {
l.Directory = "./logs"
}
if l.FileNameFormat == "" {
l.FileNameFormat = "{{.Date}}-{{.Index}}.log"
}
if l.FileDateFormat == "" {
l.FileDateFormat = "2006-01-02"
}
if l.FileMode == 0 {
l.FileMode = 384
}
if l.TimestampFormat == "" {
l.TimestampFormat = "Jan _2, 2006 15:04:05"
}
if l.RawPrintLevel == "" {
l.RawPrintLevel = "debug"
} else {
switch strings.ToUpper(l.RawPrintLevel) {
case "TRACE":
l.PrintLevel = -10
case "DEBUG":
l.PrintLevel = maulogger.LevelDebug.Severity
case "INFO":
l.PrintLevel = maulogger.LevelInfo.Severity
case "WARN", "WARNING":
l.PrintLevel = maulogger.LevelWarn.Severity
case "ERR", "ERROR":
l.PrintLevel = maulogger.LevelError.Severity
case "FATAL":
l.PrintLevel = maulogger.LevelFatal.Severity
default:
return errors.New("invalid print level " + l.RawPrintLevel)
}
}
return nil
}
func (l *logging) UnmarshalYAML(unmarshal func(interface{}) error) error {
type rawLogging logging
raw := rawLogging{}
if err := unmarshal(&raw); err != nil {
return err
}
*l = logging(raw)
return l.validate()
}
func (cfg *Config) CreateLogger() (maulogger.Logger, error) {
logger := maulogger.Create()
// create an as.LogConfig from our config so we can configure the logger
realLogConfig := as.LogConfig(cfg.Logging)
realLogConfig.Configure(logger)
// Set the default logger.
maulogger.DefaultLogger = logger.(*maulogger.BasicLogger)
// If we were given a filename format attempt to open the file.
if cfg.Logging.FileNameFormat != "" {
if err := maulogger.OpenFile(); err != nil {
return nil, err
}
}
return logger, nil
}

View File

@@ -1,38 +0,0 @@
package config
type managementRoomText struct {
Welcome string `yaml:"welcome"`
Connected string `yaml:"welcome_connected"`
NotConnected string `yaml:"welcome_unconnected"`
AdditionalHelp string `yaml:"additional_help"`
}
func (m *managementRoomText) validate() error {
if m.Welcome == "" {
m.Welcome = "Greetings, I am a Discord bridge bot!"
}
if m.Connected == "" {
m.Connected = "Use `help` to get started."
}
if m.NotConnected == "" {
m.NotConnected = "Use `help` to get started, or `login` to login."
}
return nil
}
func (m *managementRoomText) UnmarshalYAML(unmarshal func(interface{}) error) error {
type rawManagementRoomText managementRoomText
raw := rawManagementRoomText{}
if err := unmarshal(&raw); err != nil {
return err
}
*m = managementRoomText(raw)
return m.validate()
}

View File

@@ -1,43 +0,0 @@
package config
import (
"strings"
as "maunium.net/go/mautrix/appservice"
)
type provisioning struct {
Prefix string `yaml:"prefix"`
SharedSecret string `yaml:"shared_secret"`
}
func (p *provisioning) validate() error {
if p.Prefix == "" {
p.Prefix = "/_matrix/provision/v1"
}
if strings.ToLower(p.SharedSecret) == "generate" {
p.SharedSecret = as.RandomString(64)
configUpdated = true
}
return nil
}
func (p *provisioning) UnmarshalYAML(unmarshal func(interface{}) error) error {
type rawProvisioning provisioning
raw := rawProvisioning{}
if err := unmarshal(&raw); err != nil {
return err
}
*p = provisioning(raw)
return p.validate()
}
func (p *provisioning) Enabled() bool {
return strings.ToLower(p.SharedSecret) != "disable"
}

View File

@@ -1,47 +0,0 @@
package config
import (
"fmt"
"regexp"
as "maunium.net/go/mautrix/appservice"
)
func (cfg *Config) CopyToRegistration(registration *as.Registration) error {
registration.ID = cfg.Appservice.ID
registration.URL = cfg.Appservice.Address
registration.EphemeralEvents = cfg.Appservice.EphemeralEvents
falseVal := false
registration.RateLimited = &falseVal
registration.SenderLocalpart = cfg.Appservice.Bot.Username
pattern := fmt.Sprintf(
"^@%s:%s$",
cfg.Bridge.FormatUsername("[0-9]+"),
cfg.Homeserver.Domain,
)
userIDRegex, err := regexp.Compile(pattern)
if err != nil {
return err
}
registration.Namespaces.RegisterUserIDs(userIDRegex, true)
return nil
}
func (cfg *Config) getRegistration() (*as.Registration, error) {
registration := as.CreateRegistration()
if err := cfg.CopyToRegistration(registration); err != nil {
return nil, err
}
registration.AppToken = cfg.Appservice.ASToken
registration.ServerToken = cfg.Appservice.HSToken
return registration, nil
}

79
config/upgrade.go Normal file
View File

@@ -0,0 +1,79 @@
// mautrix-discord - A Matrix-Discord puppeting bridge.
// Copyright (C) 2022 Tulir Asokan
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
package config
import (
"maunium.net/go/mautrix/appservice"
"maunium.net/go/mautrix/bridge/bridgeconfig"
up "maunium.net/go/mautrix/util/configupgrade"
)
func DoUpgrade(helper *up.Helper) {
bridgeconfig.Upgrader.DoUpgrade(helper)
helper.Copy(up.Str, "bridge", "username_template")
helper.Copy(up.Str, "bridge", "displayname_template")
helper.Copy(up.Str, "bridge", "channelname_template")
helper.Copy(up.Int, "bridge", "portal_message_buffer")
helper.Copy(up.Bool, "bridge", "sync_with_custom_puppets")
helper.Copy(up.Bool, "bridge", "sync_direct_chat_list")
helper.Copy(up.Bool, "bridge", "default_bridge_receipts")
helper.Copy(up.Bool, "bridge", "default_bridge_presence")
helper.Copy(up.Map, "bridge", "double_puppet_server_map")
helper.Copy(up.Bool, "bridge", "double_puppet_allow_discovery")
helper.Copy(up.Map, "bridge", "login_shared_secret_map")
helper.Copy(up.Str, "bridge", "command_prefix")
helper.Copy(up.Str, "bridge", "management_room_text", "welcome")
helper.Copy(up.Str, "bridge", "management_room_text", "welcome_connected")
helper.Copy(up.Str, "bridge", "management_room_text", "welcome_unconnected")
helper.Copy(up.Str|up.Null, "bridge", "management_room_text", "additional_help")
helper.Copy(up.Bool, "bridge", "encryption", "allow")
helper.Copy(up.Bool, "bridge", "encryption", "default")
helper.Copy(up.Bool, "bridge", "encryption", "key_sharing", "allow")
helper.Copy(up.Bool, "bridge", "encryption", "key_sharing", "require_cross_signing")
helper.Copy(up.Bool, "bridge", "encryption", "key_sharing", "require_verification")
helper.Copy(up.Str, "bridge", "provisioning", "prefix")
if secret, ok := helper.Get(up.Str, "bridge", "provisioning", "shared_secret"); !ok || secret == "generate" {
sharedSecret := appservice.RandomString(64)
helper.Set(up.Str, sharedSecret, "bridge", "provisioning", "shared_secret")
} else {
helper.Copy(up.Str, "bridge", "provisioning", "shared_secret")
}
helper.Copy(up.Map, "bridge", "permissions")
//helper.Copy(up.Bool, "bridge", "relay", "enabled")
//helper.Copy(up.Bool, "bridge", "relay", "admin_only")
//helper.Copy(up.Map, "bridge", "relay", "message_formats")
}
var SpacedBlocks = [][]string{
{"homeserver", "asmux"},
{"appservice"},
{"appservice", "hostname"},
{"appservice", "database"},
{"appservice", "id"},
{"appservice", "as_token"},
{"bridge"},
{"bridge", "command_prefix"},
{"bridge", "management_room_text"},
{"bridge", "encryption"},
{"bridge", "provisioning"},
{"bridge", "permissions"},
//{"bridge", "relay"},
{"logging"},
}