Skip to content

Commit fb0d76e

Browse files
committed
feat: load mapping file
1 parent fc4ca1b commit fb0d76e

File tree

5 files changed

+211
-10
lines changed

5 files changed

+211
-10
lines changed

README.md

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ The proxy is configured via environment variables. Minimal required env:
1717
- `PROXY_PORT` (optional): port to listen on (default: `8080`)
1818
- `AS_USER_ID` (optional): the user ID of the Application Service bot (default: `@_acrobits_proxy:matrix.example`)
1919
- `LOGLEVEL` (optional): logging verbosity level - `DEBUG`, `INFO`, `WARNING`, `CRITICAL` (default: `INFO`)
20+
- `MAPPING_FILE` (optional): path to a JSON file containing SMS-to-Matrix mappings to load at startup
2021

2122
Building and running
2223

@@ -44,6 +45,36 @@ The `LOGLEVEL` environment variable controls the verbosity of application logs:
4445

4546
For debugging mapping and API issues, set `LOGLEVEL=DEBUG` to see detailed trace information.
4647

48+
### Loading Mappings from File
49+
50+
You can pre-load SMS-to-Matrix mappings at startup by providing a `MAPPING_FILE` environment variable pointing to a JSON file. This is useful for initializing the proxy with a set of known mappings.
51+
52+
The JSON file should be an array of mapping objects:
53+
54+
```json
55+
[
56+
{
57+
"sms_number": "91201",
58+
"matrix_id": "@giacomo:synapse.gs.nethserver.net",
59+
"room_id": "!giacomo-room:synapse.gs.nethserver.net"
60+
},
61+
{
62+
"sms_number": "91202",
63+
"matrix_id": "@mario:synapse.gs.nethserver.net",
64+
"room_id": "!mario-room:synapse.gs.nethserver.net"
65+
}
66+
]
67+
```
68+
69+
Usage:
70+
71+
```bash
72+
export MAPPING_FILE="/path/to/mappings.json"
73+
./matrix2acrobits
74+
```
75+
76+
The loaded mappings will be logged at startup with the message: `mappings loaded from file count=N file=/path/to/mappings.json`
77+
4778
## Extra info
4879

4980
- [Deploying with NS8](docs/DEPLOY_NS8.md)

docs/DEPLOY_NS8.md

Lines changed: 35 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,21 @@ Run the container:
103103
podman run --rm --replace --name matrix2acrobits --network host -e MATRIX_HOMESERVER_URL=https://synapse.gs.nethserver.net -e SUPER_ADMIN_TOKEN=secret -e PROXY_PORT=8080 -e AS_USER_ID=@_acrobits_proxy:synapse.gs.nethserver.net ghcr.io/nethesis/matrix2acrobits
104104
```
105105

106+
107+
Then run the container with the mapping file mounted:
108+
```
109+
podman run --rm --replace --name matrix2acrobits --network host \
110+
-v /etc/matrix2acrobits/mappings.json:/mappings.json:ro \
111+
-e MATRIX_HOMESERVER_URL=https://synapse.gs.nethserver.net \
112+
-e SUPER_ADMIN_TOKEN=secret \
113+
-e PROXY_PORT=8080 \
114+
-e AS_USER_ID=@_acrobits_proxy:synapse.gs.nethserver.net \
115+
-e MAPPING_FILE=/mappings.json \
116+
ghcr.io/nethesis/matrix2acrobits
117+
```
118+
119+
The container will log the loaded mappings at startup: `mappings loaded from file count=N file=/mappings.json`
120+
106121
Configure traefik to route /m2a to the proxy:
107122
```
108123
api-cli run set-route --agent module/traefik1 --data '{"instance": "matrix1-m2a", "name":"synapse-m2a","host":"synapse.gs.nethserver.net","path":"/m2a","url":"http://localhost:8080","lets_encrypt":true, "strip_prefix": true}'
@@ -138,19 +153,19 @@ curl -s -X POST https://synapse.gs.nethserver.net/m2a/api/client/send_message
138153
```
139154

140155

141-
Map SMS number (201) to Matrix user (giacomo):
156+
Map SMS number (91201) to Matrix user (giacomo):
142157
```
143-
curl -v -X POST "http://127.0.0.1:8080/api/internal/map_sms_to_matrix" -H "Content-Type: application/json" -H "X-Super-Admin-Token: secret" -d '{
144-
"sms_number": "201",
158+
curl "http://127.0.0.1:8080/api/internal/map_sms_to_matrix" -H "Content-Type: application/json" -H "X-Super-Admin-Token: secret" -d '{
159+
"sms_number": "91201",
145160
"matrix_id": "@giacomo:synapse.gs.nethserver.net",
146161
"room_id": "!giacomo-room:synapse.gs.nethserver.net"
147162
}'
148163
```
149164

150-
Map SMS number (202) to Matrix user (mario):
165+
Map SMS number (91202) to Matrix user (mario):
151166
```
152-
curl -v -X POST "http://127.0.0.1:8080/api/internal/map_sms_to_matrix" -H "Content-Type: application/json" -H "X-Super-Admin-Token: secret" -d '{
153-
"sms_number": "202",
167+
curl "http://127.0.0.1:8080/api/internal/map_sms_to_matrix" -H "Content-Type: application/json" -H "X-Super-Admin-Token: secret" -d '{
168+
"sms_number": "91202",
154169
"matrix_id": "@mario:synapse.gs.nethserver.net",
155170
"room_id": "!mario-room:synapse.gs.nethserver.net"
156171
}'
@@ -161,22 +176,32 @@ Retrieve current mappings:
161176
curl "http://127.0.0.1:8080/api/internal/map_sms_to_matrix" -H "X-Super-Admin-Token: secret"
162177
```
163178

164-
Send message using mapped SMS number (201) - Giacomo to Mario:
179+
Send message using mapped SMS number (91201) - Giacomo to Mario:
165180
```
166181
curl -s -X POST https://synapse.gs.nethserver.net/m2a/api/client/send_message -H "Content-Type: application/json" -d '{
167182
"from": "@giacomo:synapse.gs.nethserver.net",
168-
"sms_to": "202",
183+
"sms_to": "91202",
169184
"sms_body": "Hello Mario — this is Giacomo (curl test using mapped number)",
170185
"content_type": "text/plain"
171186
}'
172187
```
173188

174-
Send message using mapped SMS number (202) - Mario to Giacomo:
189+
Send message using mapped SMS number (91202) - Mario to Giacomo:
175190
```
176191
curl -s -X POST https://synapse.gs.nethserver.net/m2a/api/client/send_message -H "Content-Type: application/json" -d '{
177192
"from": "@mario:synapse.gs.nethserver.net",
178-
"sms_to": "201",
193+
"sms_to": "91201",
179194
"sms_body": "Hello Giacomo — this is Mario reply (curl test using mapped number)",
180195
"content_type": "text/plain"
181196
}'
197+
```
198+
199+
Send message using mapped numbers - Giacomo to Mario:
200+
```
201+
curl -s -X POST https://synapse.gs.nethserver.net/m2a/api/client/send_message -H "Content-Type: application/json" -d '{
202+
"from": "91201",
203+
"sms_to": "91202",
204+
"sms_body": "Hello Mario — this is Giacomo (curl test using both mapped numbers)",
205+
"content_type": "text/plain"
206+
}'
182207
```

main.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,14 @@ func main() {
6868
svc := service.NewMessageService(matrixClient)
6969
api.RegisterRoutes(e, svc, adminToken)
7070

71+
// Load mappings from file if MAPPING_FILE env var is set
72+
mappingFile := os.Getenv("MAPPING_FILE")
73+
if mappingFile != "" {
74+
if err := svc.LoadMappingsFromFile(mappingFile); err != nil {
75+
logger.Error().Err(err).Str("file", mappingFile).Msg("failed to load mappings from file")
76+
}
77+
}
78+
7179
logger.Info().Str("port", port).Msg("starting server")
7280
if err := e.Start(":" + port); err != nil {
7381
logger.Fatal().Err(err).Msg("server stopped")

service/messages.go

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,10 @@ package service
22

33
import (
44
"context"
5+
"encoding/json"
56
"errors"
67
"fmt"
8+
"os"
79
"strings"
810
"sync"
911
"time"
@@ -284,6 +286,43 @@ func (s *MessageService) SaveMapping(req *models.MappingRequest) (*models.Mappin
284286
return s.buildMappingResponse(entry), nil
285287
}
286288

289+
// LoadMappingsFromFile loads mappings from a JSON file in the format:
290+
//
291+
// [
292+
// {"sms_number": "91201", "matrix_id": "@giacomo:synapse.gs.nethserver.net", "room_id": "!giacomo-room:synapse.gs.nethserver.net"},
293+
// {"sms_number": "91202", "matrix_id": "@mario:synapse.gs.nethserver.net", "room_id": "!mario-room:synapse.gs.nethserver.net"}
294+
// ]
295+
//
296+
// This is typically called at startup if MAPPING_FILE environment variable is set.
297+
func (s *MessageService) LoadMappingsFromFile(filePath string) error {
298+
data, err := os.ReadFile(filePath)
299+
if err != nil {
300+
return fmt.Errorf("failed to read mapping file: %w", err)
301+
}
302+
303+
var mappingArray []models.MappingRequest
304+
if err := json.Unmarshal(data, &mappingArray); err != nil {
305+
return fmt.Errorf("failed to parse mapping file: %w", err)
306+
}
307+
308+
for _, req := range mappingArray {
309+
if req.SMSNumber == "" {
310+
logger.Warn().Msg("skipping mapping with empty sms_number")
311+
continue
312+
}
313+
entry := mappingEntry{
314+
SMSNumber: req.SMSNumber,
315+
MatrixID: req.MatrixID,
316+
RoomID: id.RoomID(req.RoomID),
317+
UpdatedAt: s.now(),
318+
}
319+
s.setMapping(entry)
320+
}
321+
322+
logger.Info().Int("count", len(mappingArray)).Str("file", filePath).Msg("mappings loaded from file")
323+
return nil
324+
}
325+
287326
func (s *MessageService) buildMappingResponse(entry mappingEntry) *models.MappingResponse {
288327
return &models.MappingResponse{
289328
SMSNumber: entry.SMSNumber,

service/messages_test.go

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package service
22

33
import (
4+
"os"
45
"testing"
56

67
"github.com/nethesis/matrix2acrobits/models"
@@ -265,3 +266,100 @@ func TestIsPhoneNumber(t *testing.T) {
265266
})
266267
}
267268
}
269+
270+
func TestLoadMappingsFromFile(t *testing.T) {
271+
// Create a temporary JSON file with test mappings in array format
272+
tmpFile, err := os.CreateTemp("", "mappings_*.json")
273+
assert.NoError(t, err)
274+
defer os.Remove(tmpFile.Name())
275+
276+
// Write test data in array format
277+
testData := `[
278+
{
279+
"sms_number": "91201",
280+
"matrix_id": "@giacomo:example.com",
281+
"room_id": "!room1:example.com"
282+
},
283+
{
284+
"sms_number": "91202",
285+
"matrix_id": "@mario:example.com",
286+
"room_id": "!room2:example.com"
287+
}
288+
]`
289+
_, err = tmpFile.WriteString(testData)
290+
assert.NoError(t, err)
291+
tmpFile.Close()
292+
293+
// Create a message service
294+
svc := NewMessageService(nil)
295+
296+
// Load mappings from file
297+
err = svc.LoadMappingsFromFile(tmpFile.Name())
298+
assert.NoError(t, err)
299+
300+
// Verify mappings were loaded
301+
mappings, err := svc.ListMappings()
302+
assert.NoError(t, err)
303+
assert.Len(t, mappings, 2)
304+
305+
// Check specific mappings
306+
mapping1, err := svc.LookupMapping("91201")
307+
assert.NoError(t, err)
308+
assert.Equal(t, "@giacomo:example.com", mapping1.MatrixID)
309+
assert.Equal(t, "!room1:example.com", mapping1.RoomID)
310+
311+
mapping2, err := svc.LookupMapping("91202")
312+
assert.NoError(t, err)
313+
assert.Equal(t, "@mario:example.com", mapping2.MatrixID)
314+
assert.Equal(t, "!room2:example.com", mapping2.RoomID)
315+
}
316+
317+
func TestLoadMappingsFromFile_LegacyFormat(t *testing.T) {
318+
// Create a temporary JSON file with test mappings in legacy format
319+
tmpFile, err := os.CreateTemp("", "mappings_legacy_*.json")
320+
assert.NoError(t, err)
321+
defer os.Remove(tmpFile.Name())
322+
323+
// Write test data in legacy format (object with phone numbers as keys)
324+
testData := `{
325+
"91201": "@giacomo:example.com",
326+
"91202": "@mario:example.com"
327+
}`
328+
_, err = tmpFile.WriteString(testData)
329+
assert.NoError(t, err)
330+
tmpFile.Close()
331+
332+
// Create a message service
333+
svc := NewMessageService(nil)
334+
335+
// Load mappings from file - should fail since we only support extended format now
336+
err = svc.LoadMappingsFromFile(tmpFile.Name())
337+
assert.Error(t, err)
338+
assert.Contains(t, err.Error(), "failed to parse mapping file")
339+
}
340+
341+
func TestLoadMappingsFromFile_FileNotFound(t *testing.T) {
342+
svc := NewMessageService(nil)
343+
344+
err := svc.LoadMappingsFromFile("/nonexistent/file.json")
345+
assert.Error(t, err)
346+
assert.Contains(t, err.Error(), "failed to read mapping file")
347+
}
348+
349+
func TestLoadMappingsFromFile_InvalidJSON(t *testing.T) {
350+
// Create a temporary file with invalid JSON
351+
tmpFile, err := os.CreateTemp("", "invalid_*.json")
352+
assert.NoError(t, err)
353+
defer os.Remove(tmpFile.Name())
354+
355+
// Write invalid JSON
356+
_, err = tmpFile.WriteString("{ invalid json }")
357+
assert.NoError(t, err)
358+
tmpFile.Close()
359+
360+
svc := NewMessageService(nil)
361+
362+
err = svc.LoadMappingsFromFile(tmpFile.Name())
363+
assert.Error(t, err)
364+
assert.Contains(t, err.Error(), "failed to parse mapping file")
365+
}

0 commit comments

Comments
 (0)