login: add support for bot tokens

This commit is contained in:
Tulir Asokan
2025-12-06 21:08:46 +02:00
parent 48fed1c026
commit 10f1583da9
10 changed files with 158 additions and 24 deletions
+2 -2
View File
@@ -27,7 +27,7 @@ require (
github.com/rs/zerolog v1.34.0
github.com/stretchr/testify v1.11.1
github.com/tidwall/gjson v1.18.0
go.mau.fi/util v0.9.4-0.20251203150954-89222cf416b4
go.mau.fi/util v0.9.4-0.20251206205611-85e6fd6551e0
go.mau.fi/webp v0.2.0
go.mau.fi/zerozap v0.1.2
go.opentelemetry.io/otel v1.38.0
@@ -41,7 +41,7 @@ require (
golang.org/x/sync v0.18.0
golang.org/x/tools v0.39.0
gopkg.in/yaml.v3 v3.0.1
maunium.net/go/mautrix v0.26.1-0.20251206105112-4efa4bdac5e3
maunium.net/go/mautrix v0.26.1-0.20251207175222-00c58efc5906
rsc.io/qr v0.2.0
)
+4 -4
View File
@@ -114,8 +114,8 @@ github.com/tidwall/sjson v1.2.5/go.mod h1:Fvgq9kS/6ociJEDnK0Fk1cpYF4FIW6ZF7LAe+6
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
github.com/yuin/goldmark v1.7.13 h1:GPddIs617DnBLFFVJFgpo1aBfe/4xcvMc3SB5t/D0pA=
github.com/yuin/goldmark v1.7.13/go.mod h1:ip/1k0VRfGynBgxOz0yCqHrbZXhcjxyuS66Brc7iBKg=
go.mau.fi/util v0.9.4-0.20251203150954-89222cf416b4 h1:UrHJ6O9Nr+KxuAFNKyTZdGExhtUq09WcYpUlqMwBOic=
go.mau.fi/util v0.9.4-0.20251203150954-89222cf416b4/go.mod h1:viDmhBOAFfcqDdKSk53EPJV3N4Mi8Jst5/ahGJ/vwsA=
go.mau.fi/util v0.9.4-0.20251206205611-85e6fd6551e0 h1:ESebxPGULuuxxcZigjcBFyyU62tiyY6ivtX17P4BkvY=
go.mau.fi/util v0.9.4-0.20251206205611-85e6fd6551e0/go.mod h1:viDmhBOAFfcqDdKSk53EPJV3N4Mi8Jst5/ahGJ/vwsA=
go.mau.fi/webp v0.2.0 h1:QVMenHw7JDb4vall5sV75JNBQj9Hw4u8AKbi1QetHvg=
go.mau.fi/webp v0.2.0/go.mod h1:VSg9MyODn12Mb5pyG0NIyNFhujrmoFSsZBs8syOZD1Q=
go.mau.fi/zeroconfig v0.2.0 h1:e/OGEERqVRRKlgaro7E6bh8xXiKFSXB3eNNIud7FUjU=
@@ -237,7 +237,7 @@ 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.26.1-0.20251206105112-4efa4bdac5e3 h1:llPUQswvRVaWkWqwH1P6T51wkj3fWCXu4rxewN0RLsY=
maunium.net/go/mautrix v0.26.1-0.20251206105112-4efa4bdac5e3/go.mod h1:NaesYcOQWFDbixVYywCVS+Twlzab9hOUpFNlCBlvciE=
maunium.net/go/mautrix v0.26.1-0.20251207175222-00c58efc5906 h1:5Y4XfVK3QRi/LIPO0yb0oBoEiba3NAh3ayUmP1byzUY=
maunium.net/go/mautrix v0.26.1-0.20251207175222-00c58efc5906/go.mod h1:pzwIT42s+BhBjEYovmcOt69VlNW2RkJ6pCyZjYQHKIc=
rsc.io/qr v0.2.0 h1:6vBLea5/NRMVTz8V66gipeLycZMl/+UlFmk8DvqQ6WY=
rsc.io/qr v0.2.0/go.mod h1:IF+uZjkb9fqyeF/4tlBoynqmQxUoPfWEKh921coOuXs=
+3
View File
@@ -185,6 +185,9 @@ func (t *TelegramClient) stopTakeout(ctx context.Context) error {
}
func (t *TelegramClient) FetchMessages(ctx context.Context, fetchParams bridgev2.FetchMessagesParams) (*bridgev2.FetchMessagesResponse, error) {
if t.metadata.IsBot {
return nil, fmt.Errorf("bots cannot backfill messages")
}
log := zerolog.Ctx(ctx).With().Str("method", "FetchMessages").Logger()
ctx = log.WithContext(ctx)
+2 -2
View File
@@ -105,7 +105,7 @@ func (t *TelegramClient) getDMChatInfo(ctx context.Context, userID int64) (*brid
MemberMap: map[networkid.UserID]bridgev2.ChatMember{},
PowerLevels: t.getDMPowerLevels(ghost),
},
CanBackfill: true,
CanBackfill: !t.metadata.IsBot,
ExtraUpdates: updatePortalLastSyncAt,
}
chatInfo.Members.MemberMap.Add(bridgev2.ChatMember{EventSender: t.mySender()})
@@ -142,7 +142,7 @@ type memberFetchMeta struct {
func (t *TelegramClient) wrapChatInfo(portalID networkid.PortalID, rawChat tg.ChatClass) (*bridgev2.ChatInfo, *memberFetchMeta, error) {
info := bridgev2.ChatInfo{
Type: ptr.Ptr(database.RoomTypeDefault),
CanBackfill: true,
CanBackfill: !t.metadata.IsBot,
Members: &bridgev2.ChatMemberList{
ExcludeChangesFromTimeline: true,
MemberMap: bridgev2.ChatMemberMap{},
+2 -3
View File
@@ -711,8 +711,7 @@ func (t *TelegramClient) FillBridgeState(state status.BridgeState) status.Bridge
if state.Info == nil {
state.Info = make(map[string]any)
}
meta := t.userLogin.Metadata.(*UserLoginMetadata)
state.Info["is_bot"] = meta.IsBot
state.Info["login_method"] = meta.LoginMethod
state.Info["is_bot"] = t.metadata.IsBot
state.Info["login_method"] = t.metadata.LoginMethod
return state
}
+15 -5
View File
@@ -555,11 +555,9 @@ func (t *TelegramClient) PreHandleMatrixReaction(ctx context.Context, msg *bridg
return resp, fmt.Errorf("failed to get available reactions: %w", err)
} else if _, ok := availableReactions[keyNoVariation]; ok {
log.Debug().Msg("Not using custom emoji reaction since the emoji is available")
} else {
if documentID, ok := emojis.GetEmojiDocumentID(keyNoVariation); ok {
log.Debug().Msg("Using custom emoji reaction")
emojiID = ids.MakeEmojiIDFromDocumentID(documentID)
}
} else if documentID, ok := emojis.GetEmojiDocumentID(keyNoVariation); ok && !t.metadata.IsBot {
log.Debug().Msg("Using custom emoji reaction")
emojiID = ids.MakeEmojiIDFromDocumentID(documentID)
}
log.Debug().Str("emoji_id", string(emojiID)).Msg("Pre-handled reaction")
@@ -684,10 +682,15 @@ func (t *TelegramClient) HandleMatrixReadReceipt(ctx context.Context, msg *bridg
var readMentionsErr, readReactionsErr, readMessagesErr error
var wg sync.WaitGroup
isBot := t.metadata.IsBot
// Read mentions
wg.Add(1)
go func() {
defer wg.Done()
if isBot {
return
}
_, readMentionsErr = t.client.API().MessagesReadMentions(ctx, &tg.MessagesReadMentionsRequest{
Peer: inputPeer,
TopMsgID: topicID,
@@ -698,6 +701,9 @@ func (t *TelegramClient) HandleMatrixReadReceipt(ctx context.Context, msg *bridg
wg.Add(1)
go func() {
defer wg.Done()
if isBot {
return
}
_, readMentionsErr = t.client.API().MessagesReadReactions(ctx, &tg.MessagesReadReactionsRequest{
Peer: inputPeer,
TopMsgID: topicID,
@@ -709,6 +715,10 @@ func (t *TelegramClient) HandleMatrixReadReceipt(ctx context.Context, msg *bridg
go func() {
defer wg.Done()
if isBot {
return
}
message := msg.ExactMessage
if message == nil {
message, readMessagesErr = t.main.Bridge.DB.Message.GetLastPartAtOrBeforeTime(ctx, msg.Portal.PortalKey, time.Now())
+3 -1
View File
@@ -1035,7 +1035,9 @@ func (t *TelegramClient) getAvailableReactionsForCapability(ctx context.Context)
}
func (t *TelegramClient) getAvailableReactions(ctx context.Context) (map[string]struct{}, error) {
if !t.IsLoggedIn() {
if t.metadata.IsBot {
return nil, nil
} else if !t.IsLoggedIn() {
return nil, errors.New("you must be logged in to get available reactions")
}
+14 -1
View File
@@ -41,7 +41,7 @@ import (
const (
LoginFlowIDPhone = "phone"
LoginFlowIDQR = "qr"
LoginFlowIDBotToken = "bot_token"
LoginFlowIDBotToken = "bot"
LoginStepIDComplete = "fi.mau.telegram.login.complete"
)
@@ -76,6 +76,11 @@ func (tg *TelegramConnector) GetLoginFlows() []bridgev2.LoginFlow {
Description: "Login by scanning a QR code from your phone",
ID: LoginFlowIDQR,
},
{
Name: "Bot token",
Description: "Log in as a bot using the bot token provided by BotFather.",
ID: LoginFlowIDBotToken,
},
}
}
@@ -86,6 +91,8 @@ func (tg *TelegramConnector) CreateLogin(ctx context.Context, user *bridgev2.Use
flowID: flowID,
}
switch flowID {
case LoginFlowIDBotToken:
return &BotLogin{baseLogin: bl}, nil
case LoginFlowIDPhone:
return &PhoneLogin{baseLogin: bl}, nil
case LoginFlowIDQR:
@@ -205,6 +212,9 @@ func (bl *baseLogin) finalizeLogin(
client := ul.Client.(*TelegramClient)
go func() {
if metadata.IsBot {
return
}
log := ul.Log.With().Str("action", "post-login sync").Logger()
err := client.clientInitialized.Wait(ctx)
if err != nil {
@@ -215,6 +225,9 @@ func (bl *baseLogin) finalizeLogin(
}()
go func() {
if metadata.IsBot {
return
}
log := ul.Log.With().Str("component", "post-login takeout").Logger()
client.takeoutLock.Lock()
defer client.takeoutLock.Unlock()
+109
View File
@@ -0,0 +1,109 @@
// mautrix-telegram - A Matrix-Telegram puppeting bridge.
// Copyright (C) 2025 Tulir Asokan
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
package connector
import (
"context"
"encoding/json"
"fmt"
"net/http"
"github.com/rs/zerolog"
"maunium.net/go/mautrix/bridgev2"
)
const (
LoginStepIDBotToken = "fi.mau.telegram.login.bot_token"
)
type BotLogin struct {
*baseLogin
}
func (bl *BotLogin) StartWithOverride(ctx context.Context, override *bridgev2.UserLogin) (*bridgev2.LoginStep, error) {
meta := override.Metadata.(*UserLoginMetadata)
if !meta.IsBot {
return nil, fmt.Errorf("can't re-login to a non-bot account with bot token")
}
return bl.Start(ctx)
}
func (bl *BotLogin) Start(ctx context.Context) (*bridgev2.LoginStep, error) {
return &bridgev2.LoginStep{
Type: bridgev2.LoginStepTypeUserInput,
StepID: LoginStepIDBotToken,
Instructions: "Please enter the bot token you want to log in as",
UserInputParams: &bridgev2.LoginUserInputParams{
Fields: []bridgev2.LoginInputDataField{{
Type: bridgev2.LoginInputFieldTypeToken,
ID: LoginStepIDBotToken,
Name: "Bot token",
Pattern: `^\d+:[A-Za-z0-9_-]{35}$`,
}},
},
}, nil
}
func (bl *BotLogin) SubmitUserInput(ctx context.Context, input map[string]string) (*bridgev2.LoginStep, error) {
log := zerolog.Ctx(ctx).With().Str("component", "telegram bot login").Logger()
ctx = log.WithContext(ctx)
botToken := input[LoginStepIDBotToken]
err := logoutBotAPI(ctx, botToken)
if err != nil {
return nil, fmt.Errorf("failed to logout from bot API: %w", err)
}
err = bl.makeClient(ctx, nil)
if err != nil {
return nil, err
}
authorization, err := bl.client.Auth().Bot(ctx, botToken)
if err != nil {
bl.Cancel()
return nil, err
}
return bl.finalizeLogin(ctx, authorization, &UserLoginMetadata{IsBot: true})
}
type botAPIResponse struct {
OK bool `json:"ok"`
ErrorCode int `json:"error_code"`
Description string `json:"description"`
}
func logoutBotAPI(ctx context.Context, token string) error {
req, err := http.NewRequestWithContext(ctx, http.MethodPost, "https://api.telegram.org/bot"+token+"/logOut", nil)
if err != nil {
return fmt.Errorf("failed to prepare request: %w", err)
}
resp, err := http.DefaultClient.Do(req)
if err != nil {
return fmt.Errorf("failed to send request: %w", err)
}
var respData botAPIResponse
err = json.NewDecoder(resp.Body).Decode(&respData)
_ = resp.Body.Close()
if err != nil {
return fmt.Errorf("failed to decode response: %w", err)
} else if !respData.OK && respData.Description != "Logged out" {
return fmt.Errorf("response error %d: %s", respData.ErrorCode, respData.Description)
}
return nil
}
var _ bridgev2.LoginProcessUserInput = (*BotLogin)(nil)
+4 -6
View File
@@ -55,11 +55,9 @@ func (t *TelegramClient) computeReactionsList(ctx context.Context, peer tg.PeerC
if len(reactionsList) < totalCount {
if user, ok := peer.(*tg.PeerUser); ok {
reactionsList = splitDMReactionCounts(msgReactions.Results, user.UserID, t.telegramUserID)
// TODO
// } else if t.isBot {
// // Can't fetch exact reaction senders as a bot
// return
} else if t.metadata.IsBot {
// Can't fetch exact reaction senders as a bot
return
// TODO remove redundant peer roundtrip, just add a peer -> input peer helper
} else if peer, _, err := t.inputPeerForPortalID(ctx, t.makePortalKeyFromPeer(peer, 0).ID); err != nil {
@@ -209,7 +207,7 @@ func (t *TelegramClient) getReactionLimit(ctx context.Context, sender networkid.
func (t *TelegramClient) maybePollForReactions(ctx context.Context, portal *bridgev2.Portal) error {
// Only poll for reactions in supergroups
if portal == nil || !portal.Metadata.(*PortalMetadata).IsSuperGroup || portal.RoomType == database.RoomTypeSpace {
if t.metadata.IsBot || portal == nil || !portal.Metadata.(*PortalMetadata).IsSuperGroup || portal.RoomType == database.RoomTypeSpace {
return nil
}