Skip to content

Commit

Permalink
DEVEXP-567: Add 'Qualify-leads app' tutorial (#8)
Browse files Browse the repository at this point in the history
  • Loading branch information
asein-sinch authored Sep 24, 2024
1 parent 6af2f4c commit e1ab91e
Show file tree
Hide file tree
Showing 12 changed files with 1,994 additions and 0 deletions.
170 changes: 170 additions & 0 deletions tutorials/sms/auto-subscribe-app/src/auto-subsribe-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}`);
};
10 changes: 10 additions & 0 deletions tutorials/voice/qualify-leads-app/.env
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
# Express related configuration
port = 3001

# Application related credentials, used by:
# - Voice
SINCH_APPLICATION_KEY = <Your Application Key>
SINCH_APPLICATION_SECRET = <Your Application Secret>

SINCH_NUMBER = <Your Sinch Number associated to your Voice app>
SIP_ADDRESS = <Your SIP address if you are using a SIP infrastructure>
70 changes: 70 additions & 0 deletions tutorials/voice/qualify-leads-app/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
# Qualify-leads application sample

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

## DISCLAIMER

This tutorial is based on mixing a command-line function with a server-side backend service.

It is not a correct use of the CLI outside an educational purpose.

## 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

With the [Voice API](https://developers.sinch.com/docs/voice/), the incoming `Authorization` header is signed with the same credentials as when using the API:
- `SINCH_APPLICATION_KEY`=Your Application Key
- `SINCH_APPLICATION_SECRET`=Your Application Secret

#### Other parameters
- `SINCH_NUMBER`=The Sinch number associated to your [Voice App](https://dashboard.sinch.com/voice/apps).
- `SIP_ADDRESS`=If you are performing this tutorial with a SIP infrastructure, this is where you would enter your SIP address.

#### 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 `/VoiceEvent`, the resulting callback URL to configure in your [Sinch dashboard](hhttps://dashboard.sinch.com/voice/apps) will be `https://cafe-64-220-29-200.ngrok-free.app/VoiceEvent` (adapt the value according to your ngrok and controller configurations).
Loading

0 comments on commit e1ab91e

Please sign in to comment.