diff --git a/pkg/connector/client.go b/pkg/connector/client.go index de00d6a3..67e699b9 100644 --- a/pkg/connector/client.go +++ b/pkg/connector/client.go @@ -449,7 +449,7 @@ func (t *TelegramClient) onConnected(self *tg.User) { ghost, err := t.main.Bridge.GetGhostByID(ctx, t.userID) if err != nil { 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") } else { ghost.UpdateInfo(ctx, wrapped) diff --git a/pkg/connector/config.go b/pkg/connector/config.go index e1d6fab3..29f77625 100644 --- a/pkg/connector/config.go +++ b/pkg/connector/config.go @@ -56,16 +56,9 @@ type TelegramConfig struct { APIID int `yaml:"api_id"` APIHash string `yaml:"api_hash"` - DeviceInfo DeviceInfo `yaml:"device_info"` - - AnimatedSticker media.AnimatedStickerConfig `yaml:"animated_sticker"` - ImageAsFilePixels int `yaml:"image_as_file_pixels"` - - DisableViewOnce bool `yaml:"disable_view_once"` - - MemberList MemberListConfig `yaml:"member_list"` - - MaxMemberCount int `yaml:"max_member_count"` + DeviceInfo DeviceInfo `yaml:"device_info"` + AnimatedSticker media.AnimatedStickerConfig `yaml:"animated_sticker"` + MemberList MemberListConfig `yaml:"member_list"` Ping struct { IntervalSeconds int `yaml:"interval_seconds"` @@ -78,11 +71,14 @@ type TelegramConfig struct { DirectChats bool `yaml:"direct_chats"` } `yaml:"sync"` - AlwaysCustomEmojiReaction bool `yaml:"always_custom_emoji_reaction"` - - SavedMessagesAvatar id.ContentURIString `yaml:"saved_message_avatar"` - - AlwaysTombstoneOnSupergroupMigration bool `yaml:"always_tombstone_on_supergroup_migration"` + ContactAvatars bool `yaml:"contact_avatars"` + ContactNames bool `yaml:"contact_names"` + MaxMemberCount int `yaml:"max_member_count"` + AlwaysCustomEmojiReaction bool `yaml:"always_custom_emoji_reaction"` + 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 { @@ -107,20 +103,22 @@ func upgradeConfig(helper up.Helper) { helper.Copy(up.Int, "animated_sticker", "args", "width") helper.Copy(up.Int, "animated_sticker", "args", "height") 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.Bool, "member_list", "sync_broadcast_channels") 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", "timeout_seconds") helper.Copy(up.Int, "sync", "update_limit") helper.Copy(up.Int, "sync", "create_limit") 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.Str, "saved_message_avatar") 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) { @@ -132,6 +130,7 @@ func (tg *TelegramConnector) GetConfig() (example string, data any, upgrader up. {"member_list"}, {"ping"}, {"sync"}, + {"max_member_count"}, }, Base: ExampleConfig, } diff --git a/pkg/connector/example-config.yaml b/pkg/connector/example-config.yaml index bf13a57a..acf3d8b7 100644 --- a/pkg/connector/example-config.yaml +++ b/pkg/connector/example-config.yaml @@ -30,13 +30,6 @@ animated_sticker: height: 256 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. member_list: # Maximum number of members to sync per portal when starting up. Other @@ -53,12 +46,6 @@ member_list: sync_broadcast_channels: false # Whether or not to skip deleted members when syncing members. 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. ping: @@ -78,13 +65,26 @@ sync: # Whether or not to sync and create portals for direct chats at startup. 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 # Telegram? By default, the bridge only uses custom emojis for unicode emojis # that aren't allowed in reactions. always_custom_emoji_reaction: false - # The avatar to use for the Telegram Saved Messages chat saved_message_avatar: mxc://maunium.net/XhhfHoPejeneOngMyBbtyWDk - # Create a new room and tombstone the old one when upgrading rooms 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 diff --git a/pkg/connector/handletelegram.go b/pkg/connector/handletelegram.go index f635d13b..0dc2e053 100644 --- a/pkg/connector/handletelegram.go +++ b/pkg/connector/handletelegram.go @@ -716,12 +716,16 @@ func (t *TelegramClient) onUserName(ctx context.Context, e tg.Entities, update * if err != nil { return err } + meta := ghost.Metadata.(*GhostMetadata) var userInfo bridgev2.UserInfo - // TODO anti-contact-name logic name := util.FormatFullName(update.FirstName, update.LastName, false, update.UserID) 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 { for _, ident := range ghost.Identifiers { @@ -800,7 +804,7 @@ func (t *TelegramClient) updateGhost(ctx context.Context, userID int64, user *tg if err != nil { return nil, err } - userInfo, err := t.wrapUserInfo(ctx, user) + userInfo, err := t.wrapUserInfo(ctx, user, ghost) if err != nil { return nil, err } diff --git a/pkg/connector/metadata.go b/pkg/connector/metadata.go index a708ada5..8ebecdba 100644 --- a/pkg/connector/metadata.go +++ b/pkg/connector/metadata.go @@ -44,6 +44,13 @@ type GhostMetadata struct { IsBot bool `json:"is_bot,omitempty"` IsChannel bool `json:"is_channel,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 { diff --git a/pkg/connector/startchat.go b/pkg/connector/startchat.go index 223b52ef..d4896d7d 100644 --- a/pkg/connector/startchat.go +++ b/pkg/connector/startchat.go @@ -44,19 +44,23 @@ var ( func (t *TelegramClient) resolveUser(ctx context.Context, user tg.UserClass) (*bridgev2.ResolveIdentifierResponse, error) { networkUserID := ids.MakeUserID(user.GetID()) - if userInfo, err := t.wrapUserInfo(ctx, user); 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 { + if ghost, err := t.main.Bridge.GetGhostByID(ctx, networkUserID); err != nil { 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 { - return &bridgev2.ResolveIdentifierResponse{ - Ghost: ghost, - UserID: networkUserID, - UserInfo: userInfo, - Chat: &bridgev2.CreateChatResponse{ - PortalKey: t.makePortalKeyFromID(ids.PeerTypeUser, user.GetID(), 0), - }, - }, nil + return t.makeResolveIdentifierResponse(ghost, user, userInfo), nil + } +} + +func (t *TelegramClient) makeResolveIdentifierResponse(ghost *bridgev2.Ghost, user tg.UserClass, info *bridgev2.UserInfo) *bridgev2.ResolveIdentifierResponse { + return &bridgev2.ResolveIdentifierResponse{ + Ghost: ghost, + 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) } else if user.TypeID() != tg.UserTypeID { 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) } 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 diff --git a/pkg/connector/userinfo.go b/pkg/connector/userinfo.go index 13a78e8f..3b06c85b 100644 --- a/pkg/connector/userinfo.go +++ b/pkg/connector/userinfo.go @@ -27,7 +27,7 @@ func (t *TelegramClient) GetUserInfo(ctx context.Context, ghost *bridgev2.Ghost) if user, err := t.getSingleUser(ctx, id); err != nil { return nil, fmt.Errorf("failed to get user %d: %w", id, err) } else { - return t.wrapUserInfo(ctx, user) + return t.wrapUserInfo(ctx, user, ghost) } case ids.PeerTypeChannel: if channel, err := t.getSingleChannel(ctx, id); err != nil { @@ -151,7 +151,8 @@ func (t *TelegramClient) wrapChannelGhostInfo(ctx context.Context, channel *tg.C }, 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) if !ok { 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) var avatar *bridgev2.Avatar - // TODO don't apply min avatars without checking apply_min_photo or existing min status - if p, ok := user.GetPhoto(); ok && p.TypeID() == tg.UserProfilePhotoTypeID { - photo := p.(*tg.UserProfilePhoto) + photo, ok := user.Photo.(*tg.UserProfilePhoto) + if ok && + (!user.Min || user.ApplyMinPhoto || oldMeta.IsMin()) && + (!photo.Personal || t.main.Config.ContactAvatars) { var err error avatar, err = t.convertUserProfilePhoto(ctx, user, photo) 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) + namePtr := &name + if user.Contact && ghost.Name != "" && oldMeta.ContactSource != t.telegramUserID && oldMeta.ContactSource != 0 && !t.main.Config.ContactNames { + namePtr = nil + } return &bridgev2.UserInfo{ IsBot: &user.Bot, - Name: &name, + Name: namePtr, Avatar: avatar, Identifiers: identifiers, ExtraUpdates: func(ctx context.Context, ghost *bridgev2.Ghost) (changed bool) { meta := ghost.Metadata.(*GhostMetadata) 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.IsBot = user.Bot meta.Deleted = user.Deleted + meta.NotMin = true + if meta.ContactSource == 0 { + meta.ContactSource = t.telegramUserID + } } return changed },