Skip to content

Commit a0a043a

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

File tree

2 files changed

+70
-72
lines changed

2 files changed

+70
-72
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: 33 additions & 68 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,48 @@ 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.
5153
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
54+
sender := id.UserID(strings.TrimSpace(req.From))
55+
if sender == "" {
56+
logger.Warn().Msg("send message: empty sender")
57+
return nil, ErrInvalidSender
5758
}
5859

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-
}
60+
recipient := id.UserID(strings.TrimSpace(req.SMSTo))
61+
if recipient == "" {
62+
logger.Warn().Msg("send message: empty recipient")
63+
return nil, ErrInvalidRecipient
6964
}
7065

71-
logger.Debug().Str("user_id", string(userID)).Str("recipient", req.SMSTo).Msg("resolving recipient")
72-
73-
roomID, err := s.resolveRecipient(ctx, userID, req.SMSTo)
66+
// For 1-to-1 messaging, ensure a direct room exists between sender and recipient
67+
roomID, err := s.ensureDirectRoom(ctx, sender, recipient)
7468
if err != nil {
75-
logger.Error().Str("user_id", string(userID)).Str("recipient", req.SMSTo).Err(err).Msg("failed to resolve recipient")
69+
logger.Error().Str("sender", string(sender)).Str("recipient", string(recipient)).Err(err).Msg("failed to ensure direct room")
7670
return nil, err
7771
}
7872

79-
logger.Debug().Str("user_id", string(userID)).Str("room_id", string(roomID)).Msg("sending message to room")
73+
logger.Debug().Str("sender", string(sender)).Str("recipient", string(recipient)).Str("room_id", string(roomID)).Msg("sending message to direct room")
74+
75+
// Ensure the sender is a member of the room (in case join failed during room creation)
76+
_, err = s.matrixClient.JoinRoom(ctx, sender, roomID)
77+
if err != nil {
78+
logger.Error().Str("sender", string(sender)).Str("room_id", string(roomID)).Err(err).Msg("failed to join room")
79+
return nil, fmt.Errorf("send message: %w", err)
80+
}
8081

8182
content := &event.MessageEventContent{
8283
MsgType: event.MsgText,
8384
Body: req.SMSBody,
8485
}
8586

86-
resp, err := s.matrixClient.SendMessage(ctx, userID, roomID, content)
87+
resp, err := s.matrixClient.SendMessage(ctx, sender, roomID, content)
8788
if err != nil {
88-
logger.Error().Str("user_id", string(userID)).Str("room_id", string(roomID)).Err(err).Msg("failed to send message")
89+
logger.Error().Str("sender", string(sender)).Str("room_id", string(roomID)).Err(err).Msg("failed to send message")
8990
return nil, fmt.Errorf("send message: %w", mapAuthErr(err))
9091
}
9192

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

@@ -134,43 +135,6 @@ func (s *MessageService) FetchMessages(ctx context.Context, req *models.FetchMes
134135
}, nil
135136
}
136137

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
163-
}
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
169-
}
170-
logger.Warn().Str("user_id", string(actingUserID)).Str("identifier", trimmed).Msg("recipient not resolvable")
171-
return "", ErrInvalidRecipient
172-
}
173-
174138
func (s *MessageService) ensureDirectRoom(ctx context.Context, actingUserID, targetUserID id.UserID) (id.RoomID, error) {
175139
// Use both user IDs to create a consistent mapping key for the DM.
176140
key := fmt.Sprintf("%s|%s", actingUserID, targetUserID)
@@ -234,20 +198,20 @@ func (s *MessageService) setMapping(entry mappingEntry) mappingEntry {
234198
defer s.mu.Unlock()
235199
entry.UpdatedAt = s.now()
236200
s.mappings[normalized] = entry
237-
logger.Debug().Str("sms_number", entry.SMSNumber).Str("room_id", string(entry.RoomID)).Msg("mapping stored")
201+
logger.Debug().Str("key", entry.SMSNumber).Str("room_id", string(entry.RoomID)).Msg("mapping stored")
238202
return entry
239203
}
240204

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)
205+
// LookupMapping returns the currently stored mapping for a given key (phone number or user pair).
206+
func (s *MessageService) LookupMapping(key string) (*models.MappingResponse, error) {
207+
entry, ok := s.getMapping(key)
244208
if !ok {
245209
return nil, ErrMappingNotFound
246210
}
247211
return s.buildMappingResponse(entry), nil
248212
}
249213

250-
// ListMappings returns all stored mappings as MappingResponse slices.
214+
// ListMappings returns all stored mappings.
251215
func (s *MessageService) ListMappings() ([]*models.MappingResponse, error) {
252216
s.mu.RLock()
253217
defer s.mu.RUnlock()
@@ -258,7 +222,8 @@ func (s *MessageService) ListMappings() ([]*models.MappingResponse, error) {
258222
return out, nil
259223
}
260224

261-
// SaveMapping persists a new SMS-to-Matrix mapping via the admin API.
225+
// SaveMapping persists a new mapping via the admin API.
226+
// For 1-to-1 messaging, this maps a key (phone number or identifier) to a direct room.
262227
func (s *MessageService) SaveMapping(req *models.MappingRequest) (*models.MappingResponse, error) {
263228
smsNumber := strings.TrimSpace(req.SMSNumber)
264229
if smsNumber == "" {
@@ -326,7 +291,7 @@ func mapAuthErr(err error) error {
326291
if errors.Is(err, ErrAuthentication) {
327292
return err
328293
}
329-
if errors.Is(err, mautrix.MUnknownToken) || errors.Is(err, mautrix.MMissingToken) || errors.Is(err, mautrix.MForbidden) {
294+
if errors.Is(err, mautrix.MUnknownToken) || errors.Is(err, mautrix.MMissingToken) {
330295
return fmt.Errorf("%w", ErrAuthentication)
331296
}
332297
return err

0 commit comments

Comments
 (0)