snc: implement resolving Telegram IDs, usernames, and phone numbers
Signed-off-by: Sumner Evans <sumner.evans@automattic.com>
This commit is contained in:
+15
-33
@@ -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
@@ -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)
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user