login: add support for bot tokens
This commit is contained in:
@@ -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
|
||||
)
|
||||
|
||||
|
||||
@@ -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=
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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{},
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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())
|
||||
|
||||
@@ -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
@@ -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()
|
||||
|
||||
@@ -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)
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user