Skip to content

Commit

Permalink
DEVEXP-566: Add 'Auto-subscribe app' tutorial (#7)
Browse files Browse the repository at this point in the history
  • Loading branch information
asein-sinch authored Sep 24, 2024
1 parent 225d649 commit 6af2f4c
Show file tree
Hide file tree
Showing 11 changed files with 467 additions and 2 deletions.
2 changes: 1 addition & 1 deletion lerna.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{
"packages": ["getting-started/**/*", "templates/*", "tutorials/*"],
"packages": ["getting-started/**/*", "templates/*", "tutorials/**/*"],
"npmClient": "npm",
"version": "independent"
}
3 changes: 2 additions & 1 deletion tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,8 @@
"baseUrl": ".",
"noEmit": true,
"skipLibCheck": true,
"noImplicitAny": false
"noImplicitAny": false,
"strictNullChecks": false
},
"include": [
"**/*.js"
Expand Down
14 changes: 14 additions & 0 deletions tutorials/sms/auto-subscribe-app/.env
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
# Express related configuration
port = 3001

# Unified related credentials, used by:
# - SMS: US/EU are the only supported regions with unified credentials
SINCH_PROJECT_ID = <Your Sinch Project ID>
SINCH_KEY_ID = <Your Sinch Key ID>
SINCH_KEY_SECRET = <Your Sinch Key Secret>
SMS_REGION = us

# SMS Service Plan ID related credentials
# if set, these credentials will be used and enable to use regions different of US/EU
#SINCH_SERVICE_PLAN_ID = <Your Service Plan ID>
#SINCH_API_TOKEN = <Your Service Plan Token>
67 changes: 67 additions & 0 deletions tutorials/sms/auto-subscribe-app/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
# Auto-subscribe application sample

This directory contains some code related to the Node.js SDK tutorial: [auto-subscribe](https://developers.sinch.com/docs/sms/tutorials/)

## Requirements

- [Node.js LTS](https://nodejs.org/en)
- [Express](https://expressjs.com/)
- [Sinch account](https://dashboard.sinch.com/)
- [ngrok](https://ngrok.com/docs)

## Usage

### Configure application settings

Edit the [.env](.env) file to set the parameters that will be used to configure the Express server and the controller.

#### Sinch credentials

To use the [SMS API](https://developers.sinch.com/docs/sms/), you need to fill the following variables with the values from your Sinch account:
- `SINCH_PROJECT_ID`=Your Sinch Project ID
- `SINCH_KEY_ID`=Your Sinch Access Key ID
- `SINCH_KEY_SECRET`=Your Sinch Key Secret associated to your Sinch Access Key
- `SMS_REGION`=the SMS region (`us` / `eu`)

In case you want to use the [SMS API](https://developers.sinch.com/docs/sms/) with regions others than US and EU, you need to use the "Service Plan ID" and fill the following variables with the values from your [Services](https://dashboard.sinch.com/sms/api/services) section in the dashboard:
- `SINCH_SERVICE_PLAN_ID`=Your Service Plan ID
- `SINCH_API_TOKEN`=Your API Token associated to your Service Plan ID
- `SMS_REGION`=the SMS region (`au` / `br` / `ca`)

#### Server port

*Default: 3001*
- `port`: the port to be used to listen to incoming requests. Default is `3001` if not set.

### Starting the server locally

1. Install the dependencies by running the command `npm install`.
2. Edit the `.env` file with your own parameters (see the paragraph above for details).
3. Start the server with the following command:
```bash
npm start
```

### Use ngrok to forward requests to the local server

You can forward the request to your local server of the port it is listening to.

*Note: The `3001` value is coming from the default configuration and can be changed (see [Server port](#Server port) configuration section)*

```bash
ngrok http 3001
```

The `ngrok` output will contain something like:
```
ngrok (Ctrl+C to quit)
...
Forwarding https://cafe-64-220-29-200.ngrok-free.app -> http://localhost:3001
```
The line
```
Forwarding https://cafe-64-220-29-200.ngrok-free.app -> http://localhost:3001
```
contains the "`https://cafe-64-220-29-200.ngrok-free.app`" value which will be used to determine the callback URL.

With this example, given the fact the controller is exposing the path `/SmsEvent`, the resulting callback URL to configure in your [Sinch dashboard](https://dashboard.sinch.com/sms/api/services) will be `https://cafe-64-220-29-200.ngrok-free.app/SmsEvent` (adapt the value according to your ngrok and controller configurations).
16 changes: 16 additions & 0 deletions tutorials/sms/auto-subscribe-app/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
{
"name": "@sinch/tutorial_auto-subscribe-app",
"version": "0.0.0",
"author": "Sinch",
"description": "",
"type": "module",
"scripts": {
"start": "node src/server.js",
"compile": "tsc"
},
"dependencies": {
"@sinch/sdk-core": "^1.1.0",
"dotenv": "^16.4.5",
"express": "^4.20.0"
}
}
31 changes: 31 additions & 0 deletions tutorials/sms/auto-subscribe-app/src/auto-subscribe-controller.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { SmsCallbackWebhooks } from '@sinch/sdk-core';
import { validateSignature } from './utils.js';
import { processInboundEvent } from './auto-subscribe-service.js';

export const autoSubscribeController = (app, sinchClient) => {

const smsCallbackWebhooks = new SmsCallbackWebhooks();

/**
* POST /SmsEvent - Handles incoming SMS events from the Sinch callback and processes "SUBSCRIBE" or "STOP" messages.
*
* @return {Promise<void>} - Sends a 200 status on success, or a 400 status if there's an error or unexpected event type.
*/
app.post('/SmsEvent', validateSignature, async (req, res) => {
try {
// Parse the incoming SMS event from the request body
const event = smsCallbackWebhooks.parseEvent(req.body);

// Process the event if it is an MO (Mobile Originated) text message
if (event.type === 'mo_text') {
await processInboundEvent(event, sinchClient.sms);
} else {
res.status(400).json({ error: `Unexpected event type: ${event.type}` });
}
} catch (error) {
console.error('Error processing event:', error);
return res.status(400).json({ error: 'Invalid event format' });
}
res.status(200).json();
});
};
170 changes: 170 additions & 0 deletions tutorials/sms/auto-subscribe-app/src/auto-subscribe-service.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,170 @@
// eslint-disable-next-line no-unused-vars
import { SmsService, Sms } from '@sinch/sdk-core';
import { fetchGroup } from './group-lifecycle-manager.js';

const SUBSCRIBE_ACTION = 'SUBSCRIBE';
const STOP_ACTION = 'STOP';

/**
* Processes the incoming SMS event, determines if the sender wants to subscribe or unsubscribe from the group,
* and sends a response based on their input.
*
* @param { Sms.MOText } incomingTextMessage - The incoming SMS message event object
* @param { SmsService } smsService - the SMS service instance from the Sinch SDK containing the API methods
*/
export const processInboundEvent = async (incomingTextMessage, smsService) => {
console.log(`Received event: ${JSON.stringify(incomingTextMessage, null, 2)}`);

const from = incomingTextMessage.from;
const to = incomingTextMessage.to;
const senderInput = incomingTextMessage.body.trim();

const group = await fetchGroup(smsService.groups);

const membersList = await getMembersList(smsService, group);
const isInGroup = isMemberInGroup(membersList, from);

const responseText = await processSenderInput(smsService, from, to, senderInput, group, membersList, isInGroup);

await sendResponse(smsService, to, from, responseText);
};

/**
* Fetches the list of members in the group.
*
* @param {SmsService} smsService - The SMS service instance from the Sinch SDK containing the API methods.
* @param {Sms.CreateGroupResponse} group - The group object.
* @return {Promise<string[]>} - A promise that resolves to a list of member phone numbers.
*/
const getMembersList = async (smsService, group) => {
return await smsService.groups.listMembers({
group_id: group.id,
});
};

/**
* Checks if the specified member is in the list of members og the group.
*
* @param {string[]} membersList - The list of group members.
* @param {string} member - The phone number of the member to check.
* @return {boolean} - Whether the member is part of the group.
*/
const isMemberInGroup = (membersList, member) => {
return membersList.includes(member);
};

/**
* Processes the sender's input to either subscribe, unsubscribe, or handle unknown actions.
*
* @param {SmsService} smsService - the SMS service instance from the Sinch SDK containing the API methods.
* @param {string} from - The phone number of the sender.
* @param {string} to - The group's phone number.
* @param {string} action - The incoming action (e.g., "SUBSCRIBE" or "STOP").
* @param {Sms.CreateGroupResponse} group - The group object.
* @param {string[]} membersList - The list of group members.
* @param {boolean} isInGroup - Whether the sender is already in the group.
* @return {Promise<string>} - A promise that resolves to the response text for the sender.
*/
const processSenderInput = async (smsService, from, to, action, group, membersList, isInGroup) => {
if (action === SUBSCRIBE_ACTION) {
return await subscribe(smsService, group, isInGroup, to, from);
} else if (action === STOP_ACTION) {
return await unsubscribe(smsService, group, isInGroup, to, from);
}
return unknownAction(isInGroup, to);
};

/**
* Subscribes a member to the group if they are not already in it.
*
* @param {SmsService} smsService - the SMS service instance from the Sinch SDK containing the API methods
* @param {Sms.CreateGroupResponse} group - The group object.
* @param {boolean} isInGroup - Whether the member is already in the group.
* @param {string} groupPhoneNumber - The group's phone number.
* @param {string} member - The phone number of the member to subscribe.
* @return {Promise<string>} - A promise that resolves to the subscription confirmation message.
*/
const subscribe = async (smsService, group, isInGroup, groupPhoneNumber, member) => {
if (isInGroup) {
return `You have already subscribed to '${group.name}'. Text "${STOP_ACTION}" to +${groupPhoneNumber} to leave the group.`;
}

/** @type {Sms.UpdateGroupRequestData } */
const requestData = {
group_id: group.id,
updateGroupRequestBody: {
add: [member],
},
};

await smsService.groups.update(requestData);

return `Congratulations! You are now subscribed to '${group.name}'. Text "${STOP_ACTION}" to +${groupPhoneNumber} to leave the group.`;
};

/**
* Unsubscribes a member from the group if they belong to it.
*
* @param {SmsService} smsService - the SMS service instance from the Sinch SDK containing the API methods.
* @param {Sms.CreateGroupResponse} group - The group object.
* @param {boolean} isInGroup - Whether the member is in the group.
* @param {string} groupPhoneNumber - The group's phone number.
* @param {string} member - The phone number of the member to unsubscribe.
* @return {Promise<string>} - A promise that resolves to the unsubscription confirmation message.
*/
const unsubscribe = async (smsService, group, isInGroup, groupPhoneNumber, member) => {
if (!isInGroup) {
return `You haven't subscribed to '${group.name}' yet. Text "${SUBSCRIBE_ACTION}" to +${groupPhoneNumber} to join this group.`;
}

/** @type {Sms.UpdateGroupRequestData} */
const requestData = {
group_id: group.id,
updateGroupRequestBody: {
remove: [member],
},
};

await smsService.groups.update(requestData);

return `We're sorry to see you go. You can always rejoin '${group.name}' by texting "${SUBSCRIBE_ACTION}" to +${groupPhoneNumber}.`;
};

/**
* Handles an unknown action by suggesting the sender either subscribe or unsubscribe.
*
* @param {boolean} isInGroup - Whether the sender is already in the group.
* @param {string} groupPhoneNumber - The group's phone number.
* @return {string} - A message suggesting the next action for the sender.
*/
const unknownAction = (isInGroup, groupPhoneNumber) => {
if (isInGroup) {
return `Thanks for your interest. If you want to unsubscribe from this group, text "${STOP_ACTION}" to +${groupPhoneNumber}.`;
} else {
return `Thanks for your interest. If you want to subscribe to this group, text "${SUBSCRIBE_ACTION}" to +${groupPhoneNumber}.`;
}
};

/**
* Sends a response SMS to the sender.
*
* @param {SmsService} smsService - the SMS service instance from the Sinch SDK containing the API methods
* @param {string} to - The group's phone number.
* @param {string} from - The phone number of the sender.
* @param {string} responseText - The text of the response to send.
* @return {Promise<void>}
*/
const sendResponse = async (smsService, to, from, responseText) => {
/** @type {Sms.SendTextSMSRequestData} */
const requestData = {
sendSMSRequestBody: {
to: [to],
body: responseText,
from,
},
};

await smsService.batches.sendTextMessage(requestData);

console.log(`Replied: ${responseText}`);
};
59 changes: 59 additions & 0 deletions tutorials/sms/auto-subscribe-app/src/group-lifecycle-manager.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
// eslint-disable-next-line no-unused-vars
import { Sms, GroupsApi } from '@sinch/sdk-core';

/**
* The name of the group used in the Auto-Subscribe tutorial.
* @constant {string}
*/
const GROUP_NAME = 'Sinch Pirates';

/**
* A reference to the group object, initialized as null.
* @type {Sms.GroupResponse|null}
*/
let sinchPiratesGroup = null;

/**
* Fetches the group named "Sinch Pirates" from the SMS service. If the group does not exist,
* it creates a new group with the specified name.
*
* @param {GroupsApi} groupsService - The service used to interact with the SMS groups.
* @return {Promise<Sms.GroupResponse>} - A promise that resolves to the Group object.
*/
export const fetchGroup = async (groupsService) => {
for await (const group of groupsService.list({})) {
if (group.name === GROUP_NAME) {
sinchPiratesGroup = group;
break;
}
}
if (sinchPiratesGroup) {
console.log(`Group '${sinchPiratesGroup.name}' found with id '${sinchPiratesGroup.id}'`);
} else {
console.log(`The group '${GROUP_NAME}' doesn't exist. Let's create it.`);

/** @type {Sms.CreateGroupRequestData} */
const requestData = {
createGroupRequestBody: {
name: GROUP_NAME,
},
};
sinchPiratesGroup = await groupsService.create(requestData);
console.log(`Group '${sinchPiratesGroup.name}' created with id '${sinchPiratesGroup.id}'`);
}
return sinchPiratesGroup;
};

/**
* Deletes the group named "Sinch Pirates" from the SMS service.
*
* @param {GroupsApi} groupsService - The service used to interact with the SMS groups.
* @return {Promise<void>} - A promise that resolves when the group is deleted.
*/
export const deleteGroup = async (groupsService) => {
console.log(`Deleting group '${sinchPiratesGroup.name}' with id '${sinchPiratesGroup.id}'`);
await groupsService.delete({
group_id: sinchPiratesGroup.id,
});
console.log(`Group '${sinchPiratesGroup.name}' deleted!`);
};
Loading

0 comments on commit 6af2f4c

Please sign in to comment.