From 08a2fe975378b912d525cc8d32341594870cb8ae Mon Sep 17 00:00:00 2001 From: Tulir Asokan Date: Wed, 3 Dec 2025 22:34:13 +0200 Subject: [PATCH] chatinfo: refactor processing group chat info --- pkg/connector/chatinfo.go | 491 ++++++++++++++++---------------- pkg/connector/directdownload.go | 25 +- pkg/connector/handlematrix.go | 13 +- pkg/connector/handletelegram.go | 9 +- pkg/connector/media/transfer.go | 26 +- pkg/connector/metadata.go | 1 + pkg/connector/sync.go | 24 +- pkg/connector/tomatrix.go | 35 ++- 8 files changed, 324 insertions(+), 300 deletions(-) diff --git a/pkg/connector/chatinfo.go b/pkg/connector/chatinfo.go index d5f7a727..08ea920f 100644 --- a/pkg/connector/chatinfo.go +++ b/pkg/connector/chatinfo.go @@ -122,91 +122,263 @@ func (t *TelegramClient) getDMChatInfo(ctx context.Context, userID int64) (*brid return &chatInfo, nil } -func (t *TelegramClient) getGroupChatInfo(fullChat *tg.MessagesChatFull, chatID int64) (*bridgev2.ChatInfo, bool, error) { - var name *string - var isBroadcastChannel, isMegagroup, left, found bool - var participantsCount int - for _, c := range fullChat.GetChats() { - if c.GetID() == chatID { - found = true - switch chat := c.(type) { - case *tg.Chat: - name = &chat.Title - left = chat.Left - case *tg.Channel: - name = &chat.Title - isBroadcastChannel = chat.Broadcast - isMegagroup = chat.Megagroup - left = chat.Left +func isBroadcastChannel(chat tg.ChatClass) bool { + switch c := chat.(type) { + case *tg.Channel: + return c.Broadcast + default: + return false + } +} - if value, ok := chat.GetParticipantsCount(); ok { - participantsCount = value - } +type memberFetchMeta struct { + Input *tg.InputChannel + IsBroadcast bool + ParticipantsHidden bool +} + +func (t *TelegramClient) wrapChatInfo(rawChat tg.ChatClass) (*bridgev2.ChatInfo, *memberFetchMeta, error) { + info := bridgev2.ChatInfo{ + Type: ptr.Ptr(database.RoomTypeDefault), + CanBackfill: true, + Members: &bridgev2.ChatMemberList{ + ExcludeChangesFromTimeline: true, + MemberMap: bridgev2.ChatMemberMap{}, + }, + ExcludeChangesFromTimeline: true, + } + var isMegagroup, isBroadcast, left bool + var channelInput *tg.InputChannel + var avatarErr error + switch chat := rawChat.(type) { + case *tg.Chat: + info.Name = &chat.Title + info.Members.TotalMemberCount = chat.ParticipantsCount + info.Avatar, avatarErr = t.convertChatPhoto(chat.AsInputPeer(), chat.Photo) + info.Members.PowerLevels = t.getPowerLevelOverridesFromBannedRights(chat, chat.DefaultBannedRights) + left = chat.Left + case *tg.Channel: + channelInput = chat.AsInput() + info.Name = &chat.Title + info.Members.TotalMemberCount = chat.ParticipantsCount + isMegagroup = chat.Megagroup + isBroadcast = chat.Broadcast + info.Avatar, avatarErr = t.convertChatPhoto(chat.AsInputPeer(), chat.Photo) + info.Members.PowerLevels = t.getPowerLevelOverridesFromBannedRights(chat, chat.DefaultBannedRights) + left = chat.Left + if chat.Broadcast { + info.Members.MemberMap.Set(bridgev2.ChatMember{ + EventSender: bridgev2.EventSender{Sender: ids.MakeChannelUserID(chat.GetID())}, + PowerLevel: superadminPowerLevel, + }) + } else if chat.Megagroup && !t.main.Config.ShouldBridge(chat.ParticipantsCount) { + // TODO change this to a better error whenever that is implemented in mautrix-go + return nil, nil, fmt.Errorf("too many participants (%d) in chat %d", chat.ParticipantsCount, chat.GetID()) + } + default: + return nil, nil, fmt.Errorf("unsupported chat type %T", rawChat) + } + if avatarErr != nil { + return nil, nil, fmt.Errorf("failed to wrap chat avatar: %w", avatarErr) + } + if !left { + info.Members.MemberMap.Add(bridgev2.ChatMember{EventSender: t.mySender()}) + } + info.ExtraUpdates = func(ctx context.Context, portal *bridgev2.Portal) bool { + meta := portal.Metadata.(*PortalMetadata) + _ = updatePortalLastSyncAt(ctx, portal) + changed := meta.SetIsSuperGroup(isMegagroup) + if info.Members.TotalMemberCount != 0 && meta.ParticipantsCount != info.Members.TotalMemberCount { + meta.ParticipantsCount = info.Members.TotalMemberCount + changed = true + } + return changed + } + return &info, &memberFetchMeta{Input: channelInput, IsBroadcast: isBroadcast}, nil +} + +func (t *TelegramClient) getChannelParticipants(ctx context.Context, req *tg.ChannelsGetParticipantsRequest) (*tg.ChannelsChannelParticipants, error) { + return APICallWithUpdates(ctx, t, func() (*tg.ChannelsChannelParticipants, error) { + p, err := t.client.API().ChannelsGetParticipants(ctx, req) + if err != nil { + return nil, err + } + participants, _ := p.(*tg.ChannelsChannelParticipants) + return participants, nil + }) +} + +func (t *TelegramClient) fillChannelMembers(ctx context.Context, mfm *memberFetchMeta, info *bridgev2.ChatMemberList) error { + if mfm.Input == nil || mfm.ParticipantsHidden || (mfm.IsBroadcast && !t.main.Config.MemberList.SyncBroadcastChannels) { + return nil + } + memberSyncLimit := t.main.Config.MemberList.NormalizedMaxInitialSync() + + if memberSyncLimit <= 200 { + participants, err := t.getChannelParticipants(ctx, &tg.ChannelsGetParticipantsRequest{ + Channel: mfm.Input, + Filter: &tg.ChannelParticipantsRecent{}, + Limit: memberSyncLimit, + }) + if err != nil || participants == nil { + return err + } + info.IsFull = len(participants.Participants) < memberSyncLimit && + len(participants.Participants) >= info.TotalMemberCount && + info.TotalMemberCount > 0 + for participant := range t.filterChannelParticipants(participants.Participants, memberSyncLimit) { + info.MemberMap.Set(participant) + } + } else { + remaining := memberSyncLimit + var offset int + for remaining > 0 { + participants, err := t.getChannelParticipants(ctx, &tg.ChannelsGetParticipantsRequest{ + Channel: mfm.Input, + Filter: &tg.ChannelParticipantsSearch{}, + Limit: min(remaining, 200), + Offset: offset, + }) + if err != nil || participants == nil { + return err } + if len(participants.Participants) == 0 { + info.IsFull = len(info.MemberMap) >= info.TotalMemberCount && + info.TotalMemberCount > 0 + break + } + + for participant := range t.filterChannelParticipants(participants.Participants, remaining) { + info.MemberMap.Set(participant) + } + + offset += len(participants.Participants) + remaining -= len(participants.Participants) + } + } + return nil +} + +func (t *TelegramClient) fillUserLocalMeta(info *bridgev2.ChatInfo, dialog *tg.Dialog) { + info.UserLocal = &bridgev2.UserLocalPortalInfo{} + if mu, ok := dialog.NotifySettings.GetMuteUntil(); ok { + info.UserLocal.MutedUntil = ptr.Ptr(time.Unix(int64(mu), 0)) + } else { + info.UserLocal.MutedUntil = &bridgev2.Unmuted + } + if dialog.Pinned { + info.UserLocal.Tag = ptr.Ptr(event.RoomTagFavourite) + } +} + +func (t *TelegramClient) wrapFullChatInfo(fullChat *tg.MessagesChatFull) (*bridgev2.ChatInfo, *memberFetchMeta, error) { + var chat tg.ChatClass + for _, c := range fullChat.GetChats() { + if c.GetID() == fullChat.FullChat.GetID() { + chat = c break } } - if !found { - return nil, false, fmt.Errorf("chat ID %d not found in full chat", chatID) + if chat == nil { + return nil, nil, fmt.Errorf("chat ID %d not found in full chat", fullChat.FullChat.GetID()) } - chatInfo := bridgev2.ChatInfo{ - Name: name, - Type: ptr.Ptr(database.RoomTypeDefault), - Members: &bridgev2.ChatMemberList{ - IsFull: true, - MemberMap: bridgev2.ChatMemberMap{}, + info, mfm, err := t.wrapChatInfo(chat) + if err != nil { + return nil, nil, err + } - ExcludeChangesFromTimeline: true, - }, - CanBackfill: true, - ExcludeChangesFromTimeline: true, - ExtraUpdates: func(ctx context.Context, p *bridgev2.Portal) bool { - meta := p.Metadata.(*PortalMetadata) - _ = updatePortalLastSyncAt(ctx, p) - _ = meta.SetIsSuperGroup(isMegagroup) - meta.ParticipantsCount = participantsCount - - if reactions, ok := fullChat.FullChat.GetAvailableReactions(); ok { - switch typedReactions := reactions.(type) { - case *tg.ChatReactionsAll: - meta.AllowedReactions = nil - case *tg.ChatReactionsNone: - meta.AllowedReactions = []string{} - case *tg.ChatReactionsSome: - allowedReactions := make([]string, 0, len(typedReactions.Reactions)) - for _, react := range typedReactions.Reactions { - emoji, ok := react.(*tg.ReactionEmoji) - if ok { - allowedReactions = append(allowedReactions, emoji.Emoticon) - } - } - slices.Sort(allowedReactions) - if !slices.Equal(meta.AllowedReactions, allowedReactions) { - meta.AllowedReactions = allowedReactions - } + var newAllowedReactions []string + if reactions, ok := fullChat.FullChat.GetAvailableReactions(); ok { + switch typedReactions := reactions.(type) { + case *tg.ChatReactionsAll: + newAllowedReactions = nil + case *tg.ChatReactionsNone: + newAllowedReactions = []string{} + case *tg.ChatReactionsSome: + newAllowedReactions = make([]string, 0, len(typedReactions.Reactions)) + for _, react := range typedReactions.Reactions { + emoji, ok := react.(*tg.ReactionEmoji) + if ok { + newAllowedReactions = append(newAllowedReactions, emoji.Emoticon) } } - - return true - }, + slices.Sort(newAllowedReactions) + } } - if !left { - chatInfo.Members.MemberMap.Add(bridgev2.ChatMember{EventSender: t.mySender()}) - } - if ttl, ok := fullChat.FullChat.GetTTLPeriod(); ok { - chatInfo.Disappear = &database.DisappearingSetting{ + info.Disappear = &database.DisappearingSetting{ Type: event.DisappearingTypeAfterSend, Timer: time.Duration(ttl) * time.Second, } } - if about := fullChat.FullChat.GetAbout(); about != "" { - chatInfo.Topic = &about + info.Topic = &about + } + info.ExtraUpdates = bridgev2.MergeExtraUpdaters( + info.ExtraUpdates, + reactionUpdater(newAllowedReactions), + markFullSynced, + ) + + switch typedFullChat := fullChat.FullChat.(type) { + case *tg.ChatFull: + participants, _ := typedFullChat.GetParticipants().(*tg.ChatParticipants) + memberSyncLimit := t.main.Config.MemberList.NormalizedMaxInitialSync() + info.Members.IsFull = true + for i, user := range participants.GetParticipants() { + var powerLevel *int + switch user.(type) { + case *tg.ChatParticipantCreator: + powerLevel = creatorPowerLevel + case *tg.ChatParticipantAdmin: + powerLevel = modPowerLevel + default: + powerLevel = ptr.Ptr(0) + } + + info.Members.MemberMap.Set(bridgev2.ChatMember{ + EventSender: t.senderForUserID(user.GetUserID()), + PowerLevel: powerLevel, + }) + + if i >= memberSyncLimit { + info.Members.IsFull = false + break + } + } + case *tg.ChannelFull: + mfm.ParticipantsHidden = !typedFullChat.CanViewParticipants || typedFullChat.ParticipantsHidden } - return &chatInfo, isBroadcastChannel, nil + return info, mfm, nil +} + +func reactionUpdater(newAllowedReactions []string) bridgev2.ExtraUpdater[*bridgev2.Portal] { + return func(ctx context.Context, portal *bridgev2.Portal) bool { + meta := portal.Metadata.(*PortalMetadata) + if newAllowedReactions == nil { + if meta.AllowedReactions == nil { + return false + } + meta.AllowedReactions = nil + return true + } + if meta.AllowedReactions == nil || !slices.Equal(newAllowedReactions, meta.AllowedReactions) { + meta.AllowedReactions = newAllowedReactions + return true + } + return false + } +} + +func markFullSynced(ctx context.Context, portal *bridgev2.Portal) bool { + meta := portal.Metadata.(*PortalMetadata) + if !meta.FullSynced { + meta.FullSynced = true + return true + } + return false } func (t *TelegramClient) avatarFromPhoto(ctx context.Context, peerType ids.PeerType, peerID int64, photo tg.PhotoClass) *bridgev2.Avatar { @@ -265,14 +437,11 @@ func (t *TelegramClient) filterChannelParticipants(participants []tg.ChannelPart } func (t *TelegramClient) GetChatInfo(ctx context.Context, portal *bridgev2.Portal) (*bridgev2.ChatInfo, error) { - // FIXME GetFullChat should be avoided. Using only bundled info should be preferred whenever possible - // (e.g. when syncing dialogs, only use the data in the dialog list, don't fetch each chat info separately). peerType, id, err := ids.ParsePortalID(portal.ID) if err != nil { return nil, err } - memberSyncLimit := t.main.Config.MemberList.NormalizedMaxInitialSync() switch peerType { case ids.PeerTypeUser: return t.getDMChatInfo(ctx, id) @@ -283,50 +452,8 @@ func (t *TelegramClient) GetChatInfo(ctx context.Context, portal *bridgev2.Porta if err != nil { return nil, err } - chatInfo, _, err := t.getGroupChatInfo(fullChat, id) - if err != nil { - return nil, err - } - - chatFull, ok := fullChat.FullChat.(*tg.ChatFull) - if !ok { - return nil, fmt.Errorf("full chat is %T not *tg.ChatFull", fullChat.FullChat) - } - chatInfo.Avatar = t.avatarFromPhoto(ctx, peerType, id, chatFull.ChatPhoto) - chatInfo.Members.PowerLevels = t.getGroupChatPowerLevels(ctx, fullChat.GetChats()[0]) - - if chatFull.Participants.TypeID() == tg.ChatParticipantsForbiddenTypeID { - chatInfo.Members.IsFull = false - return chatInfo, nil - } - chatParticipants := chatFull.Participants.(*tg.ChatParticipants) - - if !t.main.Config.ShouldBridge(len(chatParticipants.Participants)) { - // TODO change this to a better error whenever that is implemented in mautrix-go - return nil, fmt.Errorf("too many participants (%d) in chat %d", len(chatParticipants.Participants), id) - } - - for _, user := range chatParticipants.GetParticipants() { - var powerLevel *int - switch user.(type) { - case *tg.ChatParticipantCreator: - powerLevel = creatorPowerLevel - case *tg.ChatParticipantAdmin: - powerLevel = modPowerLevel - default: - powerLevel = ptr.Ptr(0) - } - - chatInfo.Members.MemberMap.Set(bridgev2.ChatMember{ - EventSender: t.senderForUserID(user.GetUserID()), - PowerLevel: powerLevel, - }) - - if len(chatInfo.Members.MemberMap) >= memberSyncLimit { - break - } - } - return chatInfo, nil + info, _, err := t.wrapFullChatInfo(fullChat) + return info, err case ids.PeerTypeChannel: accessHash, err := t.ScopedStore.GetAccessHash(ctx, ids.PeerTypeChannel, id) if err != nil { @@ -339,115 +466,17 @@ func (t *TelegramClient) GetChatInfo(ctx context.Context, portal *bridgev2.Porta if err != nil { return nil, err } - - chatInfo, isBroadcastChannel, err := t.getGroupChatInfo(fullChat, id) + info, mfm, err := t.wrapFullChatInfo(fullChat) if err != nil { return nil, err } - - channelFull, ok := fullChat.FullChat.(*tg.ChannelFull) - if !ok { - return nil, fmt.Errorf("full chat is %T not *tg.ChannelFull", fullChat.FullChat) + err = t.fillChannelMembers(ctx, mfm, info.Members) + if err != nil { + zerolog.Ctx(ctx).Err(err).Msg("Failed to get channel members") } - - if portal.Metadata.(*PortalMetadata).IsSuperGroup && !t.main.Config.ShouldBridge(channelFull.ParticipantsCount) { - // TODO change this to a better error whenever that is implemented in mautrix-go - return nil, fmt.Errorf("too many participants (%d) in chat %d", channelFull.ParticipantsCount, id) - } - - chatInfo.Avatar = t.avatarFromPhoto(ctx, peerType, id, channelFull.ChatPhoto) - - // TODO save available reactions? - // TODO save reactions limit? - // TODO save emojiset? - - chatInfo.Members.IsFull = false - chatInfo.Members.PowerLevels = t.getGroupChatPowerLevels(ctx, fullChat.GetChats()[0]) - if !portal.Metadata.(*PortalMetadata).IsSuperGroup { - // Add the channel user - chatInfo.Members.MemberMap.Set(bridgev2.ChatMember{ - EventSender: bridgev2.EventSender{Sender: ids.MakeChannelUserID(id)}, - PowerLevel: superadminPowerLevel, - }) - } - - // Just return the current user as a member if we can't view the - // participants or the max initial sync is 0. - if t.main.Config.MemberList.MaxInitialSync == 0 || !channelFull.CanViewParticipants || channelFull.ParticipantsHidden { - return chatInfo, nil - } - - // If this is a broadcast channel and we're not syncing broadcast - // channels, just return the chat info without all of the participant - // info. - if isBroadcastChannel && !t.main.Config.MemberList.SyncBroadcastChannels { - return chatInfo, nil - } - - if memberSyncLimit <= 200 { - participants, err := APICallWithUpdates(ctx, t, func() (*tg.ChannelsChannelParticipants, error) { - p, err := t.client.API().ChannelsGetParticipants(ctx, &tg.ChannelsGetParticipantsRequest{ - Channel: inputChannel, - Filter: &tg.ChannelParticipantsRecent{}, - Limit: memberSyncLimit, - }) - if err != nil { - return nil, err - } - participants, ok := p.(*tg.ChannelsChannelParticipants) - if !ok { - return nil, fmt.Errorf("returned participants is %T not *tg.ChannelsChannelParticipants", p) - } else { - return participants, nil - } - }) - if err != nil { - return nil, err - } - chatInfo.Members.IsFull = len(participants.Participants) < memberSyncLimit - for participant := range t.filterChannelParticipants(participants.Participants, memberSyncLimit) { - chatInfo.Members.MemberMap.Set(participant) - } - } else { - remaining := memberSyncLimit - var offset int - for remaining > 0 { - participants, err := APICallWithUpdates(ctx, t, func() (*tg.ChannelsChannelParticipants, error) { - p, err := t.client.API().ChannelsGetParticipants(ctx, &tg.ChannelsGetParticipantsRequest{ - Channel: inputChannel, - Filter: &tg.ChannelParticipantsSearch{}, - Limit: min(remaining, 200), - Offset: offset, - }) - if err != nil { - return nil, err - } - participants, ok := p.(*tg.ChannelsChannelParticipants) - if !ok { - return nil, fmt.Errorf("returned participants is %T not *tg.ChannelsChannelParticipants", p) - } else { - return participants, nil - } - }) - if err != nil { - return nil, err - } - if len(participants.Participants) == 0 { - chatInfo.Members.IsFull = true - break - } - - for participant := range t.filterChannelParticipants(participants.Participants, remaining) { - chatInfo.Members.MemberMap.Set(participant) - } - - offset += len(participants.Participants) - remaining -= len(participants.Participants) - } - } - return chatInfo, nil + return info, nil default: - panic(fmt.Sprintf("unsupported peer type %s", peerType)) + return nil, fmt.Errorf("unsupported peer type %s", peerType) } } @@ -469,34 +498,6 @@ func (t *TelegramClient) getDMPowerLevels(ghost *bridgev2.Ghost) *bridgev2.Power return &plo } -func (t *TelegramClient) getGroupChatPowerLevels(ctx context.Context, entity tg.ChatClass) *bridgev2.PowerLevelOverrides { - log := zerolog.Ctx(ctx).With(). - Str("action", "get_group_chat_power_levels"). - Logger() - - dbrAble, ok := entity.(interface { - GetDefaultBannedRights() (tg.ChatBannedRights, bool) - }) - var dbr tg.ChatBannedRights - if ok { - dbr, ok = dbrAble.GetDefaultBannedRights() - if !ok { - dbr = tg.ChatBannedRights{ - InviteUsers: true, - ChangeInfo: true, - PinMessages: true, - SendStickers: false, - SendMessages: false, - } - } - } else { - log.Error(). - Type("entity_type", entity). - Msg("couldn't get default banned rights from entity, assuming you don't have any rights") - } - return t.getPowerLevelOverridesFromBannedRights(entity, dbr) -} - func (t *TelegramClient) getPowerLevelOverridesFromBannedRights(entity tg.ChatClass, dbr tg.ChatBannedRights) *bridgev2.PowerLevelOverrides { var plo bridgev2.PowerLevelOverrides plo.Ban = banUsersPowerLevel diff --git a/pkg/connector/directdownload.go b/pkg/connector/directdownload.go index 8802fb9a..228b0b9f 100644 --- a/pkg/connector/directdownload.go +++ b/pkg/connector/directdownload.go @@ -186,31 +186,12 @@ func (tc *TelegramConnector) Download(ctx context.Context, mediaID networkid.Med return nil, fmt.Errorf("failed to create user photo transferer: %w", err) } } else if info.PeerType == ids.PeerTypeChat { - fullChat, err := APICallWithUpdates(ctx, client, func() (*tg.MessagesChatFull, error) { - return client.client.API().MessagesGetFullChat(ctx, info.PeerID) - }) + readyTransferer = transferer.WithPeerPhoto(&tg.InputPeerChat{ChatID: info.PeerID}, info.ID) + } else if info.PeerType == ids.PeerTypeChannel { + readyTransferer, err = transferer.WithChannelPhoto(ctx, client.ScopedStore, info.PeerID, info.ID) if err != nil { return nil, err } - - chatFull, ok := fullChat.FullChat.(*tg.ChatFull) - if !ok { - return nil, fmt.Errorf("full chat is %T not *tg.ChatFull", fullChat.FullChat) - } else if chatFull.ChatPhoto == nil { - // FIXME: these 2 are basically not found errors - return nil, fmt.Errorf("photo not found on chat") - } else if photoID := chatFull.ChatPhoto.GetID(); photoID != info.ID { - return nil, fmt.Errorf("photo id mismatch: %d != %d", photoID, info.ID) - } - - readyTransferer = transferer.WithPhoto(chatFull.ChatPhoto) - } else if info.PeerType == ids.PeerTypeChannel { - accessHash, err := client.ScopedStore.GetAccessHash(ctx, ids.PeerTypeChannel, info.PeerID) - if err != nil { - return nil, fmt.Errorf("failed to get channel access hash: %w", err) - } - - readyTransferer = transferer.WithChannelPhoto(info.PeerID, accessHash, info.ID) } else if info.PeerType == ids.FakePeerTypeEmoji { customEmojiDocuments, err := client.client.API().MessagesGetCustomEmojiDocuments(ctx, []int64{info.ID}) if err != nil { diff --git a/pkg/connector/handlematrix.go b/pkg/connector/handlematrix.go index 9d369830..c73e9f06 100644 --- a/pkg/connector/handlematrix.go +++ b/pkg/connector/handlematrix.go @@ -95,7 +95,7 @@ func (t *TelegramClient) HandleMatrixViewingChat(ctx context.Context, msg *bridg return nil } meta := msg.Portal.Metadata.(*PortalMetadata) - if meta.LastSync.Add(24 * time.Hour).Before(time.Now()) { + if !meta.FullSynced || meta.LastSync.Add(24*time.Hour).Before(time.Now()) { t.userLogin.QueueRemoteEvent(&simplevent.ChatResync{ EventMeta: simplevent.EventMeta{ Type: bridgev2.RemoteEventChatResync, @@ -737,6 +737,17 @@ func (t *TelegramClient) HandleMatrixReadReceipt(ctx context.Context, msg *bridg reactionPollErr = t.pollForReactions(ctx, msg.Portal.PortalKey, inputPeer) }() + if peerType == ids.PeerTypeChannel && !msg.Portal.Metadata.(*PortalMetadata).FullSynced { + log.Debug().Msg("Scheduling chat resync on read receipt because channel has never got a full sync") + go t.userLogin.QueueRemoteEvent(&simplevent.ChatResync{ + EventMeta: simplevent.EventMeta{ + Type: bridgev2.RemoteEventChatResync, + PortalKey: msg.Portal.PortalKey, + }, + GetChatInfoFunc: t.GetChatInfo, + }) + } + wg.Wait() return errors.Join(readMentionsErr, readReactionsErr, readMessagesErr, reactionPollErr) } diff --git a/pkg/connector/handletelegram.go b/pkg/connector/handletelegram.go index d88192aa..fb0ea581 100644 --- a/pkg/connector/handletelegram.go +++ b/pkg/connector/handletelegram.go @@ -798,6 +798,8 @@ func (t *TelegramClient) updateChannel(ctx context.Context, channel *tg.Channel) } } + // TODO resync portal metadata? + if !channel.Broadcast { return nil, nil } @@ -810,7 +812,7 @@ func (t *TelegramClient) updateChannel(ctx context.Context, channel *tg.Channel) var avatar *bridgev2.Avatar if photo, ok := channel.GetPhoto().(*tg.ChatPhoto); ok { - avatar, err = t.convertChatPhoto(ctx, channel.ID, channel.AccessHash, photo) + avatar, err = t.convertChatPhoto(channel.AsInputPeer(), photo) if err != nil { return nil, err } @@ -864,7 +866,10 @@ func (t *TelegramClient) onMessageEdit(ctx context.Context, update IGetMessage) return nil } - t.handleTelegramReactions(ctx, msg) + err := t.handleTelegramReactions(ctx, msg) + if err != nil { + zerolog.Ctx(ctx).Err(err).Msg("Failed to handle reactions on edited message") + } portalKey := t.makePortalKeyFromPeer(msg.PeerID) portal, err := t.main.Bridge.GetPortalByKey(ctx, portalKey) diff --git a/pkg/connector/media/transfer.go b/pkg/connector/media/transfer.go index e3048556..72bf3733 100644 --- a/pkg/connector/media/transfer.go +++ b/pkg/connector/media/transfer.go @@ -212,25 +212,29 @@ func (t *Transferer) WithUserPhoto(ctx context.Context, store *store.ScopedStore if accessHash, err := store.GetAccessHash(ctx, ids.PeerTypeUser, userID); err != nil { return nil, fmt.Errorf("failed to get user access hash for %d: %w", userID, err) } else { - return &ReadyTransferer{ - inner: t, - loc: &tg.InputPeerPhotoFileLocation{ - Peer: &tg.InputPeerUser{UserID: userID, AccessHash: accessHash}, - PhotoID: photoID, - Big: true, - }, - }, nil + return t.WithPeerPhoto(&tg.InputPeerUser{UserID: userID, AccessHash: accessHash}, photoID), nil } } // WithChannelPhoto transforms a [Transferer] to a [ReadyTransferer] by setting -// the given chat photo as the location that will be downloaded by the +// the given channel's photo as the location that will be downloaded by the // [ReadyTransferer]. -func (t *Transferer) WithChannelPhoto(channelID, accessHash, photoID int64) *ReadyTransferer { +func (t *Transferer) WithChannelPhoto(ctx context.Context, store *store.ScopedStore, channelID int64, photoID int64) (*ReadyTransferer, error) { + if accessHash, err := store.GetAccessHash(ctx, ids.PeerTypeChannel, channelID); err != nil { + return nil, fmt.Errorf("failed to get channel access hash for %d: %w", channelID, err) + } else { + return t.WithPeerPhoto(&tg.InputPeerChannel{ChannelID: channelID, AccessHash: accessHash}, photoID), nil + } +} + +// WithPeerPhoto transforms a [Transferer] to a [ReadyTransferer] by setting +// the given user, chat or channel photo as the location that will be downloaded by the +// [ReadyTransferer]. +func (t *Transferer) WithPeerPhoto(peer tg.InputPeerClass, photoID int64) *ReadyTransferer { return &ReadyTransferer{ inner: t, loc: &tg.InputPeerPhotoFileLocation{ - Peer: &tg.InputPeerChannel{ChannelID: channelID, AccessHash: accessHash}, + Peer: peer, PhotoID: photoID, Big: true, }, diff --git a/pkg/connector/metadata.go b/pkg/connector/metadata.go index 116f3481..7905223c 100644 --- a/pkg/connector/metadata.go +++ b/pkg/connector/metadata.go @@ -53,6 +53,7 @@ type PortalMetadata struct { ReadUpTo int `json:"read_up_to,omitempty"` AllowedReactions []string `json:"allowed_reactions"` LastSync jsontime.Unix `json:"last_sync,omitempty"` + FullSynced bool `json:"full_synced,omitempty"` ParticipantsCount int `json:"member_count,omitempty"` } diff --git a/pkg/connector/sync.go b/pkg/connector/sync.go index de8cfc83..970112db 100644 --- a/pkg/connector/sync.go +++ b/pkg/connector/sync.go @@ -20,15 +20,12 @@ import ( "context" "fmt" "math" - "time" "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" "go.mau.fi/mautrix-telegram/pkg/gotd/tg" @@ -97,10 +94,10 @@ func (t *TelegramClient) handleDialogs(ctx context.Context, dialogs tg.ModifiedM var created int for _, d := range dialogs.GetDialogs() { - if d.TypeID() != tg.DialogTypeID { + dialog, ok := d.(*tg.Dialog) + if !ok { continue } - dialog := d.(*tg.Dialog) log := log.With(). Stringer("peer", dialog.Peer). @@ -151,6 +148,7 @@ func (t *TelegramClient) handleDialogs(ctx context.Context, dialogs tg.ModifiedM Msg("Not syncing portal because chat type is unsupported") continue } + // Need to get full chat info to get the member list chatInfo, err = t.GetChatInfo(ctx, portal) if err != nil { return fmt.Errorf("failed to get chat info for %s: %w", portalKey, err) @@ -169,10 +167,15 @@ func (t *TelegramClient) handleDialogs(ctx context.Context, dialogs tg.ModifiedM Msg("Not syncing portal because channel type is unsupported") continue } - chatInfo, err = t.GetChatInfo(ctx, portal) + var mfm *memberFetchMeta + chatInfo, mfm, err = t.wrapChatInfo(channel) if err != nil { return fmt.Errorf("failed to get chat info for %s: %w", portalKey, err) } + err = t.fillChannelMembers(ctx, mfm, chatInfo.Members) + if err != nil { + log.Err(err).Msg("Failed to get channel members") + } } if portal == nil || portal.MXID == "" { @@ -199,14 +202,7 @@ func (t *TelegramClient) handleDialogs(ctx context.Context, dialogs tg.ModifiedM } } - 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.fillUserLocalMeta(chatInfo, dialog) res := t.main.Bridge.QueueRemoteEvent(t.userLogin, &simplevent.ChatResync{ ChatInfo: chatInfo, diff --git a/pkg/connector/tomatrix.go b/pkg/connector/tomatrix.go index 159cf820..89914e4b 100644 --- a/pkg/connector/tomatrix.go +++ b/pkg/connector/tomatrix.go @@ -809,15 +809,39 @@ func (c *TelegramClient) convertUserProfilePhoto(ctx context.Context, userID int return avatar, nil } -func (c *TelegramClient) convertChatPhoto(ctx context.Context, channelID, accessHash int64, chatPhoto *tg.ChatPhoto) (*bridgev2.Avatar, error) { +func (c *TelegramClient) convertChatPhoto(chat tg.InputPeerClass, rawChatPhoto tg.ChatPhotoClass) (*bridgev2.Avatar, error) { + var chatPhoto *tg.ChatPhoto + switch typedChatPhoto := rawChatPhoto.(type) { + case *tg.ChatPhotoEmpty: + return &bridgev2.Avatar{Remove: true}, nil + case *tg.ChatPhoto: + chatPhoto = typedChatPhoto + default: + return nil, fmt.Errorf("not a chat photo: %T", rawChatPhoto) + } avatar := &bridgev2.Avatar{ ID: ids.MakeAvatarID(chatPhoto.PhotoID), } if c.main.useDirectMedia { + var peerID int64 + var peerType ids.PeerType + switch typedChat := chat.(type) { + case *tg.InputPeerChannel: + peerID = typedChat.ChannelID + peerType = ids.PeerTypeChannel + case *tg.InputPeerChat: + peerID = typedChat.ChatID + peerType = ids.PeerTypeChat + case *tg.InputPeerUser: + peerID = typedChat.UserID + peerType = ids.PeerTypeUser + default: + return nil, fmt.Errorf("unsupported chat type for chat photo: %T", chat) + } mediaID, err := ids.DirectMediaInfo{ - PeerType: ids.PeerTypeChannel, - PeerID: channelID, + PeerType: peerType, + PeerID: peerID, UserID: c.telegramUserID, ID: chatPhoto.PhotoID, }.AsMediaID() @@ -825,13 +849,14 @@ func (c *TelegramClient) convertChatPhoto(ctx context.Context, channelID, access return nil, err } - if avatar.MXC, err = c.main.Bridge.Matrix.GenerateContentURI(ctx, mediaID); err != nil { + todoRemove := c.main.Bridge.BackgroundCtx // TODO remove context parameter from GenerateContentURI + if avatar.MXC, err = c.main.Bridge.Matrix.GenerateContentURI(todoRemove, mediaID); err != nil { return nil, err } avatar.Hash = ids.HashMediaID(mediaID) } else { avatar.Get = func(ctx context.Context) (data []byte, err error) { - return media.NewTransferer(c.client.API()).WithChannelPhoto(channelID, accessHash, chatPhoto.PhotoID).DownloadBytes(ctx) + return media.NewTransferer(c.client.API()).WithPeerPhoto(chat, chatPhoto.PhotoID).DownloadBytes(ctx) } }