655 lines
21 KiB
Go
655 lines
21 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"
|
|
"crypto/sha256"
|
|
"fmt"
|
|
"iter"
|
|
"slices"
|
|
"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/event"
|
|
|
|
"go.mau.fi/mautrix-telegram/pkg/connector/ids"
|
|
"go.mau.fi/mautrix-telegram/pkg/gotd/tg"
|
|
)
|
|
|
|
var (
|
|
mutedPowerLevel = ptr.Ptr(-1)
|
|
anyonePowerLevel = ptr.Ptr(0)
|
|
modPowerLevel = ptr.Ptr(50)
|
|
superadminPowerLevel = ptr.Ptr(75)
|
|
creatorPowerLevel = ptr.Ptr(95)
|
|
nobodyPowerLevel = ptr.Ptr(99)
|
|
|
|
otherPowerLevel = ptr.Ptr(40)
|
|
anonymousPowerLevel = ptr.Ptr(41)
|
|
postMessagesPowerLevel = ptr.Ptr(42)
|
|
editMessagesPowerLevel = ptr.Ptr(43)
|
|
deleteMessagesPowerLevel = ptr.Ptr(44)
|
|
postStoriesPowerLevel = ptr.Ptr(45)
|
|
editStoriesPowerLevel = ptr.Ptr(46)
|
|
deleteStoriesPowerLevel = ptr.Ptr(47)
|
|
changeInfoPowerLevel = ptr.Ptr(50)
|
|
inviteUsersPowerLevel = ptr.Ptr(51)
|
|
manageCallPowerLevel = ptr.Ptr(52)
|
|
pinMessagesPowerLevel = ptr.Ptr(53)
|
|
manageTopicsPowerLevel = ptr.Ptr(54)
|
|
banUsersPowerLevel = ptr.Ptr(55)
|
|
addAdminsPowerLevel = ptr.Ptr(60)
|
|
)
|
|
|
|
func adminRightsToPowerLevel(rights tg.ChatAdminRights) *int {
|
|
if rights.AddAdmins {
|
|
return addAdminsPowerLevel
|
|
} else if rights.BanUsers {
|
|
return banUsersPowerLevel
|
|
} else if rights.ManageTopics {
|
|
return manageTopicsPowerLevel
|
|
} else if rights.PinMessages {
|
|
return pinMessagesPowerLevel
|
|
} else if rights.ManageCall {
|
|
return manageCallPowerLevel
|
|
} else if rights.InviteUsers {
|
|
return inviteUsersPowerLevel
|
|
} else if rights.ChangeInfo {
|
|
return changeInfoPowerLevel
|
|
} else if rights.DeleteStories {
|
|
return deleteStoriesPowerLevel
|
|
} else if rights.EditStories {
|
|
return editStoriesPowerLevel
|
|
} else if rights.PostStories {
|
|
return postStoriesPowerLevel
|
|
} else if rights.DeleteMessages {
|
|
return deleteMessagesPowerLevel
|
|
} else if rights.EditMessages {
|
|
return editMessagesPowerLevel
|
|
} else if rights.PostMessages {
|
|
return postMessagesPowerLevel
|
|
} else if rights.Anonymous {
|
|
return anonymousPowerLevel
|
|
}
|
|
return otherPowerLevel
|
|
}
|
|
|
|
func (tc *TelegramClient) getDMChatInfo(ctx context.Context, userID int64) (*bridgev2.ChatInfo, error) {
|
|
ghost, err := tc.main.Bridge.GetGhostByID(ctx, ids.MakeUserID(userID))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
chatInfo := bridgev2.ChatInfo{
|
|
Type: ptr.Ptr(database.RoomTypeDM),
|
|
Members: &bridgev2.ChatMemberList{
|
|
IsFull: true,
|
|
MemberMap: map[networkid.UserID]bridgev2.ChatMember{},
|
|
PowerLevels: tc.getDMPowerLevels(ghost),
|
|
},
|
|
CanBackfill: !tc.metadata.IsBot,
|
|
ExtraUpdates: updatePortalLastSyncAt,
|
|
}
|
|
chatInfo.Members.MemberMap.Add(bridgev2.ChatMember{EventSender: tc.mySender()})
|
|
chatInfo.Members.MemberMap.Add(bridgev2.ChatMember{EventSender: tc.senderForUserID(userID)})
|
|
if userID == tc.telegramUserID {
|
|
chatInfo.Avatar = &bridgev2.Avatar{
|
|
ID: networkid.AvatarID(tc.main.Config.SavedMessagesAvatar),
|
|
Remove: len(tc.main.Config.SavedMessagesAvatar) == 0,
|
|
MXC: tc.main.Config.SavedMessagesAvatar,
|
|
Hash: sha256.Sum256([]byte(tc.main.Config.SavedMessagesAvatar)),
|
|
}
|
|
chatInfo.Name = ptr.Ptr("Telegram Saved Messages")
|
|
chatInfo.Topic = ptr.Ptr("Your Telegram cloud storage chat")
|
|
}
|
|
return &chatInfo, nil
|
|
}
|
|
|
|
func isBroadcastChannel(chat tg.ChatClass) bool {
|
|
switch c := chat.(type) {
|
|
case *tg.Channel:
|
|
return c.Broadcast
|
|
default:
|
|
return false
|
|
}
|
|
}
|
|
|
|
type memberFetchMeta struct {
|
|
Input *tg.InputChannel
|
|
IsBroadcast bool
|
|
ParticipantsHidden bool
|
|
IsForum bool
|
|
}
|
|
|
|
func (tc *TelegramClient) wrapChatInfo(portalID networkid.PortalID, rawChat tg.ChatClass) (*bridgev2.ChatInfo, *memberFetchMeta, error) {
|
|
info := bridgev2.ChatInfo{
|
|
Type: ptr.Ptr(database.RoomTypeDefault),
|
|
CanBackfill: !tc.metadata.IsBot,
|
|
Members: &bridgev2.ChatMemberList{
|
|
ExcludeChangesFromTimeline: true,
|
|
MemberMap: bridgev2.ChatMemberMap{},
|
|
},
|
|
ExcludeChangesFromTimeline: true,
|
|
}
|
|
var mfm memberFetchMeta
|
|
var isMegagroup, isForumGeneral, left bool
|
|
var avatarErr error
|
|
var ownPL *int
|
|
switch chat := rawChat.(type) {
|
|
case *tg.Chat:
|
|
info.Name = &chat.Title
|
|
info.Members.TotalMemberCount = chat.ParticipantsCount
|
|
info.Avatar, avatarErr = tc.convertChatPhoto(chat.AsInputPeer(), chat.Photo)
|
|
info.Members.PowerLevels = tc.getPowerLevelOverridesFromBannedRights(chat, chat.DefaultBannedRights)
|
|
left = chat.Left
|
|
if chat.Creator {
|
|
ownPL = creatorPowerLevel
|
|
} else if rights, isAdmin := chat.GetAdminRights(); isAdmin {
|
|
ownPL = adminRightsToPowerLevel(rights)
|
|
} else {
|
|
ownPL = anyonePowerLevel
|
|
}
|
|
case *tg.Channel:
|
|
mfm.Input = chat.AsInput()
|
|
mfm.IsBroadcast = chat.Broadcast
|
|
info.Name = &chat.Title
|
|
info.Members.TotalMemberCount = chat.ParticipantsCount
|
|
isMegagroup = chat.Megagroup
|
|
info.Avatar, avatarErr = tc.convertChatPhoto(chat.AsInputPeer(), chat.Photo)
|
|
info.Members.PowerLevels = tc.getPowerLevelOverridesFromBannedRights(chat, chat.DefaultBannedRights)
|
|
if chat.Creator {
|
|
ownPL = creatorPowerLevel
|
|
} else if rights, isAdmin := chat.GetAdminRights(); isAdmin {
|
|
ownPL = adminRightsToPowerLevel(rights)
|
|
} else {
|
|
ownPL = anyonePowerLevel
|
|
}
|
|
_, _, topicID, _ := ids.ParsePortalID(portalID)
|
|
if chat.Forum {
|
|
if topicID == ids.TopicIDSpaceRoom {
|
|
info.Type = ptr.Ptr(database.RoomTypeSpace)
|
|
} else if topicID == 0 {
|
|
isForumGeneral = true
|
|
info.Name = ptr.Ptr("#General - " + *info.Name)
|
|
}
|
|
if topicID != ids.TopicIDSpaceRoom {
|
|
info.ParentID = ptr.Ptr(ids.MakeForumParentPortalID(chat.ID))
|
|
}
|
|
mfm.IsForum = true
|
|
} else if topicID != 0 {
|
|
return nil, nil, fmt.Errorf("channel %d is not a forum, cannot have topics", chat.GetID())
|
|
}
|
|
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 && !tc.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: tc.mySender(), PowerLevel: ownPL})
|
|
}
|
|
info.ExtraUpdates = func(ctx context.Context, portal *bridgev2.Portal) bool {
|
|
meta := portal.Metadata.(*PortalMetadata)
|
|
_ = updatePortalLastSyncAt(ctx, portal)
|
|
changed := meta.SetIsSuperGroup(isMegagroup)
|
|
changed = meta.SetIsForumGeneral(isForumGeneral) || changed
|
|
if info.Members.TotalMemberCount != 0 && meta.ParticipantsCount != info.Members.TotalMemberCount {
|
|
meta.ParticipantsCount = info.Members.TotalMemberCount
|
|
changed = true
|
|
}
|
|
return changed
|
|
}
|
|
return &info, &mfm, nil
|
|
}
|
|
|
|
func (tc *TelegramClient) overrideChatInfoWithTopic(info *bridgev2.ChatInfo, topic *tg.ForumTopic) {
|
|
info.Name = ptr.Ptr(topic.Title + " - " + *info.Name)
|
|
if topic.Closed {
|
|
info.Members.PowerLevels.EventsDefault = nobodyPowerLevel
|
|
}
|
|
}
|
|
|
|
func (tc *TelegramClient) getChannelParticipants(ctx context.Context, req *tg.ChannelsGetParticipantsRequest) (*tg.ChannelsChannelParticipants, error) {
|
|
return APICallWithUpdates(ctx, tc, func() (*tg.ChannelsChannelParticipants, error) {
|
|
p, err := tc.client.API().ChannelsGetParticipants(ctx, req)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
participants, _ := p.(*tg.ChannelsChannelParticipants)
|
|
return participants, nil
|
|
})
|
|
}
|
|
|
|
func (tc *TelegramClient) fillChannelMembers(ctx context.Context, mfm *memberFetchMeta, info *bridgev2.ChatMemberList) error {
|
|
if mfm.Input == nil || mfm.ParticipantsHidden || (mfm.IsBroadcast && !tc.main.Config.MemberList.SyncBroadcastChannels) {
|
|
return nil
|
|
}
|
|
memberSyncLimit := tc.main.Config.MemberList.NormalizedMaxInitialSync()
|
|
|
|
if memberSyncLimit <= 200 {
|
|
participants, err := tc.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 tc.filterChannelParticipants(participants.Participants, memberSyncLimit) {
|
|
info.MemberMap.Set(participant)
|
|
}
|
|
} else {
|
|
remaining := memberSyncLimit
|
|
var offset int
|
|
for remaining > 0 {
|
|
participants, err := tc.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 tc.filterChannelParticipants(participants.Participants, remaining) {
|
|
info.MemberMap.Set(participant)
|
|
}
|
|
|
|
offset += len(participants.Participants)
|
|
remaining -= len(participants.Participants)
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (tc *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 (tc *TelegramClient) wrapFullChatInfo(portalID networkid.PortalID, 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 chat == nil {
|
|
return nil, nil, fmt.Errorf("chat ID %d not found in full chat", fullChat.FullChat.GetID())
|
|
}
|
|
|
|
info, mfm, err := tc.wrapChatInfo(portalID, chat)
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
|
|
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)
|
|
}
|
|
}
|
|
slices.Sort(newAllowedReactions)
|
|
}
|
|
}
|
|
if ttl, ok := fullChat.FullChat.GetTTLPeriod(); ok {
|
|
info.Disappear = &database.DisappearingSetting{
|
|
Type: event.DisappearingTypeAfterSend,
|
|
Timer: time.Duration(ttl) * time.Second,
|
|
}
|
|
}
|
|
if about := fullChat.FullChat.GetAbout(); 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 := tc.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: tc.senderForUserID(user.GetUserID()),
|
|
PowerLevel: powerLevel,
|
|
})
|
|
|
|
if i >= memberSyncLimit {
|
|
info.Members.IsFull = false
|
|
break
|
|
}
|
|
}
|
|
case *tg.ChannelFull:
|
|
mfm.ParticipantsHidden = !typedFullChat.CanViewParticipants || typedFullChat.ParticipantsHidden
|
|
}
|
|
|
|
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 (tc *TelegramClient) avatarFromPhoto(ctx context.Context, peerType ids.PeerType, peerID int64, photo tg.PhotoClass) *bridgev2.Avatar {
|
|
if photo == nil {
|
|
zerolog.Ctx(ctx).Trace().Msg("Chat photo is nil, returning no avatar")
|
|
return nil
|
|
} else if photo.TypeID() != tg.PhotoTypeID {
|
|
zerolog.Ctx(ctx).Debug().Str("type_name", photo.TypeName()).Msg("Chat photo type unknown, returning no avatar")
|
|
return nil
|
|
}
|
|
avatar, err := tc.convertPhoto(ctx, peerType, peerID, photo)
|
|
if err != nil {
|
|
zerolog.Ctx(ctx).Err(err).Int64("id", photo.GetID()).Msg("Failed to convert avatar")
|
|
return nil
|
|
}
|
|
return avatar
|
|
}
|
|
|
|
func (tc *TelegramClient) filterChannelParticipants(participants []tg.ChannelParticipantClass, limit int) iter.Seq[bridgev2.ChatMember] {
|
|
return func(yield func(bridgev2.ChatMember) bool) {
|
|
for i, u := range participants {
|
|
var member bridgev2.ChatMember
|
|
switch participant := u.(type) {
|
|
case *tg.ChannelParticipant:
|
|
member.EventSender = tc.senderForUserID(participant.GetUserID())
|
|
member.PowerLevel = anyonePowerLevel
|
|
case *tg.ChannelParticipantSelf:
|
|
member.EventSender = tc.senderForUserID(participant.GetUserID())
|
|
member.PowerLevel = anyonePowerLevel
|
|
case *tg.ChannelParticipantCreator:
|
|
member.EventSender = tc.senderForUserID(participant.GetUserID())
|
|
member.PowerLevel = creatorPowerLevel
|
|
case *tg.ChannelParticipantAdmin:
|
|
member.EventSender = tc.senderForUserID(participant.GetUserID())
|
|
member.PowerLevel = adminRightsToPowerLevel(participant.AdminRights)
|
|
case *tg.ChannelParticipantBanned:
|
|
if participant.BannedRights.ViewMessages {
|
|
member.Membership = event.MembershipBan
|
|
} else if participant.Left {
|
|
member.Membership = event.MembershipLeave
|
|
}
|
|
if participant.BannedRights.SendMessages {
|
|
member.PowerLevel = mutedPowerLevel
|
|
} else {
|
|
member.PowerLevel = anyonePowerLevel
|
|
}
|
|
member.EventSender = tc.getPeerSender(participant.GetPeer())
|
|
member.MemberSender = tc.senderForUserID(participant.GetKickedBy())
|
|
case *tg.ChannelParticipantLeft:
|
|
member.Membership = event.MembershipLeave
|
|
member.PrevMembership = event.MembershipJoin
|
|
member.EventSender = tc.getPeerSender(participant.GetPeer())
|
|
default:
|
|
// TODO warning log?
|
|
continue
|
|
}
|
|
if i >= limit && member.Membership == "" && !member.EventSender.IsFromMe {
|
|
continue
|
|
}
|
|
|
|
if !yield(member) {
|
|
return
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func (tc *TelegramClient) GetChatInfo(ctx context.Context, portal *bridgev2.Portal) (*bridgev2.ChatInfo, error) {
|
|
peerType, id, topicID, err := ids.ParsePortalID(portal.ID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
switch peerType {
|
|
case ids.PeerTypeUser:
|
|
return tc.getDMChatInfo(ctx, id)
|
|
case ids.PeerTypeChat:
|
|
fullChat, err := APICallWithUpdates(ctx, tc, func() (*tg.MessagesChatFull, error) {
|
|
return tc.client.API().MessagesGetFullChat(ctx, id)
|
|
})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
info, _, err := tc.wrapFullChatInfo(portal.ID, fullChat)
|
|
return info, err
|
|
case ids.PeerTypeChannel:
|
|
accessHash, err := tc.ScopedStore.GetAccessHash(ctx, ids.PeerTypeChannel, id)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to get channel access hash: %w", err)
|
|
}
|
|
if topicID > 0 {
|
|
resp, err := APICallWithUpdates(ctx, tc, func() (*tg.MessagesForumTopics, error) {
|
|
return tc.client.API().MessagesGetForumTopicsByID(ctx, &tg.MessagesGetForumTopicsByIDRequest{
|
|
Peer: &tg.InputPeerChannel{ChannelID: id, AccessHash: accessHash},
|
|
Topics: []int{topicID},
|
|
})
|
|
})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
channel, topic, err := getTopicInfoFromResponse(resp, id, topicID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
info, _, err := tc.wrapChatInfo(portal.ID, channel)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
tc.overrideChatInfoWithTopic(info, topic)
|
|
return info, nil
|
|
}
|
|
fullChat, err := APICallWithUpdates(ctx, tc, func() (*tg.MessagesChatFull, error) {
|
|
return tc.client.API().ChannelsGetFullChannel(ctx, &tg.InputChannel{ChannelID: id, AccessHash: accessHash})
|
|
})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
info, mfm, err := tc.wrapFullChatInfo(portal.ID, fullChat)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
err = tc.fillChannelMembers(ctx, mfm, info.Members)
|
|
if err != nil {
|
|
zerolog.Ctx(ctx).Err(err).Msg("Failed to get channel members")
|
|
}
|
|
return info, nil
|
|
default:
|
|
return nil, fmt.Errorf("unsupported peer type %s", peerType)
|
|
}
|
|
}
|
|
|
|
func getTopicInfoFromResponse(resp *tg.MessagesForumTopics, channelID int64, topicID int) (channel *tg.Channel, topic *tg.ForumTopic, err error) {
|
|
var ok bool
|
|
for _, ch := range resp.GetChats() {
|
|
if ch.GetID() == channelID {
|
|
channel, ok = ch.(*tg.Channel)
|
|
if !ok {
|
|
return nil, nil, fmt.Errorf("chat ID %d is %T not *tg.Channel", channelID, ch)
|
|
}
|
|
break
|
|
}
|
|
}
|
|
if channel == nil {
|
|
return nil, nil, fmt.Errorf("channel ID %d not found in chats", channelID)
|
|
}
|
|
for _, tp := range resp.GetTopics() {
|
|
if tp.GetID() == topicID {
|
|
topic, ok = tp.(*tg.ForumTopic)
|
|
if !ok {
|
|
return nil, nil, fmt.Errorf("topic ID %d is %T not *tg.ForumTopic", topicID, tp)
|
|
}
|
|
break
|
|
}
|
|
}
|
|
if topic == nil {
|
|
return nil, nil, fmt.Errorf("topic ID %d not found in topics", topicID)
|
|
}
|
|
return
|
|
}
|
|
|
|
func (tc *TelegramClient) getDMPowerLevels(ghost *bridgev2.Ghost) *bridgev2.PowerLevelOverrides {
|
|
var plo bridgev2.PowerLevelOverrides
|
|
// TODO use per-login metadata for blocked status
|
|
if /*ghost.Metadata.(*GhostMetadata).Blocked*/ false {
|
|
// Don't allow sending messages to blocked users
|
|
plo.EventsDefault = superadminPowerLevel
|
|
} else {
|
|
plo.EventsDefault = anyonePowerLevel
|
|
}
|
|
plo.Events = map[event.Type]int{
|
|
event.StateRoomName: 0,
|
|
event.StateRoomAvatar: 0,
|
|
event.StateTopic: 0,
|
|
event.StateBeeperDisappearingTimer: 0,
|
|
event.BeeperDeleteChat: 0,
|
|
}
|
|
return &plo
|
|
}
|
|
|
|
func (tc *TelegramClient) getPowerLevelOverridesFromBannedRights(entity tg.ChatClass, dbr tg.ChatBannedRights) *bridgev2.PowerLevelOverrides {
|
|
var plo bridgev2.PowerLevelOverrides
|
|
plo.Ban = banUsersPowerLevel
|
|
plo.Kick = banUsersPowerLevel
|
|
plo.Redact = deleteMessagesPowerLevel
|
|
if dbr.InviteUsers {
|
|
plo.Invite = inviteUsersPowerLevel
|
|
} else {
|
|
plo.Invite = anyonePowerLevel
|
|
}
|
|
plo.StateDefault = superadminPowerLevel
|
|
plo.UsersDefault = anyonePowerLevel
|
|
if c, ok := entity.(*tg.Channel); (ok && !c.Megagroup) || dbr.SendMessages {
|
|
plo.EventsDefault = postMessagesPowerLevel
|
|
} else {
|
|
plo.EventsDefault = anyonePowerLevel
|
|
}
|
|
|
|
plo.Events = map[event.Type]int{
|
|
event.StateEncryption: 99,
|
|
event.StateTombstone: 99,
|
|
event.StatePowerLevels: 85,
|
|
event.StateHistoryVisibility: 85,
|
|
event.StateBeeperDisappearingTimer: 85,
|
|
event.BeeperDeleteChat: *creatorPowerLevel,
|
|
}
|
|
|
|
if dbr.ChangeInfo {
|
|
plo.Events[event.StateRoomName] = *changeInfoPowerLevel
|
|
plo.Events[event.StateRoomAvatar] = *changeInfoPowerLevel
|
|
plo.Events[event.StateTopic] = *changeInfoPowerLevel
|
|
plo.Events[event.StateBeeperDisappearingTimer] = *changeInfoPowerLevel
|
|
} else {
|
|
plo.Events[event.StateRoomName] = 0
|
|
plo.Events[event.StateRoomAvatar] = 0
|
|
plo.Events[event.StateTopic] = 0
|
|
plo.Events[event.StateBeeperDisappearingTimer] = 0
|
|
}
|
|
|
|
if dbr.PinMessages {
|
|
plo.Events[event.StatePinnedEvents] = *pinMessagesPowerLevel
|
|
} else {
|
|
plo.Events[event.StatePinnedEvents] = 0
|
|
}
|
|
|
|
if dbr.SendStickers {
|
|
plo.Events[event.EventSticker] = *postMessagesPowerLevel
|
|
} else {
|
|
plo.Events[event.EventSticker] = 0
|
|
}
|
|
|
|
return &plo
|
|
}
|