diff --git a/pkg/connector/client.go b/pkg/connector/client.go index 66dc095e..8932781a 100644 --- a/pkg/connector/client.go +++ b/pkg/connector/client.go @@ -47,7 +47,6 @@ import ( "go.mau.fi/mautrix-telegram/pkg/connector/matrixfmt" "go.mau.fi/mautrix-telegram/pkg/connector/store" "go.mau.fi/mautrix-telegram/pkg/connector/telegramfmt" - "go.mau.fi/mautrix-telegram/pkg/connector/util" "go.mau.fi/mautrix-telegram/pkg/gotd/telegram" "go.mau.fi/mautrix-telegram/pkg/gotd/telegram/auth" "go.mau.fi/mautrix-telegram/pkg/gotd/telegram/updates" @@ -430,12 +429,12 @@ func (t *TelegramClient) onPing() { } } -func userToRemoteProfile( +func (t *TelegramConnector) userToRemoteProfile( self *tg.User, ghost *bridgev2.Ghost, prevState *status.RemoteProfile, ) (profile status.RemoteProfile, name string) { - profile.Name = util.FormatFullName(self.FirstName, self.LastName, self.Deleted, self.ID) + profile.Name = t.Config.FormatDisplayname(self.FirstName, self.LastName, self.Username, self.Deleted, self.ID) if self.Phone != "" { profile.Phone = "+" + strings.TrimPrefix(self.Phone, "+") } else if prevState != nil { @@ -455,7 +454,7 @@ func userToRemoteProfile( } func (t *TelegramClient) updateRemoteProfile(ctx context.Context, self *tg.User, ghost *bridgev2.Ghost) bool { - newProfile, newName := userToRemoteProfile(self, ghost, &t.userLogin.RemoteProfile) + newProfile, newName := t.main.userToRemoteProfile(self, ghost, &t.userLogin.RemoteProfile) if t.userLogin.RemoteProfile != newProfile || t.userLogin.RemoteName != newName { t.userLogin.RemoteProfile = newProfile t.userLogin.RemoteName = newName diff --git a/pkg/connector/config.go b/pkg/connector/config.go index 5d14bd34..569ce4e3 100644 --- a/pkg/connector/config.go +++ b/pkg/connector/config.go @@ -20,8 +20,11 @@ import ( _ "embed" "fmt" "slices" + "strings" + "text/template" up "go.mau.fi/util/configupgrade" + "gopkg.in/yaml.v3" "maunium.net/go/mautrix/bridgev2" "maunium.net/go/mautrix/bridgev2/bridgeconfig" "maunium.net/go/mautrix/id" @@ -86,12 +89,55 @@ type TelegramConfig struct { AlwaysTombstoneOnSupergroupMigration bool `yaml:"always_tombstone_on_supergroup_migration"` ImageAsFilePixels int `yaml:"image_as_file_pixels"` DisableViewOnce bool `yaml:"disable_view_once"` + DisplaynameTemplate string `yaml:"displayname_template"` + displaynameTemplate *template.Template `yaml:"-"` } func (c TelegramConfig) ShouldBridge(participantCount int) bool { return c.MaxMemberCount < 0 || participantCount <= c.MaxMemberCount } +type DisplaynameParams struct { + FullName string + FirstName string + LastName string + Username string + UserID int64 + Deleted bool +} + +func (c *TelegramConfig) FormatDisplayname(firstName, lastName, username string, deleted bool, userID int64) string { + var buf strings.Builder + err := c.displaynameTemplate.Execute(&buf, DisplaynameParams{ + FullName: strings.TrimSpace(firstName + " " + lastName), + FirstName: firstName, + LastName: lastName, + Username: username, + UserID: userID, + Deleted: deleted, + }) + if err != nil { + panic(fmt.Errorf("displayname template is broken: %w", err)) + } + return buf.String() +} + +type umConfig TelegramConfig + +func (c *TelegramConfig) UnmarshalYAML(node *yaml.Node) error { + err := node.Decode((*umConfig)(c)) + if err != nil { + return err + } + return c.PostProcess() +} + +func (c *TelegramConfig) PostProcess() error { + var err error + c.displaynameTemplate, err = template.New("displayname").Parse(c.DisplaynameTemplate) + return err +} + //go:embed example-config.yaml var ExampleConfig string @@ -130,6 +176,7 @@ func upgradeConfig(helper up.Helper) { helper.Copy(up.Bool, "always_tombstone_on_supergroup_migration") helper.Copy(up.Int, "image_as_file_pixels") helper.Copy(up.Bool, "disable_view_once") + helper.Copy(up.Str, "displayname_template") } func (tg *TelegramConnector) GetConfig() (example string, data any, upgrader up.Upgrader) { diff --git a/pkg/connector/example-config.yaml b/pkg/connector/example-config.yaml index b1564468..8b43ab0e 100644 --- a/pkg/connector/example-config.yaml +++ b/pkg/connector/example-config.yaml @@ -104,3 +104,11 @@ always_tombstone_on_supergroup_migration: false image_as_file_pixels: 16777216 # Should view-once messages be disabled entirely? disable_view_once: false +# Displayname template for Telegram users. +# {{ .FullName }} - the full name of the Telegram user +# {{ .FirstName }} - the first name of the Telegram user +# {{ .LastName }} - the last name of the Telegram user +# {{ .Username }} - the primary username of the Telegram user, if the user has one +# {{ .UserID }} - the internal user ID of the Telegram user +# {{ .Deleted }} - true if the user has been deleted, false otherwise +displayname_template: "{{ if .Deleted }}Deleted account {{ .UserID }}{{ else }}{{ .FullName }}{{ end }}" diff --git a/pkg/connector/handletelegram.go b/pkg/connector/handletelegram.go index efe2b79f..95800b60 100644 --- a/pkg/connector/handletelegram.go +++ b/pkg/connector/handletelegram.go @@ -42,7 +42,6 @@ import ( "go.mau.fi/mautrix-telegram/pkg/connector/media" "go.mau.fi/mautrix-telegram/pkg/connector/store" "go.mau.fi/mautrix-telegram/pkg/connector/tljson" - "go.mau.fi/mautrix-telegram/pkg/connector/util" "go.mau.fi/mautrix-telegram/pkg/gotd/tg" "go.mau.fi/mautrix-telegram/pkg/gotd/tgerr" ) @@ -728,13 +727,7 @@ func (t *TelegramClient) onUserName(ctx context.Context, e tg.Entities, update * var userInfo bridgev2.UserInfo - 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 - } - + var firstUsername string if len(update.Usernames) > 0 { for _, ident := range ghost.Identifiers { if !strings.HasPrefix(ident, "telegram:") { @@ -742,7 +735,10 @@ func (t *TelegramClient) onUserName(ctx context.Context, e tg.Entities, update * } } - for _, username := range update.Usernames { + for i, username := range update.Usernames { + if i == 0 { + firstUsername = username.Username + } userInfo.Identifiers = append(userInfo.Identifiers, fmt.Sprintf("telegram:%s", username.Username)) } @@ -750,6 +746,13 @@ func (t *TelegramClient) onUserName(ctx context.Context, e tg.Entities, update * userInfo.Identifiers = slices.Compact(userInfo.Identifiers) } + name := t.main.Config.FormatDisplayname(update.FirstName, update.LastName, firstUsername, 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 + } + ghost.UpdateInfo(ctx, &userInfo) if ghost.ID == t.userID { var firstUsername string diff --git a/pkg/connector/login.go b/pkg/connector/login.go index 68b27a1e..ca564d6f 100644 --- a/pkg/connector/login.go +++ b/pkg/connector/login.go @@ -216,7 +216,7 @@ func (bl *baseLogin) finalizeLogin( } metadata.Session = bl.session metadata.LoginMethod = bl.flowID - profile, name := userToRemoteProfile(self, nil, nil) + profile, name := bl.main.userToRemoteProfile(self, nil, nil) userLoginID := ids.MakeUserLoginID(authorization.User.GetID()) ul, err := bl.user.NewLogin(ctx, &database.UserLogin{ ID: userLoginID, diff --git a/pkg/connector/tomatrix.go b/pkg/connector/tomatrix.go index 3720bba6..2744f6a1 100644 --- a/pkg/connector/tomatrix.go +++ b/pkg/connector/tomatrix.go @@ -41,7 +41,6 @@ import ( "go.mau.fi/mautrix-telegram/pkg/connector/media" "go.mau.fi/mautrix-telegram/pkg/connector/store" "go.mau.fi/mautrix-telegram/pkg/connector/telegramfmt" - "go.mau.fi/mautrix-telegram/pkg/connector/util" "go.mau.fi/mautrix-telegram/pkg/connector/waveform" "go.mau.fi/mautrix-telegram/pkg/gotd/tg" "go.mau.fi/mautrix-telegram/pkg/gotd/tgerr" @@ -729,7 +728,7 @@ func (c *TelegramClient) convertMediaRequiringUpload( func (c *TelegramClient) convertContact(media tg.MessageMediaClass) *bridgev2.ConvertedMessagePart { contact := media.(*tg.MessageMediaContact) - name := util.FormatFullName(contact.FirstName, contact.LastName, false, contact.UserID) + name := c.main.Config.FormatDisplayname(contact.FirstName, contact.LastName, "", false, contact.UserID) formattedPhone := fmt.Sprintf("+%s", strings.TrimPrefix(contact.PhoneNumber, "+")) content := event.MessageEventContent{ diff --git a/pkg/connector/userinfo.go b/pkg/connector/userinfo.go index e4f6d537..2cf61872 100644 --- a/pkg/connector/userinfo.go +++ b/pkg/connector/userinfo.go @@ -13,7 +13,6 @@ import ( "go.mau.fi/mautrix-telegram/pkg/connector/ids" "go.mau.fi/mautrix-telegram/pkg/connector/store" - "go.mau.fi/mautrix-telegram/pkg/connector/util" "go.mau.fi/mautrix-telegram/pkg/gotd/tg" ) @@ -258,7 +257,7 @@ func (t *TelegramClient) wrapUserInfo(ctx context.Context, u tg.UserClass, ghost } } - name := util.FormatFullName(user.FirstName, user.LastName, user.Deleted, user.ID) + name := t.main.Config.FormatDisplayname(user.FirstName, user.LastName, user.Username, user.Deleted, user.ID) namePtr := &name if user.Contact && ghost.Name != "" && oldMeta.ContactSource != t.telegramUserID && oldMeta.ContactSource != 0 && !t.main.Config.ContactNames { namePtr = nil diff --git a/pkg/connector/util/util.go b/pkg/connector/util/util.go deleted file mode 100644 index 4b9b4b8d..00000000 --- a/pkg/connector/util/util.go +++ /dev/null @@ -1,29 +0,0 @@ -// 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 . - -package util - -import ( - "fmt" - "strings" -) - -func FormatFullName(first, last string, deleted bool, userID int64) string { - if deleted { - return fmt.Sprintf("Deleted account %d", userID) - } - return strings.TrimSpace(fmt.Sprintf("%s %s", first, last)) -}