Skip to content

Commit c8f7115

Browse files
authored
feat(admin): Send notification by topic (#303)
- Introduces two admin api endpoints: query `notificationTopics`, mutation `sendNotification` - Notification types defined in yaml
2 parents ee1f9dd + b806f26 commit c8f7115

32 files changed

+466
-343
lines changed

.env

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,7 @@ export LND2_TYPE=offchain
5858
export LND1_NAME=lnd1
5959
export LND2_NAME=lnd2
6060

61-
export MONGODB_CON=mongodb://${DOCKER_HOST_IP}:27017/galoy
61+
export MONGODB_CON=mongodb://localhost:27017/galoy
6262

6363
export REDIS_0_DNS=${DOCKER_HOST_IP}
6464
export REDIS_0_PORT=6378
Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
#!/usr/bin/env node
2+
/**
3+
* One-off script: subscribes all device tokens stored in deviceTopics to their
4+
* respective FCM topics via the Firebase Admin SDK.
5+
*
6+
* Run AFTER the migration `20260317125624-subscribe-device-tokens-to-broadcast` has
7+
* been applied to populate the deviceTopics field.
8+
*
9+
* Required env vars:
10+
* MONGODB_CON e.g. mongodb://localhost/galoy
11+
* GOOGLE_APPLICATION_CREDENTIALS path to Firebase service account JSON
12+
* FCM_TOPIC_PREFIX (optional) e.g. "test" → topic becomes "test-broadcast"
13+
* omit on prod → topic is "broadcast"
14+
*/
15+
16+
const { MongoClient } = require("mongodb")
17+
const admin = require("firebase-admin")
18+
19+
const BATCH_SIZE = 1000
20+
21+
const MONGODB_CON = process.env.MONGODB_CON
22+
if (!MONGODB_CON) {
23+
console.error("Error: MONGODB_CON environment variable is required")
24+
process.exit(1)
25+
}
26+
27+
if (!process.env.GOOGLE_APPLICATION_CREDENTIALS) {
28+
console.error("Error: GOOGLE_APPLICATION_CREDENTIALS environment variable is required")
29+
process.exit(1)
30+
}
31+
32+
admin.initializeApp({ credential: admin.credential.applicationDefault() })
33+
const messaging = admin.messaging()
34+
35+
async function subscribeInBatches(tokens, topic) {
36+
let successCount = 0
37+
let failureCount = 0
38+
39+
for (let i = 0; i < tokens.length; i += BATCH_SIZE) {
40+
const batch = tokens.slice(i, i + BATCH_SIZE)
41+
const batchNum = Math.floor(i / BATCH_SIZE) + 1
42+
console.log(
43+
`Subscribing batch ${batchNum} (tokens ${i + 1}${i + batch.length}) to topic "${topic}"`,
44+
)
45+
46+
const response = await messaging.subscribeToTopic(batch, topic)
47+
successCount += response.successCount
48+
failureCount += response.failureCount
49+
50+
if (response.errors.length > 0) {
51+
response.errors.forEach(({ index, error }) => {
52+
console.warn(` Token[${index}] failed: ${error.message}`)
53+
})
54+
}
55+
}
56+
57+
return { successCount, failureCount }
58+
}
59+
60+
async function main() {
61+
const client = new MongoClient(MONGODB_CON)
62+
63+
try {
64+
await client.connect()
65+
const db = client.db()
66+
67+
const users = await db
68+
.collection("users")
69+
.find(
70+
{ deviceTopics: { $exists: true } },
71+
{ projection: { _id: 0, deviceTopics: 1 } },
72+
)
73+
.toArray()
74+
75+
if (users.length === 0) {
76+
console.log("No users with deviceTopics found — run the migration first")
77+
return
78+
}
79+
80+
// Group tokens by topic
81+
const tokensByTopic = {}
82+
for (const user of users) {
83+
for (const [token, topics] of Object.entries(user.deviceTopics)) {
84+
for (const topic of topics) {
85+
if (!tokensByTopic[topic]) tokensByTopic[topic] = []
86+
tokensByTopic[topic].push(token)
87+
}
88+
}
89+
}
90+
91+
for (const [topic, tokens] of Object.entries(tokensByTopic)) {
92+
console.log(`\nSubscribing ${tokens.length} tokens to topic "${topic}"`)
93+
const { successCount, failureCount } = await subscribeInBatches(tokens, topic)
94+
console.log(`Done — success: ${successCount}, failures: ${failureCount}`)
95+
}
96+
} finally {
97+
await client.close()
98+
}
99+
}
100+
101+
main().catch((err) => {
102+
console.error(err)
103+
process.exit(1)
104+
})
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
meta {
2+
name: notification-topics
3+
type: graphql
4+
seq: 3
5+
}
6+
7+
post {
8+
url: {{admin_url}}
9+
body: graphql
10+
auth: bearer
11+
}
12+
13+
auth:bearer {
14+
token: {{admin_token}}
15+
}
16+
17+
body:graphql {
18+
query {
19+
notificationTopics
20+
}
21+
}
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
meta {
2+
name: send-notification
3+
type: graphql
4+
seq: 2
5+
}
6+
7+
post {
8+
url: {{admin_url}}
9+
body: graphql
10+
auth: bearer
11+
}
12+
13+
auth:bearer {
14+
token: {{admin_token}}
15+
}
16+
17+
body:graphql {
18+
mutation SendNotification($input: SendNotificationInput!) {
19+
sendNotification(input: $input) {
20+
errors {
21+
message
22+
}
23+
success
24+
}
25+
}
26+
}
27+
28+
body:graphql:vars {
29+
{
30+
"input": {
31+
"topic": "brh28-ATTENTION",
32+
"title": "Hello",
33+
"body": "This is a notification"
34+
}
35+
}
36+
}
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
vars {
2+
main_protocol: https
3+
main_domain: api.test.flashapp.me
4+
main_port: 4002
5+
~admin_url: http://localhost:4001/graphql
6+
~admin_token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VySWQiOiJhZG1pbiIsInJvbGVzIjpbIkFjY291bnRzIE1hbmFnZXIiXX0.UOmQR2K6RdS1FVvQbjvSQfoQ-VsTC6Y7x2YAXZImdsA
7+
currency: BTC
8+
~phone: +1301
9+
~code: 000000
10+
token:
11+
walletId:
12+
walletIdUsd: c593736e-5a58-42e4-93fa-dc895856c1f1
13+
graphqlUrl: https://api.test.flashapp.me/graphql
14+
}

dev/bruno/Flash GraphQL API/notoken/folder.bru

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,11 @@
11
meta {
22
name: notoken
3-
seq: 1
3+
}
4+
5+
headers {
6+
Accept: */*
7+
Connection: keep-alive
8+
Accept-Encoding: gzip, deflate, br
49
}
510

611
auth {
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
meta {
2+
name: supportedBanks
3+
type: graphql
4+
seq: 8
5+
}
6+
7+
post {
8+
url: {{graphqlUrl}}
9+
body: graphql
10+
auth: inherit
11+
}
12+
13+
body:graphql {
14+
query {
15+
supportedBanks {
16+
name
17+
}
18+
}
19+
}
20+
21+
settings {
22+
encodeUrl: true
23+
timeout: 0
24+
}

dev/config/base-config.yaml

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,3 +51,11 @@ frappe:
5151

5252
sendgrid:
5353
apiKey: "<replace>"
54+
55+
# FCM topic names for push notifications.
56+
# Replace "dev" with a unique identifier to avoid accidentally sending to other environments.
57+
notificationTopics:
58+
- "dev-EMERGENCY"
59+
- "dev-ATTENTION"
60+
- "dev-INFO"
61+
- "dev-MARKETING"

src/app/admin/index.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
export * from "./update-user-phone"
2-
export * from "./send-admin-push-notification"
3-
export * from "./send-broadcast-notification"
2+
// export * from "./send-admin-push-notification"
3+
// export * from "./send-broadcast-notification"
44

55
import { checkedToAccountUuid, checkedToUsername } from "@domain/accounts"
66
import { IdentityRepository } from "@services/kratos"

src/app/admin/send-admin-push-notification.ts

Lines changed: 0 additions & 51 deletions
This file was deleted.

0 commit comments

Comments
 (0)