-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
DEVEXP-566: Add 'Auto-subscribe app' tutorial (#7)
- Loading branch information
1 parent
225d649
commit 6af2f4c
Showing
11 changed files
with
467 additions
and
2 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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" | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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). |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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
31
tutorials/sms/auto-subscribe-app/src/auto-subscribe-controller.js
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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
170
tutorials/sms/auto-subscribe-app/src/auto-subscribe-service.js
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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
59
tutorials/sms/auto-subscribe-app/src/group-lifecycle-manager.js
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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!`); | ||
}; |
Oops, something went wrong.