Files
mautrix-telegram/pkg/connector/sync.go
T
Toni Spets 7e75c8ef83 media: make all media direct downloadable
The only exception is emojis.

Also changed direct download encoding field names to be more generic
when used in mixed manner depending on peer type.

Direct downloads are still somewhat inefficient as they require an API
round trip to succeed but we can cache things in the database if needed.
2025-05-07 06:43:51 +03:00

282 lines
8.8 KiB
Go

// mautrix-telegram - A Matrix-Telegram puppeting bridge.
// Copyright (C) 2025 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 <https://www.gnu.org/licenses/>.
package connector
import (
"context"
"fmt"
"math"
"time"
"github.com/gotd/td/tg"
"github.com/rs/zerolog"
"go.mau.fi/util/ptr"
"maunium.net/go/mautrix/bridgev2"
"maunium.net/go/mautrix/bridgev2/database"
"maunium.net/go/mautrix/bridgev2/networkid"
"maunium.net/go/mautrix/bridgev2/simplevent"
"maunium.net/go/mautrix/event"
"go.mau.fi/mautrix-telegram/pkg/connector/ids"
)
func (t *TelegramClient) SyncChats(ctx context.Context) error {
limit := t.main.Config.Sync.UpdateLimit
if limit <= 0 {
limit = math.MaxInt32
}
zerolog.Ctx(ctx).Info().
Int("update_limit", limit).
Int("create_limit", t.main.Config.Sync.CreateLimit).
Msg("syncing chats")
dialogs, err := APICallWithUpdates(ctx, t, func() (tg.ModifiedMessagesDialogs, error) {
d, err := t.client.API().MessagesGetDialogs(ctx, &tg.MessagesGetDialogsRequest{
Limit: limit,
OffsetPeer: &tg.InputPeerEmpty{},
})
if err != nil {
return nil, err
} else if dialogs, ok := d.(tg.ModifiedMessagesDialogs); !ok {
return nil, fmt.Errorf("unexpected dialogs type %T", d)
} else {
return dialogs, nil
}
})
if err != nil {
return err
}
if err := t.resetPinnedDialogs(ctx, dialogs.GetDialogs()); err != nil {
return err
}
return t.handleDialogs(ctx, dialogs, t.main.Config.Sync.CreateLimit)
}
func (t *TelegramClient) resetPinnedDialogs(ctx context.Context, dialogs []tg.DialogClass) error {
t.userLogin.Metadata.(*UserLoginMetadata).PinnedDialogs = nil
for _, dialog := range dialogs {
if dialog.GetPinned() {
portalKey := t.makePortalKeyFromPeer(dialog.GetPeer())
t.userLogin.Metadata.(*UserLoginMetadata).PinnedDialogs = append(t.userLogin.Metadata.(*UserLoginMetadata).PinnedDialogs, portalKey.ID)
}
}
return t.userLogin.Save(ctx)
}
func (t *TelegramClient) handleDialogs(ctx context.Context, dialogs tg.ModifiedMessagesDialogs, createLimit int) error {
log := zerolog.Ctx(ctx)
users := map[networkid.UserID]tg.UserClass{}
for _, user := range dialogs.GetUsers() {
users[ids.MakeUserID(user.GetID())] = user
}
chats := map[int64]tg.ChatClass{}
for _, chat := range dialogs.GetChats() {
chats[chat.GetID()] = chat
}
messages := map[networkid.MessageID]tg.MessageClass{}
for _, message := range dialogs.GetMessages() {
messages[ids.GetMessageIDFromMessage(message)] = message
}
var created int
for _, d := range dialogs.GetDialogs() {
if d.TypeID() != tg.DialogTypeID {
continue
}
dialog := d.(*tg.Dialog)
log := log.With().
Stringer("peer", dialog.Peer).
Int("top_message", dialog.TopMessage).
Logger()
log.Debug().Msg("Syncing dialog")
portalKey := t.makePortalKeyFromPeer(dialog.GetPeer())
portal, err := t.main.Bridge.GetPortalByKey(ctx, portalKey)
if err != nil {
log.Err(err).Msg("Failed to get portal")
continue
}
if dialog.UnreadCount == 0 && !dialog.UnreadMark {
portal.Metadata.(*PortalMetadata).ReadUpTo = dialog.TopMessage
}
// If this is a DM, make sure that the user isn't deleted.
if user, ok := dialog.Peer.(*tg.PeerUser); ok {
if users[ids.MakeUserID(user.UserID)].(*tg.User).GetDeleted() {
log.Debug().Msg("Not syncing portal because user is deleted")
continue
}
}
var chatInfo *bridgev2.ChatInfo
switch peer := dialog.Peer.(type) {
case *tg.PeerUser:
userID := ids.MakeUserID(peer.UserID)
if users[userID].(*tg.User).GetDeleted() {
log.Debug().Int64("user_id", peer.UserID).Msg("Not syncing portal because user is deleted")
continue
}
chatInfo = t.getDMChatInfo(peer.UserID)
case *tg.PeerChat:
chat := chats[peer.ChatID]
if chat.TypeID() == tg.ChatForbiddenTypeID {
log.Debug().
Int64("chat_id", peer.ChatID).
Msg("Not syncing portal because chat is forbidden")
continue
} else if chat.TypeID() != tg.ChatTypeID {
log.Debug().
Int64("chat_id", peer.ChatID).
Type("chat_type", chat).
Msg("Not syncing portal because chat type is unsupported")
continue
}
fullChat, err := APICallWithUpdates(ctx, t, func() (*tg.MessagesChatFull, error) {
return t.client.API().MessagesGetFullChat(ctx, chat.GetID())
})
if err != nil {
log.Err(err).Msg("Failed to get full chat")
continue
}
chatFull, ok := fullChat.FullChat.(*tg.ChatFull)
var avatar *bridgev2.Avatar
if ok {
avatar, err = t.convertPhoto(ctx, ids.PeerTypeChat, chatFull.ID, chatFull.ChatPhoto)
if err != nil {
log.Err(err).Msg("Failed to convert group avatar")
}
}
chatInfo = &bridgev2.ChatInfo{
CanBackfill: true,
Name: &chat.(*tg.Chat).Title,
Members: &bridgev2.ChatMemberList{
PowerLevels: t.getGroupChatPowerLevels(ctx, chat),
MemberMap: map[networkid.UserID]bridgev2.ChatMember{
t.userID: {
EventSender: t.mySender(),
Membership: event.MembershipJoin,
},
},
},
Avatar: avatar,
}
case *tg.PeerChannel:
channel := chats[peer.ChannelID]
if channel.TypeID() == tg.ChannelForbiddenTypeID {
log.Debug().
Int64("channel_id", peer.ChannelID).
Msg("Not syncing portal because channel is forbidden")
continue
} else if channel.TypeID() != tg.ChannelTypeID {
log.Debug().
Int64("channel_id", peer.ChannelID).
Type("channel_type", channel).
Msg("Not syncing portal because channel type is unsupported")
continue
}
fullChannel := channel.(*tg.Channel)
var avatar *bridgev2.Avatar
if photo, ok := fullChannel.GetPhoto().(*tg.ChatPhoto); ok {
avatar, err = t.convertChatPhoto(ctx, fullChannel.ID, fullChannel.AccessHash, photo)
if err != nil {
log.Err(err).Msg("Failed to convert channel avatar")
}
}
chatInfo = &bridgev2.ChatInfo{
CanBackfill: true,
Name: &channel.(*tg.Channel).Title,
Members: &bridgev2.ChatMemberList{
PowerLevels: t.getGroupChatPowerLevels(ctx, channel),
MemberMap: map[networkid.UserID]bridgev2.ChatMember{
t.userID: {
EventSender: t.mySender(),
Membership: event.MembershipJoin,
},
},
},
Avatar: avatar,
ExtraUpdates: func(ctx context.Context, p *bridgev2.Portal) bool {
return p.Metadata.(*PortalMetadata).SetIsSuperGroup(channel.(*tg.Channel).GetMegagroup())
},
}
if !portal.Metadata.(*PortalMetadata).IsSuperGroup {
// Add the channel user
sender := ids.MakeChannelUserID(peer.ChannelID)
chatInfo.Members.MemberMap[sender] = bridgev2.ChatMember{
EventSender: bridgev2.EventSender{Sender: sender},
Membership: event.MembershipJoin,
PowerLevel: superadminPowerLevel,
}
}
}
if portal == nil || portal.MXID == "" {
// Check what the latest message is
topMessage := messages[ids.MakeMessageID(dialog.Peer, dialog.TopMessage)]
if topMessage.TypeID() == tg.MessageServiceTypeID {
action := topMessage.(*tg.MessageService).Action
if action.TypeID() == tg.MessageActionContactSignUpTypeID || action.TypeID() == tg.MessageActionHistoryClearTypeID {
log.Debug().Str("action_type", action.TypeName()).Msg("Not syncing portal because it's a contact sign up or history clear")
continue
}
}
created++ // The portal will have to be created
if createLimit >= 0 && created > createLimit {
break
}
}
if mu, ok := dialog.NotifySettings.GetMuteUntil(); ok {
chatInfo.UserLocal = &bridgev2.UserLocalPortalInfo{MutedUntil: ptr.Ptr(time.Unix(int64(mu), 0))}
} else {
chatInfo.UserLocal = &bridgev2.UserLocalPortalInfo{MutedUntil: &bridgev2.Unmuted}
}
if dialog.Pinned {
chatInfo.UserLocal.Tag = ptr.Ptr(event.RoomTagFavourite)
}
t.main.Bridge.QueueRemoteEvent(t.userLogin, &simplevent.ChatResync{
ChatInfo: chatInfo,
EventMeta: simplevent.EventMeta{
Type: bridgev2.RemoteEventChatResync,
LogContext: func(c zerolog.Context) zerolog.Context {
return c.Str("update", "sync")
},
PortalKey: portalKey,
CreatePortal: true,
},
CheckNeedsBackfillFunc: func(ctx context.Context, latestMessage *database.Message) (bool, error) {
if latestMessage == nil {
return true, nil
}
_, latestMessageID, err := ids.ParseMessageID(latestMessage.ID)
if err != nil {
panic(err)
}
return dialog.TopMessage > latestMessageID, nil
},
})
}
return nil
}