userinfo: add support for avoiding contact names/avatars

This commit is contained in:
Tulir Asokan
2026-03-15 16:14:55 +02:00
parent 62efa2e7b9
commit 0aed201869
7 changed files with 83 additions and 56 deletions
+1 -1
View File
@@ -449,7 +449,7 @@ func (t *TelegramClient) onConnected(self *tg.User) {
ghost, err := t.main.Bridge.GetGhostByID(ctx, t.userID) ghost, err := t.main.Bridge.GetGhostByID(ctx, t.userID)
if err != nil { if err != nil {
log.Err(err).Msg("Failed to get own ghost") log.Err(err).Msg("Failed to get own ghost")
} else if wrapped, err := t.wrapUserInfo(ctx, self); err != nil { } else if wrapped, err := t.wrapUserInfo(ctx, self, ghost); err != nil {
log.Err(err).Msg("Failed to wrap own user info") log.Err(err).Msg("Failed to wrap own user info")
} else { } else {
ghost.UpdateInfo(ctx, wrapped) ghost.UpdateInfo(ctx, wrapped)
+17 -18
View File
@@ -56,16 +56,9 @@ type TelegramConfig struct {
APIID int `yaml:"api_id"` APIID int `yaml:"api_id"`
APIHash string `yaml:"api_hash"` APIHash string `yaml:"api_hash"`
DeviceInfo DeviceInfo `yaml:"device_info"` DeviceInfo DeviceInfo `yaml:"device_info"`
AnimatedSticker media.AnimatedStickerConfig `yaml:"animated_sticker"`
AnimatedSticker media.AnimatedStickerConfig `yaml:"animated_sticker"` MemberList MemberListConfig `yaml:"member_list"`
ImageAsFilePixels int `yaml:"image_as_file_pixels"`
DisableViewOnce bool `yaml:"disable_view_once"`
MemberList MemberListConfig `yaml:"member_list"`
MaxMemberCount int `yaml:"max_member_count"`
Ping struct { Ping struct {
IntervalSeconds int `yaml:"interval_seconds"` IntervalSeconds int `yaml:"interval_seconds"`
@@ -78,11 +71,14 @@ type TelegramConfig struct {
DirectChats bool `yaml:"direct_chats"` DirectChats bool `yaml:"direct_chats"`
} `yaml:"sync"` } `yaml:"sync"`
AlwaysCustomEmojiReaction bool `yaml:"always_custom_emoji_reaction"` ContactAvatars bool `yaml:"contact_avatars"`
ContactNames bool `yaml:"contact_names"`
SavedMessagesAvatar id.ContentURIString `yaml:"saved_message_avatar"` MaxMemberCount int `yaml:"max_member_count"`
AlwaysCustomEmojiReaction bool `yaml:"always_custom_emoji_reaction"`
AlwaysTombstoneOnSupergroupMigration bool `yaml:"always_tombstone_on_supergroup_migration"` SavedMessagesAvatar id.ContentURIString `yaml:"saved_message_avatar"`
AlwaysTombstoneOnSupergroupMigration bool `yaml:"always_tombstone_on_supergroup_migration"`
ImageAsFilePixels int `yaml:"image_as_file_pixels"`
DisableViewOnce bool `yaml:"disable_view_once"`
} }
func (c TelegramConfig) ShouldBridge(participantCount int) bool { func (c TelegramConfig) ShouldBridge(participantCount int) bool {
@@ -107,20 +103,22 @@ func upgradeConfig(helper up.Helper) {
helper.Copy(up.Int, "animated_sticker", "args", "width") helper.Copy(up.Int, "animated_sticker", "args", "width")
helper.Copy(up.Int, "animated_sticker", "args", "height") helper.Copy(up.Int, "animated_sticker", "args", "height")
helper.Copy(up.Int, "animated_sticker", "args", "fps") helper.Copy(up.Int, "animated_sticker", "args", "fps")
helper.Copy(up.Int, "image_as_file_pixels")
helper.Copy(up.Bool, "disable_view_once")
helper.Copy(up.Int, "member_list", "max_initial_sync") helper.Copy(up.Int, "member_list", "max_initial_sync")
helper.Copy(up.Bool, "member_list", "sync_broadcast_channels") helper.Copy(up.Bool, "member_list", "sync_broadcast_channels")
helper.Copy(up.Bool, "member_list", "skip_deleted") helper.Copy(up.Bool, "member_list", "skip_deleted")
helper.Copy(up.Int, "max_member_count")
helper.Copy(up.Int, "ping", "interval_seconds") helper.Copy(up.Int, "ping", "interval_seconds")
helper.Copy(up.Int, "ping", "timeout_seconds") helper.Copy(up.Int, "ping", "timeout_seconds")
helper.Copy(up.Int, "sync", "update_limit") helper.Copy(up.Int, "sync", "update_limit")
helper.Copy(up.Int, "sync", "create_limit") helper.Copy(up.Int, "sync", "create_limit")
helper.Copy(up.Bool, "sync", "direct_chats") helper.Copy(up.Bool, "sync", "direct_chats")
helper.Copy(up.Bool, "contact_avatars")
helper.Copy(up.Bool, "contact_names")
helper.Copy(up.Int, "max_member_count")
helper.Copy(up.Bool, "always_custom_emoji_reaction") helper.Copy(up.Bool, "always_custom_emoji_reaction")
helper.Copy(up.Str, "saved_message_avatar") helper.Copy(up.Str, "saved_message_avatar")
helper.Copy(up.Bool, "always_tombstone_on_supergroup_migration") helper.Copy(up.Bool, "always_tombstone_on_supergroup_migration")
helper.Copy(up.Int, "image_as_file_pixels")
helper.Copy(up.Bool, "disable_view_once")
} }
func (tg *TelegramConnector) GetConfig() (example string, data any, upgrader up.Upgrader) { func (tg *TelegramConnector) GetConfig() (example string, data any, upgrader up.Upgrader) {
@@ -132,6 +130,7 @@ func (tg *TelegramConnector) GetConfig() (example string, data any, upgrader up.
{"member_list"}, {"member_list"},
{"ping"}, {"ping"},
{"sync"}, {"sync"},
{"max_member_count"},
}, },
Base: ExampleConfig, Base: ExampleConfig,
} }
+15 -15
View File
@@ -30,13 +30,6 @@ animated_sticker:
height: 256 height: 256
fps: 25 # only for webm, webp and gif (2, 5, 10, 20 or 25 recommended) fps: 25 # only for webm, webp and gif (2, 5, 10, 20 or 25 recommended)
# Maximum number of pixels in an image before sending to Telegram as a
# document. Defaults to 4096x4096 = 16777216.
image_as_file_pixels: 16777216
# Should view-once messages be disabled entirely?
disable_view_once: false
# Settings for syncing the member list for portals. # Settings for syncing the member list for portals.
member_list: member_list:
# Maximum number of members to sync per portal when starting up. Other # Maximum number of members to sync per portal when starting up. Other
@@ -53,12 +46,6 @@ member_list:
sync_broadcast_channels: false sync_broadcast_channels: false
# Whether or not to skip deleted members when syncing members. # Whether or not to skip deleted members when syncing members.
skip_deleted: true skip_deleted: true
# Maximum number of participants in chats to bridge. Only applies when the
# portal is being created. If there are more members when trying to create a
# room, the room creation will be cancelled.
#
# -1 means no limit (which means all chats can be bridged)
max_member_count: -1
# Settings for pings to the Telegram server. # Settings for pings to the Telegram server.
ping: ping:
@@ -78,13 +65,26 @@ sync:
# Whether or not to sync and create portals for direct chats at startup. # Whether or not to sync and create portals for direct chats at startup.
direct_chats: false direct_chats: false
# Maximum number of participants in chats to bridge. Only applies when the
# portal is being created. If there are more members when trying to create a
# room, the room creation will be cancelled.
#
# -1 means no limit (which means all chats can be bridged)
max_member_count: -1
# Should personal avatars (that are only visible to specific users) be allowed?
contact_avatars: false
# Should contact names be updated from any source even if a name is already set?
contact_names: false
# Should the bridge send all unicode reactions as custom emoji reactions to # Should the bridge send all unicode reactions as custom emoji reactions to
# Telegram? By default, the bridge only uses custom emojis for unicode emojis # Telegram? By default, the bridge only uses custom emojis for unicode emojis
# that aren't allowed in reactions. # that aren't allowed in reactions.
always_custom_emoji_reaction: false always_custom_emoji_reaction: false
# The avatar to use for the Telegram Saved Messages chat # The avatar to use for the Telegram Saved Messages chat
saved_message_avatar: mxc://maunium.net/XhhfHoPejeneOngMyBbtyWDk saved_message_avatar: mxc://maunium.net/XhhfHoPejeneOngMyBbtyWDk
# Create a new room and tombstone the old one when upgrading rooms # Create a new room and tombstone the old one when upgrading rooms
always_tombstone_on_supergroup_migration: false always_tombstone_on_supergroup_migration: false
# Maximum number of pixels in an image before sending to Telegram as a
# document. Defaults to 4096x4096 = 16777216.
image_as_file_pixels: 16777216
# Should view-once messages be disabled entirely?
disable_view_once: false
+6 -2
View File
@@ -716,12 +716,16 @@ func (t *TelegramClient) onUserName(ctx context.Context, e tg.Entities, update *
if err != nil { if err != nil {
return err return err
} }
meta := ghost.Metadata.(*GhostMetadata)
var userInfo bridgev2.UserInfo var userInfo bridgev2.UserInfo
// TODO anti-contact-name logic
name := util.FormatFullName(update.FirstName, update.LastName, false, update.UserID) name := util.FormatFullName(update.FirstName, update.LastName, false, update.UserID)
userInfo.Name = &name userInfo.Name = &name
if meta.ContactSource != 0 && meta.ContactSource != t.telegramUserID && !t.main.Config.ContactNames {
// TODO fetch full info to accurately detect if the user is a contact or not
userInfo.Name = nil
}
if len(update.Usernames) > 0 { if len(update.Usernames) > 0 {
for _, ident := range ghost.Identifiers { for _, ident := range ghost.Identifiers {
@@ -800,7 +804,7 @@ func (t *TelegramClient) updateGhost(ctx context.Context, userID int64, user *tg
if err != nil { if err != nil {
return nil, err return nil, err
} }
userInfo, err := t.wrapUserInfo(ctx, user) userInfo, err := t.wrapUserInfo(ctx, user, ghost)
if err != nil { if err != nil {
return nil, err return nil, err
} }
+7
View File
@@ -44,6 +44,13 @@ type GhostMetadata struct {
IsBot bool `json:"is_bot,omitempty"` IsBot bool `json:"is_bot,omitempty"`
IsChannel bool `json:"is_channel,omitempty"` IsChannel bool `json:"is_channel,omitempty"`
Deleted bool `json:"deleted,omitempty"` Deleted bool `json:"deleted,omitempty"`
NotMin bool `json:"not_min,omitempty"`
ContactSource int64 `json:"contact_source,omitempty"`
}
func (gm *GhostMetadata) IsMin() bool {
return !gm.NotMin
} }
type PortalMetadata struct { type PortalMetadata struct {
+20 -13
View File
@@ -44,19 +44,23 @@ var (
func (t *TelegramClient) resolveUser(ctx context.Context, user tg.UserClass) (*bridgev2.ResolveIdentifierResponse, error) { func (t *TelegramClient) resolveUser(ctx context.Context, user tg.UserClass) (*bridgev2.ResolveIdentifierResponse, error) {
networkUserID := ids.MakeUserID(user.GetID()) networkUserID := ids.MakeUserID(user.GetID())
if userInfo, err := t.wrapUserInfo(ctx, user); err != nil { if ghost, err := t.main.Bridge.GetGhostByID(ctx, networkUserID); err != nil {
return nil, fmt.Errorf("failed to get user info: %w", err)
} else if ghost, err := t.main.Bridge.GetGhostByID(ctx, networkUserID); err != nil {
return nil, fmt.Errorf("failed to get ghost: %w", err) return nil, fmt.Errorf("failed to get ghost: %w", err)
} else if userInfo, err := t.wrapUserInfo(ctx, user, ghost); err != nil {
return nil, fmt.Errorf("failed to get user info: %w", err)
} else { } else {
return &bridgev2.ResolveIdentifierResponse{ return t.makeResolveIdentifierResponse(ghost, user, userInfo), nil
Ghost: ghost, }
UserID: networkUserID, }
UserInfo: userInfo,
Chat: &bridgev2.CreateChatResponse{ func (t *TelegramClient) makeResolveIdentifierResponse(ghost *bridgev2.Ghost, user tg.UserClass, info *bridgev2.UserInfo) *bridgev2.ResolveIdentifierResponse {
PortalKey: t.makePortalKeyFromID(ids.PeerTypeUser, user.GetID(), 0), return &bridgev2.ResolveIdentifierResponse{
}, Ghost: ghost,
}, nil UserID: ids.MakeUserID(user.GetID()),
UserInfo: info,
Chat: &bridgev2.CreateChatResponse{
PortalKey: t.makePortalKeyFromID(ids.PeerTypeUser, user.GetID(), 0),
},
} }
} }
@@ -93,10 +97,13 @@ func (t *TelegramClient) resolveUserID(ctx context.Context, userID int64) (resp
return nil, fmt.Errorf("failed to get user with ID %d: %w", userID, err) return nil, fmt.Errorf("failed to get user with ID %d: %w", userID, err)
} else if user.TypeID() != tg.UserTypeID { } else if user.TypeID() != tg.UserTypeID {
return nil, fmt.Errorf("unexpected user type: %T", user) return nil, fmt.Errorf("unexpected user type: %T", user)
} else if _, err = t.updateGhost(ctx, userID, user.(*tg.User)); err != nil { } else if userInfo, err := t.updateGhost(ctx, userID, user.(*tg.User)); err != nil {
return nil, fmt.Errorf("failed to update ghost: %w", err) return nil, fmt.Errorf("failed to update ghost: %w", err)
} else { } else {
return t.resolveUser(ctx, user) if resp.Ghost == nil {
resp.Ghost, _ = t.main.Bridge.GetExistingGhostByID(ctx, networkUserID)
}
return t.makeResolveIdentifierResponse(resp.Ghost, user, userInfo), nil
} }
} }
return return
+17 -7
View File
@@ -27,7 +27,7 @@ func (t *TelegramClient) GetUserInfo(ctx context.Context, ghost *bridgev2.Ghost)
if user, err := t.getSingleUser(ctx, id); err != nil { if user, err := t.getSingleUser(ctx, id); err != nil {
return nil, fmt.Errorf("failed to get user %d: %w", id, err) return nil, fmt.Errorf("failed to get user %d: %w", id, err)
} else { } else {
return t.wrapUserInfo(ctx, user) return t.wrapUserInfo(ctx, user, ghost)
} }
case ids.PeerTypeChannel: case ids.PeerTypeChannel:
if channel, err := t.getSingleChannel(ctx, id); err != nil { if channel, err := t.getSingleChannel(ctx, id); err != nil {
@@ -151,7 +151,8 @@ func (t *TelegramClient) wrapChannelGhostInfo(ctx context.Context, channel *tg.C
}, nil }, nil
} }
func (t *TelegramClient) wrapUserInfo(ctx context.Context, u tg.UserClass) (*bridgev2.UserInfo, error) { func (t *TelegramClient) wrapUserInfo(ctx context.Context, u tg.UserClass, ghost *bridgev2.Ghost) (*bridgev2.UserInfo, error) {
oldMeta := ghost.Metadata.(*GhostMetadata)
user, ok := u.(*tg.User) user, ok := u.(*tg.User)
if !ok { if !ok {
return nil, fmt.Errorf("user is %T not *tg.User", user) return nil, fmt.Errorf("user is %T not *tg.User", user)
@@ -186,9 +187,10 @@ func (t *TelegramClient) wrapUserInfo(ctx context.Context, u tg.UserClass) (*bri
identifiers = slices.Compact(identifiers) identifiers = slices.Compact(identifiers)
var avatar *bridgev2.Avatar var avatar *bridgev2.Avatar
// TODO don't apply min avatars without checking apply_min_photo or existing min status photo, ok := user.Photo.(*tg.UserProfilePhoto)
if p, ok := user.GetPhoto(); ok && p.TypeID() == tg.UserProfilePhotoTypeID { if ok &&
photo := p.(*tg.UserProfilePhoto) (!user.Min || user.ApplyMinPhoto || oldMeta.IsMin()) &&
(!photo.Personal || t.main.Config.ContactAvatars) {
var err error var err error
avatar, err = t.convertUserProfilePhoto(ctx, user, photo) avatar, err = t.convertUserProfilePhoto(ctx, user, photo)
if err != nil { if err != nil {
@@ -197,18 +199,26 @@ func (t *TelegramClient) wrapUserInfo(ctx context.Context, u tg.UserClass) (*bri
} }
name := util.FormatFullName(user.FirstName, user.LastName, user.Deleted, user.ID) name := util.FormatFullName(user.FirstName, user.LastName, user.Deleted, user.ID)
namePtr := &name
if user.Contact && ghost.Name != "" && oldMeta.ContactSource != t.telegramUserID && oldMeta.ContactSource != 0 && !t.main.Config.ContactNames {
namePtr = nil
}
return &bridgev2.UserInfo{ return &bridgev2.UserInfo{
IsBot: &user.Bot, IsBot: &user.Bot,
Name: &name, Name: namePtr,
Avatar: avatar, Avatar: avatar,
Identifiers: identifiers, Identifiers: identifiers,
ExtraUpdates: func(ctx context.Context, ghost *bridgev2.Ghost) (changed bool) { ExtraUpdates: func(ctx context.Context, ghost *bridgev2.Ghost) (changed bool) {
meta := ghost.Metadata.(*GhostMetadata) meta := ghost.Metadata.(*GhostMetadata)
if !user.Min { if !user.Min {
changed = changed || meta.IsPremium != user.Premium || meta.IsBot != user.Bot changed = changed || meta.IsPremium != user.Premium || meta.IsBot != user.Bot || meta.IsMin()
meta.IsPremium = user.Premium meta.IsPremium = user.Premium
meta.IsBot = user.Bot meta.IsBot = user.Bot
meta.Deleted = user.Deleted meta.Deleted = user.Deleted
meta.NotMin = true
if meta.ContactSource == 0 {
meta.ContactSource = t.telegramUserID
}
} }
return changed return changed
}, },