diff --git a/.github/workflows/binary.yml b/.github/workflows/binary.yml index 031ee5e..f925d94 100644 --- a/.github/workflows/binary.yml +++ b/.github/workflows/binary.yml @@ -20,7 +20,7 @@ jobs: - name: Set up Go uses: actions/setup-go@v3 with: - go-version: "1.20.1" + go-version: "1.21.5" - name: Build olm run: | @@ -53,8 +53,8 @@ jobs: apt-get update -q apt-get install -q -y wget build-essential libolm-dev gcc git cmake rm -rf /usr/local/go - wget -q https://go.dev/dl/go1.20.1.linux-arm64.tar.gz - tar -C /usr/local -xzf go1.20.1.linux-arm64.tar.gz + wget -q https://go.dev/dl/go1.21.5.linux-arm64.tar.gz + tar -C /usr/local -xzf go1.21.5.linux-arm64.tar.gz cd /tmp git clone https://gitlab.matrix.org/matrix-org/olm.git cd olm diff --git a/Dockerfile b/Dockerfile index fb6781f..95d02fd 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -FROM golang:1.20-alpine AS builder +FROM golang:1.21-alpine AS builder RUN apk add --no-cache git ca-certificates build-base olm-dev diff --git a/example-config.yaml b/example-config.yaml index 2a0485c..33116a1 100644 --- a/example-config.yaml +++ b/example-config.yaml @@ -1,15 +1,10 @@ # Homeserver details. homeserver: # The address that this appservice can use to connect to the homeserver. - address: https://example.com + address: https://matrix.example.com # The domain of the homeserver (for MXIDs, etc). domain: example.com - # Set to null to disable using the websocket. When not using the websocket, make sure hostname and port are set in the appservice section. - websocket_proxy: - # How often should the websocket be pinged? Pinging will be disabled if this is zero. - ping_interval_seconds: 0 - # What software is the homeserver running? # Standard Matrix homeservers like Synapse, Dendrite and Conduit should just use "standard" here. software: standard @@ -22,6 +17,13 @@ homeserver: # Does the homeserver support https://github.com/matrix-org/matrix-spec-proposals/pull/2246? async_media: false + # Should the bridge use a websocket for connecting to the homeserver? + # The server side is currently not documented anywhere and is only implemented by mautrix-wsproxy, + # mautrix-asmux (deprecated), and hungryserv (proprietary). + websocket: false + # How often should the websocket be pinged? Pinging will be disabled if this is zero. + ping_interval_seconds: 0 + # Application service host/registration related details. # Changing these values requires regeneration of the registration. appservice: @@ -62,9 +64,13 @@ appservice: # Whether or not to receive ephemeral events via appservice transactions. # Requires MSC2409 support (i.e. Synapse 1.22+). - # You should disable bridge -> sync_with_custom_puppets when this is enabled. ephemeral_events: true + # Should incoming events be handled asynchronously? + # This may be necessary for large public instances with lots of messages going through. + # However, messages will not be guaranteed to be bridged in the same order they were sent in. + async_transactions: false + # Authentication tokens for AS <-> HS communication. Autogenerated; do not modify. as_token: "This value is generated when generating the registration" hs_token: "This value is generated when generating the registration" @@ -74,6 +80,7 @@ bridge: # Proxy for homeserver connection. hs_proxy: # Localpart template of MXIDs for WeChat users. + # {{.}} is replaced with the uin of the WeChat user. username_template: _wechat_{{.}} # Displayname template for WeChat users. displayname_template: "{{if .Name}}{{.Name}}{{else}}{{.Uin}}{{end}} (WeChat)" @@ -92,8 +99,6 @@ bridge: allow_redaction: false # Should puppet avatars be fetched from the server even if an avatar is already set? user_avatar_sync: true - # Should the bridge sync with double puppeting to receive EDUs that aren't normally sent to appservices. - sync_with_custom_puppets: false # Should the bridge update the m.direct account data event when double puppeting is enabled. # Note that updating the m.direct event is not atomic (except with mautrix-asmux) # and is therefore prone to race conditions. @@ -118,8 +123,11 @@ bridge: # manually. login_shared_secret_map: example.com: foobar - # Should the bridge explicitly set the avatar and room name for private chat portal rooms? - private_chat_portal_meta: false + # Whether to explicitly set the avatar and room name for private chat portal rooms. + # If set to `default`, this will be enabled in encrypted rooms and disabled in unencrypted rooms. + # If set to `always`, all DM rooms will have explicit names and avatars set. + # If set to `never`, DM rooms will never have names and avatars set. + private_chat_portal_meta: default # Should group members be synced in parallel? This makes member sync faster parallel_member_sync: false # Set this to true to tell the bridge to re-send m.bridge events to all rooms on the next run. @@ -170,7 +178,6 @@ bridge: allow: false # Default to encryption, force-enable encryption in all portals the bridge creates # This will cause the bridge bot to be in private chats for the encryption to work properly. - # It is recommended to also set private_chat_portal_meta to true when using this. default: false # Whether to use MSC2409/MSC3202 instead of /sync long polling for receiving encryption-related data. appservice: false @@ -179,6 +186,29 @@ bridge: # Enable key sharing? If enabled, key requests for rooms where users are in will be fulfilled. # You must use a client that supports requesting keys from other users to use this feature. allow_key_sharing: false + # Should users mentions be in the event wire content to enable the server to send push notifications? + plaintext_mentions: false + # Options for deleting megolm sessions from the bridge. + delete_keys: + # Beeper-specific: delete outbound sessions when hungryserv confirms + # that the user has uploaded the key to key backup. + delete_outbound_on_ack: false + # Don't store outbound sessions in the inbound table. + dont_store_outbound: false + # Ratchet megolm sessions forward after decrypting messages. + ratchet_on_decrypt: false + # Delete fully used keys (index >= max_messages) after decrypting messages. + delete_fully_used_on_decrypt: false + # Delete previous megolm sessions from same device when receiving a new one. + delete_prev_on_new_session: false + # Delete megolm sessions received from a device when the device is deleted. + delete_on_device_delete: false + # Periodically delete megolm sessions when 2x max_age has passed since receiving the session. + periodically_delete_expired: false + # Delete inbound megolm sessions that don't have the received_at field used for + # automatic ratcheting and expired session deletion. This is meant as a migration + # to delete old keys prior to the bridge update. + delete_outdated_inbound: false # What level of device verification should be required from users? # # Valid levels: @@ -214,6 +244,11 @@ bridge: # default. messages: 100 + # Disable rotating keys when a user's devices change? + # You should not enable this option unless you understand all the implications. + disable_device_change_key_rotation: false + + # Permissions for using the bridge. # Permitted values: # user - Access to use the bridge to chat with a WeChat account. @@ -226,19 +261,15 @@ bridge: "example.com": user "@admin:example.com": admin -# Logging config. +# Logging config. See https://github.com/tulir/zeroconfig for details. logging: - # The directory for log files. Will be created if not found. - directory: ./logs - # Available variables: .Date for the file date and .Index for different log files on the same day. - # Set this to null to disable logging to file. - file_name_format: "{{.Date}}-{{.Index}}.log" - # Date format for file names in the Go time format: https://golang.org/pkg/time/#pkg-constants - file_date_format: "2006-01-02" - # Log file permissions. - file_mode: 0o600 - # Timestamp format for log entries in the Go time format. - timestamp_format: "Jan _2, 2006 15:04:05" - # Minimum severity for log messages printed to stdout/stderr. This doesn't affect the log file. - # Options: debug, info, warn, error, fatal - print_level: debug + min_level: debug + writers: + - type: stdout + format: pretty-colored + - type: file + format: json + filename: ./logs/matrix-wechat.log + max_size: 100 + max_backups: 10 + compress: true diff --git a/go.mod b/go.mod index e63f1c4..ddab69f 100644 --- a/go.mod +++ b/go.mod @@ -1,38 +1,43 @@ module github.com/duo/matrix-wechat -go 1.19 +go 1.21 require ( - github.com/gabriel-vasile/mimetype v1.4.1 - github.com/gorilla/websocket v1.5.0 - github.com/lib/pq v1.10.7 - github.com/mattn/go-sqlite3 v1.14.16 + github.com/gabriel-vasile/mimetype v1.4.3 + github.com/gorilla/websocket v1.5.1 + github.com/lib/pq v1.10.9 + github.com/mattn/go-sqlite3 v1.14.19 + github.com/rs/zerolog v1.31.0 github.com/wdvxdr1123/go-silk v0.0.0-20220304095002-f67345df09ea - maunium.net/go/maulogger/v2 v2.4.1 - maunium.net/go/mautrix v0.14.0 + go.mau.fi/util v0.2.1 + maunium.net/go/mautrix v0.16.2 ) require ( + github.com/coreos/go-systemd/v22 v22.5.0 // indirect github.com/dustin/go-humanize v1.0.1 // indirect - github.com/google/uuid v1.3.0 // indirect - github.com/gorilla/mux v1.8.0 // indirect + github.com/google/uuid v1.5.0 // indirect + github.com/gorilla/mux v1.8.1 // indirect github.com/kr/pretty v0.3.1 // indirect github.com/mattn/go-colorable v0.1.13 // indirect - github.com/mattn/go-isatty v0.0.17 // indirect + github.com/mattn/go-isatty v0.0.20 // indirect github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect - github.com/rs/zerolog v1.29.0 // indirect - github.com/tidwall/gjson v1.14.4 // indirect + github.com/tidwall/gjson v1.17.0 // indirect github.com/tidwall/match v1.1.1 // indirect github.com/tidwall/pretty v1.2.1 // indirect github.com/tidwall/sjson v1.2.5 // indirect - github.com/yuin/goldmark v1.5.4 // indirect - golang.org/x/crypto v0.6.0 // indirect - golang.org/x/net v0.7.0 // indirect - golang.org/x/sys v0.5.0 // indirect + github.com/yuin/goldmark v1.6.0 // indirect + go.mau.fi/zeroconfig v0.1.2 // indirect + golang.org/x/crypto v0.17.0 // indirect + golang.org/x/exp v0.0.0-20231219180239-dc181d75b848 // indirect + golang.org/x/net v0.19.0 // indirect + golang.org/x/sys v0.15.0 // indirect gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect + gopkg.in/natefinch/lumberjack.v2 v2.2.1 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect maunium.net/go/mauflag v1.0.0 // indirect - modernc.org/libc v1.22.3 // indirect - modernc.org/mathutil v1.5.0 // indirect - modernc.org/memory v1.5.0 // indirect + maunium.net/go/maulogger/v2 v2.4.1 // indirect + modernc.org/libc v1.38.0 // indirect + modernc.org/mathutil v1.6.0 // indirect + modernc.org/memory v1.7.2 // indirect ) diff --git a/go.sum b/go.sum index 3b205b2..9d65806 100644 --- a/go.sum +++ b/go.sum @@ -1,19 +1,22 @@ github.com/DATA-DOG/go-sqlmock v1.5.0 h1:Shsta01QNfFxHCfpW6YH2STWB0MudeXXEWMr20OEh60= -github.com/coreos/go-systemd/v22 v22.3.3-0.20220203105225-a9a7ef127534/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= +github.com/DATA-DOG/go-sqlmock v1.5.0/go.mod h1:f/Ixk793poVmq4qj/V1dPUg2JEAKC73Q5eFN3EC/SaM= +github.com/coreos/go-systemd/v22 v22.5.0 h1:RrqgGjYQKalulkV8NGVIfkXQf6YYmOyiJKk8iXXhfZs= +github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= -github.com/gabriel-vasile/mimetype v1.4.1 h1:TRWk7se+TOjCYgRth7+1/OYLNiRNIotknkFtf/dnN7Q= -github.com/gabriel-vasile/mimetype v1.4.1/go.mod h1:05Vi0w3Y9c/lNvJOdmIwvrrAhX3rYhfQQCaf9VJcv7M= +github.com/gabriel-vasile/mimetype v1.4.3 h1:in2uUcidCuFcDKtdcBxlR0rJ1+fsokWf+uqxgUFjbI0= +github.com/gabriel-vasile/mimetype v1.4.3/go.mod h1:d8uq/6HKRL6CGdk+aubisF/M5GcPfT7nKyLpA0lbSSk= github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= -github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= -github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI= -github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So= -github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc= -github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= +github.com/google/uuid v1.5.0 h1:1p67kYwdtXjb0gL0BPiP1Av9wiZPo5A8z2cWkTZ+eyU= +github.com/google/uuid v1.5.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY= +github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ= +github.com/gorilla/websocket v1.5.1 h1:gmztn0JnHVt9JZquRuzLw3g4wouNVzKL15iLr/zn/QY= +github.com/gorilla/websocket v1.5.1/go.mod h1:x3kM2JMyaluk02fnUJpQuwD2dCS5NDG2ZHL0uE0tcaY= github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= @@ -21,18 +24,17 @@ github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= -github.com/lib/pq v1.10.7 h1:p7ZhMD+KsSRozJr34udlUrhboJwWAgCg34+/ZZNvZZw= -github.com/lib/pq v1.10.7/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= -github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4= +github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw= +github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= -github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= -github.com/mattn/go-isatty v0.0.17 h1:BTarxUcIeDqL27Mc+vyvdWYSL28zpIhv3RoTdsLMPng= -github.com/mattn/go-isatty v0.0.17/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= -github.com/mattn/go-sqlite3 v1.14.16 h1:yOQRA0RpS5PFz/oikGwBEqvAWhWg5ufRz4ETLjwpU1Y= -github.com/mattn/go-sqlite3 v1.14.16/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg= +github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= +github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/mattn/go-sqlite3 v1.14.19 h1:fhGleo2h1p8tVChob4I9HpmVFIAkKGpiukdrgQbWfGI= +github.com/mattn/go-sqlite3 v1.14.19/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg= github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= @@ -42,15 +44,16 @@ github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94 github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8= github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= -github.com/rs/xid v1.4.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg= -github.com/rs/zerolog v1.29.0 h1:Zes4hju04hjbvkVkOhdl2HpZa+0PmVwigmo8XoORE5w= -github.com/rs/zerolog v1.29.0/go.mod h1:NILgTygv/Uej1ra5XxGf82ZFSLk58MFGAUS2o6usyD0= +github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg= +github.com/rs/zerolog v1.31.0 h1:FcTR3NnLWW+NnTwwhFWiJSZr4ECLpqCm6QsEnyvbV4A= +github.com/rs/zerolog v1.31.0/go.mod h1:/7mN4D5sKwJLZQ2b/znpjC3/GQWY/xaDXUM0kKWRHss= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk= +github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= +github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= github.com/tidwall/gjson v1.14.2/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= -github.com/tidwall/gjson v1.14.4 h1:uo0p8EbA09J7RQaflQ1aBRffTR7xedD2bcIVSYxLnkM= -github.com/tidwall/gjson v1.14.4/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= +github.com/tidwall/gjson v1.17.0 h1:/Jocvlh98kcTfpN2+JzGQWQcqrPQwDrVEMApx/M5ZwM= +github.com/tidwall/gjson v1.17.0/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA= github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM= github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= @@ -60,28 +63,30 @@ github.com/tidwall/sjson v1.2.5 h1:kLy8mja+1c9jlljvWTlSazM7cKDRfJuR/bOJhcY5NcY= github.com/tidwall/sjson v1.2.5/go.mod h1:Fvgq9kS/6ociJEDnK0Fk1cpYF4FIW6ZF7LAe+6jwd28= github.com/wdvxdr1123/go-silk v0.0.0-20220304095002-f67345df09ea h1:sl1pYm1kHtIndckTY8YDt+QFt77vI0JnKHP0U8rZtKc= github.com/wdvxdr1123/go-silk v0.0.0-20220304095002-f67345df09ea/go.mod h1:ecFKZPX81BaB70I6ruUgEwYcDOtuNgJGnjdK+MIl5ko= -github.com/yuin/goldmark v1.5.4 h1:2uY/xC0roWy8IBEGLgB1ywIoEJFGmRrX21YQcvGZzjU= -github.com/yuin/goldmark v1.5.4/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= -golang.org/x/crypto v0.6.0 h1:qfktjS5LUO+fFKeJXZ+ikTRijMmljikvG68fpMMruSc= -golang.org/x/crypto v0.6.0/go.mod h1:OFC/31mSvZgRz0V1QTNCzfAI1aIRzbiufJtkMIlEp58= -golang.org/x/net v0.0.0-20220624214902-1bab6f366d9e/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= -golang.org/x/net v0.7.0 h1:rJrUqqhjsgNp7KqAIc25s9pZnjU7TUcSY7HcVZjdn1g= -golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= +github.com/yuin/goldmark v1.6.0 h1:boZcn2GTjpsynOsC0iJHnBWa4Bi0qzfJjthwauItG68= +github.com/yuin/goldmark v1.6.0/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= +go.mau.fi/util v0.2.1 h1:eazulhFE/UmjOFtPrGg6zkF5YfAyiDzQb8ihLMbsPWw= +go.mau.fi/util v0.2.1/go.mod h1:MjlzCQEMzJ+G8RsPawHzpLB8rwTo3aPIjG5FzBvQT/c= +go.mau.fi/zeroconfig v0.1.2 h1:DKOydWnhPMn65GbXZOafgkPm11BvFashZWLct0dGFto= +go.mau.fi/zeroconfig v0.1.2/go.mod h1:NcSJkf180JT+1IId76PcMuLTNa1CzsFFZ0nBygIQM70= +golang.org/x/crypto v0.17.0 h1:r8bRNjWL3GshPW3gkd+RpvzWrZAwPS49OmTGZ/uhM4k= +golang.org/x/crypto v0.17.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4= +golang.org/x/exp v0.0.0-20231219180239-dc181d75b848 h1:+iq7lrkxmFNBM7xx+Rae2W6uyPfhPeDWD+n+JgppptE= +golang.org/x/exp v0.0.0-20231219180239-dc181d75b848/go.mod h1:iRJReGqOEeBhDZGkGbynYwcHlctCvnjTYIamk7uXpHI= +golang.org/x/net v0.19.0 h1:zTwKpTd2XuCqf8huc7Fo2iSy+4RHPd10s4KzeTnVr1c= +golang.org/x/net v0.19.0/go.mod h1:CfAk/cbD4CthTvqiEl8NpboMuiuOYsAr/7NOjZJtv1U= golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201126233918-771906719818/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.5.0 h1:MUK/U/4lj1t1oPg0HfuXDN/Z1wv31ZJ/YcPiGccS4DU= -golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= -golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= -golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.15.0 h1:h48lPFYpsTvQJZF4EKyI4aLHaev3CxivZmv7yZig9pc= +golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/natefinch/lumberjack.v2 v2.2.1 h1:bBRl1b0OH9s/DuPhuXpNl+VtCaJXFZ5/uEFST95x9zc= +gopkg.in/natefinch/lumberjack.v2 v2.2.1/go.mod h1:YD8tP3GAjkrDg1eZH7EGmyESg/lsYskCTPBJVb9jqSc= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= @@ -89,15 +94,15 @@ 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/maulogger/v2 v2.4.1 h1:N7zSdd0mZkB2m2JtFUsiGTQQAdP0YeFWT7YMc80yAL8= maunium.net/go/maulogger/v2 v2.4.1/go.mod h1:omPuYwYBILeVQobz8uO3XC8DIRuEb5rXYlQSuqrbCho= -maunium.net/go/mautrix v0.14.0 h1:kdQ06HzmMaLGZqmSh/ykDhp5C2gIREQL9TS8hY+FqLs= -maunium.net/go/mautrix v0.14.0/go.mod h1:voJPvnTkA60rxBl6mvdPxcP7y7iY5w3d/K55IoX+2oY= +maunium.net/go/mautrix v0.16.2 h1:a6GUJXNWsTEOO8VE4dROBfCIfPp50mqaqzv7KPzChvg= +maunium.net/go/mautrix v0.16.2/go.mod h1:YL4l4rZB46/vj/ifRMEjcibbvHjgxHftOF1SgmruLu4= modernc.org/libc v1.8.1/go.mod h1:U1eq8YWr/Kc1RWCMFUWEdkTg8OTcfLw2kY8EDwl039w= -modernc.org/libc v1.22.3 h1:D/g6O5ftAfavceqlLOFwaZuA5KYafKwmr30A6iSqoyY= -modernc.org/libc v1.22.3/go.mod h1:MQrloYP209xa2zHome2a8HLiLm6k0UT8CoHpV74tOFw= +modernc.org/libc v1.38.0 h1:o4Lpk0zNDSdsjfEXnF1FGXWQ9PDi1NOdWcLP5n13FGo= +modernc.org/libc v1.38.0/go.mod h1:YAXkAZ8ktnkCKaN9sw/UDeUVkGYJ/YquGO4FTi5nmHE= modernc.org/mathutil v1.1.1/go.mod h1:mZW8CKdRPY1v87qxC/wUdX5O1qDzXMP5TH3wjfpga6E= modernc.org/mathutil v1.2.2/go.mod h1:mZW8CKdRPY1v87qxC/wUdX5O1qDzXMP5TH3wjfpga6E= -modernc.org/mathutil v1.5.0 h1:rV0Ko/6SfM+8G+yKiyI830l3Wuz1zRutdslNoQ0kfiQ= -modernc.org/mathutil v1.5.0/go.mod h1:mZW8CKdRPY1v87qxC/wUdX5O1qDzXMP5TH3wjfpga6E= +modernc.org/mathutil v1.6.0 h1:fRe9+AmYlaej+64JsEEhoWuAYBkOtQiMEU7n/XgfYi4= +modernc.org/mathutil v1.6.0/go.mod h1:Ui5Q9q1TR2gFm0AQRqQUaBWFLAhQpCwNcuhBOSedWPo= modernc.org/memory v1.0.4/go.mod h1:nV2OApxradM3/OVbs2/0OsP6nPfakXpi50C7dcoHXlc= -modernc.org/memory v1.5.0 h1:N+/8c5rE6EqugZwHii4IFsaJ7MUhoWX07J5tC/iI5Ds= -modernc.org/memory v1.5.0/go.mod h1:PkUhL0Mugw21sHPeskwZW4D6VscE/GQJOnIpCnW6pSU= +modernc.org/memory v1.7.2 h1:Klh90S215mmH8c9gO98QxQFsY+W451E8AnzjoE2ee1E= +modernc.org/memory v1.7.2/go.mod h1:NO4NVCQy0N7ln+T9ngWqOQfi7ley4vpwvARR+Hjw95E= diff --git a/internal/command.go b/internal/command.go index 496c589..3b43319 100644 --- a/internal/command.go +++ b/internal/command.go @@ -79,7 +79,7 @@ func fnLogin(ce *WrappedCommandEvent) { err := ce.User.Connect() if err != nil { - ce.User.log.Errorf("Failed to connect:", err) + ce.User.log.Error().Msgf("Failed to connect: %v", err) ce.Reply("Failed to connect: %v", err) return } @@ -128,7 +128,7 @@ func (u *User) sendQR(ce *WrappedCommandEvent, qrCode []byte) (id.EventID, error } resp, err := ce.Bot.SendMessageEvent(ce.RoomID, event.EventMessage, &content) if err != nil { - u.log.Errorln("Failed to send edited QR code to user:", err) + u.log.Error().Msgf("Failed to send edited QR code to user: %v", err) } return resp.EventID, nil } @@ -138,7 +138,7 @@ func (u *User) uploadQR(ce *WrappedCommandEvent, qrCode []byte) (id.ContentURI, resp, err := bot.UploadBytes(qrCode, "image/png") if err != nil { - u.log.Errorln("Failed to upload QR code:", err) + u.log.Error().Msgf("Failed to upload QR code: %v", err) ce.Reply("Failed to upload QR code: %v", err) return id.ContentURI{}, err } @@ -160,12 +160,7 @@ func fnLogout(ce *WrappedCommandEvent) { return } puppet := ce.Bridge.GetPuppetByUID(ce.User.UID) - if puppet.CustomMXID != "" { - err := puppet.SwitchCustomMXID("", "") - if err != nil { - ce.User.log.Warnln("Failed to logout-matrix while logging out of WeChat:", err) - } - } + puppet.ClearCustomMXID() ce.User.removeFromUIDMap(status.BridgeState{StateEvent: status.StateLoggedOut}) ce.User.DeleteConnection() ce.User.DeleteSession() @@ -196,7 +191,7 @@ func canDeletePortal(portal *Portal, userID id.UserID) bool { members, err := portal.MainIntent().JoinedMembers(portal.MXID) if err != nil { - portal.log.Errorfln("Failed to get joined members to check if portal can be deleted by %s: %v", userID, err) + portal.log.Error().Msgf("Failed to get joined members to check if portal can be deleted by %s: %v", userID, err) return false } for otherUser := range members.Joined { @@ -228,7 +223,7 @@ func fnDeletePortal(ce *WrappedCommandEvent) { return } - ce.Portal.log.Infoln(ce.User.MXID, "requested deletion of portal.") + ce.Portal.log.Info().Msgf("%s requested deletion of portal.", ce.User.MXID) ce.Portal.Delete() ce.Portal.Cleanup(false) } diff --git a/internal/config/bridgeconfig.go b/internal/config/bridgeconfig.go index 7e3c02f..a114c8c 100644 --- a/internal/config/bridgeconfig.go +++ b/internal/config/bridgeconfig.go @@ -35,21 +35,18 @@ type BridgeConfig struct { UserAvatarSync bool `yaml:"user_avatar_sync"` - SyncWithCustomPuppets bool `yaml:"sync_with_custom_puppets"` SyncDirectChatList bool `yaml:"sync_direct_chat_list"` DefaultBridgePresence bool `yaml:"default_bridge_presence"` SendPresenceOnTyping bool `yaml:"send_presence_on_typing"` - DoublePuppetServerMap map[string]string `yaml:"double_puppet_server_map"` - DoublePuppetAllowDiscovery bool `yaml:"double_puppet_allow_discovery"` - LoginSharedSecretMap map[string]string `yaml:"login_shared_secret_map"` + DoublePuppetConfig bridgeconfig.DoublePuppetConfig `yaml:",inline"` - PrivateChatPortalMeta bool `yaml:"private_chat_portal_meta"` - ParallelMemberSync bool `yaml:"parallel_member_sync"` - ResendBridgeInfo bool `yaml:"resend_bridge_info"` - MuteBridging bool `yaml:"mute_bridging"` - AllowUserInvite bool `yaml:"allow_user_invite"` - FederateRooms bool `yaml:"federate_rooms"` + PrivateChatPortalMeta string `yaml:"private_chat_portal_meta"` + ParallelMemberSync bool `yaml:"parallel_member_sync"` + ResendBridgeInfo bool `yaml:"resend_bridge_info"` + MuteBridging bool `yaml:"mute_bridging"` + AllowUserInvite bool `yaml:"allow_user_invite"` + FederateRooms bool `yaml:"federate_rooms"` MessageHandlingTimeout struct { ErrorAfterStr string `yaml:"error_after"` @@ -75,6 +72,10 @@ type BridgeConfig struct { type umBridgeConfig BridgeConfig +func (bc BridgeConfig) GetDoublePuppetConfig() bridgeconfig.DoublePuppetConfig { + return bc.DoublePuppetConfig +} + func (bc BridgeConfig) GetEncryptionConfig() bridgeconfig.EncryptionConfig { return bc.Encryption } diff --git a/internal/config/config.go b/internal/config/config.go index b9a6df3..5c9cd18 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -13,7 +13,7 @@ type Config struct { func (c *Config) CanAutoDoublePuppet(userID id.UserID) bool { _, homeserver, _ := userID.Parse() - _, hasSecret := c.Bridge.LoginSharedSecretMap[homeserver] + _, hasSecret := c.Bridge.DoublePuppetConfig.SharedSecretMap[homeserver] return hasSecret } diff --git a/internal/config/upgrade.go b/internal/config/upgrade.go index 03770d3..fbbcc46 100644 --- a/internal/config/upgrade.go +++ b/internal/config/upgrade.go @@ -3,7 +3,7 @@ package config import ( "maunium.net/go/mautrix/bridge/bridgeconfig" - up "maunium.net/go/mautrix/util/configupgrade" + up "go.mau.fi/util/configupgrade" ) func DoUpgrade(helper *up.Helper) { @@ -20,7 +20,6 @@ func DoUpgrade(helper *up.Helper) { helper.Copy(up.Int, "bridge", "portal_message_buffer") helper.Copy(up.Bool, "bridge", "allow_redaction") helper.Copy(up.Bool, "bridge", "user_avatar_sync") - 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_presence") helper.Copy(up.Bool, "bridge", "send_presence_on_typing") @@ -33,7 +32,15 @@ func DoUpgrade(helper *up.Helper) { } else { helper.Copy(up.Map, "bridge", "login_shared_secret_map") } - helper.Copy(up.Bool, "bridge", "private_chat_portal_meta") + if legacyPrivateChatPortalMeta, ok := helper.Get(up.Bool, "bridge", "private_chat_portal_meta"); ok { + updatedPrivateChatPortalMeta := "default" + if legacyPrivateChatPortalMeta == "true" { + updatedPrivateChatPortalMeta = "always" + } + helper.Set(up.Str, updatedPrivateChatPortalMeta, "bridge", "private_chat_portal_meta") + } else { + helper.Copy(up.Str, "bridge", "private_chat_portal_meta") + } helper.Copy(up.Bool, "bridge", "parallel_member_sync") helper.Copy(up.Bool, "bridge", "resend_bridge_info") helper.Copy(up.Bool, "bridge", "mute_bridging") @@ -52,6 +59,15 @@ func DoUpgrade(helper *up.Helper) { helper.Copy(up.Bool, "bridge", "encryption", "default") helper.Copy(up.Bool, "bridge", "encryption", "require") helper.Copy(up.Bool, "bridge", "encryption", "appservice") + helper.Copy(up.Bool, "bridge", "encryption", "plaintext_mentions") + helper.Copy(up.Bool, "bridge", "encryption", "delete_keys", "delete_outbound_on_ack") + helper.Copy(up.Bool, "bridge", "encryption", "delete_keys", "dont_store_outbound") + helper.Copy(up.Bool, "bridge", "encryption", "delete_keys", "ratchet_on_decrypt") + helper.Copy(up.Bool, "bridge", "encryption", "delete_keys", "delete_fully_used_on_decrypt") + helper.Copy(up.Bool, "bridge", "encryption", "delete_keys", "delete_prev_on_new_session") + helper.Copy(up.Bool, "bridge", "encryption", "delete_keys", "delete_on_device_delete") + helper.Copy(up.Bool, "bridge", "encryption", "delete_keys", "periodically_delete_expired") + helper.Copy(up.Bool, "bridge", "encryption", "delete_keys", "delete_outdated_inbound") helper.Copy(up.Str, "bridge", "encryption", "verification_levels", "receive") helper.Copy(up.Str, "bridge", "encryption", "verification_levels", "send") helper.Copy(up.Str, "bridge", "encryption", "verification_levels", "share") @@ -71,10 +87,12 @@ func DoUpgrade(helper *up.Helper) { helper.Copy(up.Bool, "bridge", "encryption", "rotation", "enable_custom") helper.Copy(up.Int, "bridge", "encryption", "rotation", "milliseconds") helper.Copy(up.Int, "bridge", "encryption", "rotation", "messages") + helper.Copy(up.Bool, "bridge", "encryption", "rotation", "disable_device_change_key_rotation") helper.Copy(up.Map, "bridge", "permissions") } var SpacedBlocks = [][]string{ + {"homeserver", "software"}, {"appservice"}, {"appservice", "hostname"}, {"appservice", "database"}, diff --git a/internal/custompuppet.go b/internal/custompuppet.go index 33d89b9..1f8cd8e 100644 --- a/internal/custompuppet.go +++ b/internal/custompuppet.go @@ -1,268 +1,72 @@ package internal -import ( - "crypto/hmac" - "crypto/sha512" - "encoding/hex" - "errors" - "fmt" - "time" - - "maunium.net/go/mautrix" - "maunium.net/go/mautrix/appservice" - "maunium.net/go/mautrix/event" - "maunium.net/go/mautrix/id" -) - -var ( - ErrNoCustomMXID = errors.New("no custom mxid set") - ErrMismatchingMXID = errors.New("whoami result does not match custom mxid") -) +import "maunium.net/go/mautrix/id" func (p *Puppet) SwitchCustomMXID(accessToken string, mxid id.UserID) error { - prevCustomMXID := p.CustomMXID - if p.customIntent != nil { - p.stopSyncing() - } p.CustomMXID = mxid p.AccessToken = accessToken - + p.EnablePresence = p.bridge.Config.Bridge.DefaultBridgePresence + p.Update() err := p.StartCustomMXID(false) if err != nil { return err } - - if len(prevCustomMXID) > 0 { - delete(p.bridge.puppetsByCustomMXID, prevCustomMXID) - } - if len(p.CustomMXID) > 0 { - p.bridge.puppetsByCustomMXID[p.CustomMXID] = p - } - p.EnablePresence = p.bridge.Config.Bridge.DefaultBridgePresence - p.bridge.AS.StateStore.MarkRegistered(p.CustomMXID) - p.Update() // TODO leave rooms with default puppet - return nil } -func (p *Puppet) loginWithSharedSecret(mxid id.UserID) (string, error) { - _, homeserver, _ := mxid.Parse() - p.log.Debugfln("Logging into %s with shared secret", mxid) - loginSecret := p.bridge.Config.Bridge.LoginSharedSecretMap[homeserver] - client, err := p.bridge.newDoublePuppetClient(mxid, "") - if err != nil { - return "", fmt.Errorf("failed to create mautrix client to log in: %v", err) - } - req := mautrix.ReqLogin{ - Identifier: mautrix.UserIdentifier{Type: mautrix.IdentifierTypeUser, User: string(mxid)}, - DeviceID: "WeChat Bridge", - InitialDeviceDisplayName: "WeChat Bridge", - } - if loginSecret == "appservice" { - client.AccessToken = p.bridge.AS.Registration.AppToken - req.Type = mautrix.AuthTypeAppservice - } else { - mac := hmac.New(sha512.New, []byte(loginSecret)) - mac.Write([]byte(mxid)) - req.Password = hex.EncodeToString(mac.Sum(nil)) - req.Type = mautrix.AuthTypePassword - } - resp, err := client.Login(&req) - if err != nil { - return "", err - } - - return resp.AccessToken, nil -} - -func (br *WechatBridge) newDoublePuppetClient(mxid id.UserID, accessToken string) (*mautrix.Client, error) { - _, homeserver, err := mxid.Parse() - if err != nil { - return nil, err - } - homeserverURL, found := br.Config.Bridge.DoublePuppetServerMap[homeserver] - if !found { - if homeserver == br.AS.HomeserverDomain { - homeserverURL = br.AS.HomeserverURL - } else if br.Config.Bridge.DoublePuppetAllowDiscovery { - resp, err := mautrix.DiscoverClientAPI(homeserver) - if err != nil { - return nil, fmt.Errorf("failed to find homeserver URL for %s: %v", homeserver, err) - } - homeserverURL = resp.Homeserver.BaseURL - br.Log.Debugfln("Discovered URL %s for %s to enable double puppeting for %s", homeserverURL, homeserver, mxid) - } else { - return nil, fmt.Errorf("double puppeting from %s is not allowed", homeserver) - } - } - client, err := mautrix.NewClient(homeserverURL, mxid, accessToken) - if err != nil { - return nil, err - } - client.Logger = br.AS.Log.Sub(mxid.String()) - client.Client = br.AS.HTTPClient - client.DefaultHTTPRetries = br.AS.DefaultHTTPRetries - - return client, nil -} - -func (p *Puppet) newCustomIntent() (*appservice.IntentAPI, error) { - if len(p.CustomMXID) == 0 { - return nil, ErrNoCustomMXID +func (p *Puppet) ClearCustomMXID() { + save := p.CustomMXID != "" || p.AccessToken != "" + p.bridge.puppetsLock.Lock() + if p.CustomMXID != "" && p.bridge.puppetsByCustomMXID[p.CustomMXID] == p { + delete(p.bridge.puppetsByCustomMXID, p.CustomMXID) } - client, err := p.bridge.newDoublePuppetClient(p.CustomMXID, p.AccessToken) - if err != nil { - return nil, err - } - client.Syncer = p - client.Store = p - - ia := p.bridge.AS.NewIntentAPI("custom") - ia.Client = client - ia.Localpart, _, _ = p.CustomMXID.Parse() - ia.UserID = p.CustomMXID - ia.IsCustomPuppet = true - - return ia, nil -} - -func (p *Puppet) clearCustomMXID() { + p.bridge.puppetsLock.Unlock() p.CustomMXID = "" p.AccessToken = "" p.customIntent = nil p.customUser = nil + if save { + p.Update() + } } func (p *Puppet) StartCustomMXID(reloginOnFail bool) error { - if len(p.CustomMXID) == 0 { - p.clearCustomMXID() - return nil - } - intent, err := p.newCustomIntent() + newIntent, newAccessToken, err := p.bridge.DoublePuppet.Setup(p.CustomMXID, p.AccessToken, reloginOnFail) if err != nil { - p.clearCustomMXID() + p.ClearCustomMXID() return err } - resp, err := intent.Whoami() - if err != nil { - if !reloginOnFail || (errors.Is(err, mautrix.MUnknownToken) && !p.tryRelogin(err, "initializing double puppeting")) { - p.clearCustomMXID() - return err - } - intent.AccessToken = p.AccessToken - } else if resp.UserID != p.CustomMXID { - p.clearCustomMXID() - return ErrMismatchingMXID + p.bridge.puppetsLock.Lock() + p.bridge.puppetsByCustomMXID[p.CustomMXID] = p + p.bridge.puppetsLock.Unlock() + if p.AccessToken != newAccessToken { + p.AccessToken = newAccessToken + p.Update() } - p.customIntent = intent + p.customIntent = newIntent p.customUser = p.bridge.GetUserByMXID(p.CustomMXID) - p.startSyncing() return nil } -func (p *Puppet) startSyncing() { - if !p.bridge.Config.Bridge.SyncWithCustomPuppets { +func (u *User) tryAutomaticDoublePuppeting() { + if !u.bridge.Config.CanAutoDoublePuppet(u.MXID) { return } - go func() { - p.log.Debugln("Starting syncing...") - p.customIntent.SyncPresence = "offline" - err := p.customIntent.Sync() - if err != nil { - p.log.Errorln("Fatal error syncing:", err) - } - }() -} - -func (p *Puppet) stopSyncing() { - if !p.bridge.Config.Bridge.SyncWithCustomPuppets { + u.log.Debug().Msgf("Checking if double puppeting needs to be enabled") + puppet := u.bridge.GetPuppetByUID(u.UID) + if len(puppet.CustomMXID) > 0 { + u.log.Debug().Msgf("User already has double-puppeting enabled") + // Custom puppet already enabled return } - p.customIntent.StopSync() -} - -func (p *Puppet) ProcessResponse(resp *mautrix.RespSync, _ string) error { - if !p.customUser.IsLoggedIn() { - p.log.Debugln("Skipping sync processing: custom user not connected to wechat") - return nil - } - for roomID, events := range resp.Rooms.Join { - for _, evt := range events.Ephemeral.Events { - evt.RoomID = roomID - err := evt.Content.ParseRaw(evt.Type) - if err != nil { - continue - } - switch evt.Type { - case event.EphemeralEventTyping: - go p.bridge.MatrixHandler.HandleTyping(evt) - } - } - } - if p.EnablePresence { - for _, evt := range resp.Presence.Events { - if evt.Sender != p.CustomMXID { - continue - } - err := evt.Content.ParseRaw(evt.Type) - if err != nil { - continue - } - go p.bridge.HandlePresence(evt) - } - } - return nil -} - -func (p *Puppet) tryRelogin(cause error, action string) bool { - if !p.bridge.Config.CanAutoDoublePuppet(p.CustomMXID) { - return false - } - p.log.Debugfln("Trying to relogin after '%v' while %s", cause, action) - accessToken, err := p.loginWithSharedSecret(p.CustomMXID) + puppet.CustomMXID = u.MXID + puppet.EnablePresence = puppet.bridge.Config.Bridge.DefaultBridgePresence + err := puppet.StartCustomMXID(true) if err != nil { - p.log.Errorfln("Failed to relogin after '%v' while %s: %v", cause, action, err) - return false - } - p.log.Infofln("Successfully relogined after '%v' while %s", cause, action) - p.AccessToken = accessToken - return true -} - -func (p *Puppet) OnFailedSync(_ *mautrix.RespSync, err error) (time.Duration, error) { - p.log.Warnln("Sync error:", err) - if errors.Is(err, mautrix.MUnknownToken) { - if !p.tryRelogin(err, "syncing") { - return 0, err - } - p.customIntent.AccessToken = p.AccessToken - return 0, nil - } - return 10 * time.Second, nil -} - -func (p *Puppet) GetFilterJSON(_ id.UserID) *mautrix.Filter { - everything := []event.Type{{Type: "*"}} - return &mautrix.Filter{ - Presence: mautrix.FilterPart{ - Senders: []id.UserID{p.CustomMXID}, - Types: []event.Type{event.EphemeralEventPresence}, - }, - AccountData: mautrix.FilterPart{NotTypes: everything}, - Room: mautrix.RoomFilter{ - Ephemeral: mautrix.FilterPart{Types: []event.Type{event.EphemeralEventTyping}}, - IncludeLeave: false, - AccountData: mautrix.FilterPart{NotTypes: everything}, - State: mautrix.FilterPart{NotTypes: everything}, - Timeline: mautrix.FilterPart{NotTypes: everything}, - }, + u.log.Warn().Err(err).Msg("Failed to login with shared secret") + } else { + // TODO leave rooms with default puppet + u.log.Info().Msg("Successfully automatically enabled custom puppet") } } - -func (p *Puppet) SaveFilterID(_ id.UserID, _ string) {} -func (p *Puppet) SaveNextBatch(_ id.UserID, nbt string) { p.NextBatch = nbt; p.Update() } -func (p *Puppet) SaveRoom(_ *mautrix.Room) {} -func (p *Puppet) LoadFilterID(_ id.UserID) string { return "" } -func (p *Puppet) LoadNextBatch(_ id.UserID) string { return p.NextBatch } -func (p *Puppet) LoadRoom(_ id.RoomID) *mautrix.Room { return nil } diff --git a/internal/database/databse.go b/internal/database/databse.go index f34f4aa..7caf1c4 100644 --- a/internal/database/databse.go +++ b/internal/database/databse.go @@ -8,8 +8,8 @@ import ( "github.com/duo/matrix-wechat/internal/database/upgrades" - "maunium.net/go/maulogger/v2" - "maunium.net/go/mautrix/util/dbutil" + "github.com/rs/zerolog" + "go.mau.fi/util/dbutil" ) type Database struct { @@ -21,24 +21,24 @@ type Database struct { Message *MessageQuery } -func New(baseDB *dbutil.Database, log maulogger.Logger) *Database { +func New(baseDB *dbutil.Database, log zerolog.Logger) *Database { db := &Database{Database: baseDB} db.UpgradeTable = upgrades.Table db.User = &UserQuery{ db: db, - log: log.Sub("User"), + log: log.With().Str("query", "User").Logger(), } db.Puppet = &PuppetQuery{ db: db, - log: log.Sub("Puppet"), + log: log.With().Str("query", "Puppet").Logger(), } db.Portal = &PortalQuery{ db: db, - log: log.Sub("Portal"), + log: log.With().Str("query", "Portal").Logger(), } db.Message = &MessageQuery{ db: db, - log: log.Sub("Message"), + log: log.With().Str("query", "Message").Logger(), } return db } diff --git a/internal/database/message.go b/internal/database/message.go index ca1ba2d..f7d4223 100644 --- a/internal/database/message.go +++ b/internal/database/message.go @@ -8,10 +8,9 @@ import ( "github.com/duo/matrix-wechat/internal/types" + "github.com/rs/zerolog" + "go.mau.fi/util/dbutil" "maunium.net/go/mautrix/id" - "maunium.net/go/mautrix/util/dbutil" - - log "maunium.net/go/maulogger/v2" ) type MessageErrorType string @@ -32,7 +31,7 @@ const ( type Message struct { db *Database - log log.Logger + log zerolog.Logger Chat PortalKey MsgID string @@ -60,7 +59,7 @@ func (m *Message) Scan(row dbutil.Scannable) *Message { ) if err != nil { if !errors.Is(err, sql.ErrNoRows) { - m.log.Errorln("Database scan failed:", err) + m.log.Error().Msgf("Database scan failed: %v", err) } return nil @@ -89,7 +88,7 @@ func (m *Message) Insert(txn dbutil.Transaction) { _, err = m.db.Exec(query, args...) } if err != nil { - m.log.Warnfln("Failed to insert %s %s: %v", m.Chat, m.MsgID, err) + m.log.Warn().Msgf("Failed to insert %s %s: %v", m.Chat, m.MsgID, err) } } @@ -113,7 +112,7 @@ func (m *Message) UpdateMXID(txn dbutil.Transaction, mxid id.EventID, newType Me _, err = m.db.Exec(query, args...) } if err != nil { - m.log.Warnfln("Failed to update %s %s: %v", m.Chat, m.MsgID, err) + m.log.Warn().Msgf("Failed to update %s %s: %v", m.Chat, m.MsgID, err) } } @@ -127,6 +126,6 @@ func (m *Message) Delete() { } _, err := m.db.Exec(query, args...) if err != nil { - m.log.Warnfln("Failed to delete %s %s: %v", m.Chat, m.MsgID, err) + m.log.Warn().Msgf("Failed to delete %s %s: %v", m.Chat, m.MsgID, err) } } diff --git a/internal/database/messagequery.go b/internal/database/messagequery.go index e0ec9cf..1152341 100644 --- a/internal/database/messagequery.go +++ b/internal/database/messagequery.go @@ -1,14 +1,13 @@ package database import ( + "github.com/rs/zerolog" "maunium.net/go/mautrix/id" - - log "maunium.net/go/maulogger/v2" ) type MessageQuery struct { db *Database - log log.Logger + log zerolog.Logger } func (mq *MessageQuery) New() *Message { diff --git a/internal/database/portal.go b/internal/database/portal.go index e30a453..30fdb7d 100644 --- a/internal/database/portal.go +++ b/internal/database/portal.go @@ -4,15 +4,14 @@ import ( "database/sql" "time" + "github.com/rs/zerolog" + "go.mau.fi/util/dbutil" "maunium.net/go/mautrix/id" - "maunium.net/go/mautrix/util/dbutil" - - log "maunium.net/go/maulogger/v2" ) type Portal struct { db *Database - log log.Logger + log zerolog.Logger Key PortalKey MXID id.RoomID @@ -41,7 +40,7 @@ func (p *Portal) Scan(row dbutil.Scannable) *Portal { ) if err != nil { if err != sql.ErrNoRows { - p.log.Errorln("Database scan failed:", err) + p.log.Error().Msgf("Database scan failed: %v", err) } return nil @@ -71,7 +70,7 @@ func (p *Portal) Insert() { _, err := p.db.Exec(query, args...) if err != nil { - p.log.Warnfln("Failed to insert %s: %v", p.Key, err) + p.log.Warn().Msgf("Failed to insert %s: %v", p.Key, err) } } @@ -94,7 +93,7 @@ func (p *Portal) Update(txn dbutil.Transaction) { _, err = p.db.Exec(query, args...) } if err != nil { - p.log.Warnfln("Failed to update %s: %v", p.Key, err) + p.log.Warn().Msgf("Failed to update %s: %v", p.Key, err) } } @@ -109,7 +108,7 @@ func (p *Portal) Delete() { _, err := p.db.Exec(query, args...) if err != nil { - p.log.Warnfln("Failed to delete %s: %v", p.Key, err) + p.log.Warn().Msgf("Failed to delete %s: %v", p.Key, err) } } diff --git a/internal/database/portalquery.go b/internal/database/portalquery.go index 33ec14a..2e5ebc4 100644 --- a/internal/database/portalquery.go +++ b/internal/database/portalquery.go @@ -5,9 +5,8 @@ import ( "github.com/duo/matrix-wechat/internal/types" + "github.com/rs/zerolog" "maunium.net/go/mautrix/id" - - log "maunium.net/go/maulogger/v2" ) const portalColumns = ` @@ -17,7 +16,7 @@ const portalColumns = ` type PortalQuery struct { db *Database - log log.Logger + log zerolog.Logger } func (pq *PortalQuery) New() *Portal { diff --git a/internal/database/puppet.go b/internal/database/puppet.go index e2793d5..f0e7623 100644 --- a/internal/database/puppet.go +++ b/internal/database/puppet.go @@ -6,15 +6,14 @@ import ( "github.com/duo/matrix-wechat/internal/types" + "github.com/rs/zerolog" + "go.mau.fi/util/dbutil" "maunium.net/go/mautrix/id" - "maunium.net/go/mautrix/util/dbutil" - - log "maunium.net/go/maulogger/v2" ) type Puppet struct { db *Database - log log.Logger + log zerolog.Logger UID types.UID Avatar string AvatarURL id.ContentURI @@ -41,7 +40,7 @@ func (p *Puppet) Scan(row dbutil.Scannable) *Puppet { ) if err != nil { if err != sql.ErrNoRows { - p.log.Errorln("Database scan failed:", err) + p.log.Error().Msgf("Database scan failed: %v", err) } return nil @@ -82,7 +81,7 @@ func (p *Puppet) Insert() { _, err := p.db.Exec(query, args...) if err != nil { - p.log.Warnfln("Failed to insert %s: %v", p.UID, err) + p.log.Warn().Msgf("Failed to insert %s: %v", p.UID, err) } } @@ -105,6 +104,6 @@ func (p *Puppet) Update() { _, err := p.db.Exec(query, args...) if err != nil { - p.log.Warnfln("Failed to update %s: %v", p.UID, err) + p.log.Warn().Msgf("Failed to update %s: %v", p.UID, err) } } diff --git a/internal/database/puppetquery.go b/internal/database/puppetquery.go index db2743a..b424bf8 100644 --- a/internal/database/puppetquery.go +++ b/internal/database/puppetquery.go @@ -5,9 +5,8 @@ import ( "github.com/duo/matrix-wechat/internal/types" + "github.com/rs/zerolog" "maunium.net/go/mautrix/id" - - log "maunium.net/go/maulogger/v2" ) const puppetColumns = ` @@ -17,7 +16,7 @@ const puppetColumns = ` type PuppetQuery struct { db *Database - log log.Logger + log zerolog.Logger } func (pq *PuppetQuery) New() *Puppet { diff --git a/internal/database/upgrades/upgrades.go b/internal/database/upgrades/upgrades.go index f152b4c..22c5414 100644 --- a/internal/database/upgrades/upgrades.go +++ b/internal/database/upgrades/upgrades.go @@ -3,7 +3,7 @@ package upgrades import ( "embed" - "maunium.net/go/mautrix/util/dbutil" + "go.mau.fi/util/dbutil" ) var Table dbutil.UpgradeTable diff --git a/internal/database/user.go b/internal/database/user.go index 85bf738..2c417e3 100644 --- a/internal/database/user.go +++ b/internal/database/user.go @@ -7,15 +7,14 @@ import ( "github.com/duo/matrix-wechat/internal/types" + "github.com/rs/zerolog" + "go.mau.fi/util/dbutil" "maunium.net/go/mautrix/id" - "maunium.net/go/mautrix/util/dbutil" - - log "maunium.net/go/maulogger/v2" ) type User struct { db *Database - log log.Logger + log zerolog.Logger MXID id.UserID UID types.UID @@ -33,7 +32,7 @@ func (u *User) Scan(row dbutil.Scannable) *User { err := row.Scan(&u.MXID, &uin, &u.ManagementRoom, &u.SpaceRoom) if err != nil { if err != sql.ErrNoRows { - u.log.Errorln("Database scan failed:", err) + u.log.Error().Msgf("Database scan failed: %v", err) } return nil @@ -56,7 +55,7 @@ func (u *User) Insert() { _, err := u.db.Exec(query, args...) if err != nil { - u.log.Warnfln("Failed to insert %s: %v", u.MXID, err) + u.log.Warn().Msgf("Failed to insert %s: %v", u.MXID, err) } } @@ -71,6 +70,6 @@ func (u *User) Update() { } _, err := u.db.Exec(query, args...) if err != nil { - u.log.Warnfln("Failed to update %s: %v", u.MXID, err) + u.log.Warn().Msgf("Failed to update %s: %v", u.MXID, err) } } diff --git a/internal/database/userportal.go b/internal/database/userportal.go index d42b21a..641bf65 100644 --- a/internal/database/userportal.go +++ b/internal/database/userportal.go @@ -26,7 +26,7 @@ func (u *User) GetLastReadTS(portal PortalKey) time.Time { var ts int64 err := u.db.QueryRow(query, args...).Scan(&ts) if err != nil && !errors.Is(err, sql.ErrNoRows) { - u.log.Warnfln("Failed to scan last read timestamp from user portal table: %v", err) + u.log.Warn().Msgf("Failed to scan last read timestamp from user portal table: %v", err) } if ts == 0 { u.lastReadCache[portal] = time.Time{} @@ -56,9 +56,9 @@ func (u *User) SetLastReadTS(portal PortalKey, ts time.Time) { _, err := u.db.Exec(query, args...) if err != nil { - u.log.Warnfln("Failed to update last read timestamp: %v", err) + u.log.Warn().Msgf("Failed to update last read timestamp: %v", err) } else { - u.log.Debugfln("Set last read timestamp of %s in %s to %d", u.MXID, portal, ts.Unix()) + u.log.Debug().Msgf("Set last read timestamp of %s in %s to %d", u.MXID, portal, ts.Unix()) u.lastReadCache[portal] = ts } } @@ -83,7 +83,7 @@ func (u *User) IsInSpace(portal PortalKey) bool { var inSpace bool err := u.db.QueryRow(query, args...).Scan(&inSpace) if err != nil && !errors.Is(err, sql.ErrNoRows) { - u.log.Warnfln("Failed to scan in space status from user portal table: %v", err) + u.log.Warn().Msgf("Failed to scan in space status from user portal table: %v", err) } u.inSpaceCache[portal] = inSpace @@ -108,7 +108,7 @@ func (u *User) MarkInSpace(portal PortalKey) { _, err := u.db.Exec(query, args...) if err != nil { - u.log.Warnfln("Failed to update in space status: %v", err) + u.log.Warn().Msgf("Failed to update in space status: %v", err) } else { u.inSpaceCache[portal] = true } diff --git a/internal/database/userquery.go b/internal/database/userquery.go index dd2c19e..9c89ca1 100644 --- a/internal/database/userquery.go +++ b/internal/database/userquery.go @@ -4,16 +4,15 @@ import ( "fmt" "time" + "github.com/rs/zerolog" "maunium.net/go/mautrix/id" - - log "maunium.net/go/maulogger/v2" ) const userColumns = "mxid, uin, management_room, space_room" type UserQuery struct { db *Database - log log.Logger + log zerolog.Logger } func (uq *UserQuery) New() *User { diff --git a/internal/formatter.go b/internal/formatter.go index a9fe779..1f73e8d 100644 --- a/internal/formatter.go +++ b/internal/formatter.go @@ -1,13 +1,19 @@ package internal import ( + "fmt" + "slices" + "sort" + "github.com/duo/matrix-wechat/internal/types" + "maunium.net/go/mautrix/event" "maunium.net/go/mautrix/format" "maunium.net/go/mautrix/id" ) const mentionedUIDsContextKey = "me.lxduo.wechat.mentioned_uids" +const allowedMentionsContextKey = "me.lxduo.wechat.allowed_mentions" type Formatter struct { bridge *WechatBridge @@ -23,20 +29,28 @@ func NewFormatter(br *WechatBridge) *Formatter { Newline: "\n", PillConverter: func(displayname, mxid, eventID string, ctx format.Context) string { + allowedMentions, _ := ctx.ReturnData[allowedMentionsContextKey].(map[types.UID]bool) if mxid[0] == '@' { puppet := br.GetPuppetByMXID(id.UserID(mxid)) - if puppet != nil { - uids, ok := ctx.ReturnData[mentionedUIDsContextKey].([]string) - if !ok { - ctx.ReturnData[mentionedUIDsContextKey] = []string{puppet.UID.Uin} - } else { - ctx.ReturnData[mentionedUIDsContextKey] = append(uids, puppet.UID.Uin) + if puppet != nil && (allowedMentions == nil || allowedMentions[puppet.UID]) { + if allowedMentions == nil { + uids, ok := ctx.ReturnData[mentionedUIDsContextKey].([]string) + if !ok { + ctx.ReturnData[mentionedUIDsContextKey] = []string{puppet.UID.Uin} + } else { + ctx.ReturnData[mentionedUIDsContextKey] = append(uids, puppet.UID.Uin) + } } return "@" + puppet.UID.Uin } } - return mxid + return displayname }, + BoldConverter: func(text string, _ format.Context) string { return fmt.Sprintf("*%s*", text) }, + ItalicConverter: func(text string, _ format.Context) string { return fmt.Sprintf("_%s_", text) }, + StrikethroughConverter: func(text string, _ format.Context) string { return fmt.Sprintf("~%s~", text) }, + MonospaceConverter: func(text string, _ format.Context) string { return fmt.Sprintf("```%s```", text) }, + MonospaceBlockConverter: func(text, language string, _ format.Context) string { return fmt.Sprintf("```%s```", text) }, }, } return formatter @@ -60,9 +74,35 @@ func (f *Formatter) GetMatrixInfoByUID(roomID id.RoomID, uid types.UID) (id.User return mxid, displayname } -func (f *Formatter) ParseMatrix(html string) (string, []string) { +func (f *Formatter) ParseMatrix(html string, mentions *event.Mentions) (string, []string) { ctx := format.NewContext() + + var mentionedUIDs []string + if mentions != nil { + var allowedMentions = make(map[types.UID]bool) + mentionedUIDs = make([]string, 0, len(mentions.UserIDs)) + for _, userID := range mentions.UserIDs { + var uid types.UID + if puppet := f.bridge.GetPuppetByMXID(userID); puppet != nil { + uid = puppet.UID + mentionedUIDs = append(mentionedUIDs, puppet.UID.Uin) + } else if user := f.bridge.GetUserByMXIDIfExists(userID); user != nil { + uid = user.UID + } + if !uid.IsEmpty() && !allowedMentions[uid] { + allowedMentions[uid] = true + mentionedUIDs = append(mentionedUIDs, uid.Uin) + } + } + ctx.ReturnData[allowedMentionsContextKey] = allowedMentions + } + result := f.matrixHTMLParser.Parse(html, ctx) - mentionedUIDs, _ := ctx.ReturnData[mentionedUIDsContextKey].([]string) + if mentions == nil { + mentionedUIDs, _ = ctx.ReturnData[mentionedUIDsContextKey].([]string) + sort.Strings(mentionedUIDs) + mentionedUIDs = slices.Compact(mentionedUIDs) + } + f.bridge.ZLog.Error().Msgf("WTF mentions: %+v", mentionedUIDs) return result, mentionedUIDs } diff --git a/internal/matrix.go b/internal/matrix.go deleted file mode 100644 index 49a6b5e..0000000 --- a/internal/matrix.go +++ /dev/null @@ -1,112 +0,0 @@ -package internal - -import ( - "encoding/json" - "sync/atomic" - "time" - - "maunium.net/go/maulogger/v2" - "maunium.net/go/mautrix" - "maunium.net/go/mautrix/appservice" - "maunium.net/go/mautrix/id" -) - -const DefaultSyncProxyBackoff = 1 * time.Second -const MaxSyncProxyBackoff = 60 * time.Second - -const BridgeStatusConnected = "CONNECTED" - -type WebsocketCommandHandler struct { - bridge *WechatBridge - log maulogger.Logger - errorTxnIDC *appservice.TransactionIDCache - - lastSyncProxyError time.Time - syncProxyBackoff time.Duration - syncProxyWaiting int64 -} - -type BridgeStatus struct { - StateEvent string `json:"state_event"` - Timestamp int64 `json:"timestamp"` - TTL int `json:"ttl"` - Source string `json:"source"` - Error string `json:"error,omitempty"` - Message string `json:"message,omitempty"` - UserID id.UserID `json:"user_id,omitempty"` - RemoteID string `json:"remote_id,omitempty"` - RemoteName string `json:"remote_name,omitempty"` - - Info map[string]interface{} `json:"info,omitempty"` -} - -func NewWebsocketCommandHandler(br *WechatBridge) *WebsocketCommandHandler { - handler := &WebsocketCommandHandler{ - bridge: br, - log: br.Log.Sub("MatrixWebsocket"), - errorTxnIDC: appservice.NewTransactionIDCache(8), - syncProxyBackoff: DefaultSyncProxyBackoff, - } - br.AS.PrepareWebsocket() - br.AS.SetWebsocketCommandHandler("ping", handler.handleWSPing) - br.AS.SetWebsocketCommandHandler("syncproxy_error", handler.handleWSSyncProxyError) - return handler -} - -func (mx *WebsocketCommandHandler) handleWSPing(cmd appservice.WebsocketCommand) (bool, interface{}) { - mx.log.Warnfln("Receive ws ping") - status := BridgeStatus{ - StateEvent: BridgeStatusConnected, - Timestamp: time.Now().Unix(), - TTL: 600, - Source: "bridge", - } - - return true, &status -} - -func (mx *WebsocketCommandHandler) handleWSSyncProxyError(cmd appservice.WebsocketCommand) (bool, interface{}) { - var data mautrix.RespError - err := json.Unmarshal(cmd.Data, &data) - - if err != nil { - mx.log.Warnln("Failed to unmarshal syncproxy_error data:", err) - } else if txnID, ok := data.ExtraData["txn_id"].(string); !ok { - mx.log.Warnln("Got syncproxy_error data with no transaction ID") - } else if mx.errorTxnIDC.IsProcessed(txnID) { - mx.log.Debugln("Ignoring syncproxy_error with duplicate transaction ID", txnID) - } else { - go mx.HandleSyncProxyError(&data, nil) - mx.errorTxnIDC.MarkProcessed(txnID) - } - - return true, &data -} - -func (mx *WebsocketCommandHandler) HandleSyncProxyError(syncErr *mautrix.RespError, startErr error) { - if !atomic.CompareAndSwapInt64(&mx.syncProxyWaiting, 0, 1) { - var err interface{} = startErr - if err == nil { - err = syncErr.Err - } - mx.log.Debugfln("Got sync proxy error (%v), but there's already another thread waiting to restart sync proxy", err) - return - } - if time.Since(mx.lastSyncProxyError) < MaxSyncProxyBackoff { - mx.syncProxyBackoff *= 2 - if mx.syncProxyBackoff > MaxSyncProxyBackoff { - mx.syncProxyBackoff = MaxSyncProxyBackoff - } - } else { - mx.syncProxyBackoff = DefaultSyncProxyBackoff - } - mx.lastSyncProxyError = time.Now() - if syncErr != nil { - mx.log.Errorfln("Syncproxy told us that syncing failed: %s - Requesting a restart in %s", syncErr.Err, mx.syncProxyBackoff) - } else if startErr != nil { - mx.log.Errorfln("Failed to request sync proxy to start syncing: %v - Requesting a restart in %s", startErr, mx.syncProxyBackoff) - } - time.Sleep(mx.syncProxyBackoff) - atomic.StoreInt64(&mx.syncProxyWaiting, 0) - mx.bridge.RequestStartSync() -} diff --git a/internal/portal.go b/internal/portal.go index 30b8ca9..7ba55d7 100644 --- a/internal/portal.go +++ b/internal/portal.go @@ -3,11 +3,11 @@ package internal import ( "bytes" "context" + "crypto/rand" "encoding/hex" "errors" "fmt" "image" - "math/rand" "runtime/debug" "strconv" "strings" @@ -19,6 +19,9 @@ import ( "github.com/duo/matrix-wechat/internal/wechat" "github.com/gabriel-vasile/mimetype" + "github.com/rs/zerolog" + "go.mau.fi/util/dbutil" + "go.mau.fi/util/exerrors" "maunium.net/go/mautrix" "maunium.net/go/mautrix/appservice" "maunium.net/go/mautrix/bridge" @@ -27,14 +30,10 @@ import ( "maunium.net/go/mautrix/event" "maunium.net/go/mautrix/format" "maunium.net/go/mautrix/id" - "maunium.net/go/mautrix/util" - "maunium.net/go/mautrix/util/dbutil" _ "image/gif" _ "image/jpeg" _ "image/png" - - log "maunium.net/go/maulogger/v2" ) const ( @@ -68,7 +67,7 @@ type Portal struct { *database.Portal bridge *WechatBridge - log log.Logger + log zerolog.Logger roomCreateLock sync.Mutex encryptLock sync.Mutex @@ -136,15 +135,15 @@ func (p *Portal) handleWechatMessageLoopItem(msg PortalMessage) { defer func() { panicErr := recover() if panicErr != nil { - p.log.Warnfln("Panic while process %+v: %v\n%s", msg, panicErr, debug.Stack()) + p.log.Warn().Msgf("Panic while process %+v: %v\n%s", msg, panicErr, debug.Stack()) } }() if len(p.MXID) == 0 { - p.log.Debugln("Creating Matrix room from incoming message") + p.log.Debug().Msgf("Creating Matrix room from incoming message") err := p.CreateMatrixRoom(msg.source, nil, false) if err != nil { - p.log.Errorln("Failed to create portal room:", err) + p.log.Error().Msgf("Failed to create portal room: %v", err) return } @@ -157,7 +156,7 @@ func (p *Portal) handleWechatMessageLoopItem(msg PortalMessage) { msg.fake.ID = "FAKE::" + msg.fake.ID p.handleFakeMessage(*msg.fake) default: - p.log.Warnln("Unexpected PortalMessage with no message: %+v", msg) + p.log.Warn().Msgf("Unexpected PortalMessage with no message: %+v", msg) } } @@ -165,7 +164,7 @@ func (p *Portal) handleMatrixMessageLoopItem(msg PortalMatrixMessage) { defer func() { panicErr := recover() if panicErr != nil { - p.log.Warnfln("Panic while process %+v: %v\n%s", msg, panicErr, debug.Stack()) + p.log.Warn().Msgf("Panic while process %+v: %v\n%s", msg, panicErr, debug.Stack()) } }() @@ -177,7 +176,7 @@ func (p *Portal) handleMatrixMessageLoopItem(msg PortalMatrixMessage) { case event.EventReaction: p.HandleMatrixReaction(msg.user, msg.evt) default: - p.log.Warnln("Unsupported event type %+v in portal message channel", msg.evt.Type) + p.log.Warn().Msgf("Unsupported event type %+v in portal message channel", msg.evt.Type) } } @@ -194,16 +193,16 @@ func (p *Portal) handleMessageLoop() { func (p *Portal) handleFakeMessage(msg fakeMessage) { if p.isRecentlyHandled(msg.ID, database.MsgNoError) { - p.log.Debugfln("Not handling %s (fake): message was recently handled", msg.ID) + p.log.Debug().Msgf("Not handling %s (fake): message was recently handled", msg.ID) return } else if existingMsg := p.bridge.DB.Message.GetByMsgID(p.Key, msg.ID); existingMsg != nil { - p.log.Debugfln("Not handling %s (fake): message is duplicate", msg.ID) + p.log.Debug().Msgf("Not handling %s (fake): message is duplicate", msg.ID) return } intent := p.bridge.GetPuppetByUID(msg.Sender).IntentFor(p) if !intent.IsCustomPuppet && p.IsPrivateChat() && msg.Sender.Uin == p.Key.Receiver.Uin { - p.log.Debugfln("Not handling %s (fake): user doesn't have double puppeting enabled", msg.ID) + p.log.Debug().Msgf("Not handling %s (fake): user doesn't have double puppeting enabled", msg.ID) return } @@ -222,7 +221,7 @@ func (p *Portal) handleFakeMessage(msg fakeMessage) { resp, err := p.sendMessage(intent, event.EventMessage, content, nil, msg.Time.UnixMilli()) if err != nil { - p.log.Errorfln("Failed to send %s to Matrix: %v", msg.ID, err) + p.log.Error().Msgf("Failed to send %s to Matrix: %v", msg.ID, err) } else { p.finishHandling(nil, msg.ID, msg.Time, msg.Sender, resp.EventID, database.MsgFake, database.MsgNoError) } @@ -255,7 +254,7 @@ func (p *Portal) handleWechatRevoke(source *User, message *wechat.Event) { if errors.Is(err, mautrix.MForbidden) { _, err = p.MainIntent().RedactEvent(p.MXID, msg.MXID) if err != nil { - p.log.Errorln("Failed to redact %s: %v", msg.MsgID, err) + p.log.Error().Msgf("Failed to redact %s: %v", msg.MsgID, err) } } //} else { @@ -265,7 +264,7 @@ func (p *Portal) handleWechatRevoke(source *User, message *wechat.Event) { func (p *Portal) handleWechatEvent(source *User, msg *wechat.Event) { if len(p.MXID) == 0 { - p.log.Warnln("handleWechatEvent called even though portal.MXID is empty") + p.log.Warn().Msgf("handleWechatEvent called even though portal.MXID is empty") return } @@ -278,7 +277,7 @@ func (p *Portal) handleWechatEvent(source *User, msg *wechat.Event) { if msg.Type == wechat.EventRevoke { p.handleWechatRevoke(source, msg) } else { - p.log.Debugfln("Not handling %s: message is duplicate", msgID) + p.log.Debug().Msgf("Not handling %s: message is duplicate", msgID) } return } @@ -287,7 +286,7 @@ func (p *Portal) handleWechatEvent(source *User, msg *wechat.Event) { if intent == nil { return } else if !intent.IsCustomPuppet && p.IsPrivateChat() && sender.Uin == p.Key.Receiver.Uin { - p.log.Debugfln("Not handling %s: user doesn't have double puppeting enabled", msgID) + p.log.Debug().Msgf("Not handling %s: user doesn't have double puppeting enabled", msgID) return } @@ -332,7 +331,7 @@ func (p *Portal) handleWechatEvent(source *User, msg *wechat.Event) { var eventID id.EventID resp, err := p.sendMessage(converted.Intent, converted.Type, converted.Content, converted.Extra, ts) if err != nil { - p.log.Errorfln("Failed to send %s to Matrix: %v", msgID, err) + p.log.Error().Msgf("Failed to send %s to Matrix: %v", msgID, err) } else { eventID = resp.EventID } @@ -531,7 +530,7 @@ func (p *Portal) markHandled(txn dbutil.Transaction, msg *database.Message, msgI func (p *Portal) getMessagePuppet(user *User, sender types.UID) *Puppet { puppet := p.bridge.GetPuppetByUID(sender) if puppet == nil { - p.log.Warnfln("Message doesn't seem to have a valid sender (%s): puppet is nil", sender) + p.log.Warn().Msgf("Message doesn't seem to have a valid sender (%s): puppet is nil", sender) return nil } @@ -552,13 +551,13 @@ func (p *Portal) getMessageIntent(user *User, sender types.UID) *appservice.Inte func (p *Portal) finishHandling(existing *database.Message, msgId string, ts time.Time, sender types.UID, mxid id.EventID, msgType database.MessageType, errType database.MessageErrorType) { p.markHandled(nil, existing, msgId, ts, sender, mxid, true, true, msgType, errType) - p.log.Debugfln("Handled message %s (%s) -> %s", msgId, msgType, mxid) + p.log.Debug().Msgf("Handled message %s (%s) -> %s", msgId, msgType, mxid) } func (p *Portal) kickExtraUsers(participantMap map[types.UID]bool) { members, err := p.MainIntent().JoinedMembers(p.MXID) if err != nil { - p.log.Warnln("Failed to get member list:", err) + p.log.Warn().Msgf("Failed to get member list: %v", err) return } for member := range members.Joined { @@ -571,7 +570,7 @@ func (p *Portal) kickExtraUsers(participantMap map[types.UID]bool) { Reason: "User had left this WeChat chat", }) if err != nil { - p.log.Warnfln("Failed to kick user %s who had left: %v", member, err) + p.log.Warn().Msgf("Failed to kick user %s who had left: %v", member, err) } } } @@ -582,7 +581,7 @@ func (p *Portal) syncParticipant(source *User, participant string, puppet *Puppe defer func() { wg.Done() if err := recover(); err != nil { - p.log.Errorfln("Syncing participant %s panicked: %v\n%s", participant, err, debug.Stack()) + p.log.Error().Msgf("Syncing participant %s panicked: %v\n%s", participant, err, debug.Stack()) } }() @@ -594,7 +593,7 @@ func (p *Portal) syncParticipant(source *User, participant string, puppet *Puppe if user == nil || !puppet.IntentFor(p).IsCustomPuppet { err := puppet.IntentFor(p).EnsureJoined(p.MXID) if err != nil { - p.log.Warnfln("Failed to make puppet of %s join %s: %v", participant, p.MXID, err) + p.log.Warn().Msgf("Failed to make puppet of %s join %s: %v", participant, p.MXID, err) } } } @@ -610,7 +609,7 @@ func (p *Portal) SyncParticipants(source *User, metadata *wechat.GroupInfo, forc if len(metadata.Members) == 0 { m := source.Client.GetGroupMembers(metadata.ID) if m == nil { - p.log.Warnfln("Failed to get group members through %s", source.UID) + p.log.Warn().Msgf("Failed to get group members through %s", source.UID) } else { metadata.Members = m } @@ -643,13 +642,13 @@ func (p *Portal) SyncParticipants(source *User, metadata *wechat.GroupInfo, forc if changed { _, err = p.MainIntent().SetPowerLevels(p.MXID, levels) if err != nil { - p.log.Errorln("Failed to change power levels:", err) + p.log.Error().Msgf("Failed to change power levels: %v", err) } } p.kickExtraUsers(participantMap) wg.Wait() - p.log.Debugln("Participant sync completed") + p.log.Debug().Msgf("Participant sync completed") } func (p *Portal) UpdateRoomNickname(source *User, wxid string) { @@ -695,7 +694,7 @@ func (p *Portal) UpdateAvatar(user *User, setBy types.UID, updateInfo bool) bool _, err = p.MainIntent().SetRoomAvatar(p.MXID, p.AvatarURL) } if err != nil { - p.log.Warnln("Failed to set room avatar:", err) + p.log.Warn().Msgf("Failed to set room avatar: %v", err) return true } else { p.AvatarSet = true @@ -711,15 +710,17 @@ func (p *Portal) UpdateAvatar(user *User, setBy types.UID, updateInfo bool) bool } func (p *Portal) UpdateName(name string, setBy types.UID, updateInfo bool) bool { - if p.Name != name || (!p.NameSet && len(p.MXID) > 0) { - p.log.Debugfln("Updating name %q -> %q", p.Name, name) + if p.Name != name || (!p.NameSet && len(p.MXID) > 0 && p.shouldSetDMRoomMetadata()) { + p.log.Debug().Msgf("Updating name %q -> %q", p.Name, name) p.Name = name p.NameSet = false if updateInfo { defer p.Update(nil) } - if len(p.MXID) > 0 { + if len(p.MXID) > 0 && !p.shouldSetDMRoomMetadata() { + p.UpdateBridgeInfo() + } else if len(p.MXID) > 0 { intent := p.MainIntent() if !setBy.IsEmpty() { intent = p.bridge.GetPuppetByUID(setBy).IntentFor(p) @@ -736,7 +737,7 @@ func (p *Portal) UpdateName(name string, setBy types.UID, updateInfo bool) bool return true } else { - p.log.Warnln("Failed to set room name:", err) + p.log.Warn().Msgf("Failed to set room name: %v", err) } } } @@ -746,7 +747,7 @@ func (p *Portal) UpdateName(name string, setBy types.UID, updateInfo bool) bool func (p *Portal) UpdateTopic(topic string, setBy types.UID, updateInfo bool) bool { if p.Topic != topic || !p.TopicSet { - p.log.Debugfln("Updating topic %q -> %q", p.Topic, topic) + p.log.Debug().Msgf("Updating topic %q -> %q", p.Topic, topic) p.Topic = topic p.TopicSet = false @@ -767,7 +768,7 @@ func (p *Portal) UpdateTopic(topic string, setBy types.UID, updateInfo bool) boo return true } else { - p.log.Warnln("Failed to set room topic:", err) + p.log.Warn().Msgf("Failed to set room topic: %v", err) } } @@ -780,7 +781,7 @@ func (p *Portal) UpdateMetadata(user *User, groupInfo *wechat.GroupInfo, forceAv } if groupInfo == nil { - p.log.Errorln("Failed to get group info") + p.log.Error().Msgf("Failed to get group info") return false } @@ -805,7 +806,7 @@ func (p *Portal) UpdateMatrixRoom(user *User, groupInfo *wechat.GroupInfo, force if len(p.MXID) == 0 { return false } - p.log.Infofln("Syncing portal %s for %s", p.Key, user.MXID) + p.log.Info().Msgf("Syncing portal %s for %s", p.Key, user.MXID) p.ensureUserInvited(user) go p.addToSpace(user) @@ -883,7 +884,7 @@ func (p *Portal) ChangeAdminStatus(uids []types.UID, setAdmin bool) id.EventID { if changed { resp, err := p.MainIntent().SetPowerLevels(p.MXID, levels) if err != nil { - p.log.Errorln("Failed to change power levels:", err) + p.log.Error().Msgf("Failed to change power levels: %v", err) } else { return resp.EventID } @@ -911,7 +912,7 @@ func (p *Portal) RestrictMessageSending(restrict bool) id.EventID { levels.EventsDefault = newLevel resp, err := p.MainIntent().SetPowerLevels(p.MXID, levels) if err != nil { - p.log.Errorln("Failed to change power levels:", err) + p.log.Error().Msgf("Failed to change power levels: %v", err) return "" } else { return resp.EventID @@ -935,7 +936,7 @@ func (p *Portal) RestrictMetadataChanges(restrict bool) id.EventID { if changed { resp, err := p.MainIntent().SetPowerLevels(p.MXID, levels) if err != nil { - p.log.Errorln("Failed to change power levels:", err) + p.log.Error().Msgf("Failed to change power levels: %v", err) } else { return resp.EventID } @@ -970,22 +971,28 @@ func (p *Portal) getBridgeInfo() (string, event.BridgeEventContent) { func (p *Portal) UpdateBridgeInfo() { if len(p.MXID) == 0 { - p.log.Debugln("Not updating bridge info: no Matrix room created") + p.log.Debug().Msgf("Not updating bridge info: no Matrix room created") return } - p.log.Debugln("Updating bridge info...") + p.log.Debug().Msgf("Updating bridge info...") stateKey, content := p.getBridgeInfo() _, err := p.MainIntent().SendStateEvent(p.MXID, event.StateBridge, stateKey, content) if err != nil { - p.log.Warnln("Failed to update m.bridge:", err) + p.log.Warn().Msgf("Failed to update m.bridge: %v", err) } // TODO remove this once https://github.com/matrix-org/matrix-doc/pull/2346 is in spec _, err = p.MainIntent().SendStateEvent(p.MXID, event.StateHalfShotBridge, stateKey, content) if err != nil { - p.log.Warnln("Failed to update uk.half-shot.bridge:", err) + p.log.Warn().Msgf("Failed to update uk.half-shot.bridge: %v", err) } } +func (p *Portal) shouldSetDMRoomMetadata() bool { + return !p.IsPrivateChat() || + p.bridge.Config.Bridge.PrivateChatPortalMeta == "always" || + (p.IsEncrypted() && p.bridge.Config.Bridge.PrivateChatPortalMeta != "never") +} + func (p *Portal) GetEncryptionEventContent() (evt *event.EncryptionEventContent) { evt = &event.EncryptionEventContent{Algorithm: id.AlgorithmMegolmV1} if rot := p.bridge.Config.Bridge.Encryption.Rotation; rot.EnableCustom { @@ -1008,28 +1015,24 @@ func (p *Portal) CreateMatrixRoom(user *User, groupInfo *wechat.GroupInfo, isFul return err } - p.log.Infoln("Creating Matrix room. Info source:", user.MXID) + p.log.Info().Msgf("Creating Matrix room. Info source: %s", user.MXID) if p.IsPrivateChat() { puppet := p.bridge.GetPuppetByUID(p.Key.UID) puppet.SyncContact(user, true, "creating private chat portal") - if p.bridge.Config.Bridge.PrivateChatPortalMeta { - p.Name = puppet.Displayname - p.AvatarURL = puppet.AvatarURL - p.Avatar = puppet.Avatar - } else { - p.Name = "" - } + p.Name = puppet.Displayname + p.AvatarURL = puppet.AvatarURL + p.Avatar = puppet.Avatar p.Topic = PrivateChatTopic } else { if groupInfo == nil || !isFullInfo { foundInfo := user.Client.GetGroupInfo(p.Key.UID.Uin) if foundInfo == nil { - p.log.Warnfln("Failed to get group info through %s", user.UID) + p.log.Warn().Msgf("Failed to get group info through %s", user.UID) } else { m := user.Client.GetGroupMembers(p.Key.UID.Uin) if m == nil { - p.log.Warnfln("Failed to get group members through %s: %v", user.UID) + p.log.Warn().Msgf("Failed to get group members through %s", user.UID) } else { foundInfo.Members = m groupInfo = foundInfo @@ -1061,15 +1064,6 @@ func (p *Portal) CreateMatrixRoom(user *User, groupInfo *wechat.GroupInfo, isFul Content: event.Content{Parsed: bridgeInfo}, StateKey: &bridgeInfoStateKey, }} - if !p.AvatarURL.IsEmpty() { - initialState = append(initialState, &event.Event{ - Type: event.StateRoomAvatar, - Content: event.Content{ - Parsed: event.RoomAvatarEventContent{URL: p.AvatarURL}, - }, - }) - p.AvatarSet = true - } var invite []id.UserID @@ -1086,11 +1080,23 @@ func (p *Portal) CreateMatrixRoom(user *User, groupInfo *wechat.GroupInfo, isFul } } + if !p.AvatarURL.IsEmpty() && p.shouldSetDMRoomMetadata() { + initialState = append(initialState, &event.Event{ + Type: event.StateRoomAvatar, + Content: event.Content{ + Parsed: event.RoomAvatarEventContent{URL: p.AvatarURL}, + }, + }) + p.AvatarSet = true + } else { + p.AvatarSet = false + } + creationContent := make(map[string]interface{}) if !p.bridge.Config.Bridge.FederateRooms { creationContent["m.federate"] = false } - resp, err := intent.CreateRoom(&mautrix.ReqCreateRoom{ + req := &mautrix.ReqCreateRoom{ Visibility: "private", Name: p.Name, Topic: p.Topic, @@ -1099,10 +1105,18 @@ func (p *Portal) CreateMatrixRoom(user *User, groupInfo *wechat.GroupInfo, isFul IsDirect: p.IsPrivateChat(), InitialState: initialState, CreationContent: creationContent, - }) + } + if !p.shouldSetDMRoomMetadata() { + req.Name = "" + } + + resp, err := intent.CreateRoom(req) if err != nil { return err } + + p.log.Info().Msgf("Matrix room created: %s", resp.RoomID) + p.NameSet = len(p.Name) > 0 p.TopicSet = len(p.Topic) > 0 p.MXID = resp.RoomID @@ -1110,7 +1124,6 @@ func (p *Portal) CreateMatrixRoom(user *User, groupInfo *wechat.GroupInfo, isFul p.bridge.portalsByMXID[p.MXID] = p p.bridge.portalsLock.Unlock() p.Update(nil) - p.log.Infoln("Matrix room created:", p.MXID) for _, userID := range invite { p.bridge.StateStore.SetMembership(p.MXID, userID, event.MembershipInvite) @@ -1131,7 +1144,7 @@ func (p *Portal) CreateMatrixRoom(user *User, groupInfo *wechat.GroupInfo, isFul if p.bridge.Config.Bridge.Encryption.Default { err = p.bridge.Bot.EnsureJoined(p.MXID) if err != nil { - p.log.Errorln("Failed to join created portal with bridge bot for e2be:", err) + p.log.Error().Msgf("Failed to join created portal with bridge bot for e2be: %v", err) } } @@ -1140,7 +1153,7 @@ func (p *Portal) CreateMatrixRoom(user *User, groupInfo *wechat.GroupInfo, isFul firstEventResp, err := p.MainIntent().SendMessageEvent(p.MXID, PortalCreationDummyEvent, struct{}{}) if err != nil { - p.log.Errorln("Failed to send dummy event to mark portal creation:", err) + p.log.Error().Msgf("Failed to send dummy event to mark portal creation: %v", err) } else { p.FirstEventID = firstEventResp.EventID p.Update(nil) @@ -1158,9 +1171,9 @@ func (p *Portal) addToSpace(user *User) { Via: []string{p.bridge.Config.Homeserver.Domain}, }) if err != nil { - p.log.Errorfln("Failed to add room to %s's personal filtering space (%s): %v", user.MXID, spaceID, err) + p.log.Error().Msgf("Failed to add room to %s's personal filtering space (%s): %v", user.MXID, spaceID, err) } else { - p.log.Debugfln("Added room to %s's personal filtering space (%s)", user.MXID, spaceID) + p.log.Debug().Msgf("Added room to %s's personal filtering space (%s)", user.MXID, spaceID) user.MarkInSpace(p.Key) } } @@ -1191,7 +1204,7 @@ func (p *Portal) SetReply(content *event.MessageEventContent, replyTo *ReplyInfo } evt, err := p.MainIntent().GetEvent(p.MXID, message.MXID) if err != nil { - p.log.Warnln("Failed to get reply target:", err) + p.log.Warn().Msgf("Failed to get reply target: %v", err) content.RelatesTo = (&event.RelatesTo{}).SetReplyTo(message.MXID) return true } @@ -1199,7 +1212,7 @@ func (p *Portal) SetReply(content *event.MessageEventContent, replyTo *ReplyInfo if evt.Type == event.EventEncrypted { decryptedEvt, err := p.bridge.Crypto.Decrypt(evt) if err != nil { - p.log.Warnln("Failed to decrypt reply target:", err) + p.log.Warn().Msgf("Failed to decrypt reply target: %v", err) } else { evt = decryptedEvt } @@ -1244,7 +1257,7 @@ func (p *Portal) sendMessage(intent *appservice.IntentAPI, eventType event.Type, } func (p *Portal) makeMediaBridgeFailureMessage(msgID string, bridgeErr error, converted *ConvertedMessage) *ConvertedMessage { - p.log.Errorfln("Failed to bridge media for %s: %v", msgID, bridgeErr) + p.log.Error().Msgf("Failed to bridge media for %s: %v", msgID, bridgeErr) converted.Type = event.EventMessage converted.Content = &event.MessageEventContent{ MsgType: event.MsgNotice, @@ -1276,7 +1289,7 @@ func (p *Portal) uploadMedia(intent *appservice.IntentAPI, data []byte, content } var mxc id.ContentURI if p.bridge.Config.Homeserver.AsyncMedia { - uploaded, err := intent.UnstableUploadAsync(req) + uploaded, err := intent.UploadAsync(req) if err != nil { return err } @@ -1323,12 +1336,12 @@ func (p *Portal) preprocessMatrixMedia(content *event.MessageEventContent) (stri } data, err := p.MainIntent().DownloadBytesContext(context.Background(), mxc) if err != nil { - return fileName, nil, util.NewDualError(errMediaDownloadFailed, err) + return fileName, nil, exerrors.NewDualError(errMediaDownloadFailed, err) } if file != nil { err = file.DecryptInPlace(data) if err != nil { - return fileName, nil, util.NewDualError(errMediaDecryptFailed, err) + return fileName, nil, exerrors.NewDualError(errMediaDecryptFailed, err) } } @@ -1343,7 +1356,7 @@ func (p *Portal) HandleMatrixMessage(sender *User, evt *event.Event) { content, ok := evt.Content.Parsed.(*event.MessageEventContent) if !ok { notice := "Failed to parse matrix message content" - p.log.Warnfln(notice) + p.log.Warn().Msg(notice) p.replyFailure(sender, evt, notice) return } @@ -1378,7 +1391,7 @@ func (p *Portal) HandleMatrixMessage(sender *User, evt *event.Event) { text := content.Body if content.Format == event.FormatHTML { - formatted, mentionedUIDs := p.bridge.Formatter.ParseMatrix(content.FormattedBody) + formatted, mentionedUIDs := p.bridge.Formatter.ParseMatrix(content.FormattedBody, content.Mentions) for _, mention := range mentionedUIDs { groupNickname := sender.Client.GetGroupMemberNickname(p.Key.UID.Uin, mention) if len(groupNickname) > 0 { @@ -1413,7 +1426,7 @@ func (p *Portal) HandleMatrixMessage(sender *User, evt *event.Event) { name, data, err := p.preprocessMatrixMedia(content) if data == nil { notice := fmt.Sprintf("Failed to process matrix media: %v", err) - p.log.Warnfln(notice) + p.log.Warn().Msg(notice) p.replyFailure(sender, evt, notice) return } @@ -1427,7 +1440,7 @@ func (p *Portal) HandleMatrixMessage(sender *User, evt *event.Event) { } else if content.MsgType == event.MsgAudio { if binary, err := ogg2mp3(data); err != nil { notice := fmt.Sprintf("Failed to convert audio to mp3: %v", err) - p.log.Warnfln(notice) + p.log.Warn().Msg(notice) p.replyFailure(sender, evt, notice) return } else { @@ -1444,13 +1457,13 @@ func (p *Portal) HandleMatrixMessage(sender *User, evt *event.Event) { } default: notice := fmt.Sprintf("%s not support", content.MsgType) - p.log.Warnfln(notice) + p.log.Warn().Msg(notice) p.replyFailure(sender, evt, notice) return } msgID := "FAKE::" + strconv.FormatInt(evt.Timestamp, 10) - p.log.Debugln("Sending event", evt.ID, "to WeChat") + p.log.Debug().Msgf("Sending event %s to WeChat", evt.ID) if _, err := sender.Client.SendEvent(msg); err != nil { p.replyFailure(sender, evt, err.Error()) } else { @@ -1487,7 +1500,7 @@ func (p *Portal) replyFailure(sender *User, evt *event.Event, text string) { if evt.Type == event.EventEncrypted { decryptedEvt, err := p.bridge.Crypto.Decrypt(evt) if err != nil { - p.log.Warnln("Failed to decrypt reply target:", err) + p.log.Warn().Msgf("Failed to decrypt reply target: %v", err) } else { evt = decryptedEvt } @@ -1495,7 +1508,7 @@ func (p *Portal) replyFailure(sender *User, evt *event.Event, text string) { content.SetReply(evt) if _, err := p.sendMessage(intent, event.EventMessage, content, nil, 0); err != nil { - p.log.Warnfln("Failed to reply to failure for %s: %v", sender.GetMXID(), err) + p.log.Warn().Msgf("Failed to reply to failure for %s: %v", sender.GetMXID(), err) } } @@ -1538,12 +1551,12 @@ func (p *Portal) GetMatrixUsers() ([]id.UserID, error) { func (p *Portal) CleanupIfEmpty() { users, err := p.GetMatrixUsers() if err != nil { - p.log.Errorfln("Failed to get Matrix user list to determine if portal needs to be cleaned up: %v", err) + p.log.Error().Msgf("Failed to get Matrix user list to determine if portal needs to be cleaned up: %v", err) return } if len(users) == 0 { - p.log.Infoln("Room seems to be empty, cleaning up...") + p.log.Info().Msg("Room seems to be empty, cleaning up...") p.Delete() p.Cleanup(false) } @@ -1556,7 +1569,7 @@ func (p *Portal) Cleanup(puppetsOnly bool) { intent := p.MainIntent() members, err := intent.JoinedMembers(p.MXID) if err != nil { - p.log.Errorln("Failed to get portal members for cleanup:", err) + p.log.Error().Msgf("Failed to get portal members for cleanup: %v", err) return } for member := range members.Joined { @@ -1567,18 +1580,18 @@ func (p *Portal) Cleanup(puppetsOnly bool) { if puppet != nil { _, err = puppet.DefaultIntent().LeaveRoom(p.MXID) if err != nil { - p.log.Errorln("Error leaving as puppet while cleaning up portal:", err) + p.log.Error().Msgf("Error leaving as puppet while cleaning up portal: %v", err) } } else if !puppetsOnly { _, err = intent.KickUser(p.MXID, &mautrix.ReqKickUser{UserID: member, Reason: "Deleting portal"}) if err != nil { - p.log.Errorln("Error kicking user while cleaning up portal:", err) + p.log.Error().Msgf("Error kicking user while cleaning up portal: %v", err) } } } _, err = intent.LeaveRoom(p.MXID) if err != nil { - p.log.Errorln("Error leaving with main intent while cleaning up portal:", err) + p.log.Error().Msgf("Error leaving with main intent while cleaning up portal: %v", err) } } @@ -1689,7 +1702,7 @@ func (br *WechatBridge) loadDBPortal(dbPortal *database.Portal, key *database.Po func (br *WechatBridge) newBlankPortal(key database.PortalKey) *Portal { portal := &Portal{ bridge: br, - log: br.Log.Sub(fmt.Sprintf("Portal/%s", key)), + log: br.ZLog.With().Str("portal_key", key.String()).Logger(), messages: make(chan PortalMessage, br.Config.Bridge.PortalMessageBuffer), matrixMessages: make(chan PortalMatrixMessage, br.Config.Bridge.PortalMessageBuffer), diff --git a/internal/puppet.go b/internal/puppet.go index c54ab1a..c40f3dd 100644 --- a/internal/puppet.go +++ b/internal/puppet.go @@ -10,11 +10,10 @@ import ( "github.com/duo/matrix-wechat/internal/database" "github.com/duo/matrix-wechat/internal/types" + "github.com/rs/zerolog" "maunium.net/go/mautrix/appservice" "maunium.net/go/mautrix/bridge" "maunium.net/go/mautrix/id" - - log "maunium.net/go/maulogger/v2" ) var userIDRegex *regexp.Regexp @@ -25,7 +24,7 @@ type Puppet struct { *database.Puppet bridge *WechatBridge - log log.Logger + log zerolog.Logger MXID id.UserID @@ -76,7 +75,7 @@ func (p *Puppet) UpdateAvatar(source *User, forceAvatarSync bool, forcePortalSyn } err := p.DefaultIntent().SetAvatarURL(p.AvatarURL) if err != nil { - p.log.Warnln("Failed to set avatar:", err) + p.log.Warn().Msgf("Failed to set avatar: %v", err) } else { p.AvatarSet = true } @@ -93,11 +92,11 @@ func (p *Puppet) UpdateName(contact types.ContactInfo, forcePortalSync bool) boo p.NameSet = false err := p.DefaultIntent().SetDisplayName(newName) if err == nil { - p.log.Debugln("Updated name", p.Displayname, "->", newName) + p.log.Debug().Msgf("Updated name %s -> %s", p.Displayname, newName) p.NameSet = true go p.updatePortalName() } else { - p.log.Warnln("Failed to set display name:", err) + p.log.Warn().Msgf("Failed to set display name: %v", err) } return true } else if forcePortalSync { @@ -108,29 +107,29 @@ func (p *Puppet) UpdateName(contact types.ContactInfo, forcePortalSync bool) boo } func (p *Puppet) updatePortalMeta(meta func(portal *Portal)) { - if p.bridge.Config.Bridge.PrivateChatPortalMeta { - for _, portal := range p.bridge.GetAllPortalsByUID(p.UID) { - // Get room create lock to prevent races between receiving contact info and room creation. - portal.roomCreateLock.Lock() - meta(portal) - portal.roomCreateLock.Unlock() - } + for _, portal := range p.bridge.GetAllPortalsByUID(p.UID) { + // Get room create lock to prevent races between receiving contact info and room creation. + portal.roomCreateLock.Lock() + meta(portal) + portal.roomCreateLock.Unlock() } } func (p *Puppet) updatePortalAvatar() { p.updatePortalMeta(func(portal *Portal) { - if portal.Avatar == p.Avatar && portal.AvatarURL == p.AvatarURL && portal.AvatarSet { + if portal.Avatar == p.Avatar && portal.AvatarURL == p.AvatarURL && (portal.AvatarSet || !portal.shouldSetDMRoomMetadata()) { return } portal.AvatarURL = p.AvatarURL portal.Avatar = p.Avatar portal.AvatarSet = false defer portal.Update(nil) - if len(portal.MXID) > 0 { + if len(portal.MXID) > 0 && !portal.shouldSetDMRoomMetadata() { + portal.UpdateBridgeInfo() + } else if len(portal.MXID) > 0 { _, err := portal.MainIntent().SetRoomAvatar(portal.MXID, p.AvatarURL) if err != nil { - portal.log.Warnln("Failed to set avatar:", err) + portal.log.Warn().Msgf("Failed to set avatar: %v", err) } else { portal.AvatarSet = true portal.UpdateBridgeInfo() @@ -150,7 +149,7 @@ func (p *Puppet) SyncContact(source *User, forceAvatarSync bool, reason string) if info != nil { p.Sync(source, types.NewContact(info.ID, info.Name, info.Remark), forceAvatarSync, false) } else { - p.log.Warnfln("No contact info found through %s in SyncContact (sync reason: %s)", source.MXID, reason) + p.log.Warn().Msgf("No contact info found through %s in SyncContact (sync reason: %s)", source.MXID, reason) } } @@ -160,10 +159,10 @@ func (p *Puppet) Sync(source *User, contact *types.ContactInfo, forceAvatarSync, err := p.DefaultIntent().EnsureRegistered() if err != nil { - p.log.Errorln("Failed to ensure registered:", err) + p.log.Error().Msgf("Failed to ensure registered: %v", err) } - p.log.Debugfln("Syncing info through %s", source.UID) + p.log.Debug().Msgf("Syncing info through %s", source.UID) update := false if contact != nil { @@ -178,19 +177,16 @@ func (p *Puppet) Sync(source *User, contact *types.ContactInfo, forceAvatarSync, } } -func (br *WechatBridge) ParsePuppetMXID(mxid id.UserID) (types.UID, bool) { - var uid types.UID +func (br *WechatBridge) ParsePuppetMXID(mxid id.UserID) (uid types.UID, ok bool) { if userIDRegex == nil { - userIDRegex = regexp.MustCompile(fmt.Sprintf("^@%s:%s$", - br.Config.Bridge.FormatUsername("(.+)"), - br.Config.Homeserver.Domain)) + userIDRegex = br.Config.MakeUserIDRegex("(.+)") } match := userIDRegex.FindStringSubmatch(string(mxid)) if len(match) == 2 { uid = types.NewUserUID(match[1]) - return uid, true + ok = true } - return uid, false + return } func (br *WechatBridge) GetPuppetByMXID(mxid id.UserID) *Puppet { @@ -323,7 +319,7 @@ func (br *WechatBridge) NewPuppet(dbPuppet *database.Puppet) *Puppet { return &Puppet{ Puppet: dbPuppet, bridge: br, - log: br.Log.Sub(fmt.Sprintf("Puppet/%s", dbPuppet.UID)), + log: br.ZLog.With().Str("puppet", dbPuppet.UID.String()).Logger(), MXID: br.FormatPuppetMXID(dbPuppet.UID), } diff --git a/internal/user.go b/internal/user.go index 319eff9..4dd2d10 100644 --- a/internal/user.go +++ b/internal/user.go @@ -14,6 +14,7 @@ import ( "github.com/duo/matrix-wechat/internal/types" "github.com/duo/matrix-wechat/internal/wechat" + "github.com/rs/zerolog" "maunium.net/go/mautrix" "maunium.net/go/mautrix/appservice" "maunium.net/go/mautrix/bridge" @@ -21,8 +22,6 @@ import ( "maunium.net/go/mautrix/bridge/status" "maunium.net/go/mautrix/event" "maunium.net/go/mautrix/id" - - log "maunium.net/go/maulogger/v2" ) const ( @@ -46,7 +45,7 @@ type User struct { Client *wechat.WechatClient bridge *WechatBridge - log log.Logger + log zerolog.Logger Admin bool Whitelisted bool @@ -112,7 +111,7 @@ func (u *User) EnqueuePuppetResync(puppet *Puppet) { u.resyncQueueLock.Lock() if _, exists := u.resyncQueue[puppet.UID]; !exists { u.resyncQueue[puppet.UID] = resyncQueueItem{puppet: puppet} - u.log.Debugfln("Enqueued resync for %s (next sync in %s)", puppet.UID, time.Until(u.nextResync)) + u.log.Debug().Msgf("Enqueued resync for %s (next sync in %s)", puppet.UID, time.Until(u.nextResync)) } u.resyncQueueLock.Unlock() } @@ -124,7 +123,7 @@ func (u *User) EnqueuePortalResync(portal *Portal) { u.resyncQueueLock.Lock() if _, exists := u.resyncQueue[portal.Key.UID]; !exists { u.resyncQueue[portal.Key.UID] = resyncQueueItem{portal: portal} - u.log.Debugfln("Enqueued resync for %s (next sync in %s)", portal.Key.UID, time.Until(u.nextResync)) + u.log.Debug().Msgf("Enqueued resync for %s (next sync in %s)", portal.Key.UID, time.Until(u.nextResync)) } u.resyncQueueLock.Unlock() } @@ -151,7 +150,7 @@ func (u *User) doPuppetResync() { lastSync = item.portal.LastSync } if lastSync.Add(resyncMinInterval).After(time.Now()) { - u.log.Debugfln("Not resyncing %s, last sync was %s ago", uid, time.Until(lastSync)) + u.log.Debug().Msgf("Not resyncing %s, last sync was %s ago", uid, time.Until(lastSync)) continue } if item.puppet != nil { @@ -165,23 +164,23 @@ func (u *User) doPuppetResync() { if groupInfo != nil { m := u.Client.GetGroupMembers(portal.Key.Receiver.Uin) if m == nil { - u.log.Warnfln("Failed to get group members for %s to do background sync", portal.Key.UID) + u.log.Warn().Msgf("Failed to get group members for %s to do background sync", portal.Key.UID) } else { groupInfo.Members = m - u.log.Debugfln("Doing background sync for %s", portal.Key.UID) + u.log.Debug().Msgf("Doing background sync for %s", portal.Key.UID) portal.UpdateMatrixRoom(u, groupInfo, false) } } else { - u.log.Warnfln("Failed to get group info for %s to do background sync", portal.Key.UID) + u.log.Warn().Msgf("Failed to get group info for %s to do background sync", portal.Key.UID) } } for _, puppet := range puppets { - u.log.Debugfln("Doing background sync for user: %v", puppet.UID) + u.log.Debug().Msgf("Doing background sync for user: %v", puppet.UID) info := u.Client.GetUserInfo(puppet.UID.Uin) if info != nil { puppet.Sync(u, types.NewContact(info.ID, info.Name, info.Remark), true, true) } else { - u.log.Warnfln("Failed to get contact info for %s in background sync", puppet.UID) + u.log.Warn().Msgf("Failed to get contact info for %s in background sync", puppet.UID) } } } @@ -202,7 +201,7 @@ func (u *User) ensureInvited(intent *appservice.IntentAPI, roomID id.RoomID, isD ok = true return } else if err != nil { - u.log.Warnfln("Failed to invite user to %s: %v", roomID, err) + u.log.Warn().Msgf("Failed to invite user to %s: %v", roomID, err) } else { ok = true } @@ -210,7 +209,7 @@ func (u *User) ensureInvited(intent *appservice.IntentAPI, roomID id.RoomID, isD if customPuppet != nil && customPuppet.CustomIntent() != nil { err = customPuppet.CustomIntent().EnsureJoined(roomID, appservice.EnsureJoinedParams{IgnoreCache: true}) if err != nil { - u.log.Warnfln("Failed to auto-join %s: %v", roomID, err) + u.log.Warn().Msgf("Failed to auto-join %s: %v", roomID, err) ok = false } else { ok = true @@ -255,7 +254,7 @@ func (u *User) GetSpaceRoom() id.RoomID { }) if err != nil { - u.log.Errorln("Failed to auto-create space room:", err) + u.log.Error().Msgf("Failed to auto-create space room: %v", err) } else { u.SpaceRoom = resp.RoomID u.Update() @@ -287,7 +286,7 @@ func (u *User) GetManagementRoom() id.RoomID { CreationContent: creationContent, }) if err != nil { - u.log.Errorln("Failed to auto-create management room:", err) + u.log.Error().Msgf("Failed to auto-create management room: %v", err) } else { u.SetManagementRoom(resp.RoomID) } @@ -309,7 +308,7 @@ func (u *User) SetManagementRoom(roomID id.RoomID) { } func (u *User) failedConnect(err error) { - u.log.Warnln("Error connecting to WeChat:", err) + u.log.Warn().Msgf("Error connecting to WeChat: %v", err) u.Client.Disconnect() u.BridgeState.Send(status.BridgeState{ StateEvent: status.StateUnknownError, @@ -318,7 +317,6 @@ func (u *User) failedConnect(err error) { "go_error": err.Error(), }, }) - u.BridgeState.Send(status.BridgeState{StateEvent: status.StateUnknownError, Error: WechatConnectionFailed}) } func (u *User) Connect() error { @@ -353,9 +351,9 @@ func (u *User) MarkLogin() { go u.BridgeState.Send(status.BridgeState{StateEvent: status.StateConnected}) go u.tryAutomaticDoublePuppeting() - u.log.Debugln("Login to wechat", u.UID) + u.log.Debug().Msgf("Login to wechat %s", u.UID) } else { - u.log.Warnln("Failed to get self info.") + u.log.Warn().Msgf("Failed to get self info.") } } @@ -377,30 +375,6 @@ func (u *User) IsLoggedIn() bool { return u.Client != nil && u.Client.IsLoggedIn() } -func (u *User) tryAutomaticDoublePuppeting() { - if !u.bridge.Config.CanAutoDoublePuppet(u.MXID) { - return - } - u.log.Debugln("Checking if double puppeting needs to be enabled") - puppet := u.bridge.GetPuppetByUID(u.UID) - if len(puppet.CustomMXID) > 0 { - u.log.Debugln("User already has double-puppeting enabled") - // Custom puppet already enabled - return - } - accessToken, err := puppet.loginWithSharedSecret(u.MXID) - if err != nil { - u.log.Warnln("Failed to login with shared secret:", err) - return - } - err = puppet.SwitchCustomMXID(accessToken, u.MXID) - if err != nil { - puppet.log.Warnln("Failed to switch to auto-logined custom puppet:", err) - return - } - u.log.Infoln("Successfully automatically enabled custom puppet") -} - func (u *User) getDirectChats() map[id.UserID][]id.RoomID { res := make(map[id.UserID][]id.RoomID) privateChats := u.bridge.DB.Portal.FindPrivateChats(u.UID) @@ -427,26 +401,37 @@ func (u *User) UpdateDirectChats(chats map[id.UserID][]id.RoomID) { chats = u.getDirectChats() method = http.MethodPut } - u.log.Debugln("Updating m.direct list on homeserver") + u.log.Debug().Msgf("Updating m.direct list on homeserver") var err error - existingChats := make(map[id.UserID][]id.RoomID) - err = intent.GetAccountData(event.AccountDataDirectChats.Type, &existingChats) - if err != nil { - u.log.Warnln("Failed to get m.direct list to update it:", err) - return - } - for userID, rooms := range existingChats { - if _, ok := u.bridge.ParsePuppetMXID(userID); !ok { - // This is not a ghost user, include it in the new list - chats[userID] = rooms - } else if _, ok := chats[userID]; !ok && method == http.MethodPatch { - // This is a ghost user, but we're not replacing the whole list, so include it too - chats[userID] = rooms + if u.bridge.Config.Homeserver.Software == bridgeconfig.SoftwareAsmux { + urlPath := intent.BuildClientURL("unstable", "com.beeper.asmux", "dms") + _, err = intent.MakeFullRequest(mautrix.FullRequest{ + Method: method, + URL: urlPath, + Headers: http.Header{"X-Asmux-Auth": {u.bridge.AS.Registration.AppToken}}, + RequestJSON: chats, + }) + } else { + existingChats := make(map[id.UserID][]id.RoomID) + err = intent.GetAccountData(event.AccountDataDirectChats.Type, &existingChats) + if err != nil { + u.log.Warn().Msgf("Failed to get m.direct list to update it: %v", err) + return } + for userID, rooms := range existingChats { + if _, ok := u.bridge.ParsePuppetMXID(userID); !ok { + // This is not a ghost user, include it in the new list + chats[userID] = rooms + } else if _, ok := chats[userID]; !ok && method == http.MethodPatch { + // This is a ghost user, but we're not replacing the whole list, so include it too + chats[userID] = rooms + } + } + err = intent.SetAccountData(event.AccountDataDirectChats.Type, &chats) } - err = intent.SetAccountData(event.AccountDataDirectChats.Type, &chats) + if err != nil { - u.log.Warnln("Failed to update m.direct list:", err) + u.log.Warn().Msgf("Failed to update m.direct list: %v", err) } } @@ -461,7 +446,7 @@ func (u *User) ResyncContacts(forceAvatarSync bool) error { if puppet != nil { puppet.Sync(u, types.NewContact(contact.ID, contact.Name, contact.Remark), forceAvatarSync, true) } else { - u.log.Warnfln("Got a nil puppet for %s while syncing contacts", uid) + u.log.Warn().Msgf("Got a nil puppet for %s while syncing contacts", uid) } } @@ -487,14 +472,14 @@ func (u *User) ResyncGroups(createPortals bool) error { } func (u *User) StartPM(uid types.UID, reason string) (*Portal, *Puppet, bool, error) { - u.log.Debugln("Starting PM with", uid, "from", reason) + u.log.Debug().Msgf("Starting PM with %s from %s", uid, reason) puppet := u.bridge.GetPuppetByUID(uid) puppet.SyncContact(u, true, reason) portal := u.GetPortalByUID(puppet.UID) if len(portal.MXID) > 0 { ok := portal.ensureUserInvited(u) if !ok { - portal.log.Warnfln("ensureUserInvited(%s) returned false, creating new portal", u.MXID) + portal.log.Warn().Msgf("ensureUserInvited(%s) returned false, creating new portal", u.MXID) portal.MXID = "" } else { return portal, puppet, false, nil @@ -505,7 +490,7 @@ func (u *User) StartPM(uid types.UID, reason string) (*Portal, *Puppet, bool, er return portal, puppet, true, err } -func (u *User) updateAvatar(uid types.UID, avatarID *string, avatarURL *id.ContentURI, avatarSet *bool, log log.Logger, intent *appservice.IntentAPI) bool { +func (u *User) updateAvatar(uid types.UID, avatarID *string, avatarURL *id.ContentURI, avatarSet *bool, log zerolog.Logger, intent *appservice.IntentAPI) bool { var url string if uid.IsUser() { if info := u.Client.GetUserInfo(uid.Uin); info != nil { @@ -523,7 +508,7 @@ func (u *User) updateAvatar(uid types.UID, avatarID *string, avatarURL *id.Conte resp, err := reuploadAvatar(intent, url) if err != nil { - u.log.Warnln("Failed to reupload avatar:", err) + u.log.Warn().Msgf("Failed to reupload avatar: %v", err) return false } @@ -683,7 +668,7 @@ func (br *WechatBridge) NewUser(dbUser *database.User) *User { user := &User{ User: dbUser, bridge: br, - log: br.Log.Sub("User").Sub(string(dbUser.MXID)), + log: br.ZLog.With().Str("user_id", dbUser.MXID.String()).Logger(), resyncQueue: make(map[types.UID]resyncQueueItem), } @@ -691,7 +676,7 @@ func (br *WechatBridge) NewUser(dbUser *database.User) *User { user.PermissionLevel = user.bridge.Config.Bridge.Permissions.Get(user.MXID) user.Whitelisted = user.PermissionLevel >= bridgeconfig.PermissionLevelUser user.Admin = user.PermissionLevel >= bridgeconfig.PermissionLevelAdmin - user.BridgeState = br.NewBridgeStateQueue(user, user.log) + user.BridgeState = br.NewBridgeStateQueue(user) go user.puppetResyncLoop() diff --git a/internal/wechat/client.go b/internal/wechat/client.go index 878cb6b..69b4d3b 100644 --- a/internal/wechat/client.go +++ b/internal/wechat/client.go @@ -3,13 +3,13 @@ package wechat import ( "sync" - log "maunium.net/go/maulogger/v2" + "github.com/rs/zerolog" ) type WechatClient struct { mxid string - log log.Logger + log zerolog.Logger processFunc func(*Event) requestFunc func(*WechatClient, *Request) (any, error) @@ -18,11 +18,11 @@ type WechatClient struct { connKeyLock sync.RWMutex } -func newWechatClient(mxid string, f func(*WechatClient, *Request) (any, error), log log.Logger) *WechatClient { +func newWechatClient(mxid string, f func(*WechatClient, *Request) (any, error), log zerolog.Logger) *WechatClient { return &WechatClient{ mxid: mxid, requestFunc: f, - log: log.Sub("Client").Sub(mxid), + log: log.With().Str("client", mxid).Logger(), } } @@ -48,7 +48,7 @@ func (wc *WechatClient) LoginWithQRCode() []byte { if data, err := wc.requestFunc(wc, &Request{ Type: ReqLoginQR, }); err != nil { - wc.log.Warnln("Failed to login with QR code:", err) + wc.log.Warn().Msgf("Failed to login with QR code: %v", err) return nil } else { return data.([]byte) @@ -59,7 +59,7 @@ func (wc *WechatClient) IsLoggedIn() bool { if data, err := wc.requestFunc(wc, &Request{ Type: ReqIsLogin, }); err != nil { - wc.log.Warnln("Failed to get login status:", err) + wc.log.Warn().Msgf("Failed to get login status: %v", err) return false } else { return data.(bool) @@ -70,7 +70,7 @@ func (wc *WechatClient) GetSelf() *UserInfo { if data, err := wc.requestFunc(wc, &Request{ Type: ReqGetSelf, }); err != nil { - wc.log.Warnln("Failed to get self info:", err) + wc.log.Warn().Msgf("Failed to get self info: %v", err) return nil } else { return data.(*UserInfo) @@ -82,7 +82,7 @@ func (wc *WechatClient) GetUserInfo(wxid string) *UserInfo { Type: ReqGetUserInfo, Data: []string{wxid}, }); err != nil { - wc.log.Warnln("Failed to get user info:", err) + wc.log.Warn().Msgf("Failed to get user info: %v", err) return nil } else { return data.(*UserInfo) @@ -94,7 +94,7 @@ func (wc *WechatClient) GetGroupInfo(wxid string) *GroupInfo { Type: ReqGetGroupInfo, Data: []string{wxid}, }); err != nil { - wc.log.Warnln("Failed to get group info:", err) + wc.log.Warn().Msgf("Failed to get group info: %v", err) return nil } else { return data.(*GroupInfo) @@ -106,7 +106,7 @@ func (wc *WechatClient) GetGroupMembers(wxid string) []string { Type: ReqGetGroupMembers, Data: []string{wxid}, }); err != nil { - wc.log.Warnln("Failed to get group members:", err) + wc.log.Warn().Msgf("Failed to get group members: %v", err) return nil } else { return data.([]string) @@ -118,7 +118,7 @@ func (wc *WechatClient) GetGroupMemberNickname(group, wxid string) string { Type: ReqGetGroupMemberNickname, Data: []string{group, wxid}, }); err != nil { - wc.log.Warnln("Failed to get group member nickname:", err) + wc.log.Warn().Msgf("Failed to get group member nickname: %v", err) return "" } else { return data.(string) @@ -129,7 +129,7 @@ func (wc *WechatClient) GetFriendList() []*UserInfo { if data, err := wc.requestFunc(wc, &Request{ Type: ReqGetFriendList, }); err != nil { - wc.log.Warnln("Failed to get friend list:", err) + wc.log.Warn().Msgf("Failed to get friend list: %v", err) return nil } else { return data.([]*UserInfo) @@ -140,7 +140,7 @@ func (wc *WechatClient) GetGroupList() []*GroupInfo { if data, err := wc.requestFunc(wc, &Request{ Type: ReqGetGroupList, }); err != nil { - wc.log.Warnln("Failed to get group list:", err) + wc.log.Warn().Msgf("Failed to get group list: %v", err) return nil } else { return data.([]*GroupInfo) @@ -152,7 +152,7 @@ func (wc *WechatClient) SendEvent(event *Event) (*Event, error) { Type: ReqEvent, Data: event, }); err != nil { - wc.log.Warnfln("Failed to send event:", err) + wc.log.Warn().Msgf("Failed to send event: %v", err) return nil, err } else { return data.(*Event), nil diff --git a/internal/wechat/service.go b/internal/wechat/service.go index 675ce50..22cd2b6 100644 --- a/internal/wechat/service.go +++ b/internal/wechat/service.go @@ -10,8 +10,7 @@ import ( "time" "github.com/gorilla/websocket" - - log "maunium.net/go/maulogger/v2" + "github.com/rs/zerolog" ) var ( @@ -56,7 +55,7 @@ func (c *Conn) close() { } type WechatService struct { - log log.Logger + log zerolog.Logger addr string secret string @@ -74,9 +73,9 @@ type WechatService struct { requestID int64 } -func NewWechatService(addr, secret string, log log.Logger) *WechatService { +func NewWechatService(addr, secret string, log zerolog.Logger) *WechatService { service := &WechatService{ - log: log.Sub("WeChat"), + log: log.With().Str("service", "WeChat").Logger(), addr: addr, secret: secret, clients: make(map[string]*WechatClient), @@ -105,16 +104,16 @@ func (ws *WechatService) NewClient(mxid string) *WechatClient { } func (ws *WechatService) Start() { - ws.log.Infoln("WechatService starting to listen on", ws.addr) + ws.log.Info().Msgf("WechatService starting to listen on %s", ws.addr) err := ws.server.ListenAndServe() if err != nil && err != http.ErrServerClosed { - ws.log.Fatalln("Error in listener:", err) + ws.log.Fatal().Msgf("Error in listener: %v", err) } } func (ws *WechatService) Stop() { - ws.log.Infofln("WechatService stopping") + ws.log.Info().Msgf("WechatService stopping") ws.connLock.Lock() defer ws.connLock.Unlock() @@ -126,7 +125,7 @@ func (ws *WechatService) Stop() { defer cancel() err := ws.server.Shutdown(ctx) if err != nil { - ws.log.Warnln("Failed to close server:", err) + ws.log.Warn().Msgf("Failed to close server: %v", err) } } @@ -144,15 +143,15 @@ func (ws *WechatService) ServeHTTP(w http.ResponseWriter, r *http.Request) { conn, err := upgrader.Upgrade(w, r, nil) if err != nil { - ws.log.Warnln("Failed to upgrade websocket request:", err) + ws.log.Warn().Msgf("Failed to upgrade websocket request: %v", err) return } key := conn.RemoteAddr().String() - ws.log.Infoln("Agent connected from:", key) + ws.log.Info().Msgf("Agent connected from %s", key) defer func() { - ws.log.Infoln("Agent disconnected from:", key) + ws.log.Info().Msgf("Agent disconnected from %s", key) ws.connLock.Lock() delete(ws.conns, key) ws.connLock.Unlock() @@ -167,7 +166,7 @@ func (ws *WechatService) ServeHTTP(w http.ResponseWriter, r *http.Request) { var msg Message err := conn.ReadJSON(&msg) if err != nil { - ws.log.Warnln("Error reading from websocket:", err) + ws.log.Warn().Msgf("Error reading from websocket: %v", err) break } @@ -181,10 +180,10 @@ func (ws *WechatService) ServeHTTP(w http.ResponseWriter, r *http.Request) { if ok { go client.processFunc(request.Data.(*Event)) } else { - ws.log.Warnln("Dropping event for %d: no receiver", msg.MXID) + ws.log.Warn().Msgf("Dropping event for %s: no receiver", msg.MXID) } } else { - ws.log.Warnfln("Request %s not support", request.Type) + ws.log.Warn().Msgf("Request %s not support", request.Type) } case MsgResponse: ws.requestsLock.RLock() @@ -194,10 +193,10 @@ func (ws *WechatService) ServeHTTP(w http.ResponseWriter, r *http.Request) { select { case respChan <- msg.Data.(*Response): default: - ws.log.Warnfln("Failed to handle response to %d: channel didn't accept response", msg.ID) + ws.log.Warn().Msgf("Failed to handle response to %d: channel didn't accept response", msg.ID) } } else { - ws.log.Warnfln("Dropping response to %d: unknown request ID", msg.ID) + ws.log.Warn().Msgf("Dropping response to %d: unknown request ID", msg.ID) } } } @@ -223,14 +222,14 @@ func (ws *WechatService) request(client *WechatClient, req *Request) (any, error return nil, errors.New("no agent connection avaiable") } - ws.log.Debugfln("Send request message #%d %s", msg.ID, req.Type) + ws.log.Debug().Msgf("Send request message #%d %s", msg.ID, req.Type) if err := conn.sendMessage(msg); err != nil { return nil, err } select { case resp := <-respChan: - ws.log.Debugfln("Receive response message #%d %s", msg.ID, resp.Type) + ws.log.Debug().Msgf("Receive response message #%d %s", msg.ID, resp.Type) //return resp.Data, resp.Error if resp.Error != nil { return nil, resp.Error diff --git a/internal/wechatbridge.go b/internal/wechatbridge.go index e4b9761..fa1fc13 100644 --- a/internal/wechatbridge.go +++ b/internal/wechatbridge.go @@ -1,14 +1,10 @@ package internal import ( - "context" - "errors" "fmt" "net/http" "net/url" - "os" "sync" - "time" "github.com/duo/matrix-wechat/internal/config" "github.com/duo/matrix-wechat/internal/database" @@ -16,9 +12,7 @@ import ( "github.com/duo/matrix-wechat/internal/wechat" "maunium.net/go/mautrix" - "maunium.net/go/mautrix/appservice" "maunium.net/go/mautrix/bridge" - "maunium.net/go/mautrix/bridge/bridgeconfig" "maunium.net/go/mautrix/bridge/commands" "maunium.net/go/mautrix/event" "maunium.net/go/mautrix/format" @@ -45,15 +39,6 @@ type WechatBridge struct { puppetsLock sync.Mutex checkers map[id.UserID]chan struct{} checkersLock sync.Mutex - - WebsocketHandler *WebsocketCommandHandler - - stopping bool - stopPinger chan struct{} - - shortCircuitReconnectBackoff chan struct{} - websocketStarted chan struct{} - websocketStopped chan struct{} } func NewWechatBridge(exampleConfig string) *WechatBridge { @@ -67,10 +52,6 @@ func NewWechatBridge(exampleConfig string) *WechatBridge { puppets: make(map[types.UID]*Puppet), puppetsByCustomMXID: make(map[id.UserID]*Puppet), checkers: make(map[id.UserID]chan struct{}), - - shortCircuitReconnectBackoff: make(chan struct{}), - websocketStarted: make(chan struct{}), - websocketStopped: make(chan struct{}), } } @@ -93,20 +74,18 @@ func (br *WechatBridge) Init() { br.EventProcessor.On(event.EphemeralEventPresence, br.HandlePresence) - br.DB = database.New(br.Bridge.DB, br.Log.Sub("Database")) + br.DB = database.New(br.Bridge.DB, br.ZLog.With().Str("component", "Database").Logger()) br.Formatter = NewFormatter(br) br.WechatService = wechat.NewWechatService( br.Config.Bridge.ListenAddress, br.Config.Bridge.ListenSecret, - br.Log, + *br.ZLog, ) - br.WebsocketHandler = NewWebsocketCommandHandler(br) - if br.Config.Bridge.HomeserverProxy != "" { if proxyUrl, err := url.Parse(br.Config.Bridge.HomeserverProxy); err != nil { - br.Log.Warnfln("Failed to parse bridge.hs_proxy: %v", err) + br.ZLog.Warn().Msgf("Failed to parse bridge.hs_proxy: %v", err) } else { br.AS.HTTPClient.Transport = &http.Transport{Proxy: http.ProxyURL(proxyUrl)} } @@ -114,94 +93,11 @@ func (br *WechatBridge) Init() { } func (br *WechatBridge) Start() { - if br.Config.Homeserver.WSProxy != "" { - var startupGroup sync.WaitGroup - startupGroup.Add(1) - - br.Log.Debugln("Starting application service websocket") - go br.startWebsocket(&startupGroup) - - startupGroup.Wait() - - br.stopPinger = make(chan struct{}) - if br.Config.Homeserver.WSPingInterval > 0 { - go br.serverPinger() - } - } else { - if br.Config.AppService.Port == 0 { - br.Log.Fatalln("Both the websocket proxy and appservice listener are disabled, can't receive events") - os.Exit(23) - } - br.Log.Debugln("Websocket proxy not configured, not starting application service websocket") - } - + br.WaitWebsocketConnected() go br.WechatService.Start() go br.StartUsers() } -type PingData struct { - Timestamp int64 `json:"timestamp"` -} - -func (br *WechatBridge) PingServer() (start, serverTs, end time.Time) { - if !br.AS.HasWebsocket() { - br.Log.Debugln("Received server ping request, but no websocket connected. Trying to short-circuit backoff sleep") - select { - case br.shortCircuitReconnectBackoff <- struct{}{}: - default: - br.Log.Warnfln("Failed to ping websocket: not connected and no backoff?") - return - } - select { - case <-br.websocketStarted: - case <-time.After(15 * time.Second): - if !br.AS.HasWebsocket() { - br.Log.Warnfln("Failed to ping websocket: didn't connect after 15 seconds of waiting") - return - } - } - } - start = time.Now() - var resp PingData - br.Log.Debugln("Pinging appservice websocket") - ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) - defer cancel() - err := br.AS.RequestWebsocket(ctx, &appservice.WebsocketRequest{ - Command: "ping", - Data: &PingData{Timestamp: start.UnixMilli()}, - }, &resp) - end = time.Now() - if err != nil { - br.Log.Warnfln("Websocket ping returned error in %s: %v", end.Sub(start), err) - br.AS.StopWebsocket(fmt.Errorf("websocket ping returned error in %s: %w", end.Sub(start), err)) - } else { - serverTs = time.Unix(0, resp.Timestamp*int64(time.Millisecond)) - br.Log.Debugfln("Websocket ping returned success in %s (request: %s, response: %s)", end.Sub(start), serverTs.Sub(start), end.Sub(serverTs)) - } - return -} - -func (br *WechatBridge) serverPinger() { - interval := time.Duration(br.Config.Homeserver.WSPingInterval) * time.Second - clock := time.NewTicker(interval) - defer func() { - br.Log.Infofln("Websocket pinger stopped") - clock.Stop() - }() - br.Log.Infofln("Pinging websocket every %s", interval) - for { - select { - case <-clock.C: - br.PingServer() - case <-br.stopPinger: - return - } - if br.stopping { - return - } - } -} - func (br *WechatBridge) Stop() { br.checkersLock.Lock() for _, checker := range br.checkers { @@ -217,44 +113,22 @@ func (br *WechatBridge) Stop() { if user.Client == nil { continue } - br.Log.Debugln("Disconnecting", user.MXID) + br.ZLog.Debug().Msgf("Disconnecting %s", user.MXID) user.DeleteConnection() } br.usersLock.Unlock() br.WechatService.Stop() - - br.stopping = true - - if br.Config.Homeserver.WSProxy != "" { - select { - case br.stopPinger <- struct{}{}: - default: - } - br.Log.Debugln("Stopping transaction websocket") - br.AS.StopWebsocket(appservice.ErrWebsocketManualStop) - br.Log.Debugln("Stopping event processor") - // Short-circuit reconnect backoff so the websocket loop exits even if it's disconnected - select { - case br.shortCircuitReconnectBackoff <- struct{}{}: - default: - } - select { - case <-br.websocketStopped: - case <-time.After(4 * time.Second): - br.Log.Warnln("Timed out waiting for websocket to close") - } - } } func (br *WechatBridge) StartUsers() { - br.Log.Debugln("Starting custom puppets") + br.ZLog.Debug().Msgf("Starting custom puppets") for _, loopuppet := range br.GetAllPuppetsWithCustomMXID() { go func(puppet *Puppet) { - puppet.log.Debugln("Starting custom puppet", puppet.CustomMXID) + puppet.log.Debug().Msgf("Starting custom puppet %s", puppet.CustomMXID) err := puppet.StartCustomMXID(true) if err != nil { - puppet.log.Errorln("Failed to start custom puppet:", err) + puppet.log.Error().Msgf("Failed to start custom puppet: %v", err) } }(loopuppet) } @@ -273,7 +147,7 @@ func (br *WechatBridge) CreatePrivatePortal(roomID id.RoomID, brInviter bridge.U ok := portal.ensureUserInvited(inviter) if !ok { - br.Log.Warnfln("Failed to invite %s to existing private chat portal %s with %s. Redirecting portal to new room...", inviter.MXID, portal.MXID, puppet.UID) + br.ZLog.Warn().Msgf("Failed to invite %s to existing private chat portal %s with %s. Redirecting portal to new room...", inviter.MXID, portal.MXID, puppet.UID) br.createPrivatePortalFromInvite(roomID, inviter, puppet, portal) return } @@ -281,44 +155,56 @@ func (br *WechatBridge) CreatePrivatePortal(roomID id.RoomID, brInviter bridge.U errorMessage := fmt.Sprintf("You already have a private chat portal with me at [%[1]s](https://matrix.to/#/%[1]s)", portal.MXID) errorContent := format.RenderMarkdown(errorMessage, true, false) _, _ = intent.SendMessageEvent(roomID, event.EventMessage, errorContent) - br.Log.Debugfln("Leaving private chat room %s as %s after accepting invite from %s as we already have chat with the user", roomID, puppet.MXID, inviter.MXID) + br.ZLog.Debug().Msgf("Leaving private chat room %s as %s after accepting invite from %s as we already have chat with the user", roomID, puppet.MXID, inviter.MXID) _, _ = intent.LeaveRoom(roomID) } func (br *WechatBridge) createPrivatePortalFromInvite(roomID id.RoomID, inviter *User, puppet *Puppet, portal *Portal) { - portal.MXID = roomID - portal.Topic = PrivateChatTopic - _, _ = portal.MainIntent().SetRoomTopic(portal.MXID, portal.Topic) - if portal.bridge.Config.Bridge.PrivateChatPortalMeta { - portal.Name = puppet.Displayname - portal.AvatarURL = puppet.AvatarURL - portal.Avatar = puppet.Avatar - _, _ = portal.MainIntent().SetRoomName(portal.MXID, portal.Name) - _, _ = portal.MainIntent().SetRoomAvatar(portal.MXID, portal.AvatarURL) + // TODO check if room is already encrypted + var existingEncryption event.EncryptionEventContent + var encryptionEnabled bool + err := portal.MainIntent().StateEvent(roomID, event.StateEncryption, "", &existingEncryption) + if err != nil { + portal.log.Warn().Msgf("Failed to check if encryption is enabled in private chat room %s", roomID) } else { - portal.Name = "" + encryptionEnabled = existingEncryption.Algorithm == id.AlgorithmMegolmV1 } - portal.log.Infofln("Created private chat portal in %s after invite from %s", roomID, inviter.MXID) + + portal.MXID = roomID + portal.Topic = PrivateChatTopic + portal.Name = puppet.Displayname + portal.AvatarURL = puppet.AvatarURL + portal.Avatar = puppet.Avatar + portal.log.Info().Msgf("Created private chat portal in %s after invite from %s", roomID, inviter.MXID) intent := puppet.DefaultIntent() - if br.Config.Bridge.Encryption.Default { + if br.Config.Bridge.Encryption.Default || encryptionEnabled { _, err := intent.InviteUser(roomID, &mautrix.ReqInviteUser{UserID: br.Bot.UserID}) if err != nil { - portal.log.Warnln("Failed to invite bridge bot to enable e2be:", err) + portal.log.Warn().Msgf("Failed to invite bridge bot to enable e2be: %v", err) } err = br.Bot.EnsureJoined(roomID) if err != nil { - portal.log.Warnln("Failed to join as bridge bot to enable e2be:", err) + portal.log.Warn().Msgf("Failed to join as bridge bot to enable e2be: %v", err) } - _, err = intent.SendStateEvent(roomID, event.StateEncryption, "", portal.GetEncryptionEventContent()) - if err != nil { - portal.log.Warnln("Failed to enable e2be:", err) + if !encryptionEnabled { + _, err = intent.SendStateEvent(roomID, event.StateEncryption, "", portal.GetEncryptionEventContent()) + if err != nil { + portal.log.Warn().Msgf("Failed to enable e2be: %v", err) + } } br.AS.StateStore.SetMembership(roomID, inviter.MXID, event.MembershipJoin) br.AS.StateStore.SetMembership(roomID, puppet.MXID, event.MembershipJoin) br.AS.StateStore.SetMembership(roomID, br.Bot.UserID, event.MembershipJoin) portal.Encrypted = true } + _, _ = portal.MainIntent().SetRoomTopic(portal.MXID, portal.Topic) + if portal.shouldSetDMRoomMetadata() { + _, err = portal.MainIntent().SetRoomName(portal.MXID, portal.Name) + portal.NameSet = err == nil + _, err = portal.MainIntent().SetRoomAvatar(portal.MXID, portal.AvatarURL) + portal.AvatarSet = err == nil + } portal.Update(nil) portal.UpdateBridgeInfo() _, _ = intent.SendNotice(roomID, "Private chat portal created") @@ -327,116 +213,3 @@ func (br *WechatBridge) createPrivatePortalFromInvite(roomID id.RoomID, inviter func (br *WechatBridge) HandlePresence(evt *event.Event) { // TODO: } - -const defaultReconnectBackoff = 2 * time.Second -const maxReconnectBackoff = 2 * time.Minute -const reconnectBackoffReset = 5 * time.Minute - -type StartSyncRequest struct { - AccessToken string `json:"access_token"` - DeviceID id.DeviceID `json:"device_id"` - UserID id.UserID `json:"user_id"` -} - -func (br *WechatBridge) SendBridgeStatus() { - state := BridgeStatus{} - - state.StateEvent = BridgeStatusConnected - state.Timestamp = time.Now().Unix() - state.TTL = 600 - state.Source = "bridge" - //state.RemoteID = "unknown" - - if err := br.AS.SendWebsocket(&appservice.WebsocketRequest{ - Command: "bridge_status", - Data: &state, - }); err != nil { - br.Log.Warnln("Error sending bridge status:", err) - } -} - -func (br *WechatBridge) RequestStartSync() { - if !br.Config.Bridge.Encryption.Appservice || - br.Config.Homeserver.Software == bridgeconfig.SoftwareHungry || - br.Crypto == nil || - !br.AS.HasWebsocket() { - return - } - resp := map[string]interface{}{} - br.Log.Debugln("Sending /sync start request through websocket") - cryptoClient := br.Crypto.Client() - ctx, cancel := context.WithTimeout(context.Background(), 3*time.Minute) - defer cancel() - err := br.AS.RequestWebsocket(ctx, &appservice.WebsocketRequest{ - Command: "start_sync", - Deadline: 30 * time.Second, - Data: &StartSyncRequest{ - AccessToken: cryptoClient.AccessToken, - DeviceID: cryptoClient.DeviceID, - UserID: cryptoClient.UserID, - }, - }, &resp) - if err != nil { - go br.WebsocketHandler.HandleSyncProxyError(nil, err) - } else { - br.Log.Debugln("Started receiving encryption data with sync proxy:", resp) - } -} - -func (br *WechatBridge) startWebsocket(wg *sync.WaitGroup) { - var wgOnce sync.Once - onConnect := func() { - go br.SendBridgeStatus() - - br.RequestStartSync() - - wgOnce.Do(wg.Done) - - select { - case br.websocketStarted <- struct{}{}: - default: - } - } - - reconnectBackoff := defaultReconnectBackoff - lastDisconnect := time.Now().UnixNano() - defer func() { - br.Log.Debugfln("Appservice websocket loop finished") - close(br.websocketStopped) - }() - - for { - err := br.AS.StartWebsocket(br.Config.Homeserver.WSProxy, onConnect) - if err == appservice.ErrWebsocketManualStop { - return - } else if closeCommand := (&appservice.CloseCommand{}); errors.As(err, &closeCommand) && closeCommand.Status == appservice.MeowConnectionReplaced { - br.Log.Infoln("Appservice websocket closed by another instance of the bridge, shutting down...") - br.Stop() - return - } else if err != nil { - br.Log.Errorln("Error in appservice websocket:", err) - } - if br.stopping { - return - } - now := time.Now().UnixNano() - if lastDisconnect+reconnectBackoffReset.Nanoseconds() < now { - reconnectBackoff = defaultReconnectBackoff - } else { - reconnectBackoff *= 2 - if reconnectBackoff > maxReconnectBackoff { - reconnectBackoff = maxReconnectBackoff - } - } - lastDisconnect = now - br.Log.Infofln("Websocket disconnected, reconnecting in %d seconds...", int(reconnectBackoff.Seconds())) - select { - case <-br.shortCircuitReconnectBackoff: - br.Log.Debugln("Reconnect backoff was short-circuited") - case <-time.After(reconnectBackoff): - } - if br.stopping { - return - } - } -} diff --git a/main.go b/main.go index 3ab111d..af77c16 100644 --- a/main.go +++ b/main.go @@ -6,8 +6,8 @@ import ( "github.com/duo/matrix-wechat/internal" "github.com/duo/matrix-wechat/internal/config" + "go.mau.fi/util/configupgrade" "maunium.net/go/mautrix/bridge" - "maunium.net/go/mautrix/util/configupgrade" ) // Information to find out exactly which commit the bridge was built from. @@ -27,7 +27,7 @@ func main() { Name: "matrix-wechat", URL: "https://github.com/duo/matrix-wechat", Description: "A Matrix-WeChat puppeting bridge.", - Version: "0.2.1", + Version: "0.2.3", ProtocolName: "Wechat", CryptoPickleKey: "github.com/duo/matrix-wechat",