Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Introduce FCM and APNS push notification services #50

Merged
merged 6 commits into from
Jan 27, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 15 additions & 11 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,17 +8,21 @@ Service designed for the management and storage of messaging for the Message Pic

## Environment Variables

| Variable | Description | Default Value |
| ---------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------- |
| `APP_PORT` | The port number on which the application will run. | `3500` |
| `WS_PORT` | The port number on which the WebSocket server runs. | `3100` |
| `FCM_SERVICE_BASE_URL` | The base URL for the push notification service. | _Not set by default_ |
| `MONGODB_URI` | The MongoDB URI for connecting to the database. | `mongodb://user:password@localhost:27017/MessagePickupRepository` |
| `REDIS_TYPE` | Allows set redis type works `single` or `cluster` | `single` |
| `REDIS_NODES` | A comma-separated list of Redis nodes' `host:port` for cluster mode. Only required if `REDIS_TYPE` is set to `cluster`. Ignored in single mode. | `redis-node1:6379,redis-node2:6379,redis-node3:6379` |
| `REDIS_NATMAP` | The NAT mapping for Redis nodes in `externalAddress:host:port` format. Required for Redis cluster configurations where external IPs or ports are mapped to internal Redis node addresses. | `10.0.0.1:6379:redis-node1:6379,10.0.0.2:6379:redis-node2:6379` |
| `REDIS_URL` | The Redis database URL for connecting to the server.(only single mode) | `redis://localhost:6379` |
| `THRESHOLD_TIMESTAMP` | Allows set threshold time to execute message persist module on milisecond | `60000` |
| Variable | Description | Default Value |
| ------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------- |
| `APP_PORT` | The port number on which the application will run. | `3500` |
| `WS_PORT` | The port number on which the WebSocket server runs. | `3100` |
| `MONGODB_URI` | The MongoDB URI for connecting to the database. | `mongodb://user:password@localhost:27017/MessagePickupRepository` |
| `REDIS_TYPE` | Allows set redis type works `single` or `cluster` | `single` |
| `REDIS_NODES` | A comma-separated list of Redis nodes' `host:port` for cluster mode. Only required if `REDIS_TYPE` is set to `cluster`. Ignored in single mode. | `redis-node1:6379,redis-node2:6379,redis-node3:6379` |
| `REDIS_NATMAP` | The NAT mapping for Redis nodes in `externalAddress:host:port` format. Required for Redis cluster configurations where external IPs or ports are mapped to internal Redis node addresses. | `10.0.0.1:6379:redis-node1:6379,10.0.0.2:6379:redis-node2:6379` |
| `REDIS_URL` | The Redis database URL for connecting to the server.(only single mode) | `redis://localhost:6379` |
| `THRESHOLD_TIMESTAMP` | Allows set threshold time to execute message persist module on milisecond | `60000` |
| `FIREBASE_CFG_FILE` | The file path to the Firebase configuration JSON file. | `./test/firebase-cfg.json` |
| `APNS_CFG_FILE` | The file path to the APNs configuration JSON file. | `./test/apns-cfg.json` |
| `APNS_PATH_KEY` | The file path to the APNs authentication key file. | `./test/apns-authkey.p8` |
| `APNS_TOPIC` | The APNs topic, which is usually the app's bundle identifier. | `'default'` |
| `NOTIFICATION_DATA_ONLY` | Indicates whether notifications should be sent as data-only. | `false` |

## Installation

Expand Down
13 changes: 11 additions & 2 deletions docs/message-pickup-repository-client.md
Original file line number Diff line number Diff line change
Expand Up @@ -56,14 +56,23 @@ Registers a callback to handle `messagesReceived` events from the WebSocket serv

### `setConnectionInfo(callback)`

Registers a callback function to retrieve connection-specific information based on a given `connectionId`. This callback provides the `ConnectionInfo` object, which contains details such as the FCM notification token and maximum bytes allowed for receiving messages. This function is useful for dynamically fetching connection-related information whenever it is required by the client.
Registers a callback function to retrieve connection-specific information based on a given `connectionId`. This callback provides the `ConnectionInfo` object, which contains details such as the push notification token and maximum bytes allowed for receiving messages. This function is useful for dynamically fetching connection-related information whenever it is required by the client.

- **Parameters**:

- `callback`: A function that takes a `connectionId` (string) and returns a `Promise` resolving to a `ConnectionInfo` object or `undefined` if no information is available for the given `connectionId`.
- `connectionId` (string): The ID of the connection for which to retrieve information.

- **Returns**: `Promise<ConnectionInfo | undefined>`: The connection information, including: - `fcmNotificationToken` (optional, string): The FCM notification token for the specified connection. - `maxReceiveBytes` (optional, number): The maximum allowed bytes for receiving messages for this connection.
- **Returns**: `Promise<ConnectionInfo | undefined>`:

The resolved connection-specific information. This object includes:

- `pushNotificationToken` (optional, object): Contains details about the push notification token for the connection.

- `type` (string): Specifies the notification type, e.g., `fcm` for Firebase Cloud Messaging or `apn` for Apple Push Notification Service ().
- `token` (optional, string): The actual notification token for the device.

- `maxReceiveBytes` (optional, number): The maximum byte size allowed for messages received on this connection.

---

Expand Down
30 changes: 25 additions & 5 deletions docs/message-pickup-repository-server.md
Original file line number Diff line number Diff line change
Expand Up @@ -117,13 +117,33 @@ To resolve synchronization across multiple instances the Message Pickup Reposito

---

### Push Notifications Handler
## Push Notifications Handler

In the **Message Pickup Repository**, the `sendPushNotification` function is responsible for sending push notifications to clients when they receive new messages. This mechanism ensures that clients are alerted even if they are not LiveSession into the server.
In the **Message Pickup Repository**, the system ensures that clients are notified about new messages using **push notifications**. These notifications are sent using **Firebase Cloud Messaging (FCM)** for Android devices and **Apple Push Notification Service (APNs)** for iOS devices. This mechanism guarantees that clients receive alerts even if they are not in a live session with the server.

#### When are Push Notifications Triggered?
### When Are Push Notifications Triggered?

Push notifications are triggered under the following conditions:

1. **New Message Arrival**: Whenever a new message is added to the message queue for a specific `connectionId`, the server checks whether the client is online or not.
2. **Client Offline**: If the client is not liveSession, a push notification is sent to notify the client of the new message via FCM Notification Sender API.
1. **New Message Arrival**:
Whenever a new message is added to the message queue for a specific `connectionId`, the server determines if the client is online or not.

2. **Client Offline**:
If the client is not in a live session, a push notification is sent to notify the client of the new message:
- **For Android devices**: The notification is sent via **FCM Notification Sender API**.
- **For iOS devices**: The notification is sent via **APNs Notification Sender API**.

### How Does It Work?

1. **Message Queuing**:

- When a new message is received, it is added to a queue in **Redis** for fast access and stored in **MongoDB** for persistence.
- If the client is offline, the system initiates the push notification process.

2. **Notification Services**:

- **FCM**: The `FcmNotificationSender` class handles the sending of notifications to Android devices.
- **APNs**: The `ApnNotificationSender` class handles the sending of notifications to iOS devices.

3. **Token Management**:
- The `PushNotificationQueueService` ensures that duplicate notifications are avoided by managing a queue of tokens.
6 changes: 3 additions & 3 deletions packages/client/src/MessagePickupRepositoryClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -118,7 +118,7 @@ export class MessagePickupRepositoryClient implements MessagePickupRepository {
* const getConnectionInfo = async (connectionId: string) => {
* const connectionRecord = await agent.connections.findById(connectionId);
* return {
* fcmNotificationToken: connectionRecord?.getTag('device_token') as string | undefined,
* pushNotificationToken: { type: 'fcm', token: connectionRecord?.getTag('device_token') as string | undefined }
* maxReceiveBytes: config.messagePickupMaxReceiveBytes,
* };
* };
Expand Down Expand Up @@ -227,8 +227,8 @@ export class MessagePickupRepositoryClient implements MessagePickupRepository {
? await this.connectionInfoCallback(params.connectionId)
: undefined

// Set the token and max bytes from the connection info, if available
params.token = connectionInfo?.fcmNotificationToken
// Set the pushNotificationToken and maxReceiveBytes from the connection info, if available
params.pushNotificationToken = connectionInfo?.pushNotificationToken

// Call the 'addMessage' RPC method on the WebSocket server
const result: unknown = await client.call('addMessage', params, 2000)
Expand Down
1 change: 1 addition & 0 deletions packages/client/src/index.ts
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
export { MessagePickupRepositoryClient } from './MessagePickupRepositoryClient'
export { ConnectionInfo } from './interfaces'
10 changes: 8 additions & 2 deletions packages/client/src/interfaces.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,10 +24,16 @@ export interface ExtendedTakeFromQueueOptions extends TakeFromQueueOptions {
}

export interface ExtendedAddMessageOptions extends AddMessageOptions {
token?: string
pushNotificationToken?: {
type: string
token?: string
}
}

export interface ConnectionInfo {
fcmNotificationToken?: string
pushNotificationToken?: {
type: string
token?: string
}
maxReceiveBytes?: number
}
3 changes: 3 additions & 0 deletions packages/server/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@
"@nestjs/websockets": "^10.4.1",
"axios": "^1.7.5",
"class-validator": "^0.14.1",
"class-transformer": "^0.5.1",
"ioredis": "^5.4.1",
"mongodb": "^6.8.0",
"mongodb-memory-server": "^10.0.0",
Expand All @@ -46,6 +47,8 @@
"ws": "^8.18.0"
},
"devDependencies": {
"apn": "^2.2.0",
"firebase-admin": "11.0.0",
"@nestjs/cli": "^10.0.0",
"@nestjs/schematics": "^10.0.0",
"@nestjs/testing": "^10.0.0",
Expand Down
58 changes: 41 additions & 17 deletions packages/server/src/config/app.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,20 +13,6 @@ export default registerAs('appConfig', () => ({
*/
appPort: parseInt(process.env.APP_PORT, 10) || 3500,

/**
* The port number on which the WebSocket server will run.
* Defaults to 3100 if WS_PORT is not set in the environment variables.
* @type {number}
*/
wsPort: parseInt(process.env.WS_PORT, 10) || 3100,

/**
* The base URL for the push notification service.
* Retrieved from the FCM_SERVICE_BASE_URL environment variable.
* @type {string | undefined}
*/
pushNotificationUrl: process.env.FCM_SERVICE_BASE_URL,

/**
* The MongoDB URI for connecting to the database.
* Defaults to a local MongoDB instance if MONGODB_URI is not set in the environment variables.
Expand Down Expand Up @@ -54,15 +40,53 @@ export default registerAs('appConfig', () => ({
* @type {string | undefined}
*/
redisNatmap: process.env.REDIS_NATMAP,

/**
* The Redis database URL for connecting to the Redis server Single Mode.
* The Redis database URL for connecting to the Redis server in single mode.
* Defaults to a specified local Redis instance if REDIS_URL is not set in the environment variables.
* @type {string}
*/
redisDbUrl: process.env.REDIS_URL || 'redis://localhost:6379',

/**
*Allows set threshold time to execute messagePersist module on milisecond
* The threshold time (in milliseconds) to execute the message persistence module.
* Defaults to 60000 milliseconds if THRESHOLD_TIMESTAMP is not set in the environment variables.
* @type {number}
*/
thresholdTimestamp: parseInt(process.env.THRESHOLD_TIMESTAMP, 10) || 60000,

/**
* The file path to the Firebase configuration JSON file.
* Defaults to './test/firebase-cfg.json' if FIREBASE_CFG_FILE is not set in the environment variables.
* @type {string}
*/
firebaseCfgFile: process.env.FIREBASE_CFG_FILE || './test/firebase-cfg.json',

/**
* The file path to the APNs configuration JSON file.
* Defaults to './test/apns-cfg.json' if APNS_CFG_FILE is not set in the environment variables.
* @type {string}
*/
apnConfigFile: process.env.APNS_CFG_FILE || './test/apns-cfg.json',

/**
* The file path to the APNs authentication key.
* Defaults to './test/apns-authkey.p8' if APNS_PATH_KEY is not set in the environment variables.
* @type {string}
*/
apnsPathKey: process.env.APNS_PATH_KEY || './test/apns-authkey.p8',

/**
* The APNs topic, usually the app's bundle identifier.
* Defaults to 'default' if APNS_TOPIC is not set in the environment variables.
* @type {string}
*/
apnsTopic: process.env.APNS_TOPIC || 'default',

/**
* Indicates if the notification should be sent as data-only.
* Defaults to false if NOTIFICATION_DATA_ONLY is not set in the environment variables.
* @type {boolean}
*/
thresholdTimestamp: parseInt(process.env.THRESHOLD_TIMESTAMP) || 60000,
notificationDataOnly: Boolean(process.env.NOTIFICATION_DATA_ONLY) || false,
}))
Loading
Loading