Skip to content

Commit fc4ca1b

Browse files
committed
fix: add auto join for messages
1 parent 6518fca commit fc4ca1b

File tree

2 files changed

+105
-67
lines changed

2 files changed

+105
-67
lines changed

matrix/client.go

Lines changed: 37 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import (
55
"errors"
66
"fmt"
77
"net/http"
8+
"strings"
89
"sync"
910
"time"
1011

@@ -26,8 +27,10 @@ type Config struct {
2627
// Note: The underlying mautrix client is stateful for impersonation in this version.
2728
// A mutex is used to make operations thread-safe.
2829
type MatrixClient struct {
29-
cli *mautrix.Client
30-
mu sync.Mutex
30+
cli *mautrix.Client
31+
homeserverURL string
32+
homeserverName string
33+
mu sync.Mutex
3134
}
3235

3336
// NewClient creates a MatrixClient authenticated as an Application Service.
@@ -56,8 +59,19 @@ func NewClient(cfg Config) (*MatrixClient, error) {
5659
// This flag enables the `user_id` query parameter for impersonation.
5760
client.SetAppServiceUserID = true
5861

62+
// Extract homeserver name from URL (e.g., https://synapse.example.com -> synapse.example.com)
63+
homeserverName := strings.TrimPrefix(cfg.HomeserverURL, "https://")
64+
homeserverName = strings.TrimPrefix(homeserverName, "http://")
65+
homeserverName = strings.TrimSuffix(homeserverName, "/")
66+
// Remove path if present
67+
if idx := strings.Index(homeserverName, "/"); idx > 0 {
68+
homeserverName = homeserverName[:idx]
69+
}
70+
5971
return &MatrixClient{
60-
cli: client,
72+
cli: client,
73+
homeserverURL: cfg.HomeserverURL,
74+
homeserverName: homeserverName,
6175
}, nil
6276
}
6377

@@ -128,7 +142,26 @@ func (mc *MatrixClient) JoinRoom(ctx context.Context, userID id.UserID, roomID i
128142
defer mc.mu.Unlock()
129143

130144
mc.cli.UserID = userID
131-
return mc.cli.JoinRoom(ctx, string(roomID), nil)
145+
146+
// Extract homeserver from room ID for federated joins
147+
// Room ID format: !opaque:homeserver.name
148+
roomIDStr := string(roomID)
149+
req := &mautrix.ReqJoinRoom{}
150+
151+
if idx := strings.Index(roomIDStr, ":"); idx > 0 && idx < len(roomIDStr)-1 {
152+
roomServerName := roomIDStr[idx+1:]
153+
154+
// Only add Via if the room is on a different homeserver (federated)
155+
if roomServerName != "" && roomServerName != mc.homeserverName {
156+
req.Via = []string{roomServerName}
157+
logger.Debug().Str("user_id", string(userID)).Str("room_id", roomIDStr).Str("room_server", roomServerName).Str("local_server", mc.homeserverName).Msg("matrix: joining federated room with homeserver hint")
158+
} else {
159+
logger.Debug().Str("user_id", string(userID)).Str("room_id", roomIDStr).Msg("matrix: joining local room without via")
160+
}
161+
}
162+
163+
logger.Debug().Str("user_id", string(userID)).Str("room_id", roomIDStr).Bool("has_via", len(req.Via) > 0).Msg("matrix: calling JoinRoom")
164+
return mc.cli.JoinRoom(ctx, roomIDStr, req)
132165
}
133166

134167
// CreateRoom creates a new room impersonating the specified userID.

service/messages.go

Lines changed: 68 additions & 63 deletions
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,9 @@ import (
1818

1919
var (
2020
ErrAuthentication = errors.New("matrix authentication failed")
21-
ErrInvalidRecipient = errors.New("recipient is not resolvable to a Matrix room")
21+
ErrInvalidRecipient = errors.New("recipient is not resolvable to a Matrix user or room")
2222
ErrMappingNotFound = errors.New("mapping not found")
23+
ErrInvalidSender = errors.New("sender is not resolvable to a Matrix user")
2324
)
2425

2526
// MessageService handles sending/fetching messages plus the mapping store.
@@ -48,48 +49,65 @@ func NewMessageService(matrixClient *matrix.MatrixClient) *MessageService {
4849
}
4950

5051
// SendMessage translates an Acrobits send_message request into Matrix /send.
52+
// Only 1-to-1 direct messaging is supported.
53+
// Both sender and recipient are resolved to Matrix user IDs using local mappings if necessary.
5154
func (s *MessageService) SendMessage(ctx context.Context, req *models.SendMessageRequest) (*models.SendMessageResponse, error) {
52-
// The user to impersonate is taken from the 'From' field.
53-
userID := id.UserID(req.From)
54-
if userID == "" {
55-
logger.Warn().Msg("send message: empty user ID")
56-
return nil, ErrAuthentication
55+
senderStr := strings.TrimSpace(req.From)
56+
if senderStr == "" {
57+
logger.Warn().Msg("send message: empty sender")
58+
return nil, ErrInvalidSender
5759
}
5860

59-
// If 'From' is a phone number, try to map it to a Matrix user
60-
if isPhoneNumber(req.From) {
61-
logger.Debug().Str("from", req.From).Msg("sender is a phone number, attempting to resolve to Matrix user")
62-
if entry, ok := s.getMapping(req.From); ok && entry.MatrixID != "" {
63-
resolvedUserID := id.UserID(entry.MatrixID)
64-
logger.Info().Str("phone_number", req.From).Str("matrix_id", entry.MatrixID).Msg("phone number mapped to Matrix user")
65-
userID = resolvedUserID
66-
} else {
67-
logger.Warn().Str("phone_number", req.From).Msg("phone number mapping not found, using original sender ID")
68-
}
61+
recipientStr := strings.TrimSpace(req.SMSTo)
62+
if recipientStr == "" {
63+
logger.Warn().Msg("send message: empty recipient")
64+
return nil, ErrInvalidRecipient
6965
}
7066

71-
logger.Debug().Str("user_id", string(userID)).Str("recipient", req.SMSTo).Msg("resolving recipient")
67+
// Resolve sender to a valid Matrix user ID
68+
sender := s.resolveMatrixUser(senderStr)
69+
if sender == "" {
70+
logger.Warn().Str("sender", senderStr).Msg("sender is not a valid Matrix user ID")
71+
return nil, ErrInvalidSender
72+
}
7273

73-
roomID, err := s.resolveRecipient(ctx, userID, req.SMSTo)
74+
// Resolve recipient to a valid Matrix user ID
75+
recipient := s.resolveMatrixUser(recipientStr)
76+
if recipient == "" {
77+
logger.Warn().Str("recipient", recipientStr).Msg("recipient is not a valid Matrix user ID")
78+
return nil, ErrInvalidRecipient
79+
}
80+
81+
logger.Debug().Str("sender", string(sender)).Str("recipient", string(recipient)).Msg("resolved sender and recipient to Matrix user IDs")
82+
83+
// For 1-to-1 messaging, ensure a direct room exists between sender and recipient
84+
roomID, err := s.ensureDirectRoom(ctx, sender, recipient)
7485
if err != nil {
75-
logger.Error().Str("user_id", string(userID)).Str("recipient", req.SMSTo).Err(err).Msg("failed to resolve recipient")
86+
logger.Error().Str("sender", string(sender)).Str("recipient", string(recipient)).Err(err).Msg("failed to ensure direct room")
7687
return nil, err
7788
}
7889

79-
logger.Debug().Str("user_id", string(userID)).Str("room_id", string(roomID)).Msg("sending message to room")
90+
logger.Debug().Str("sender", string(sender)).Str("recipient", string(recipient)).Str("room_id", string(roomID)).Msg("sending message to direct room")
91+
92+
// Ensure the sender is a member of the room (in case join failed during room creation)
93+
_, err = s.matrixClient.JoinRoom(ctx, sender, roomID)
94+
if err != nil {
95+
logger.Error().Str("sender", string(sender)).Str("room_id", string(roomID)).Err(err).Msg("failed to join room")
96+
return nil, fmt.Errorf("send message: %w", err)
97+
}
8098

8199
content := &event.MessageEventContent{
82100
MsgType: event.MsgText,
83101
Body: req.SMSBody,
84102
}
85103

86-
resp, err := s.matrixClient.SendMessage(ctx, userID, roomID, content)
104+
resp, err := s.matrixClient.SendMessage(ctx, sender, roomID, content)
87105
if err != nil {
88-
logger.Error().Str("user_id", string(userID)).Str("room_id", string(roomID)).Err(err).Msg("failed to send message")
106+
logger.Error().Str("sender", string(sender)).Str("room_id", string(roomID)).Err(err).Msg("failed to send message")
89107
return nil, fmt.Errorf("send message: %w", mapAuthErr(err))
90108
}
91109

92-
logger.Debug().Str("user_id", string(userID)).Str("room_id", string(roomID)).Str("event_id", string(resp.EventID)).Msg("message sent successfully")
110+
logger.Debug().Str("sender", string(sender)).Str("room_id", string(roomID)).Str("event_id", string(resp.EventID)).Msg("message sent successfully")
93111
return &models.SendMessageResponse{SMSID: string(resp.EventID)}, nil
94112
}
95113

@@ -134,41 +152,27 @@ func (s *MessageService) FetchMessages(ctx context.Context, req *models.FetchMes
134152
}, nil
135153
}
136154

137-
func (s *MessageService) resolveRecipient(ctx context.Context, actingUserID id.UserID, raw string) (id.RoomID, error) {
138-
trimmed := strings.TrimSpace(raw)
139-
if trimmed == "" {
140-
logger.Warn().Str("user_id", string(actingUserID)).Msg("empty recipient")
141-
return "", ErrInvalidRecipient
142-
}
143-
// If it looks like a RoomID, use it directly.
144-
if strings.HasPrefix(trimmed, "!") {
145-
logger.Debug().Str("user_id", string(actingUserID)).Str("room_id", trimmed).Msg("using direct room ID")
146-
return id.RoomID(trimmed), nil
147-
}
148-
// If it looks like a UserID, ensure a DM exists and use that room.
149-
if strings.HasPrefix(trimmed, "@") {
150-
logger.Debug().Str("user_id", string(actingUserID)).Str("target_user", trimmed).Msg("resolving recipient as user ID (DM)")
151-
return s.ensureDirectRoom(ctx, actingUserID, id.UserID(trimmed))
152-
}
153-
// If it's a room alias, resolve it.
154-
if strings.HasPrefix(trimmed, "#") {
155-
logger.Debug().Str("user_id", string(actingUserID)).Str("alias", trimmed).Msg("resolving room alias")
156-
resp, err := s.matrixClient.ResolveRoomAlias(ctx, trimmed)
157-
if err != nil {
158-
logger.Error().Str("user_id", string(actingUserID)).Str("alias", trimmed).Err(err).Msg("failed to resolve room alias")
159-
return "", fmt.Errorf("resolve room alias: %w", err)
160-
}
161-
logger.Debug().Str("user_id", string(actingUserID)).Str("alias", trimmed).Str("room_id", string(resp.RoomID)).Msg("room alias resolved")
162-
return resp.RoomID, nil
155+
// resolveMatrixUser resolves an identifier to a valid Matrix user ID.
156+
// If the identifier is already a valid Matrix user ID (starts with @), it's returned as-is.
157+
// Otherwise, it tries to look up the identifier in the mapping store (e.g., phone number to user).
158+
// Returns empty string if the identifier cannot be resolved.
159+
func (s *MessageService) resolveMatrixUser(identifier string) id.UserID {
160+
identifier = strings.TrimSpace(identifier)
161+
162+
// If it's already a valid Matrix user ID, return it
163+
if strings.HasPrefix(identifier, "@") {
164+
return id.UserID(identifier)
163165
}
164-
// Otherwise, check our internal mapping for a phone number.
165-
logger.Debug().Str("user_id", string(actingUserID)).Str("identifier", trimmed).Msg("checking mapping store")
166-
if entry, ok := s.getMapping(trimmed); ok && entry.RoomID != "" {
167-
logger.Debug().Str("user_id", string(actingUserID)).Str("identifier", trimmed).Str("room_id", string(entry.RoomID)).Msg("mapping found")
168-
return entry.RoomID, nil
166+
167+
// Try to look up in mappings (e.g., phone number to Matrix user)
168+
if entry, ok := s.getMapping(identifier); ok && entry.MatrixID != "" {
169+
logger.Debug().Str("original_identifier", identifier).Str("resolved_user", entry.MatrixID).Msg("identifier resolved from mapping")
170+
return id.UserID(entry.MatrixID)
169171
}
170-
logger.Warn().Str("user_id", string(actingUserID)).Str("identifier", trimmed).Msg("recipient not resolvable")
171-
return "", ErrInvalidRecipient
172+
173+
// Could not resolve
174+
logger.Warn().Str("identifier", identifier).Msg("identifier could not be resolved to a Matrix user ID")
175+
return ""
172176
}
173177

174178
func (s *MessageService) ensureDirectRoom(ctx context.Context, actingUserID, targetUserID id.UserID) (id.RoomID, error) {
@@ -234,20 +238,20 @@ func (s *MessageService) setMapping(entry mappingEntry) mappingEntry {
234238
defer s.mu.Unlock()
235239
entry.UpdatedAt = s.now()
236240
s.mappings[normalized] = entry
237-
logger.Debug().Str("sms_number", entry.SMSNumber).Str("room_id", string(entry.RoomID)).Msg("mapping stored")
241+
logger.Debug().Str("key", entry.SMSNumber).Str("room_id", string(entry.RoomID)).Msg("mapping stored")
238242
return entry
239243
}
240244

241-
// LookupMapping returns the currently stored mapping for a given sms number.
242-
func (s *MessageService) LookupMapping(smsNumber string) (*models.MappingResponse, error) {
243-
entry, ok := s.getMapping(smsNumber)
245+
// LookupMapping returns the currently stored mapping for a given key (phone number or user pair).
246+
func (s *MessageService) LookupMapping(key string) (*models.MappingResponse, error) {
247+
entry, ok := s.getMapping(key)
244248
if !ok {
245249
return nil, ErrMappingNotFound
246250
}
247251
return s.buildMappingResponse(entry), nil
248252
}
249253

250-
// ListMappings returns all stored mappings as MappingResponse slices.
254+
// ListMappings returns all stored mappings.
251255
func (s *MessageService) ListMappings() ([]*models.MappingResponse, error) {
252256
s.mu.RLock()
253257
defer s.mu.RUnlock()
@@ -258,7 +262,8 @@ func (s *MessageService) ListMappings() ([]*models.MappingResponse, error) {
258262
return out, nil
259263
}
260264

261-
// SaveMapping persists a new SMS-to-Matrix mapping via the admin API.
265+
// SaveMapping persists a new mapping via the admin API.
266+
// For 1-to-1 messaging, this maps a key (phone number or identifier) to a direct room.
262267
func (s *MessageService) SaveMapping(req *models.MappingRequest) (*models.MappingResponse, error) {
263268
smsNumber := strings.TrimSpace(req.SMSNumber)
264269
if smsNumber == "" {
@@ -326,7 +331,7 @@ func mapAuthErr(err error) error {
326331
if errors.Is(err, ErrAuthentication) {
327332
return err
328333
}
329-
if errors.Is(err, mautrix.MUnknownToken) || errors.Is(err, mautrix.MMissingToken) || errors.Is(err, mautrix.MForbidden) {
334+
if errors.Is(err, mautrix.MUnknownToken) || errors.Is(err, mautrix.MMissingToken) {
330335
return fmt.Errorf("%w", ErrAuthentication)
331336
}
332337
return err

0 commit comments

Comments
 (0)