diff --git a/pkg/connector/client.go b/pkg/connector/client.go index 352e6b67..cc23e99d 100644 --- a/pkg/connector/client.go +++ b/pkg/connector/client.go @@ -584,27 +584,6 @@ func (t *TelegramClient) Disconnect() { t.userLogin.Log.Info().Msg("Disconnect complete") } -func (t *TelegramClient) getInputUser(ctx context.Context, id int64) (*tg.InputUser, error) { - accessHash, err := t.ScopedStore.GetAccessHash(ctx, ids.PeerTypeUser, id) - if err != nil { - return nil, fmt.Errorf("failed to get access hash for user %d: %w", id, err) - } - return &tg.InputUser{UserID: id, AccessHash: accessHash}, nil -} - -func (t *TelegramClient) getSingleUser(ctx context.Context, id int64) (tg.UserClass, error) { - if inputUser, err := t.getInputUser(ctx, id); err != nil { - return nil, err - } else if users, err := t.client.API().UsersGetUsers(ctx, []tg.InputUserClass{inputUser}); err != nil { - return nil, err - } else if len(users) == 0 { - // TODO does this mean the user is deleted? Need to handle this a bit better - return nil, fmt.Errorf("failed to get user info for user %d", id) - } else { - return users[0], nil - } -} - func (t *TelegramClient) getSingleChannel(ctx context.Context, id int64) (*tg.Channel, error) { accessHash, err := t.ScopedStore.GetAccessHash(ctx, ids.PeerTypeChannel, id) if err != nil { diff --git a/pkg/connector/startchat.go b/pkg/connector/startchat.go index 497f988d..223b52ef 100644 --- a/pkg/connector/startchat.go +++ b/pkg/connector/startchat.go @@ -42,7 +42,7 @@ var ( _ bridgev2.GroupCreatingNetworkAPI = (*TelegramClient)(nil) ) -func (t *TelegramClient) getResolveIdentifierResponseForUser(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()) if userInfo, err := t.wrapUserInfo(ctx, user); err != nil { return nil, fmt.Errorf("failed to get user info: %w", err) @@ -60,9 +60,19 @@ func (t *TelegramClient) getResolveIdentifierResponseForUser(ctx context.Context } } -func (t *TelegramClient) getResolveIdentifierResponseForUserID(ctx context.Context, userID int64) (resp *bridgev2.ResolveIdentifierResponse, err error) { +func (t *TelegramClient) resolveUserID(ctx context.Context, userID int64) (resp *bridgev2.ResolveIdentifierResponse, err error) { _, err = t.ScopedStore.GetAccessHash(ctx, ids.PeerTypeUser, userID) if errors.Is(err, store.ErrNoAccessHash) { + username, usernameErr := t.main.Store.Username.Get(ctx, ids.PeerTypeUser, userID) + if usernameErr != nil { + return nil, fmt.Errorf("failed to get username after missing access hash: %w", usernameErr) + } else if username != "" { + zerolog.Ctx(ctx).Debug(). + Str("target_username", username). + Int64("target_user_id", userID). + Msg("Access hash not found for user ID, trying to look up username") + return t.resolveUsername(ctx, username, userID) + } return nil, fmt.Errorf("%w: %w", bridgev2.ErrResolveIdentifierTryNext, err) } else if err != nil { return nil, fmt.Errorf("failed to get access hash from store: %w", err) @@ -76,20 +86,56 @@ func (t *TelegramClient) getResolveIdentifierResponseForUserID(ctx context.Conte } resp.Ghost, err = t.main.Bridge.GetExistingGhostByID(ctx, networkUserID) if err != nil { + return nil, fmt.Errorf("failed to get ghost: %w", err) + } else if resp.Ghost == nil || resp.Ghost.Name == "" { // Try to fetch the user from Telegram if user, err := t.getSingleUser(ctx, userID); err != nil { return nil, fmt.Errorf("failed to get user with ID %d: %w", userID, err) } else if user.TypeID() != tg.UserTypeID { - return nil, err - } else if _, err := t.updateGhost(ctx, userID, user.(*tg.User)); err != nil { + return nil, fmt.Errorf("unexpected user type: %T", user) + } else if _, err = t.updateGhost(ctx, userID, user.(*tg.User)); err != nil { return nil, fmt.Errorf("failed to update ghost: %w", err) } else { - return t.getResolveIdentifierResponseForUser(ctx, user) + return t.resolveUser(ctx, user) } } return } +func (t *TelegramClient) resolveUsername(ctx context.Context, username string, expectedID int64) (*bridgev2.ResolveIdentifierResponse, error) { + resolved, err := APICallWithUpdates(ctx, t, func() (*tg.ContactsResolvedPeer, error) { + return t.client.API().ContactsResolveUsername(ctx, &tg.ContactsResolveUsernameRequest{ + Username: username, + }) + }) + if tg.IsUsernameNotOccupied(err) { + if expectedID != 0 { + err = t.main.Store.Username.Delete(ctx, username) + if err != nil { + zerolog.Ctx(ctx).Warn().Err(err).Str("username", username). + Msg("Failed to delete stale username mapping") + } + return nil, fmt.Errorf("%w: resolving %s didn't return a result (wanted %d)", bridgev2.ErrResolveIdentifierTryNext, username, expectedID) + } + return nil, nil + } else if err != nil { + return nil, fmt.Errorf("failed to resolve username: %w", err) + } + peer, ok := resolved.GetPeer().(*tg.PeerUser) + if !ok { + return nil, fmt.Errorf("unexpected peer type: %T", resolved.GetPeer()) + } + if expectedID != 0 && peer.GetUserID() != expectedID { + return nil, fmt.Errorf("%w: resolving %s returned %d instead of %d", bridgev2.ErrResolveIdentifierTryNext, username, peer.GetUserID(), expectedID) + } + for _, user := range resolved.GetUsers() { + if user.GetID() == peer.GetUserID() { + return t.resolveUser(ctx, user) + } + } + return nil, fmt.Errorf("peer user not found in contact resolved response") +} + // Parses usernames with or without the @ sign in front of the username. // This verifies the following restrictions: // - Usernames must be at least 5 characters long @@ -97,6 +143,8 @@ func (t *TelegramClient) getResolveIdentifierResponseForUserID(ctx context.Conte // - Usernames must start with a letter // - Usernames must contain only letters, numbers, and underscores // - Usernames cannot end with an underscore +// TODO some usernames are shorter, figure out actual limits +// (some bots like @pic and @gif have 3 characters, fragment might allow 4 characters) var usernameRe = regexp.MustCompile(`^@?([a-zA-Z]\w{3,30}[a-zA-Z\d])$`) func (t *TelegramClient) ResolveIdentifier(ctx context.Context, identifier string, createChat bool) (*bridgev2.ResolveIdentifierResponse, error) { @@ -115,49 +163,24 @@ func (t *TelegramClient) ResolveIdentifier(ctx context.Context, identifier strin log.Info().Msg("Phone number not found in database") return nil, nil } else { - return t.getResolveIdentifierResponseForUserID(ctx, userID) + return t.resolveUserID(ctx, userID) } - } else if userID, err := strconv.ParseInt(identifier, 10, 64); err == nil { + } else if userID, err := strconv.ParseInt(identifier, 10, 64); err == nil && userID > 0 { // This is an integer, try and parse it as a Telegram User ID - return t.getResolveIdentifierResponseForUserID(ctx, userID) + return t.resolveUserID(ctx, userID) } else if match := usernameRe.FindStringSubmatch(identifier); match != nil && !strings.Contains(identifier, "__") { // This is a username entityType, userID, err := t.main.Store.Username.GetEntityID(ctx, match[1]) if entityType == ids.PeerTypeUser && (err == nil || userID != 0) { // We know this username. - resp, err := t.getResolveIdentifierResponseForUserID(ctx, userID) + resp, err := t.resolveUserID(ctx, userID) if err == nil || !errors.Is(err, store.ErrNoAccessHash) { return resp, err } } - // We don't know this username, try to resolve the username from - // Telegram. - resolved, err := APICallWithUpdates(ctx, t, func() (*tg.ContactsResolvedPeer, error) { - return t.client.API().ContactsResolveUsername(ctx, &tg.ContactsResolveUsernameRequest{ - Username: match[1], - }) - }) - if err != nil { - if tg.IsUsernameNotOccupied(err) { - log.Info().Msg("Username not found in database") - return nil, nil - } else { - return nil, fmt.Errorf("failed to resolve username: %w", err) - } - } - peer, ok := resolved.GetPeer().(*tg.PeerUser) - if !ok { - return nil, fmt.Errorf("unexpected peer type: %T", resolved.GetPeer()) - } - for _, user := range resolved.GetUsers() { - if user.GetID() == peer.GetUserID() { - return t.getResolveIdentifierResponseForUser(ctx, user) - } - } - return nil, fmt.Errorf("peer user not found in contact resolved response") - } else { - return nil, fmt.Errorf("invalid identifier: %s (must be a phone number, username, or Telegram user ID)", identifier) + return t.resolveUsername(ctx, match[1], 0) } + return nil, fmt.Errorf("invalid identifier: %q (must be a phone number, username, or Telegram user ID)", identifier) } func (t *TelegramClient) SearchUsers(ctx context.Context, query string) (resp []*bridgev2.ResolveIdentifierResponse, err error) { @@ -176,7 +199,7 @@ func (t *TelegramClient) SearchUsers(ctx context.Context, query string) (resp [] if peer, ok := p.(*tg.PeerUser); !ok { return nil } else if user, ok := users[peer.GetUserID()]; ok { - if r, err := t.getResolveIdentifierResponseForUser(ctx, user); err != nil { + if r, err := t.resolveUser(ctx, user); err != nil { return err } else { resp = append(resp, r) @@ -239,7 +262,7 @@ func (t *TelegramClient) GetContactList(ctx context.Context) (resp []*bridgev2.R for _, contact := range contacts.Contacts { if user, ok := users[contact.UserID]; ok { - if r, err := t.getResolveIdentifierResponseForUser(ctx, user); err != nil { + if r, err := t.resolveUser(ctx, user); err != nil { return nil, err } else { resp = append(resp, r) diff --git a/pkg/connector/store/username.go b/pkg/connector/store/username.go index 04fe2504..20f45908 100644 --- a/pkg/connector/store/username.go +++ b/pkg/connector/store/username.go @@ -40,8 +40,9 @@ const ( entity_type=excluded.entity_type, entity_id=excluded.entity_id ` - getByUsernameQuery = "SELECT entity_type, entity_id FROM telegram_username WHERE LOWER(username)=$1" - clearUsernameQuery = `DELETE FROM telegram_username WHERE entity_type=$1 AND entity_id=$2` + getByUsernameQuery = "SELECT entity_type, entity_id FROM telegram_username WHERE LOWER(username)=$1" + clearUsernameQuery = `DELETE FROM telegram_username WHERE entity_type=$1 AND entity_id=$2` + deleteUsernameQuery = `DELETE FROM telegram_username WHERE LOWER(username)=$1` ) func (s *UsernameQuery) Get(ctx context.Context, entityType ids.PeerType, userID int64) (username string, err error) { @@ -61,6 +62,11 @@ func (s *UsernameQuery) Set(ctx context.Context, entityType ids.PeerType, entity return } +func (s *UsernameQuery) Delete(ctx context.Context, username string) (err error) { + _, err = s.db.Exec(ctx, deleteUsernameQuery, strings.ToLower(username)) + return +} + func (s *UsernameQuery) GetEntityID(ctx context.Context, username string) (entityType ids.PeerType, entityID int64, err error) { err = s.db.QueryRow(ctx, getByUsernameQuery, strings.ToLower(username)).Scan(&entityType, &entityID) if errors.Is(err, sql.ErrNoRows) { diff --git a/pkg/connector/userinfo.go b/pkg/connector/userinfo.go index ed23f11a..f6a8b7ec 100644 --- a/pkg/connector/userinfo.go +++ b/pkg/connector/userinfo.go @@ -36,6 +36,27 @@ func (t *TelegramClient) GetUserInfo(ctx context.Context, ghost *bridgev2.Ghost) } } +func (t *TelegramClient) getInputUser(ctx context.Context, id int64) (*tg.InputUser, error) { + accessHash, err := t.ScopedStore.GetAccessHash(ctx, ids.PeerTypeUser, id) + if err != nil { + return nil, fmt.Errorf("failed to get access hash for user %d: %w", id, err) + } + return &tg.InputUser{UserID: id, AccessHash: accessHash}, nil +} + +func (t *TelegramClient) getSingleUser(ctx context.Context, id int64) (tg.UserClass, error) { + if inputUser, err := t.getInputUser(ctx, id); err != nil { + return nil, err + } else if users, err := t.client.API().UsersGetUsers(ctx, []tg.InputUserClass{inputUser}); err != nil { + return nil, err + } else if len(users) == 0 { + // TODO does this mean the user is deleted? Need to handle this a bit better + return nil, fmt.Errorf("failed to get user info for user %d", id) + } else { + return users[0], nil + } +} + func (t *TelegramClient) wrapChannelGhostInfo(ctx context.Context, channel *tg.Channel) (*bridgev2.UserInfo, error) { var err error if accessHash, ok := channel.GetAccessHash(); ok && !channel.Min {