diff --git a/.gitignore b/.gitignore index 59b2f68e..45f7a94b 100644 --- a/.gitignore +++ b/.gitignore @@ -6,7 +6,7 @@ !provisioning-spec.yaml *.json -*.db +*.db* *.log *.bak diff --git a/cmd/mautrix-telegram/main.go b/cmd/mautrix-telegram/main.go new file mode 100644 index 00000000..64fb4ddf --- /dev/null +++ b/cmd/mautrix-telegram/main.go @@ -0,0 +1,47 @@ +// mautrix-telegram - A Matrix-Telegram puppeting bridge. +// Copyright (C) 2024 Sumner Evans +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +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" + + "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) + + 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() +} diff --git a/go.mod b/go.mod index 17c75544..9d0be19c 100644 --- a/go.mod +++ b/go.mod @@ -5,31 +5,48 @@ 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 go.mau.fi/zerozap v0.1.1 go.uber.org/zap v1.27.0 - maunium.net/go/mautrix v0.18.1 + gopkg.in/yaml.v3 v3.0.1 + maunium.net/go/mautrix v0.18.2-0.20240605171421-d7ffa7183824 ) require ( github.com/cenkalti/backoff/v4 v4.3.0 // indirect + github.com/coreos/go-systemd/v22 v22.5.0 // indirect github.com/go-faster/errors v0.7.1 // indirect github.com/go-faster/jx v1.1.0 // indirect github.com/go-faster/xor v1.0.0 // indirect + github.com/gorilla/mux v1.8.0 // indirect + github.com/gorilla/websocket v1.5.0 // indirect github.com/gotd/ige v0.2.2 // indirect github.com/gotd/neo v0.1.5 // indirect github.com/klauspost/compress v1.17.8 // indirect + github.com/lib/pq v1.10.9 // indirect github.com/mattn/go-colorable v0.1.13 // indirect github.com/mattn/go-isatty v0.0.19 // indirect + github.com/mattn/go-sqlite3 v1.14.22 // indirect + github.com/rs/xid v1.5.0 // indirect github.com/segmentio/asm v1.2.0 // indirect - go.mau.fi/util v0.4.2 // indirect + github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e // indirect + github.com/tidwall/gjson v1.17.1 // indirect + 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 + 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.22.0 // indirect - golang.org/x/net v0.24.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/sync v0.7.0 // indirect - golang.org/x/sys v0.19.0 // indirect + golang.org/x/sys v0.20.0 // indirect + gopkg.in/natefinch/lumberjack.v2 v2.2.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 f0445aab..956dcc73 100644 --- a/go.sum +++ b/go.sum @@ -1,5 +1,8 @@ +github.com/DATA-DOG/go-sqlmock v1.5.2 h1:OcvFkGmslmlZibjAjaHm3L//6LiuBgolP7OputlJIzU= +github.com/DATA-DOG/go-sqlmock v1.5.2/go.mod h1:88MAG/4G7SMwSE3CeA0ZKzrT5CiOU3OJ+JlNzwDqpNU= github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8= github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= +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/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= @@ -13,6 +16,10 @@ github.com/go-faster/xor v1.0.0/go.mod h1:x5CaDY9UKErKzqfRfFZdfu+OSTfoZny3w5Ak7U github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +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/gotd/ige v0.2.2 h1:XQ9dJZwBfDnOGSTxKXBGP4gMud3Qku2ekScRjDWWfEk= github.com/gotd/ige v0.2.2/go.mod h1:tuCRb+Y5Y3eNTo3ypIfNpQ4MFjrnONiL2jN2AKZXmb0= github.com/gotd/neo v0.1.5 h1:oj0iQfMbGClP8xI59x7fE/uHoTJD7NZH9oV1WNuPukQ= @@ -21,23 +28,49 @@ github.com/gotd/td v0.102.0 h1:V6zNba9FV21YiBm1t42ak5jyBFSQzY8+8fwZpOT5lGM= github.com/gotd/td v0.102.0/go.mod h1:k9JQ7ktxOs4yTpE7X2ZvNtAl+blARhz1ak+Aw0VUHiQ= github.com/klauspost/compress v1.17.8 h1:YcnTYrq7MikUT7k0Yb5eceMmALQPYBW/Xltxn0NAMnU= github.com/klauspost/compress v1.17.8/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +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.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.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA= github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU= +github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8= +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/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= +github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e/go.mod h1:XV66xRDqSt+GTGFMVlhk3ULuV0y9ZmzeVGR4mloJI3M= github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= -go.mau.fi/util v0.4.2 h1:RR3TOcRHmCF9Bx/3YG4S65MYfa+nV6/rn8qBWW4Mi30= -go.mau.fi/util v0.4.2/go.mod h1:PlAVfUUcPyHPrwnvjkJM9UFcPE7qGPDJqk+Oufa1Gtw= +github.com/tidwall/gjson v1.14.2/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= +github.com/tidwall/gjson v1.17.1 h1:wlYEnwqAHgzmhNUFfw7Xalt2JzQvsMx2Se4PcoFCT/U= +github.com/tidwall/gjson v1.17.1/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 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= +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= go.mau.fi/zerozap v0.1.1/go.mod h1:eRYfQIyL4nTvxaBtVoFqfhdd2vp7pxiHdtvMy2w7XVg= go.opentelemetry.io/otel v1.26.0 h1:LQwgL5s/1W7YiiRwxf03QGnWLb2HW4pLiAhaA5cZXBs= @@ -52,24 +85,31 @@ 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.22.0 h1:g1v0xeRhjcugydODzvb3mEM9SQ0HGp9s/nh3COQ/C30= -golang.org/x/crypto v0.22.0/go.mod h1:vr6Su+7cTlO45qkww3VDJlzDn0ctJvRgYbC2NvXHt+M= -golang.org/x/exp v0.0.0-20240409090435-93d18d7e34b8 h1:ESSUROHIBHg7USnszlcdmjBEwdMj9VUvU+OPk4yl2mc= -golang.org/x/exp v0.0.0-20240409090435-93d18d7e34b8/go.mod h1:/lliqkxwWAhPjf5oSOIJup2XcqJaw8RGS6k3TGEc7GI= -golang.org/x/net v0.24.0 h1:1PcaxkF854Fu3+lvBIx5SYn9wRlBzzcnHZSiaFFAb0w= -golang.org/x/net v0.24.0/go.mod h1:2Q7sJY5mzlzWjKtYUEXSlBWCdyaioyXzRB2RtU8KVE8= +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/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.19.0 h1:q5f1RH2jigJ1MoAWp2KTp3gm5zAGFUTarQZ5U386+4o= -golang.org/x/sys v0.19.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.20.0 h1:Od9JTbYCk261bKm4M/mw7AklTlFYIa0bIp9BgSm1S8Y= +golang.org/x/sys v0.20.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.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -maunium.net/go/mautrix v0.18.1 h1:a6mUsJixegBNTXUoqC5RQ9gsumIPzKvCubKwF+zmCt4= -maunium.net/go/mautrix v0.18.1/go.mod h1:2oHaq792cSXFGvxLvYw3Gf1L4WVVP4KZcYys5HVk/h8= +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= 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/main.go b/main.go index 4dd3a826..d17840fe 100644 --- a/main.go +++ b/main.go @@ -22,7 +22,7 @@ import ( "maunium.net/go/mautrix/event" "maunium.net/go/mautrix/id" - "go.mau.fi/mautrix-telegram/msgconv" + "go.mau.fi/mautrix-telegram/pkg/connector/msgconv" ) type FileSession struct{} diff --git a/pkg/connector/client.go b/pkg/connector/client.go new file mode 100644 index 00000000..a72797de --- /dev/null +++ b/pkg/connector/client.go @@ -0,0 +1,103 @@ +package connector + +import ( + "context" + "errors" + + "github.com/gotd/td/telegram" + "github.com/rs/zerolog" + "maunium.net/go/mautrix/bridgev2" + "maunium.net/go/mautrix/bridgev2/database" + "maunium.net/go/mautrix/bridgev2/networkid" +) + +type TelegramClient struct { + main *TelegramConnector + userLogin *bridgev2.UserLogin + client *telegram.Client + clientCancel context.CancelFunc +} + +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 +func connectTelegramClient(ctx context.Context, client *telegram.Client) (context.CancelFunc, error) { + ctx, cancel := context.WithCancel(ctx) + + errC := make(chan error, 1) + initDone := make(chan struct{}) + go func() { + defer close(errC) + errC <- client.Run(ctx, func(ctx context.Context) error { + close(initDone) + <-ctx.Done() + if errors.Is(ctx.Err(), context.Canceled) { + return nil + } + return ctx.Err() + }) + }() + + select { + case <-ctx.Done(): // context canceled + cancel() + return func() {}, ctx.Err() + case err := <-errC: // startup timeout + cancel() + return func() {}, err + case <-initDone: // init done + } + + return cancel, nil +} + +func (t *TelegramClient) Connect(ctx context.Context) (err error) { + t.clientCancel, err = connectTelegramClient(ctx, t.client) + return +} + +func (t *TelegramClient) GetChatInfo(ctx context.Context, portal *bridgev2.Portal) (*bridgev2.PortalInfo, error) { + panic("unimplemented getchatinfo") +} + +func (t *TelegramClient) GetUserInfo(ctx context.Context, ghost *bridgev2.Ghost) (*bridgev2.UserInfo, error) { + panic("unimplemented getuserinfo") +} + +func (t *TelegramClient) HandleMatrixEdit(ctx context.Context, msg *bridgev2.MatrixEdit) error { + panic("unimplemented edit") +} + +func (t *TelegramClient) HandleMatrixMessage(ctx context.Context, msg *bridgev2.MatrixMessage) (message *database.Message, err error) { + panic("unimplemented message") +} + +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 { + panic("unimplemented istheiruser") +} + +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") + } +} diff --git a/pkg/connector/connector.go b/pkg/connector/connector.go new file mode 100644 index 00000000..d8c623da --- /dev/null +++ b/pkg/connector/connector.go @@ -0,0 +1,81 @@ +// mautrix-telegram - A Matrix-Telegram puppeting bridge. +// Copyright (C) 2024 Sumner Evans +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +package connector + +import ( + "context" + "strconv" + + "github.com/gotd/td/telegram" + "github.com/rs/zerolog" + "go.mau.fi/util/dbutil" + "go.mau.fi/zerozap" + "go.uber.org/zap" + "maunium.net/go/mautrix/bridgev2" + + "go.mau.fi/mautrix-telegram/pkg/store" +) + +type TelegramConfig struct { + AppID int `yaml:"app_id"` + AppHash string `yaml:"app_hash"` +} + +type TelegramConnector struct { + Bridge *bridgev2.Bridge + Config *TelegramConfig + + store *store.Container +} + +func NewConnector() *TelegramConnector { + return &TelegramConnector{ + Config: &TelegramConfig{}, + } +} + +func (tg *TelegramConnector) Init(bridge *bridgev2.Bridge) { + // TODO + tg.store = store.NewStore(bridge.DB.Database, dbutil.ZeroLogger(bridge.Log.With().Str("db_section", "telegram").Logger())) + tg.Bridge = bridge +} + +func (tg *TelegramConnector) Start(ctx context.Context) error { + return tg.store.Upgrade(ctx) +} + +func (tg *TelegramConnector) LoadUserLogin(ctx context.Context, login *bridgev2.UserLogin) error { + loginID, err := strconv.ParseInt(string(login.ID), 10, 64) + if err != nil { + return err + } + + logger := zerolog.Ctx(ctx).With(). + Str("component", "telegram_client"). + Int64("login_id", loginID). + Logger() + + login.Client = &TelegramClient{ + main: tg, + userLogin: login, + client: telegram.NewClient(tg.Config.AppID, tg.Config.AppHash, telegram.Options{ + SessionStorage: tg.store.GetSessionStore(loginID), + Logger: zap.New(zerozap.New(logger)), + }), + } + return nil +} diff --git a/pkg/connector/login.go b/pkg/connector/login.go new file mode 100644 index 00000000..f1f769c2 --- /dev/null +++ b/pkg/connector/login.go @@ -0,0 +1,229 @@ +// mautrix-telegram - A Matrix-Telegram puppeting bridge. +// Copyright (C) 2024 Sumner Evans +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +package connector + +import ( + "context" + "errors" + "fmt" + "strconv" + "strings" + + "github.com/gotd/td/session" + "github.com/gotd/td/telegram" + "github.com/gotd/td/telegram/auth" + "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" +) + +const LoginFlowIDPhone = "phone" + +func (tg *TelegramConnector) GetLoginFlows() []bridgev2.LoginFlow { + return []bridgev2.LoginFlow{{ + Name: "Phone Number", + Description: "Login using your Telegram phone number", + ID: LoginFlowIDPhone, + }} +} + +func (tg *TelegramConnector) CreateLogin(ctx context.Context, user *bridgev2.User, flowID string) (bridgev2.LoginProcess, error) { + if flowID != LoginFlowIDPhone { + return nil, fmt.Errorf("unknown flow ID %s", flowID) + } + return &PhoneLogin{user: user, main: tg}, nil +} + +const ( + phoneNumberStep = "fi.mau.telegram.phone_number" + codeStep = "fi.mau.telegram.code" + passwordStep = "fi.mau.telegram.password" + completeStep = "fi.mau.telegram.complete" +) + +type PhoneLogin struct { + user *bridgev2.User + main *TelegramConnector + storage *session.StorageMemory + client *telegram.Client + clientCancel context.CancelFunc + + phone string + hash string +} + +var _ bridgev2.LoginProcessUserInput = (*PhoneLogin)(nil) + +func (p *PhoneLogin) Cancel() { + p.clientCancel() +} + +func (p *PhoneLogin) Start(ctx context.Context) (*bridgev2.LoginStep, error) { + return &bridgev2.LoginStep{ + Type: bridgev2.LoginStepTypeUserInput, + StepID: phoneNumberStep, + Instructions: "Please enter your phone number", + UserInputParams: &bridgev2.LoginUserInputParams{ + Fields: []bridgev2.LoginInputDataField{ + { + Type: bridgev2.LoginInputFieldTypePhoneNumber, + ID: phoneNumberStep, + Name: "Phone Number", + Description: "Include the country code with +", + }, + }, + }, + }, nil +} + +func (p *PhoneLogin) SubmitUserInput(ctx context.Context, input map[string]string) (*bridgev2.LoginStep, error) { + if phone, ok := input[phoneNumberStep]; ok { + p.phone = phone + p.storage = &session.StorageMemory{} + p.client = telegram.NewClient(p.main.Config.AppID, p.main.Config.AppHash, telegram.Options{ + SessionStorage: p.storage, + Logger: zap.New(zerozap.New(zerolog.Ctx(ctx).With().Str("component", "telegram_login_client").Logger())), + }) + var err error + p.clientCancel, err = connectTelegramClient(context.Background(), p.client) + if err != nil { + return nil, err + } + sentCode, err := p.client.Auth().SendCode(ctx, p.phone, auth.SendCodeOptions{}) + if err != nil { + return nil, err + } + switch s := sentCode.(type) { + case *tg.AuthSentCode: + p.hash = s.PhoneCodeHash + return &bridgev2.LoginStep{ + Type: bridgev2.LoginStepTypeUserInput, + StepID: codeStep, + Instructions: "Please enter the code sent to your phone", + UserInputParams: &bridgev2.LoginUserInputParams{ + Fields: []bridgev2.LoginInputDataField{ + { + Type: bridgev2.LoginInputFieldType2FACode, + ID: codeStep, + Name: "Code", + }, + }, + }, + }, nil + case *tg.AuthSentCodeSuccess: + switch a := s.Authorization.(type) { + case *tg.AuthAuthorization: + // Looks that we are already authorized. + return p.handleAuthSuccess(ctx, a) + case *tg.AuthAuthorizationSignUpRequired: + return nil, fmt.Errorf("phone number does not correspond with an existing Telegram account and sign-up is not supported") + default: + return nil, fmt.Errorf("unexpected authorization type: %T", sentCode) + } + default: + return nil, fmt.Errorf("unexpected sent code type: %T", sentCode) + } + } else if code, ok := input[codeStep]; ok { + authorization, err := p.client.Auth().SignIn(ctx, p.phone, code, p.hash) + if errors.Is(err, auth.ErrPasswordAuthNeeded) { + return &bridgev2.LoginStep{ + Type: bridgev2.LoginStepTypeUserInput, + StepID: passwordStep, + Instructions: "Please enter your password", + UserInputParams: &bridgev2.LoginUserInputParams{ + Fields: []bridgev2.LoginInputDataField{ + { + Type: bridgev2.LoginInputFieldTypePassword, + ID: passwordStep, + Name: "Password", + }, + }, + }, + }, nil + } else if errors.Is(err, &auth.SignUpRequired{}) { + return nil, fmt.Errorf("sign-up is not supported") + } else if err != nil { + return nil, fmt.Errorf("failed to submit code: %w", err) + } + return p.handleAuthSuccess(ctx, authorization) + } else if password, ok := input[passwordStep]; ok { + authorization, err := p.client.Auth().Password(ctx, password) + if err != nil { + return nil, fmt.Errorf("failed to submit password: %w", err) + } + return p.handleAuthSuccess(ctx, authorization) + } + + return nil, fmt.Errorf("unexpected state during phone login") +} + +func makeUserLoginID(userID int64) networkid.UserLoginID { + return networkid.UserLoginID(strconv.FormatInt(userID, 10)) +} + +func (p *PhoneLogin) handleAuthSuccess(ctx context.Context, authorization *tg.AuthAuthorization) (*bridgev2.LoginStep, error) { + // Now that we have the Telegram user ID, store it in the database and + // close the login client. + sessionStore := p.main.store.GetSessionStore(authorization.User.GetID()) + var sessionData []byte + sessionData, err := p.storage.Bytes(sessionData) + if err != nil { + return nil, err + } + err = sessionStore.StoreSession(ctx, sessionData) + if err != nil { + return nil, err + } + p.clientCancel() + + userLoginID := makeUserLoginID(authorization.User.GetID()) + ul, err := p.user.NewLogin(ctx, &database.UserLogin{ + ID: userLoginID, + Metadata: map[string]any{ + "phone": p.phone, + }, + }, nil) + if err != nil { + return nil, fmt.Errorf("failed to save new login: %w", err) + } + backgroundCtx := ul.Log.WithContext(context.Background()) + err = p.main.LoadUserLogin(backgroundCtx, ul) + if err != nil { + return nil, fmt.Errorf("failed to prepare connection after login: %w", err) + } + err = ul.Client.Connect(backgroundCtx) + if err != nil { + return nil, fmt.Errorf("failed to connect after login: %w", err) + } + user, err := ul.Client.(*TelegramClient).client.Self(ctx) + if err != nil { + return nil, err + } + name := strings.TrimSpace(fmt.Sprintf("%s %s", user.FirstName, user.LastName)) + return &bridgev2.LoginStep{ + Type: bridgev2.LoginStepTypeComplete, + StepID: completeStep, + Instructions: fmt.Sprintf("Successfully logged in as %d / +%s (%s)", user.ID, user.Phone, name), + CompleteParams: &bridgev2.LoginCompleteParams{ + UserLoginID: ul.ID, + }, + }, nil +} diff --git a/msgconv/from-tg.go b/pkg/connector/msgconv/from-tg.go similarity index 93% rename from msgconv/from-tg.go rename to pkg/connector/msgconv/from-tg.go index 7cb019b6..40015999 100644 --- a/msgconv/from-tg.go +++ b/pkg/connector/msgconv/from-tg.go @@ -78,12 +78,12 @@ func (mc *MessageConverter) ToMatrix(ctx context.Context, msg tg.MessageClass) * fmt.Printf("photo: %v\n", photo) largest := getLargestPhotoSize(photo.GetSizes()) - // file := tg.InputPhotoFileLocation{ - // ID: photo.GetID(), - // AccessHash: photo.GetAccessHash(), - // FileReference: photo.GetFileReference(), - // ThumbSize: largest.GetType(), - // } + file := tg.InputPhotoFileLocation{ + ID: photo.GetID(), + AccessHash: photo.GetAccessHash(), + FileReference: photo.GetFileReference(), + ThumbSize: largest.GetType(), + } mxc := id.ContentURIString( fmt.Sprintf("mxc://telegram.sumner.user.beeper.com/p.i%d.a%d.f%s.t%s", photo.GetID(), photo.GetAccessHash(), base64.RawURLEncoding.EncodeToString(photo.GetFileReference()), largest.GetType()), diff --git a/msgconv/msgconv.go b/pkg/connector/msgconv/msgconv.go similarity index 100% rename from msgconv/msgconv.go rename to pkg/connector/msgconv/msgconv.go diff --git a/pkg/store/container.go b/pkg/store/container.go new file mode 100644 index 00000000..ab4e7657 --- /dev/null +++ b/pkg/store/container.go @@ -0,0 +1,41 @@ +// mautrix-telegram - A Matrix-Telegram puppeting bridge. +// Copyright (C) 2024 Sumner Evans +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +package store + +import ( + "context" + + "go.mau.fi/util/dbutil" + + "go.mau.fi/mautrix-telegram/pkg/store/upgrades" +) + +type Container struct { + db *dbutil.Database +} + +func NewStore(db *dbutil.Database, log dbutil.DatabaseLogger) *Container { + return &Container{db: db.Child("telegram_version", upgrades.Table, log)} +} + +func (c *Container) Upgrade(ctx context.Context) error { + return c.db.Upgrade(ctx) +} + +func (c *Container) GetSessionStore(telegramUserID int64) *SessionStore { + return &SessionStore{c.db, telegramUserID} +} diff --git a/pkg/store/session_store.go b/pkg/store/session_store.go new file mode 100644 index 00000000..efa85aa6 --- /dev/null +++ b/pkg/store/session_store.go @@ -0,0 +1,39 @@ +package store + +import ( + "context" + + "github.com/gotd/td/session" + "go.mau.fi/util/dbutil" +) + +// SessionStore is a wrapper around a database that implements +// [session.Storage] scoped to a specific Telegram user ID. +type SessionStore struct { + db *dbutil.Database + telegramUserID int64 +} + +var _ session.Storage = (*SessionStore)(nil) + +const ( + loadSessionQuery = `SELECT session_data FROM telegram_session WHERE user_id=$1` + storeSessionQuery = ` + INSERT INTO telegram_session (user_id, session_data) + VALUES ($1, $2) + ON CONFLICT (user_id) DO UPDATE SET session_data=excluded.session_data + ` +) + +// LoadSession loads session data from the database. +func (s *SessionStore) LoadSession(ctx context.Context) (sessionData []byte, err error) { + row := s.db.QueryRow(ctx, loadSessionQuery, s.telegramUserID) + err = row.Scan(&sessionData) + return +} + +// StoreSession stores session data for a login into the database. +func (s *SessionStore) StoreSession(ctx context.Context, data []byte) error { + _, err := s.db.Exec(ctx, storeSessionQuery, s.telegramUserID, data) + return err +} diff --git a/pkg/store/upgrades/00-latest.sql b/pkg/store/upgrades/00-latest.sql new file mode 100644 index 00000000..72ffbb99 --- /dev/null +++ b/pkg/store/upgrades/00-latest.sql @@ -0,0 +1,7 @@ +-- v0 -> v1: Latest revision + +-- TODO do I need to have bridge ID here? +CREATE TABLE telegram_session ( + user_id INTEGER PRIMARY KEY, + session_data BYTEA NOT NULL +); diff --git a/pkg/store/upgrades/upgrades.go b/pkg/store/upgrades/upgrades.go new file mode 100644 index 00000000..6413dfdc --- /dev/null +++ b/pkg/store/upgrades/upgrades.go @@ -0,0 +1,32 @@ +// mautrix-telegram - A Matrix-Telegram puppeting bridge. +// Copyright (C) 2024 Sumner Evans +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +package upgrades + +import ( + "embed" + + "go.mau.fi/util/dbutil" +) + +var Table dbutil.UpgradeTable + +//go:embed *.sql +var rawUpgrades embed.FS + +func init() { + Table.RegisterFS(rawUpgrades) +}