snc: implement resolving Telegram IDs, usernames, and phone numbers

Signed-off-by: Sumner Evans <sumner.evans@automattic.com>
This commit is contained in:
Sumner Evans
2024-08-21 10:15:47 -06:00
parent 9d8f162f41
commit 15b0dc51b3
5 changed files with 177 additions and 53 deletions
+15 -33
View File
@@ -15,41 +15,23 @@ import (
"go.mau.fi/mautrix-telegram/pkg/connector/media"
)
func (t *TelegramClient) getDMChatInfo(ctx context.Context, userID int64) (*bridgev2.ChatInfo, error) {
// FIXME there's no reason for this function to fetch the user info
func (t *TelegramClient) getDMChatInfo(userID int64) (*bridgev2.ChatInfo, error) {
networkUserID := ids.MakeUserID(userID)
chatInfo := bridgev2.ChatInfo{
Type: ptr.Ptr(database.RoomTypeDM),
Members: &bridgev2.ChatMemberList{IsFull: true},
CanBackfill: true,
}
accessHash, err := t.ScopedStore.GetAccessHash(ctx, userID)
if err != nil {
return nil, fmt.Errorf("failed to get access hash for user %d: %w", userID, err)
}
users, err := t.client.API().UsersGetUsers(ctx, []tg.InputUserClass{&tg.InputUser{
UserID: userID,
AccessHash: accessHash,
}})
if err != nil {
return nil, err
} else if len(users) == 0 {
return nil, fmt.Errorf("failed to get user info for user %d", userID)
} else if userInfo, err := t.getUserInfoFromTelegramUser(ctx, users[0]); err != nil {
return nil, err
} else if err = t.updateGhostWithUserInfo(ctx, userID, userInfo); err != nil {
return nil, err
} else {
networkUserID := ids.MakeUserID(userID)
chatInfo.Members.MemberMap = map[networkid.UserID]bridgev2.ChatMember{
networkUserID: {
EventSender: bridgev2.EventSender{
SenderLogin: ids.MakeUserLoginID(userID),
Sender: ids.MakeUserID(userID),
Type: ptr.Ptr(database.RoomTypeDM),
Members: &bridgev2.ChatMemberList{
IsFull: true,
MemberMap: map[networkid.UserID]bridgev2.ChatMember{
networkUserID: {
EventSender: bridgev2.EventSender{
SenderLogin: ids.MakeUserLoginID(userID),
Sender: networkUserID,
},
},
UserInfo: userInfo,
t.userID: {EventSender: t.mySender()},
},
t.userID: {EventSender: t.mySender()},
}
},
CanBackfill: true,
}
if userID == t.telegramUserID {
// TODO also hardcode the avatar used by telegram?
@@ -150,7 +132,7 @@ func (t *TelegramClient) GetChatInfo(ctx context.Context, portal *bridgev2.Porta
switch peerType {
case ids.PeerTypeUser:
return t.getDMChatInfo(ctx, id)
return t.getDMChatInfo(id)
case ids.PeerTypeChat:
fullChat, err := APICallWithUpdates(ctx, t, func() (*tg.MessagesChatFull, error) {
return t.client.API().MessagesGetFullChat(ctx, id)
+32 -20
View File
@@ -55,11 +55,11 @@ var (
_ bridgev2.ReactionHandlingNetworkAPI = (*TelegramClient)(nil)
_ bridgev2.RedactionHandlingNetworkAPI = (*TelegramClient)(nil)
_ bridgev2.ReadReceiptHandlingNetworkAPI = (*TelegramClient)(nil)
_ bridgev2.ReadReceiptHandlingNetworkAPI = (*TelegramClient)(nil)
_ bridgev2.TypingHandlingNetworkAPI = (*TelegramClient)(nil)
_ bridgev2.BackfillingNetworkAPI = (*TelegramClient)(nil)
_ bridgev2.BackfillingNetworkAPIWithLimits = (*TelegramClient)(nil)
// _ bridgev2.IdentifierResolvingNetworkAPI = (*TelegramClient)(nil)
_ bridgev2.IdentifierResolvingNetworkAPI = (*TelegramClient)(nil)
_ bridgev2.UserSearchingNetworkAPI = (*TelegramClient)(nil)
// _ bridgev2.GroupCreatingNetworkAPI = (*TelegramClient)(nil)
// _ bridgev2.ContactListingNetworkAPI = (*TelegramClient)(nil)
)
@@ -390,30 +390,38 @@ func (t *TelegramClient) Disconnect() {
t.clientCancel()
}
func (t *TelegramClient) getInputUser(ctx context.Context, id int64) (*tg.InputUser, error) {
accessHash, err := t.ScopedStore.GetAccessHash(ctx, 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 {
return nil, fmt.Errorf("failed to get user info for user %d", id)
} else {
return users[0], nil
}
}
func (t *TelegramClient) GetUserInfo(ctx context.Context, ghost *bridgev2.Ghost) (*bridgev2.UserInfo, error) {
id, err := ids.ParseUserID(ghost.ID)
if err != nil {
return nil, err
}
accessHash, err := t.ScopedStore.GetAccessHash(ctx, id)
if err != nil {
return nil, fmt.Errorf("failed to get access hash for user %d: %w", id, err)
}
users, err := t.client.API().UsersGetUsers(ctx, []tg.InputUserClass{&tg.InputUser{
UserID: id,
AccessHash: accessHash,
}})
if err != nil {
if user, err := t.getSingleUser(ctx, id); err != nil {
return nil, fmt.Errorf("failed to get user %d: %w", id, err)
} else if userInfo, err := t.getUserInfoFromTelegramUser(ctx, user); err != nil {
return nil, err
} else {
return userInfo, t.updateGhostWithUserInfo(ctx, id, userInfo)
}
if len(users) == 0 {
return nil, fmt.Errorf("failed to get user info for user %d", id)
}
userInfo, err := t.getUserInfoFromTelegramUser(ctx, users[0])
if err != nil {
return nil, err
}
return userInfo, t.updateGhostWithUserInfo(ctx, id, userInfo)
}
func (t *TelegramClient) getUserInfoFromTelegramUser(ctx context.Context, u tg.UserClass) (*bridgev2.UserInfo, error) {
@@ -437,7 +445,11 @@ func (t *TelegramClient) getUserInfoFromTelegramUser(ctx context.Context, u tg.U
identifiers = append(identifiers, fmt.Sprintf("telegram:%s", username.Username))
}
if phone, ok := user.GetPhone(); ok {
identifiers = append(identifiers, fmt.Sprintf("tel:+%s", strings.TrimPrefix(phone, "+")))
normalized := strings.TrimPrefix(phone, "+")
identifiers = append(identifiers, fmt.Sprintf("tel:+%s", normalized))
if err := t.ScopedStore.SetPhoneNumber(ctx, user.ID, normalized); err != nil {
return nil, err
}
}
}
slices.Sort(identifiers)
+95
View File
@@ -0,0 +1,95 @@
package connector
import (
"context"
"fmt"
"regexp"
"strconv"
"strings"
"github.com/gotd/td/tg"
"maunium.net/go/mautrix/bridgev2"
"go.mau.fi/mautrix-telegram/pkg/connector/ids"
)
func (t *TelegramClient) SearchUsers(ctx context.Context, query string) ([]*bridgev2.ResolveIdentifierResponse, error) {
// t.client.API().ContactsSearch()
panic("unimplemented")
}
// 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
// - Usernames must be at most 32 characters long
// - Usernames must start with a letter
// - Usernames must contain only letters, numbers, and underscores
// - Usernames cannot end with an underscore
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) {
if len(identifier) == 0 {
return nil, fmt.Errorf("empty identifier")
}
if identifier[0] == '+' {
normalized := strings.TrimPrefix(identifier, "+")
if userID, err := t.ScopedStore.GetUserIDByPhoneNumber(ctx, normalized); err != nil {
return nil, fmt.Errorf("failed to get user ID by phone number: %w", err)
} else if userID == 0 {
return nil, fmt.Errorf("no user found with phone number '%s'", normalized)
} else if user, err := t.getSingleUser(ctx, userID); err != nil {
return nil, fmt.Errorf("failed to get user with ID %d: %w", userID, err)
} else {
return t.getResolveIdentifierResponseForUserID(ctx, user)
}
} else if userID, err := strconv.ParseInt(identifier, 10, 64); err == nil {
// This is an integer, try and parse it as a Telegram User ID
if user, err := t.getSingleUser(ctx, userID); err != nil {
return nil, fmt.Errorf("failed to get user with ID %d: %w", userID, err)
} else {
return t.getResolveIdentifierResponseForUserID(ctx, user)
}
} else if match := usernameRe.FindStringSubmatch(identifier); match != nil && !strings.Contains(identifier, "__") {
resolved, err := APICallWithUpdates(ctx, t, func() (*tg.ContactsResolvedPeer, error) {
return t.client.API().ContactsResolveUsername(ctx, match[1])
})
if err != nil {
if tg.IsUsernameNotOccupied(err) {
return nil, fmt.Errorf("no user found with username '%s'", match[1])
} 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.getResolveIdentifierResponseForUserID(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)
}
}
func (t *TelegramClient) getResolveIdentifierResponseForUserID(ctx context.Context, user tg.UserClass) (*bridgev2.ResolveIdentifierResponse, error) {
networkUserID := ids.MakeUserID(user.GetID())
if userInfo, err := t.getUserInfoFromTelegramUser(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 {
return nil, fmt.Errorf("failed to get ghost: %w", err)
} else {
return &bridgev2.ResolveIdentifierResponse{
Ghost: ghost,
UserID: networkUserID,
UserInfo: userInfo,
Chat: &bridgev2.CreateChatResponse{
PortalKey: ids.PeerTypeUser.AsPortalKey(user.GetID(), t.loginID),
},
}, nil
}
}
+26
View File
@@ -57,6 +57,15 @@ const (
ON CONFLICT (username) DO UPDATE SET entity_id=excluded.entity_id
`
clearUsernameQuery = `DELETE FROM telegram_username WHERE entity_id=$1`
// User Phone Number Queries
getEntityIDForPhoneNumber = "SELECT entity_id FROM telegram_phone_number WHERE phone_number=$1"
setPhoneNumberQuery = `
INSERT INTO telegram_phone_number (phone_number, entity_id)
VALUES ($1, $2)
ON CONFLICT (phone_number) DO UPDATE SET entity_id=excluded.entity_id
`
clearPhoneNumberQuery = "DELETE FROM telegram_phone_number WHERE entity_id=$1"
)
var _ updates.StateStorage = (*ScopedStore)(nil)
@@ -187,6 +196,23 @@ func (s *ScopedStore) SetUsername(ctx context.Context, userID int64, username st
return
}
func (s *ScopedStore) GetUserIDByPhoneNumber(ctx context.Context, phoneNumber string) (userID int64, err error) {
err = s.db.QueryRow(ctx, getEntityIDForPhoneNumber, phoneNumber).Scan(&userID)
if errors.Is(err, sql.ErrNoRows) {
err = nil
}
return
}
func (s *ScopedStore) SetPhoneNumber(ctx context.Context, userID int64, phoneNumber string) (err error) {
if phoneNumber == "" {
_, err = s.db.Exec(ctx, clearPhoneNumberQuery, userID)
} else {
_, err = s.db.Exec(ctx, setPhoneNumberQuery, phoneNumber, userID)
}
return
}
// Helper Functions
func (s *ScopedStore) assertUserIDMatches(userID int64) {
@@ -35,6 +35,15 @@ CREATE TABLE telegram_username (
CREATE INDEX telegram_username_entity_idx ON telegram_username (entity_id);
CREATE TABLE telegram_phone_number (
phone_number TEXT NOT NULL,
entity_id BIGINT NOT NULL,
PRIMARY KEY (phone_number)
);
CREATE INDEX telegram_phone_number_entity_idx ON telegram_phone_number (entity_id);
CREATE TABLE telegram_file (
id TEXT PRIMARY KEY,
mxc TEXT NOT NULL,