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)
+}