diff --git a/cmd/mautrix-telegram/main.go b/cmd/mautrix-telegram/main.go index 64fb4ddf..c40d360e 100644 --- a/cmd/mautrix-telegram/main.go +++ b/cmd/mautrix-telegram/main.go @@ -17,31 +17,29 @@ package main import ( - "os" - - "go.mau.fi/util/dbutil" _ "go.mau.fi/util/dbutil/litestream" - "go.mau.fi/util/exerrors" - "go.mau.fi/util/exzerolog" - "gopkg.in/yaml.v3" - "maunium.net/go/mautrix/bridgev2" - "maunium.net/go/mautrix/bridgev2/bridgeconfig" - "maunium.net/go/mautrix/bridgev2/matrix" + "maunium.net/go/mautrix/bridgev2/matrix/mxmain" "go.mau.fi/mautrix-telegram/pkg/connector" ) -func main() { - var cfg bridgeconfig.Config - config := exerrors.Must(os.ReadFile("config.yaml")) - exerrors.PanicIfNotNil(yaml.Unmarshal(config, &cfg)) - log := exerrors.Must(cfg.Logging.Compile()) - exzerolog.SetupDefaults(log) +// Information to find out exactly which commit the bridge was built from. +// These are filled at build time with the -X linker flag. +var ( + Tag = "unknown" + Commit = "unknown" + BuildTime = "unknown" +) - db := exerrors.Must(dbutil.NewFromConfig("mautrix-telegram", cfg.Database, dbutil.ZeroLogger(log.With().Str("db_section", "main").Logger()))) - telegramConnector := connector.NewConnector() - exerrors.PanicIfNotNil(cfg.Network.Decode(telegramConnector.Config)) - bridge := bridgev2.NewBridge("", db, *log, matrix.NewConnector(&cfg), telegramConnector) - bridge.CommandPrefix = "!telegram" - bridge.Start() +func main() { + m := mxmain.BridgeMain{ + Name: "mautrix-telegram", + URL: "https://github.com/mautrix/telegram", + Description: "A Matrix-Telegram puppeting bridge.", + Version: "0.16.0", + + Connector: connector.NewConnector(), + } + m.InitVersion(Tag, Commit, BuildTime) + m.Run() } diff --git a/example-config.yaml b/example-config.yaml deleted file mode 100644 index b1a02ce0..00000000 --- a/example-config.yaml +++ /dev/null @@ -1,325 +0,0 @@ -# Homeserver details. -homeserver: - # The address that this appservice can use to connect to the homeserver. - address: https://matrix.example.com - # The domain of the homeserver (also known as server_name, used for MXIDs, etc). - domain: example.com - - # What software is the homeserver running? - # Standard Matrix homeservers like Synapse, Dendrite and Conduit should just use "standard" here. - software: standard - # The URL to push real-time bridge status to. - # If set, the bridge will make POST requests to this URL whenever a user's Telegram connection state changes. - # The bridge will use the appservice as_token to authorize requests. - status_endpoint: null - # Endpoint for reporting per-message status. - message_send_checkpoint_endpoint: null - # 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: - # The address that the homeserver can use to connect to this appservice. - address: http://localhost:29317 - - # The hostname and port where this appservice should listen. - hostname: 0.0.0.0 - port: 29317 - - # The unique ID of this appservice - id: telegram - - # Appservice bot details. - bot: - # Username of the appservice bot. - username: telegrambot - # Display name and avatar for bot. - # Set to "remove" to remove display name/avatar, leave empty to leave display name/avatar as-is. - displayname: Telegram bridge bot - avatar: mxc://maunium.net/tJCRmUyJDsgRNgqhOgoiHWbX - - # 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" - - # Whether or not to receive ephemeral events via appservice transactions. - # Requires MSC2409 support (i.e. Synapse 1.22+). - 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 - - # Localpart template of MXIDs for Telegram users - # {{.}} is replaced with the internal ID of the Telegram user - username_template: telegram_{{.}} - -# Database config. -database: - # The database type. "sqlite3-fk-wal" and "postgres" are supported. - type: sqlite3-fk-wal - # The database URI. - # SQLite: A raw file path is supported, but `file:?_txlock=immediate` is recommended. - # https://github.com/mattn/go-sqlite3#connection-string - # Postgres: Connection string. For example, postgres://user:password@host/database?sslmode=disable - # To connect via Unix socket, use something like postgres:///dbname?host=/var/run/postgresql - uri: file:telegramgo.db?_txlock=immediate - # Maximum number of connections. Mostly relevant for Postgres. - max_open_conns: 20 - max_idle_conns: 2 - # Maximum connection idle time and lifetime before they're closed. Disabled if null. - # Parsed with https://pkg.go.dev/time#ParseDuration - max_conn_idle_time: null - max_conn_lifetime: null - -# Bridge config -bridge: - message_status_events: true - delivery_receipts: true - message_error_notices: true - sync_direct_chat_list: false - federate_rooms: false - -# Provisioning API configuration -provisioning: - # Prefix for the provisioning API paths. - prefix: /_matrix/provision - # Shared secret for authentication. If set to "generate", a random secret will be generated, - # or if set to "disable", the provisioning API will be disabled. - shared_secret: generate - # Enable debug API at /debug with provisioning authentication. - debug_endpoints: false - - # Provisioning API part of the web server for automated portal creation and fetching information. - # Used by things like mautrix-manager (https://github.com/tulir/mautrix-manager). - provisioning: - # Whether or not the provisioning API should be enabled. - enabled: true - # The prefix to use in the provisioning API endpoints. - prefix: /_matrix/provision - # The shared secret to authorize users of the API. - # Set to "generate" to generate and save a new token. - shared_secret: generate - -# Double-puppet configuration -double_puppet: - # Servers to always allow double puppeting from - servers: - example.com: https://example.com - # Allow using double puppeting from any server with a valid client .well-known file. - allow_discovery: false - # Per-server shared secrets for https://github.com/devture/matrix-synapse-shared-secret-auth - # - # If set, double puppeting will be enabled automatically for local users instead of users - # having to find an access token and run `login-matrix` manually. - secrets: - example.com: foobar - -# End-to-bridge encryption support options. -# -# See https://docs.mau.fi/bridges/general/end-to-bridge-encryption.html for more info. -encryption: - # Allow encryption, work in group chat rooms with e2ee enabled - 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. - default: false - # Whether to use MSC2409/MSC3202 instead of /sync long polling for receiving encryption-related data. - appservice: false - # Require encryption, drop any unencrypted messages. - require: false - - # Enable plaintext mentions? - plaintext_mentions: true - - # Pickle key - pickle_key: null - - # 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 - # 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: - # unverified - Send keys to all device in the room. - # cross-signed-untrusted - Require valid cross-signing, but trust all cross-signing keys. - # cross-signed-tofu - Require valid cross-signing, trust cross-signing keys on first use (and reject changes). - # cross-signed-verified - Require valid cross-signing, plus a valid user signature from the bridge bot. - # Note that creating user signatures from the bridge bot is not currently possible. - # verified - Require manual per-device verification - # (currently only possible by modifying the `trust` column in the `crypto_device` database table). - verification_levels: - # Minimum level for which the bridge should send keys to when bridging messages from Telegram to Matrix. - receive: unverified - # Minimum level that the bridge should accept for incoming Matrix messages. - send: unverified - # Minimum level that the bridge should require for accepting key requests. - share: cross-signed-tofu - # Options for Megolm room key rotation. These options allow you to - # configure the m.room.encryption event content. See: - # https://spec.matrix.org/v1.3/client-server-api/#mroomencryption for - # more information about that event. - rotation: - # Enable custom Megolm room key rotation settings. Note that these - # settings will only apply to rooms created after this option is - # set. - enable_custom: false - # The maximum number of milliseconds a session should be used - # before changing it. The Matrix spec recommends 604800000 (a week) - # as the default. - milliseconds: 604800000 - # The maximum number of messages that should be sent with a given a - # session before changing it. The Matrix spec recommends 100 as the - # 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: -# relaybot - Only use the bridge via the relaybot, no access to commands. -# user - Relaybot level + access to commands to create bridges. -# puppeting - User level + logging in with a Telegram account. -# full - Full access to use the bridge, i.e. previous levels + Matrix login. -# admin - Full access to use the bridge and some extra administration commands. -# Permitted keys: -# * - All Matrix users -# domain - All users on that homeserver -# mxid - Specific user -permissions: - "*": "relaybot" - "public.example.com": "user" - "example.com": "full" - "@admin:example.com": "admin" - -# Messages sent upon joining a management room. -# Markdown is supported. The defaults are listed below. -management_room_texts: - # Sent when joining a room. - welcome: "Hello, I'm a Telegram bridge bot." - # Sent when joining a management room and the user is already logged in. - welcome_connected: "Use `help` for help." - # Sent when joining a management room and the user is not logged in. - welcome_unconnected: "Use `help` for help or `login` to log in." - # Optional extra text sent when joining a management room. - additional_help: "" - -# Logging config. See https://github.com/tulir/zeroconfig for details. -logging: - min_level: debug - writers: - - type: stdout - format: pretty-colored - - type: file - format: json - filename: ./logs/mautrix-signal.log - max_size: 100 - max_backups: 10 - compress: true - -# Telegram config -network: - # Get your own API keys at https://my.telegram.org/apps - api_id: 12345 - api_hash: tjyd5yge35lbodk1xwzw2jstp90k55qz - # (Optional) Create your own bot at https://t.me/BotFather - bot_token: disabled - - # Should the bridge request missed updates from Telegram when restarting? - catch_up: true - # Should incoming updates be handled sequentially to make sure order is preserved on Matrix? - sequential_updates: true - exit_on_update_error: false - - # Telethon connection options. - connection: - # The timeout in seconds to be used when connecting. - timeout: 120 - # How many times the reconnection should retry, either on the initial connection or when - # Telegram disconnects us. May be set to a negative or null value for infinite retries, but - # this is not recommended, since the program can get stuck in an infinite loop. - retries: 5 - # The delay in seconds to sleep between automatic reconnections. - retry_delay: 1 - # The threshold below which the library should automatically sleep on flood wait errors - # (inclusive). For instance, if a FloodWaitError for 17s occurs and flood_sleep_threshold - # is 20s, the library will sleep automatically. If the error was for 21s, it would raise - # the error instead. Values larger than a day (86400) will be changed to a day. - flood_sleep_threshold: 60 - # How many times a request should be retried. Request are retried when Telegram is having - # internal issues, when there is a FloodWaitError less than flood_sleep_threshold, or when - # there's a migrate error. May take a negative or null value for infinite retries, but this - # is not recommended, since some requests can always trigger a call fail (such as searching - # for messages). - request_retries: 5 - # Use IPv6 for Telethon connection - use_ipv6: false - - # Device info sent to Telegram. - device_info: - # "auto" = OS name+version. - device_model: mautrix-telegram - # "auto" = Telethon version. - system_version: auto - # "auto" = mautrix-telegram version. - app_version: auto - lang_code: en - system_lang_code: en - - # Custom server to connect to. - server: - # Set to true to use these server settings. If false, will automatically - # use production server assigned by Telegram. Set to false in production. - enabled: false - # The DC ID to connect to. - dc: 2 - # The IP to connect to. - ip: 149.154.167.40 - # The port to connect to. 443 may not work, 80 is better and both are equally secure. - port: 80 - - # Telethon proxy configuration. - # You must install PySocks from pip for proxies to work. - proxy: - # Allowed types: disabled, socks4, socks5, http, mtproxy - type: disabled - # Proxy IP address and port. - address: 127.0.0.1 - port: 1080 - # Whether or not to perform DNS resolving remotely. Only for socks/http proxies. - rdns: true - # Proxy authentication (optional). Put MTProxy secret in password field. - username: "" - password: "" diff --git a/go.mod b/go.mod index 9d0be19c..731ec20a 100644 --- a/go.mod +++ b/go.mod @@ -4,12 +4,11 @@ go 1.21 require ( github.com/gotd/td v0.102.0 - github.com/rs/zerolog v1.32.0 - go.mau.fi/util v0.4.3-0.20240516141139-2ebe792cd8f7 + github.com/rs/zerolog v1.33.0 + go.mau.fi/util v0.5.0 go.mau.fi/zerozap v0.1.1 go.uber.org/zap v1.27.0 - gopkg.in/yaml.v3 v3.0.1 - maunium.net/go/mautrix v0.18.2-0.20240605171421-d7ffa7183824 + maunium.net/go/mautrix v0.19.0-beta.1.0.20240619154325-69e2b42d857a ) require ( @@ -34,18 +33,19 @@ require ( github.com/tidwall/match v1.1.1 // indirect github.com/tidwall/pretty v1.2.0 // indirect github.com/tidwall/sjson v1.2.5 // indirect - github.com/yuin/goldmark v1.7.1 // indirect + github.com/yuin/goldmark v1.7.2 // indirect go.mau.fi/zeroconfig v0.1.2 // indirect go.opentelemetry.io/otel v1.26.0 // indirect go.opentelemetry.io/otel/trace v1.26.0 // indirect go.uber.org/atomic v1.11.0 // indirect go.uber.org/multierr v1.11.0 // indirect - golang.org/x/crypto v0.23.0 // indirect - golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842 // indirect - golang.org/x/net v0.25.0 // indirect + golang.org/x/crypto v0.24.0 // indirect + golang.org/x/exp v0.0.0-20240613232115-7f521ea00fb8 // indirect + golang.org/x/net v0.26.0 // indirect golang.org/x/sync v0.7.0 // indirect - golang.org/x/sys v0.20.0 // indirect + golang.org/x/sys v0.21.0 // 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 nhooyr.io/websocket v1.8.11 // indirect rsc.io/qr v0.2.0 // indirect diff --git a/go.sum b/go.sum index 956dcc73..05290b7e 100644 --- a/go.sum +++ b/go.sum @@ -48,8 +48,8 @@ github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4= github.com/rs/xid v1.5.0 h1:mKX4bl4iPYJtEIxp6CYiUuLQ/8DYMoz0PUdtGgMFRVc= github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg= -github.com/rs/zerolog v1.32.0 h1:keLypqrlIjaFsbmJOBdB/qvyF8KEtCWHwobLp5l/mQ0= -github.com/rs/zerolog v1.32.0/go.mod h1:/7mN4D5sKwJLZQ2b/znpjC3/GQWY/xaDXUM0kKWRHss= +github.com/rs/zerolog v1.33.0 h1:1cU2KZkvPxNyfgEmhHAz/1A9Bz+llsdYzklWFzgp0r8= +github.com/rs/zerolog v1.33.0/go.mod h1:/7mN4D5sKwJLZQ2b/znpjC3/GQWY/xaDXUM0kKWRHss= github.com/segmentio/asm v1.2.0 h1:9BQrFxC+YOHJlTlHGkTrFWf59nbL3XnCoFLTwDCI7ys= github.com/segmentio/asm v1.2.0/go.mod h1:BqMnlJP91P8d+4ibuonYZw9mfnzI9HfxselHZr5aAcs= github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e h1:MRM5ITcdelLK2j1vwZ3Je0FKVCfqOLp5zO6trqMLYs0= @@ -65,10 +65,10 @@ github.com/tidwall/pretty v1.2.0 h1:RWIZEg2iJ8/g6fDDYzMpobmaoGh5OLl4AXtGUGPcqCs= github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= 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/yuin/goldmark v1.7.1 h1:3bajkSilaCbjdKVsKdZjZCLBNPL9pYzrCakKaf4U49U= -github.com/yuin/goldmark v1.7.1/go.mod h1:uzxRWxtg69N339t3louHJ7+O03ezfj6PlliRlaOzY1E= -go.mau.fi/util v0.4.3-0.20240516141139-2ebe792cd8f7 h1:2hnc2iS7usHT3aqIQ8HVtKtPgic+13EVSdZ1m8UBL/E= -go.mau.fi/util v0.4.3-0.20240516141139-2ebe792cd8f7/go.mod h1:m+PJpPMadAW6cj3ldyuO5bLhFreWdwcu+3QTwYNGlGk= +github.com/yuin/goldmark v1.7.2 h1:NjGd7lO7zrUn/A7eKwn5PEOt4ONYGqpxSEeZuduvgxc= +github.com/yuin/goldmark v1.7.2/go.mod h1:uzxRWxtg69N339t3louHJ7+O03ezfj6PlliRlaOzY1E= +go.mau.fi/util v0.5.0 h1:8yELAl+1CDRrwGe9NUmREgVclSs26Z68pTWePHVxuDo= +go.mau.fi/util v0.5.0/go.mod h1:DsJzUrJAG53lCZnnYvq9/mOyLuPScWwYhvETiTrpdP4= go.mau.fi/zeroconfig v0.1.2 h1:DKOydWnhPMn65GbXZOafgkPm11BvFashZWLct0dGFto= go.mau.fi/zeroconfig v0.1.2/go.mod h1:NcSJkf180JT+1IId76PcMuLTNa1CzsFFZ0nBygIQM70= go.mau.fi/zerozap v0.1.1 h1:mxE/dW4wtkqBYOXOEEzXldk5qKB+ahsZXjoTGnvEhZQ= @@ -85,20 +85,20 @@ go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8= go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= -golang.org/x/crypto v0.23.0 h1:dIJU/v2J8Mdglj/8rJ6UUOM3Zc9zLZxVZwwxMooUSAI= -golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8= -golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842 h1:vr/HnozRka3pE4EsMEg1lgkXJkTFJCVUX+S/ZT6wYzM= -golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842/go.mod h1:XtvwrStGgqGPLc4cjQfWqZHG1YFdYs6swckp8vpsjnc= -golang.org/x/net v0.25.0 h1:d/OCCoBEUq33pjydKrGQhw7IlUPI2Oylr+8qLx49kac= -golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM= +golang.org/x/crypto v0.24.0 h1:mnl8DM0o513X8fdIkmyFE/5hTYxbwYOjDS/+rK6qpRI= +golang.org/x/crypto v0.24.0/go.mod h1:Z1PMYSOR5nyMcyAVAIQSKCDwalqy85Aqn1x3Ws4L5DM= +golang.org/x/exp v0.0.0-20240613232115-7f521ea00fb8 h1:yixxcjnhBmY0nkL253HFVIm0JsFHwrHdT3Yh6szTnfY= +golang.org/x/exp v0.0.0-20240613232115-7f521ea00fb8/go.mod h1:jj3sYF3dwk5D+ghuXyeI3r5MFf+NT2An6/9dOA95KSI= +golang.org/x/net v0.26.0 h1:soB7SVo0PWrY4vPW/+ay0jKDNScG2X9wFeYlXIvJsOQ= +golang.org/x/net v0.26.0/go.mod h1:5YKkiSynbBIh3p6iOc/vibscux0x38BZDkn8sCUPxHE= golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M= golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 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.20.0 h1:Od9JTbYCk261bKm4M/mw7AklTlFYIa0bIp9BgSm1S8Y= -golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.21.0 h1:rF+pYz3DAGSQAxAu1CbC7catZg4ebC4UIeIhKxBZvws= +golang.org/x/sys v0.21.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= @@ -108,8 +108,8 @@ gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 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/mautrix v0.18.2-0.20240605171421-d7ffa7183824 h1:Jgzj/3gklZ3NROeTPc9CKh8SsvpIRNbur985eoxvZHk= -maunium.net/go/mautrix v0.18.2-0.20240605171421-d7ffa7183824/go.mod h1:P/FV8cXY262MezYX7ViuhfzeJ0nK4+M8K6ZmxEC/aEA= +maunium.net/go/mautrix v0.19.0-beta.1.0.20240619154325-69e2b42d857a h1:g2X/TEW9MR9lfn4RUHUGcpta9FmFes62/4OEEVEKFJg= +maunium.net/go/mautrix v0.19.0-beta.1.0.20240619154325-69e2b42d857a/go.mod h1:cxv1w6+syudmEpOewHYIQT9yO7TM5UOWmf6xEBVI4H4= nhooyr.io/websocket v1.8.11 h1:f/qXNc2/3DpoSZkHt1DQu6rj4zGC8JmkkLkWss0MgN0= nhooyr.io/websocket v1.8.11/go.mod h1:rN9OFWIUwuxg4fR5tELlYC04bXYowCP9GX47ivo2l+c= rsc.io/qr v0.2.0 h1:6vBLea5/NRMVTz8V66gipeLycZMl/+UlFmk8DvqQ6WY= diff --git a/pkg/connector/client.go b/pkg/connector/client.go index 44f16d2b..cfef7ed5 100644 --- a/pkg/connector/client.go +++ b/pkg/connector/client.go @@ -9,18 +9,13 @@ import ( "time" "github.com/gotd/td/telegram" - "github.com/gotd/td/telegram/message" - "github.com/gotd/td/telegram/message/html" "github.com/gotd/td/telegram/updates" - "github.com/gotd/td/telegram/uploader" "github.com/gotd/td/tg" "github.com/rs/zerolog" "go.mau.fi/zerozap" "go.uber.org/zap" "maunium.net/go/mautrix/bridgev2" - "maunium.net/go/mautrix/bridgev2/database" "maunium.net/go/mautrix/bridgev2/networkid" - "maunium.net/go/mautrix/event" "go.mau.fi/mautrix-telegram/pkg/connector/msgconv" ) @@ -34,6 +29,8 @@ type TelegramClient struct { msgConv *msgconv.MessageConverter } +var _ bridgev2.NetworkAPI = (*TelegramClient)(nil) + func NewTelegramClient(ctx context.Context, tc *TelegramConnector, login *bridgev2.UserLogin) (*TelegramClient, error) { loginID, err := strconv.ParseInt(string(login.ID), 10, 64) if err != nil { @@ -86,8 +83,6 @@ func NewTelegramClient(ctx context.Context, tc *TelegramConnector, login *bridge return &client, err } -var _ bridgev2.NetworkAPI = (*TelegramClient)(nil) - // connectTelegramClient blocks until client is connected, calling Run // internally. // Technique from: https://github.com/gotd/contrib/blob/master/bg/connect.go @@ -162,10 +157,11 @@ func (t *TelegramClient) onUpdateNewMessage(ctx context.Context, e tg.Entities, }, ID: makeMessageID(msg.ID), Sender: sender, - PortalID: makePortalID(msg.PeerID), + PortalKey: makePortalID(msg.PeerID), Data: msg, CreatePortal: true, ConvertMessageFunc: t.msgConv.ToMatrix, + Timestamp: time.Unix(int64(msg.Date), 0), }) return nil } @@ -180,6 +176,10 @@ func (t *TelegramClient) Connect(ctx context.Context) (err error) { return } +func (t *TelegramClient) Disconnect() { + t.clientCancel() +} + func getFullName(user *tg.User) string { return strings.TrimSpace(fmt.Sprintf("%s %s", user.FirstName, user.LastName)) } @@ -274,116 +274,18 @@ func (t *TelegramClient) GetUserInfo(ctx context.Context, ghost *bridgev2.Ghost) } } -func (t *TelegramClient) HandleMatrixEdit(ctx context.Context, msg *bridgev2.MatrixEdit) error { - panic("unimplemented edit") -} - -func (t *TelegramClient) HandleMatrixMessage(ctx context.Context, msg *bridgev2.MatrixMessage) (dbMessage *database.Message, err error) { - sender := message.NewSender(t.client.API()) - peer, err := inputPeerForPortalID(msg.Portal.ID) - if err != nil { - return nil, err - } - builder := sender.To(peer) - - // TODO handle sticker - - var updates tg.UpdatesClass - switch msg.Content.MsgType { - case event.MsgText: - updates, err = builder.Text(ctx, msg.Content.Body) - if err != nil { - return nil, err - } - case event.MsgImage, event.MsgFile, event.MsgAudio, event.MsgVideo: - var filename, caption string - if msg.Content.FileName != "" { - filename = msg.Content.FileName - caption = msg.Content.FormattedBody - if caption == "" { - caption = msg.Content.Body - } - } else { - filename = msg.Content.Body - } - - // TODO stream this download straight into the uploader - fileData, err := t.main.Bridge.Bot.DownloadMedia(ctx, msg.Content.URL, msg.Content.File) - if err != nil { - return nil, fmt.Errorf("failed to download media from Matrix: %w", err) - } - uploader := uploader.NewUploader(t.client.API()) - upload, err := uploader.FromBytes(ctx, filename, fileData) - if err != nil { - return nil, fmt.Errorf("failed to upload media to Telegram: %w", err) - } - var photo *message.UploadedPhotoBuilder - if caption != "" { - // TODO resolver? - photo = message.UploadedPhoto(upload, html.String(nil, caption)) - } else { - photo = message.UploadedPhoto(upload) - } - updates, err = builder.Media(ctx, photo) - if err != nil { - return nil, err - } - } - - var tgMessageID, tgDate int - switch sentMessage := updates.(type) { - case *tg.UpdateShortSentMessage: - tgMessageID = sentMessage.ID - tgDate = sentMessage.Date - case *tg.Updates: - tgDate = sentMessage.Date - for _, u := range sentMessage.Updates { - if update, ok := u.(*tg.UpdateMessageID); ok { - tgMessageID = update.ID - break - } - } - if tgMessageID == 0 { - return nil, fmt.Errorf("couldn't find update message ID update") - } - default: - return nil, fmt.Errorf("unknown update from message response %T", updates) - } - - dbMessage = &database.Message{ - ID: makeMessageID(tgMessageID), - MXID: msg.Event.ID, - RoomID: msg.Portal.ID, - SenderID: makeUserID(t.loginID), - Timestamp: time.Unix(int64(tgDate), 0), - } - return -} - -func (t *TelegramClient) HandleMatrixMessageRemove(ctx context.Context, msg *bridgev2.MatrixMessageRemove) error { - panic("unimplemented remove") -} - -func (t *TelegramClient) HandleMatrixReaction(ctx context.Context, msg *bridgev2.MatrixReaction) (emojiID networkid.EmojiID, err error) { - panic("unimplemented reaction") -} - -func (t *TelegramClient) HandleMatrixReactionRemove(ctx context.Context, msg *bridgev2.MatrixReactionRemove) error { - panic("unimplemented reaction remove") -} - func (t *TelegramClient) IsLoggedIn() bool { _, err := t.client.Self(context.TODO()) return err == nil } -func (t *TelegramClient) IsThisUser(ctx context.Context, userID networkid.UserID) bool { - return userID == networkid.UserID(t.userLogin.ID) -} - func (t *TelegramClient) LogoutRemote(ctx context.Context) { _, err := t.client.API().AuthLogOut(ctx) if err != nil { zerolog.Ctx(ctx).Err(err).Msg("failed to logout on Telegram") } } + +func (t *TelegramClient) IsThisUser(ctx context.Context, userID networkid.UserID) bool { + return userID == networkid.UserID(t.userLogin.ID) +} diff --git a/pkg/connector/connector.go b/pkg/connector/connector.go index 1e124784..e6f03b5a 100644 --- a/pkg/connector/connector.go +++ b/pkg/connector/connector.go @@ -18,7 +18,10 @@ package connector import ( "context" + _ "embed" + "fmt" + up "go.mau.fi/util/configupgrade" "go.mau.fi/util/dbutil" "maunium.net/go/mautrix/bridgev2" @@ -38,6 +41,9 @@ type TelegramConnector struct { } var _ bridgev2.NetworkConnector = (*TelegramConnector)(nil) +var _ bridgev2.ConfigValidatingNetwork = (*TelegramConnector)(nil) + +// var _ bridgev2.MaxFileSizeingNetwork = (*TelegramConnector)(nil) func NewConnector() *TelegramConnector { return &TelegramConnector{ @@ -59,3 +65,40 @@ func (tc *TelegramConnector) LoadUserLogin(ctx context.Context, login *bridgev2. login.Client, err = NewTelegramClient(ctx, tc, login) return } + +//go:embed example-config.yaml +var ExampleConfig string + +func upgradeConfig(helper up.Helper) { + helper.Copy(up.Int, "app_id") + helper.Copy(up.Str, "app_hash") +} + +func (tg *TelegramConnector) GetConfig() (example string, data any, upgrader up.Upgrader) { + return ExampleConfig, tg.Config, up.SimpleUpgrader(upgradeConfig) +} + +func (tg *TelegramConnector) ValidateConfig() error { + if tg.Config.AppID == 0 { + return fmt.Errorf("app_id is required") + } + if tg.Config.AppHash == "" { + return fmt.Errorf("app_hash is required") + } + return nil +} + +// TODO +// func (tg *TelegramConnector) SetMaxFileSize(maxSize int64) { +// } + +func (tg *TelegramConnector) GetName() bridgev2.BridgeName { + return bridgev2.BridgeName{ + DisplayName: "Telegram", + NetworkURL: "https://telegram.org/", + NetworkIcon: "mxc://maunium.net/tJCRmUyJDsgRNgqhOgoiHWbX", + NetworkID: "telegram", + BeeperBridgeType: "telegram", + DefaultPort: 29317, + } +} diff --git a/pkg/connector/example-config.yaml b/pkg/connector/example-config.yaml new file mode 100644 index 00000000..b5c9a3bc --- /dev/null +++ b/pkg/connector/example-config.yaml @@ -0,0 +1,72 @@ +# Get your own API keys at https://my.telegram.org/apps +app_id: 12345 +app_hash: tjyd5yge35lbodk1xwzw2jstp90k55qz +# (Optional) Create your own bot at https://t.me/BotFather +bot_token: disabled + +# Should the bridge request missed updates from Telegram when restarting? +catch_up: true +# Should incoming updates be handled sequentially to make sure order is preserved on Matrix? +sequential_updates: true +exit_on_update_error: false + +# Telethon connection options. +connection: + # The timeout in seconds to be used when connecting. + timeout: 120 + # How many times the reconnection should retry, either on the initial connection or when + # Telegram disconnects us. May be set to a negative or null value for infinite retries, but + # this is not recommended, since the program can get stuck in an infinite loop. + retries: 5 + # The delay in seconds to sleep between automatic reconnections. + retry_delay: 1 + # The threshold below which the library should automatically sleep on flood wait errors + # (inclusive). For instance, if a FloodWaitError for 17s occurs and flood_sleep_threshold + # is 20s, the library will sleep automatically. If the error was for 21s, it would raise + # the error instead. Values larger than a day (86400) will be changed to a day. + flood_sleep_threshold: 60 + # How many times a request should be retried. Request are retried when Telegram is having + # internal issues, when there is a FloodWaitError less than flood_sleep_threshold, or when + # there's a migrate error. May take a negative or null value for infinite retries, but this + # is not recommended, since some requests can always trigger a call fail (such as searching + # for messages). + request_retries: 5 + # Use IPv6 for Telethon connection + use_ipv6: false + +# Device info sent to Telegram. +device_info: + # "auto" = OS name+version. + device_model: mautrix-telegram + # "auto" = Telethon version. + system_version: auto + # "auto" = mautrix-telegram version. + app_version: auto + lang_code: en + system_lang_code: en + +# Custom server to connect to. +server: + # Set to true to use these server settings. If false, will automatically + # use production server assigned by Telegram. Set to false in production. + enabled: false + # The DC ID to connect to. + dc: 2 + # The IP to connect to. + ip: 149.154.167.40 + # The port to connect to. 443 may not work, 80 is better and both are equally secure. + port: 80 + +# Telethon proxy configuration. +# You must install PySocks from pip for proxies to work. +proxy: + # Allowed types: disabled, socks4, socks5, http, mtproxy + type: disabled + # Proxy IP address and port. + address: 127.0.0.1 + port: 1080 + # Whether or not to perform DNS resolving remotely. Only for socks/http proxies. + rdns: true + # Proxy authentication (optional). Put MTProxy secret in password field. + username: "" + password: "" diff --git a/pkg/connector/ids.go b/pkg/connector/ids.go index 43f44c63..5cc8201e 100644 --- a/pkg/connector/ids.go +++ b/pkg/connector/ids.go @@ -33,14 +33,14 @@ const ( peerTypeChannel peerType = "channel" ) -func makePortalID(peer tg.PeerClass) networkid.PortalID { +func makePortalID(peer tg.PeerClass) networkid.PortalKey { switch v := peer.(type) { case *tg.PeerUser: - return networkid.PortalID(fmt.Sprintf("%s:%d", peerTypeUser, v.UserID)) + return networkid.PortalKey{ID: networkid.PortalID(fmt.Sprintf("%s:%d", peerTypeUser, v.UserID))} case *tg.PeerChat: - return networkid.PortalID(fmt.Sprintf("%s:%d", peerTypeChat, v.ChatID)) + return networkid.PortalKey{ID: networkid.PortalID(fmt.Sprintf("%s:%d", peerTypeChat, v.ChatID))} case *tg.PeerChannel: - return networkid.PortalID(fmt.Sprintf("%s:%d", peerTypeChannel, v.ChannelID)) + return networkid.PortalKey{ID: networkid.PortalID(fmt.Sprintf("%s:%d", peerTypeChannel, v.ChannelID))} default: panic(fmt.Errorf("unknown peer class type %T", v)) } diff --git a/pkg/connector/login.go b/pkg/connector/login.go index 694feaaa..55a593be 100644 --- a/pkg/connector/login.go +++ b/pkg/connector/login.go @@ -190,8 +190,10 @@ func (p *PhoneLogin) handleAuthSuccess(ctx context.Context, authorization *tg.Au userLoginID := makeUserLoginID(authorization.User.GetID()) ul, err := p.user.NewLogin(ctx, &database.UserLogin{ ID: userLoginID, - Metadata: map[string]any{ - "phone": p.phone, + Metadata: database.UserLoginMetadata{ + Extra: map[string]any{ + "phone": p.phone, + }, }, }, nil) if err != nil { diff --git a/pkg/connector/matrix.go b/pkg/connector/matrix.go new file mode 100644 index 00000000..3ff812bd --- /dev/null +++ b/pkg/connector/matrix.go @@ -0,0 +1,132 @@ +package connector + +import ( + "context" + "fmt" + "time" + + "github.com/gotd/td/telegram/message" + "github.com/gotd/td/telegram/message/html" + "github.com/gotd/td/telegram/uploader" + "github.com/gotd/td/tg" + "maunium.net/go/mautrix/bridgev2" + "maunium.net/go/mautrix/bridgev2/database" + "maunium.net/go/mautrix/bridgev2/networkid" + "maunium.net/go/mautrix/event" + + "go.mau.fi/mautrix-telegram/pkg/connector/ids" +) + +func (t *TelegramClient) HandleMatrixMessage(ctx context.Context, msg *bridgev2.MatrixMessage) (resp *bridgev2.MatrixMessageResponse, err error) { + sender := message.NewSender(t.client.API()) + peer, err := ids.InputPeerForPortalID(msg.Portal.ID) + if err != nil { + return nil, err + } + builder := sender.To(peer) + + // TODO handle sticker + + var updates tg.UpdatesClass + switch msg.Content.MsgType { + case event.MsgText: + updates, err = builder.Text(ctx, msg.Content.Body) + if err != nil { + return nil, err + } + case event.MsgImage, event.MsgFile, event.MsgAudio, event.MsgVideo: + var filename, caption string + if msg.Content.FileName != "" { + filename = msg.Content.FileName + caption = msg.Content.FormattedBody + if caption == "" { + caption = msg.Content.Body + } + } else { + filename = msg.Content.Body + } + + // TODO stream this download straight into the uploader + fileData, err := t.main.Bridge.Bot.DownloadMedia(ctx, msg.Content.URL, msg.Content.File) + if err != nil { + return nil, fmt.Errorf("failed to download media from Matrix: %w", err) + } + uploader := uploader.NewUploader(t.client.API()) + upload, err := uploader.FromBytes(ctx, filename, fileData) + if err != nil { + return nil, fmt.Errorf("failed to upload media to Telegram: %w", err) + } + var photo *message.UploadedPhotoBuilder + if caption != "" { + // TODO resolver? + photo = message.UploadedPhoto(upload, html.String(nil, caption)) + } else { + photo = message.UploadedPhoto(upload) + } + updates, err = builder.Media(ctx, photo) + if err != nil { + return nil, err + } + } + + var tgMessageID, tgDate int + switch sentMessage := updates.(type) { + case *tg.UpdateShortSentMessage: + tgMessageID = sentMessage.ID + tgDate = sentMessage.Date + case *tg.Updates: + tgDate = sentMessage.Date + for _, u := range sentMessage.Updates { + if update, ok := u.(*tg.UpdateMessageID); ok { + tgMessageID = update.ID + break + } + } + if tgMessageID == 0 { + return nil, fmt.Errorf("couldn't find update message ID update") + } + default: + return nil, fmt.Errorf("unknown update from message response %T", updates) + } + + resp = &bridgev2.MatrixMessageResponse{ + DB: &database.Message{ + ID: ids.MakeMessageID(tgMessageID), + MXID: msg.Event.ID, + Room: networkid.PortalKey{ID: msg.Portal.ID}, + SenderID: ids.MakeUserID(t.loginID), + Timestamp: time.Unix(int64(tgDate), 0), + }, + } + return +} + +func (t *TelegramClient) HandleMatrixEdit(ctx context.Context, msg *bridgev2.MatrixEdit) error { + panic("unimplemented edit") +} + +func (t *TelegramClient) HandleMatrixMessageRemove(ctx context.Context, msg *bridgev2.MatrixMessageRemove) error { + panic("unimplemented remove") +} + +func (t *TelegramClient) PreHandleMatrixReaction(ctx context.Context, msg *bridgev2.MatrixReaction) (bridgev2.MatrixReactionPreResponse, error) { + panic("pre handle matrix reaction") +} + +func (t *TelegramClient) HandleMatrixReaction(ctx context.Context, msg *bridgev2.MatrixReaction) (reaction *database.Reaction, err error) { + panic("unimplemented reaction") +} + +func (t *TelegramClient) HandleMatrixReactionRemove(ctx context.Context, msg *bridgev2.MatrixReactionRemove) error { + panic("unimplemented reaction remove") +} + +func (t *TelegramClient) HandleMatrixReadReceipt(ctx context.Context, msg *bridgev2.MatrixReadReceipt) error { + // TODO + return nil +} + +func (t *TelegramClient) HandleMatrixTyping(ctx context.Context, msg *bridgev2.MatrixTyping) error { + // TODO + return nil +} diff --git a/pkg/connector/msgconv/tomatrix.go b/pkg/connector/msgconv/tomatrix.go index 982ada1f..bf50655a 100644 --- a/pkg/connector/msgconv/tomatrix.go +++ b/pkg/connector/msgconv/tomatrix.go @@ -5,7 +5,6 @@ import ( "context" "fmt" "net/http" - "time" "github.com/gotd/td/telegram/downloader" "github.com/gotd/td/tg" @@ -20,9 +19,7 @@ func (mc *MessageConverter) ToMatrix(ctx context.Context, portal *bridgev2.Porta log := zerolog.Ctx(ctx).With().Str("conversion_direction", "to_matrix").Logger() ctx = log.WithContext(ctx) - cm := &bridgev2.ConvertedMessage{ - Timestamp: time.Unix(int64(msg.Date), 0), - } + cm := &bridgev2.ConvertedMessage{} if msg.Message != "" { cm.Parts = append(cm.Parts, &bridgev2.ConvertedMessagePart{ ID: networkid.PartID("caption"),