Преглед на файлове

Switch to go-yaml v3 and add config updater. Fixes #243

Tulir Asokan преди 3 години
родител
ревизия
465fa4aa16
променени са 8 файла, в които са добавени 527 реда и са изтрити 75 реда
  1. 3 7
      config/bridge.go
  2. 10 26
      config/config.go
  3. 176 0
      config/upgrade.go
  4. 276 0
      config/upgradehelper.go
  5. 16 12
      example-config.yaml
  6. 2 1
      go.mod
  7. 2 1
      go.sum
  8. 42 28
      main.go

+ 3 - 7
config/bridge.go

@@ -54,7 +54,6 @@ type BridgeConfig struct {
 	DoublePuppetServerMap      map[string]string `yaml:"double_puppet_server_map"`
 	DoublePuppetServerMap      map[string]string `yaml:"double_puppet_server_map"`
 	DoublePuppetAllowDiscovery bool              `yaml:"double_puppet_allow_discovery"`
 	DoublePuppetAllowDiscovery bool              `yaml:"double_puppet_allow_discovery"`
 	LoginSharedSecretMap       map[string]string `yaml:"login_shared_secret_map"`
 	LoginSharedSecretMap       map[string]string `yaml:"login_shared_secret_map"`
-	LegacyLoginSharedSecret    string            `yaml:"login_shared_secret"`
 
 
 	PrivateChatPortalMeta bool   `yaml:"private_chat_portal_meta"`
 	PrivateChatPortalMeta bool   `yaml:"private_chat_portal_meta"`
 	BridgeNotices         bool   `yaml:"bridge_notices"`
 	BridgeNotices         bool   `yaml:"bridge_notices"`
@@ -65,12 +64,9 @@ type BridgeConfig struct {
 	TagOnlyOnCreate       bool   `yaml:"tag_only_on_create"`
 	TagOnlyOnCreate       bool   `yaml:"tag_only_on_create"`
 	MarkReadOnlyOnCreate  bool   `yaml:"mark_read_only_on_create"`
 	MarkReadOnlyOnCreate  bool   `yaml:"mark_read_only_on_create"`
 	EnableStatusBroadcast bool   `yaml:"enable_status_broadcast"`
 	EnableStatusBroadcast bool   `yaml:"enable_status_broadcast"`
-
-	WhatsappThumbnail bool `yaml:"whatsapp_thumbnail"`
-
-	AllowUserInvite bool `yaml:"allow_user_invite"`
-
-	FederateRooms bool `yaml:"federate_rooms"`
+	WhatsappThumbnail     bool   `yaml:"whatsapp_thumbnail"`
+	AllowUserInvite       bool   `yaml:"allow_user_invite"`
+	FederateRooms         bool   `yaml:"federate_rooms"`
 
 
 	CommandPrefix string `yaml:"command_prefix"`
 	CommandPrefix string `yaml:"command_prefix"`
 
 

+ 10 - 26
config/config.go

@@ -18,13 +18,11 @@ package config
 
 
 import (
 import (
 	"fmt"
 	"fmt"
-	"os"
 
 
-	"gopkg.in/yaml.v2"
-
-	"maunium.net/go/mautrix/id"
+	"gopkg.in/yaml.v3"
 
 
 	"maunium.net/go/mautrix/appservice"
 	"maunium.net/go/mautrix/appservice"
+	"maunium.net/go/mautrix/id"
 )
 )
 
 
 var ExampleConfig string
 var ExampleConfig string
@@ -99,37 +97,23 @@ func (config *Config) CanDoublePuppetBackfill(userID id.UserID) bool {
 	return true
 	return true
 }
 }
 
 
-func Load(path string) (*Config, error) {
-	data, err := os.ReadFile(path)
-	if err != nil {
-		return nil, err
-	}
-
+func Load(data []byte, upgraded bool) (*Config, error) {
 	var config = &Config{}
 	var config = &Config{}
-	err = yaml.UnmarshalStrict([]byte(ExampleConfig), config)
-	if err != nil {
-		return nil, fmt.Errorf("failed to unmarshal example config: %w", err)
+	if !upgraded {
+		// Fallback: if config upgrading failed, load example config for base values
+		err := yaml.Unmarshal([]byte(ExampleConfig), config)
+		if err != nil {
+			return nil, fmt.Errorf("failed to unmarshal example config: %w", err)
+		}
 	}
 	}
-	err = yaml.Unmarshal(data, config)
+	err := yaml.Unmarshal(data, config)
 	if err != nil {
 	if err != nil {
 		return nil, err
 		return nil, err
 	}
 	}
 
 
-	if len(config.Bridge.LegacyLoginSharedSecret) > 0 {
-		config.Bridge.LoginSharedSecretMap[config.Homeserver.Domain] = config.Bridge.LegacyLoginSharedSecret
-	}
-
 	return config, err
 	return config, err
 }
 }
 
 
-func (config *Config) Save(path string) error {
-	data, err := yaml.Marshal(config)
-	if err != nil {
-		return err
-	}
-	return os.WriteFile(path, data, 0600)
-}
-
 func (config *Config) MakeAppService() (*appservice.AppService, error) {
 func (config *Config) MakeAppService() (*appservice.AppService, error) {
 	as := appservice.Create()
 	as := appservice.Create()
 	as.HomeserverDomain = config.Homeserver.Domain
 	as.HomeserverDomain = config.Homeserver.Domain

+ 176 - 0
config/upgrade.go

@@ -0,0 +1,176 @@
+// mautrix-whatsapp - A Matrix-WhatsApp puppeting bridge.
+// Copyright (C) 2021 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"
+	"os"
+
+	"gopkg.in/yaml.v3"
+
+	"maunium.net/go/mautrix/appservice"
+)
+
+func (helper *UpgradeHelper) TempMethod() {
+	helper.doUpgrade()
+}
+
+func (helper *UpgradeHelper) doUpgrade() {
+	helper.Copy(Str, "homeserver", "address")
+	helper.Copy(Str, "homeserver", "domain")
+	helper.Copy(Bool, "homeserver", "asmux")
+	helper.Copy(Str|Null, "homeserver", "status_endpoint")
+
+	helper.Copy(Str, "appservice", "address")
+	helper.Copy(Str, "appservice", "hostname")
+	helper.Copy(Int, "appservice", "port")
+	helper.Copy(Str, "appservice", "database", "type")
+	helper.Copy(Str, "appservice", "database", "uri")
+	helper.Copy(Int, "appservice", "database", "max_open_conns")
+	helper.Copy(Int, "appservice", "database", "max_idle_conns")
+	helper.Copy(Str, "appservice", "provisioning", "prefix")
+	if secret, ok := helper.Get(Str, "appservice", "provisioning", "shared_secret"); !ok || secret == "generate" {
+		sharedSecret := appservice.RandomString(64)
+		helper.Set(Str, sharedSecret, "appservice", "provisioning", "shared_secret")
+	} else {
+		helper.Copy(Str, "appservice", "provisioning", "shared_secret")
+	}
+	helper.Copy(Str, "appservice", "id")
+	helper.Copy(Str, "appservice", "bot", "username")
+	helper.Copy(Str, "appservice", "bot", "displayname")
+	helper.Copy(Str, "appservice", "bot", "avatar")
+	helper.Copy(Str, "appservice", "as_token")
+	helper.Copy(Str, "appservice", "hs_token")
+
+	helper.Copy(Bool, "metrics", "enabled")
+	helper.Copy(Str, "metrics", "listen")
+
+	helper.Copy(Str, "whatsapp", "os_name")
+	helper.Copy(Str, "whatsapp", "browser_name")
+
+	helper.Copy(Str, "bridge", "username_template")
+	helper.Copy(Str, "bridge", "displayname_template")
+	helper.Copy(Bool, "bridge", "delivery_receipts")
+	helper.Copy(Int, "bridge", "portal_message_buffer")
+	helper.Copy(Bool, "bridge", "call_start_notices")
+	helper.Copy(Bool, "bridge", "history_sync", "create_portals")
+	helper.Copy(Int, "bridge", "history_sync", "max_age")
+	helper.Copy(Bool, "bridge", "history_sync", "backfill")
+	helper.Copy(Bool, "bridge", "history_sync", "double_puppet_backfill")
+	helper.Copy(Bool, "bridge", "history_sync", "request_full_sync")
+	helper.Copy(Bool, "bridge", "user_avatar_sync")
+	helper.Copy(Bool, "bridge", "bridge_matrix_leave")
+	helper.Copy(Bool, "bridge", "sync_with_custom_puppets")
+	helper.Copy(Bool, "bridge", "sync_direct_chat_list")
+	helper.Copy(Bool, "bridge", "default_bridge_receipts")
+	helper.Copy(Bool, "bridge", "default_bridge_presence")
+	helper.Copy(Map, "bridge", "double_puppet_server_map")
+	helper.Copy(Bool, "bridge", "double_puppet_allow_discovery")
+	if legacySecret, ok := helper.Get(Str, "bridge", "login_shared_secret"); ok && len(legacySecret) > 0 {
+		baseNode := helper.GetBaseNode("bridge", "login_shared_secret_map")
+		baseNode.Map[helper.GetBase("homeserver", "domain")] = YAMLNode{Node: makeStringNode(legacySecret)}
+		baseNode.Content = baseNode.Map.toNodes()
+	} else {
+		helper.Copy(Map, "bridge", "login_shared_secret_map")
+	}
+	helper.Copy(Bool, "bridge", "private_chat_portal_meta")
+	helper.Copy(Bool, "bridge", "bridge_notices")
+	helper.Copy(Bool, "bridge", "resend_bridge_info")
+	helper.Copy(Bool, "bridge", "mute_bridging")
+	helper.Copy(Str|Null, "bridge", "archive_tag")
+	helper.Copy(Str|Null, "bridge", "pinned_tag")
+	helper.Copy(Bool, "bridge", "tag_only_on_create")
+	helper.Copy(Bool, "bridge", "enable_status_broadcast")
+	helper.Copy(Bool, "bridge", "whatsapp_thumbnail")
+	helper.Copy(Bool, "bridge", "allow_user_invite")
+	helper.Copy(Str, "bridge", "command_prefix")
+	helper.Copy(Bool, "bridge", "federate_rooms")
+	helper.Copy(Str, "bridge", "management_room_text", "welcome")
+	helper.Copy(Str, "bridge", "management_room_text", "welcome_connected")
+	helper.Copy(Str, "bridge", "management_room_text", "welcome_unconnected")
+	helper.Copy(Str|Null, "bridge", "management_room_text", "additional_help")
+	helper.Copy(Bool, "bridge", "encryption", "allow")
+	helper.Copy(Bool, "bridge", "encryption", "default")
+	helper.Copy(Bool, "bridge", "encryption", "key_sharing", "allow")
+	helper.Copy(Bool, "bridge", "encryption", "key_sharing", "require_cross_signing")
+	helper.Copy(Bool, "bridge", "encryption", "key_sharing", "require_verification")
+	helper.Copy(Map, "bridge", "permissions")
+	helper.Copy(Bool, "bridge", "relay", "enabled")
+	helper.Copy(Bool, "bridge", "relay", "admin_only")
+	helper.Copy(Map, "bridge", "relay", "message_formats")
+
+	helper.Copy(Str, "logging", "directory")
+	helper.Copy(Str|Null, "logging", "file_name_format")
+	helper.Copy(Str|Timestamp, "logging", "file_date_format")
+	helper.Copy(Int, "logging", "file_mode")
+	helper.Copy(Str|Timestamp, "logging", "timestamp_format")
+	helper.Copy(Str, "logging", "print_level")
+}
+
+func Mutate(path string, mutate func(helper *UpgradeHelper)) error {
+	_, _, err := upgrade(path, true, mutate)
+	return err
+}
+
+func Upgrade(path string, save bool) ([]byte, bool, error) {
+	return upgrade(path, save, nil)
+}
+
+func upgrade(path string, save bool, mutate func(helper *UpgradeHelper)) ([]byte, bool, error) {
+	sourceData, err := os.ReadFile(path)
+	if err != nil {
+		return nil, false, fmt.Errorf("failed to read config: %w", err)
+	}
+	var base, cfg yaml.Node
+	err = yaml.Unmarshal([]byte(ExampleConfig), &base)
+	if err != nil {
+		return sourceData, false, fmt.Errorf("failed to unmarshal example config: %w", err)
+	}
+	err = yaml.Unmarshal(sourceData, &cfg)
+	if err != nil {
+		return sourceData, false, fmt.Errorf("failed to unmarshal config: %w", err)
+	}
+
+	helper := NewUpgradeHelper(&base, &cfg)
+	helper.doUpgrade()
+	if mutate != nil {
+		mutate(helper)
+	}
+
+	output, err := yaml.Marshal(&base)
+	if err != nil {
+		return sourceData, false, fmt.Errorf("failed to marshal updated config: %w", err)
+	}
+	if save {
+		var tempFile *os.File
+		tempFile, err = os.CreateTemp("", "wa-config-*.yaml")
+		if err != nil {
+			return output, true, fmt.Errorf("failed to create temp file for writing config: %w", err)
+		}
+		_, err = tempFile.Write(output)
+		if err != nil {
+			_ = os.Remove(tempFile.Name())
+			return output, true, fmt.Errorf("failed to write updated config to temp file: %w", err)
+		}
+		err = os.Rename(tempFile.Name(), path)
+		if err != nil {
+			_ = os.Remove(tempFile.Name())
+			return output, true, fmt.Errorf("failed to override current config with temp file: %w", err)
+		}
+	}
+	return output, true, nil
+}

+ 276 - 0
config/upgradehelper.go

@@ -0,0 +1,276 @@
+// mautrix-whatsapp - A Matrix-WhatsApp puppeting bridge.
+// Copyright (C) 2021 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"
+	"os"
+	"strings"
+
+	"gopkg.in/yaml.v3"
+)
+
+type YAMLMap map[string]YAMLNode
+type YAMLList []YAMLNode
+
+type YAMLNode struct {
+	*yaml.Node
+	Map  YAMLMap
+	List YAMLList
+}
+
+type YAMLType uint32
+
+const (
+	Null YAMLType = 1 << iota
+	Bool
+	Str
+	Int
+	Float
+	Timestamp
+	List
+	Map
+	Binary
+)
+
+func (t YAMLType) String() string {
+	switch t {
+	case Null:
+		return NullTag
+	case Bool:
+		return BoolTag
+	case Str:
+		return StrTag
+	case Int:
+		return IntTag
+	case Float:
+		return FloatTag
+	case Timestamp:
+		return TimestampTag
+	case List:
+		return SeqTag
+	case Map:
+		return MapTag
+	case Binary:
+		return BinaryTag
+	default:
+		panic(fmt.Errorf("can't convert type %d to string", t))
+	}
+}
+
+func tagToType(tag string) YAMLType {
+	switch tag {
+	case NullTag:
+		return Null
+	case BoolTag:
+		return Bool
+	case StrTag:
+		return Str
+	case IntTag:
+		return Int
+	case FloatTag:
+		return Float
+	case TimestampTag:
+		return Timestamp
+	case SeqTag:
+		return List
+	case MapTag:
+		return Map
+	case BinaryTag:
+		return Binary
+	default:
+		return 0
+	}
+}
+
+const (
+	NullTag      = "!!null"
+	BoolTag      = "!!bool"
+	StrTag       = "!!str"
+	IntTag       = "!!int"
+	FloatTag     = "!!float"
+	TimestampTag = "!!timestamp"
+	SeqTag       = "!!seq"
+	MapTag       = "!!map"
+	BinaryTag    = "!!binary"
+)
+
+func fromNode(node *yaml.Node) YAMLNode {
+	switch node.Kind {
+	case yaml.DocumentNode:
+		return fromNode(node.Content[0])
+	case yaml.AliasNode:
+		return fromNode(node.Alias)
+	case yaml.MappingNode:
+		return YAMLNode{
+			Node: node,
+			Map:  parseYAMLMap(node),
+		}
+	case yaml.SequenceNode:
+		return YAMLNode{
+			Node: node,
+			List: parseYAMLList(node),
+		}
+	default:
+		return YAMLNode{Node: node}
+	}
+}
+
+func (yn *YAMLNode) toNode() *yaml.Node {
+	switch {
+	case yn.Map != nil && yn.Node.Kind == yaml.MappingNode:
+		yn.Content = yn.Map.toNodes()
+	case yn.List != nil && yn.Node.Kind == yaml.SequenceNode:
+		yn.Content = yn.List.toNodes()
+	}
+	return yn.Node
+}
+
+func parseYAMLList(node *yaml.Node) YAMLList {
+	data := make(YAMLList, len(node.Content))
+	for i, item := range node.Content {
+		data[i] = fromNode(item)
+	}
+	return data
+}
+
+func (yl YAMLList) toNodes() []*yaml.Node {
+	nodes := make([]*yaml.Node, len(yl))
+	for i, item := range yl {
+		nodes[i] = item.toNode()
+	}
+	return nodes
+}
+
+func parseYAMLMap(node *yaml.Node) YAMLMap {
+	if len(node.Content)%2 != 0 {
+		panic(fmt.Errorf("uneven number of items in YAML map (%d)", len(node.Content)))
+	}
+	data := make(YAMLMap, len(node.Content)/2)
+	for i := 0; i < len(node.Content); i += 2 {
+		key := node.Content[i]
+		value := node.Content[i+1]
+		if key.Kind == yaml.ScalarNode {
+			data[key.Value] = fromNode(value)
+		}
+	}
+	return data
+}
+
+func (ym YAMLMap) toNodes() []*yaml.Node {
+	nodes := make([]*yaml.Node, len(ym)*2)
+	i := 0
+	for key, value := range ym {
+		nodes[i] = makeStringNode(key)
+		nodes[i+1] = value.toNode()
+		i += 2
+	}
+	return nodes
+}
+
+func makeStringNode(val string) *yaml.Node {
+	var node yaml.Node
+	node.SetString(val)
+	return &node
+}
+
+type UpgradeHelper struct {
+	base YAMLNode
+	cfg  YAMLNode
+}
+
+func NewUpgradeHelper(base, cfg *yaml.Node) *UpgradeHelper {
+	return &UpgradeHelper{
+		base: fromNode(base),
+		cfg:  fromNode(cfg),
+	}
+}
+
+func (helper *UpgradeHelper) Copy(allowedTypes YAMLType, path ...string) {
+	base, cfg := helper.base, helper.cfg
+	var ok bool
+	for _, item := range path {
+		base = base.Map[item]
+		cfg, ok = cfg.Map[item]
+		if !ok {
+			return
+		}
+	}
+	if allowedTypes&tagToType(cfg.Tag) == 0 {
+		_, _ = fmt.Fprintf(os.Stderr, "Ignoring incorrect config field type %s at %s\n", cfg.Tag, strings.Join(path, "->"))
+		return
+	}
+	base.Tag = cfg.Tag
+	base.Style = cfg.Style
+	switch base.Kind {
+	case yaml.ScalarNode:
+		base.Value = cfg.Value
+	case yaml.SequenceNode, yaml.MappingNode:
+		base.Content = cfg.Content
+	}
+}
+
+func getNode(cfg YAMLNode, path []string) *YAMLNode {
+	var ok bool
+	for _, item := range path {
+		cfg, ok = cfg.Map[item]
+		if !ok {
+			return nil
+		}
+	}
+	return &cfg
+}
+
+func (helper *UpgradeHelper) GetNode(path ...string) *YAMLNode {
+	return getNode(helper.cfg, path)
+}
+
+func (helper *UpgradeHelper) GetBaseNode(path ...string) *YAMLNode {
+	return getNode(helper.base, path)
+}
+
+func (helper *UpgradeHelper) Get(tag YAMLType, path ...string) (string, bool) {
+	node := helper.GetNode(path...)
+	if node == nil || node.Kind != yaml.ScalarNode || tag&tagToType(node.Tag) == 0 {
+		return "", false
+	}
+	return node.Value, true
+}
+
+func (helper *UpgradeHelper) GetBase(path ...string) string {
+	return helper.GetBaseNode(path...).Value
+}
+
+func (helper *UpgradeHelper) Set(tag YAMLType, value string, path ...string) {
+	base := helper.base
+	for _, item := range path {
+		base = base.Map[item]
+	}
+	base.Tag = tag.String()
+	base.Value = value
+}
+
+func (helper *UpgradeHelper) SetMap(value YAMLMap, path ...string) {
+	base := helper.base
+	for _, item := range path {
+		base = base.Map[item]
+	}
+	if base.Tag != MapTag || base.Kind != yaml.MappingNode {
+		panic(fmt.Errorf("invalid target for SetMap(%+v): tag:%s, kind:%d", path, base.Tag, base.Kind))
+	}
+	base.Content = value.toNodes()
+}

+ 16 - 12
example-config.yaml

@@ -5,6 +5,8 @@ homeserver:
     # The domain of the homeserver (for MXIDs, etc).
     # The domain of the homeserver (for MXIDs, etc).
     domain: example.com
     domain: example.com
 
 
+    # Is the homeserver actually mautrix-asmux?
+    asmux: false
     # The URL to push real-time bridge status to.
     # The URL to push real-time bridge status to.
     # If set, the bridge will make POST requests to this URL whenever a user's whatsapp connection state changes.
     # If set, the bridge will make POST requests to this URL whenever a user's whatsapp connection state changes.
     # The bridge will use the appservice as_token to authorize requests.
     # The bridge will use the appservice as_token to authorize requests.
@@ -37,8 +39,9 @@ appservice:
     provisioning:
     provisioning:
         # Prefix for the provisioning API paths.
         # Prefix for the provisioning API paths.
         prefix: /_matrix/provision/v1
         prefix: /_matrix/provision/v1
-        # Shared secret for authentication. If set to "disable", the provisioning API will be disabled.
-        shared_secret: disable
+        # Shared secret for authentication. If set to "generate", a random secret will be generated,
+        # or if set to "disable", the provisioning API will be disabled.
+        shared_secret: generate
 
 
     # The unique ID of this appservice.
     # The unique ID of this appservice.
     id: whatsapp
     id: whatsapp
@@ -55,12 +58,14 @@ appservice:
     as_token: "This value is generated when generating the registration"
     as_token: "This value is generated when generating the registration"
     hs_token: "This value is generated when generating the registration"
     hs_token: "This value is generated when generating the registration"
 
 
+# Prometheus config.
 metrics:
 metrics:
     # Enable prometheus metrics?
     # Enable prometheus metrics?
     enabled: false
     enabled: false
     # IP and port where the metrics listener should be. The path is always /metrics
     # IP and port where the metrics listener should be. The path is always /metrics
     listen: 127.0.0.1:8001
     listen: 127.0.0.1:8001
 
 
+# Config for things that are directly sent to WhatsApp.
 whatsapp:
 whatsapp:
     # Device name that's shown in the "WhatsApp Web" section in the mobile app.
     # Device name that's shown in the "WhatsApp Web" section in the mobile app.
     os_name: Mautrix-WhatsApp bridge
     os_name: Mautrix-WhatsApp bridge
@@ -87,6 +92,7 @@ bridge:
     delivery_receipts: false
     delivery_receipts: false
     # Should incoming calls send a message to the Matrix room?
     # Should incoming calls send a message to the Matrix room?
     call_start_notices: true
     call_start_notices: true
+    portal_message_buffer: 128
 
 
     # Settings for handling history sync payloads. These settings only apply right after login,
     # Settings for handling history sync payloads. These settings only apply right after login,
     # because the phone only sends the history sync data once, and there's no way to re-request it
     # because the phone only sends the history sync data once, and there's no way to re-request it
@@ -158,22 +164,19 @@ bridge:
     # Should WhatsApp status messages be bridged into a Matrix room?
     # Should WhatsApp status messages be bridged into a Matrix room?
     # Disabling this won't affect already created status broadcast rooms.
     # Disabling this won't affect already created status broadcast rooms.
     enable_status_broadcast: true
     enable_status_broadcast: true
-
     # Should the bridge use thumbnails from WhatsApp?
     # Should the bridge use thumbnails from WhatsApp?
     # They're disabled by default due to very low resolution.
     # They're disabled by default due to very low resolution.
     whatsapp_thumbnail: false
     whatsapp_thumbnail: false
-
     # Allow invite permission for user. User can invite any bots to room with whatsapp
     # Allow invite permission for user. User can invite any bots to room with whatsapp
     # users (private chat and groups)
     # users (private chat and groups)
     allow_user_invite: false
     allow_user_invite: false
-
-    # The prefix for commands. Only required in non-management rooms.
-    command_prefix: "!wa"
-
     # Whether or not created rooms should have federation enabled.
     # Whether or not created rooms should have federation enabled.
     # If false, created portal rooms will never be federated.
     # If false, created portal rooms will never be federated.
     federate_rooms: true
     federate_rooms: true
 
 
+    # The prefix for commands. Only required in non-management rooms.
+    command_prefix: "!wa"
+
     # Messages sent upon joining a management room.
     # Messages sent upon joining a management room.
     # Markdown is supported. The defaults are listed below.
     # Markdown is supported. The defaults are listed below.
     management_room_text:
     management_room_text:
@@ -184,7 +187,7 @@ bridge:
         # Sent when joining a management room and the user is not logged in.
         # Sent when joining a management room and the user is not logged in.
         welcome_unconnected: "Use `help` for help or `login` to log in."
         welcome_unconnected: "Use `help` for help or `login` to log in."
         # Optional extra text sent when joining a management room.
         # Optional extra text sent when joining a management room.
-        # additional_help: "This would be some additional text in case you need it."
+        additional_help: ""
 
 
     # End-to-bridge encryption support options.
     # End-to-bridge encryption support options.
     #
     #
@@ -223,6 +226,7 @@ bridge:
         "example.com": user
         "example.com": user
         "@admin:example.com": admin
         "@admin:example.com": admin
 
 
+    # Settings for relay mode
     relay:
     relay:
         # Whether relay mode should be allowed. If allowed, `!wa set-relay` can be used to turn any
         # Whether relay mode should be allowed. If allowed, `!wa set-relay` can be used to turn any
         # authenticated user into a relaybot for that chat.
         # authenticated user into a relaybot for that chat.
@@ -248,11 +252,11 @@ logging:
     # Set this to null to disable logging to file.
     # Set this to null to disable logging to file.
     file_name_format: "{{.Date}}-{{.Index}}.log"
     file_name_format: "{{.Date}}-{{.Index}}.log"
     # Date format for file names in the Go time format: https://golang.org/pkg/time/#pkg-constants
     # Date format for file names in the Go time format: https://golang.org/pkg/time/#pkg-constants
-    file_date_format: 2006-01-02
+    file_date_format: "2006-01-02"
     # Log file permissions.
     # Log file permissions.
-    file_mode: 0600
+    file_mode: 0o600
     # Timestamp format for log entries in the Go time format.
     # Timestamp format for log entries in the Go time format.
-    timestamp_format: Jan _2, 2006 15:04:05
+    timestamp_format: "Jan _2, 2006 15:04:05"
     # Minimum severity for log messages printed to stdout/stderr. This doesn't affect the log file.
     # Minimum severity for log messages printed to stdout/stderr. This doesn't affect the log file.
     # Options: debug, info, warn, error, fatal
     # Options: debug, info, warn, error, fatal
     print_level: debug
     print_level: debug

+ 2 - 1
go.mod

@@ -11,7 +11,7 @@ require (
 	go.mau.fi/whatsmeow v0.0.0-20211105153256-c8ed00a3615d
 	go.mau.fi/whatsmeow v0.0.0-20211105153256-c8ed00a3615d
 	golang.org/x/image v0.0.0-20210628002857-a66eb6448b8d
 	golang.org/x/image v0.0.0-20210628002857-a66eb6448b8d
 	google.golang.org/protobuf v1.27.1
 	google.golang.org/protobuf v1.27.1
-	gopkg.in/yaml.v2 v2.4.0
+	gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b
 	maunium.net/go/mauflag v1.0.0
 	maunium.net/go/mauflag v1.0.0
 	maunium.net/go/maulogger/v2 v2.3.1
 	maunium.net/go/maulogger/v2 v2.3.1
 	maunium.net/go/mautrix v0.10.1
 	maunium.net/go/mautrix v0.10.1
@@ -37,4 +37,5 @@ require (
 	golang.org/x/crypto v0.0.0-20210921155107-089bfa567519 // indirect
 	golang.org/x/crypto v0.0.0-20210921155107-089bfa567519 // indirect
 	golang.org/x/net v0.0.0-20211020060615-d418f374d309 // indirect
 	golang.org/x/net v0.0.0-20211020060615-d418f374d309 // indirect
 	golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1 // indirect
 	golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1 // indirect
+	gopkg.in/yaml.v2 v2.4.0 // indirect
 )
 )

+ 2 - 1
go.sum

@@ -213,8 +213,9 @@ gopkg.in/yaml.v2 v2.2.5/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
 gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
 gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
 gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
 gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
 gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
 gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
-gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo=
 gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
 gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
+gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b h1:h8qDotaEPuJATrMmW04NCwg7v22aHH28wwpauUhK9Oo=
+gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
 maunium.net/go/mauflag v1.0.0 h1:YiaRc0tEI3toYtJMRIfjP+jklH45uDHtT80nUamyD4M=
 maunium.net/go/mauflag v1.0.0 h1:YiaRc0tEI3toYtJMRIfjP+jklH45uDHtT80nUamyD4M=
 maunium.net/go/mauflag v1.0.0/go.mod h1:nLivPOpTpHnpzEh8jEdSL9UqO9+/KBJFmNRlwKfkPeA=
 maunium.net/go/mauflag v1.0.0/go.mod h1:nLivPOpTpHnpzEh8jEdSL9UqO9+/KBJFmNRlwKfkPeA=
 maunium.net/go/maulogger/v2 v2.3.1 h1:fwBYJne0pHvJrrIPHK+TAPfyxxbBEz46oVGez2x0ODE=
 maunium.net/go/maulogger/v2 v2.3.1 h1:fwBYJne0pHvJrrIPHK+TAPfyxxbBEz46oVGez2x0ODE=

+ 42 - 28
main.go

@@ -97,8 +97,7 @@ func init() {
 }
 }
 
 
 var configPath = flag.MakeFull("c", "config", "The path to your config file.", "config.yaml").String()
 var configPath = flag.MakeFull("c", "config", "The path to your config file.", "config.yaml").String()
-
-//var baseConfigPath = flag.MakeFull("b", "base-config", "The path to the example config file.", "example-config.yaml").String()
+var dontSaveConfig = flag.MakeFull("n", "no-update", "Don't save updated config to disk.", "false").Bool()
 var registrationPath = flag.MakeFull("r", "registration", "The path where to save the appservice registration.", "registration.yaml").String()
 var registrationPath = flag.MakeFull("r", "registration", "The path where to save the appservice registration.", "registration.yaml").String()
 var generateRegistration = flag.MakeFull("g", "generate-registration", "Generate registration and quit.", "false").Bool()
 var generateRegistration = flag.MakeFull("g", "generate-registration", "Generate registration and quit.", "false").Bool()
 var version = flag.MakeFull("v", "version", "View bridge version and quit.", "false").Bool()
 var version = flag.MakeFull("v", "version", "View bridge version and quit.", "false").Bool()
@@ -107,6 +106,11 @@ var migrateFrom = flag.Make().LongKey("migrate-db").Usage("Source database type
 var wantHelp, _ = flag.MakeHelpFlag()
 var wantHelp, _ = flag.MakeHelpFlag()
 
 
 func (bridge *Bridge) GenerateRegistration() {
 func (bridge *Bridge) GenerateRegistration() {
+	if *dontSaveConfig {
+		// We need to save the generated as_token and hs_token in the config
+		_, _ = fmt.Fprintln(os.Stderr, "--no-update is not compatible with --generate-registration")
+		os.Exit(5)
+	}
 	reg, err := bridge.Config.NewRegistration()
 	reg, err := bridge.Config.NewRegistration()
 	if err != nil {
 	if err != nil {
 		_, _ = fmt.Fprintln(os.Stderr, "Failed to generate registration:", err)
 		_, _ = fmt.Fprintln(os.Stderr, "Failed to generate registration:", err)
@@ -119,7 +123,10 @@ func (bridge *Bridge) GenerateRegistration() {
 		os.Exit(21)
 		os.Exit(21)
 	}
 	}
 
 
-	err = bridge.Config.Save(*configPath)
+	err = config.Mutate(*configPath, func(helper *config.UpgradeHelper) {
+		helper.Set(config.Str, bridge.Config.AppService.ASToken, "appservice", "as_token")
+		helper.Set(config.Str, bridge.Config.AppService.HSToken, "appservice", "hs_token")
+	})
 	if err != nil {
 	if err != nil {
 		_, _ = fmt.Fprintln(os.Stderr, "Failed to save config:", err)
 		_, _ = fmt.Fprintln(os.Stderr, "Failed to save config:", err)
 		os.Exit(22)
 		os.Exit(22)
@@ -193,26 +200,6 @@ type Crypto interface {
 	Stop()
 	Stop()
 }
 }
 
 
-func NewBridge() *Bridge {
-	bridge := &Bridge{
-		usersByMXID:         make(map[id.UserID]*User),
-		usersByUsername:     make(map[string]*User),
-		managementRooms:     make(map[id.RoomID]*User),
-		portalsByMXID:       make(map[id.RoomID]*Portal),
-		portalsByJID:        make(map[database.PortalKey]*Portal),
-		puppets:             make(map[types.JID]*Puppet),
-		puppetsByCustomMXID: make(map[id.UserID]*Puppet),
-	}
-
-	var err error
-	bridge.Config, err = config.Load(*configPath)
-	if err != nil {
-		_, _ = fmt.Fprintln(os.Stderr, "Failed to load config:", err)
-		os.Exit(10)
-	}
-	return bridge
-}
-
 func (bridge *Bridge) ensureConnection() {
 func (bridge *Bridge) ensureConnection() {
 	for {
 	for {
 		resp, err := bridge.Bot.Whoami()
 		resp, err := bridge.Bot.Whoami()
@@ -344,10 +331,15 @@ func (bridge *Bridge) Start() {
 }
 }
 
 
 func (bridge *Bridge) ResendBridgeInfo() {
 func (bridge *Bridge) ResendBridgeInfo() {
-	bridge.Config.Bridge.ResendBridgeInfo = false
-	err := bridge.Config.Save(*configPath)
-	if err != nil {
-		bridge.Log.Errorln("Failed to save config after setting resend_bridge_info to false:", err)
+	if *dontSaveConfig {
+		bridge.Log.Warnln("Not setting resend_bridge_info to false in config due to --no-update flag")
+	} else {
+		err := config.Mutate(*configPath, func(helper *config.UpgradeHelper) {
+			helper.Set(config.Bool, "false", "bridge", "resend_bridge_info")
+		})
+		if err != nil {
+			bridge.Log.Errorln("Failed to save config after setting resend_bridge_info to false:", err)
+		}
 	}
 	}
 	bridge.Log.Infoln("Re-sending bridge info state event to all portals")
 	bridge.Log.Infoln("Re-sending bridge info state event to all portals")
 	for _, portal := range bridge.GetAllPortals() {
 	for _, portal := range bridge.GetAllPortals() {
@@ -426,6 +418,20 @@ func (bridge *Bridge) Stop() {
 }
 }
 
 
 func (bridge *Bridge) Main() {
 func (bridge *Bridge) Main() {
+	configData, upgraded, err := config.Upgrade(*configPath, !*dontSaveConfig)
+	if err != nil {
+		_, _ = fmt.Fprintln(os.Stderr, "Error updating config:", err)
+		if configData == nil {
+			os.Exit(10)
+		}
+	}
+
+	bridge.Config, err = config.Load(configData, upgraded)
+	if err != nil {
+		_, _ = fmt.Fprintln(os.Stderr, "Failed to parse config:", err)
+		os.Exit(10)
+	}
+
 	if *generateRegistration {
 	if *generateRegistration {
 		bridge.GenerateRegistration()
 		bridge.GenerateRegistration()
 		return
 		return
@@ -466,5 +472,13 @@ func main() {
 		return
 		return
 	}
 	}
 
 
-	NewBridge().Main()
+	(&Bridge{
+		usersByMXID:         make(map[id.UserID]*User),
+		usersByUsername:     make(map[string]*User),
+		managementRooms:     make(map[id.RoomID]*User),
+		portalsByMXID:       make(map[id.RoomID]*Portal),
+		portalsByJID:        make(map[database.PortalKey]*Portal),
+		puppets:             make(map[types.JID]*Puppet),
+		puppetsByCustomMXID: make(map[id.UserID]*Puppet),
+	}).Main()
 }
 }