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

Adds Telnyx SMS provider, enables SMS resubscribe #570

Merged
merged 1 commit into from
Dec 9, 2024
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
107 changes: 107 additions & 0 deletions apps/platform/src/providers/text/TelnyxTextProvider.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
import App from '../../app'
import { encodeHashid } from '../../utilities'
import { ExternalProviderParams, ProviderControllers, ProviderSchema, ProviderSetupMeta } from '../Provider'
import { createController } from '../ProviderService'
import TextError, { UndeliverableTextError, UnsubscribeTextError } from './TextError'
import { InboundTextMessage, TextMessage, TextResponse } from './TextMessage'
import { TextProvider } from './TextProvider'

/**
* https://developers.telnyx.com/api/messaging/send-message
*/

interface TelnyxDataParams {
api_key: string
phone_number: string
}

interface TelnyxProviderParams extends ExternalProviderParams {
data: TelnyxDataParams
}

export default class TelnyxTextProvider extends TextProvider {
api_key!: string
phone_number!: string

static namespace = 'telnyx'
static meta = {
name: 'Telnyx',
description: '',
url: 'https://telnyx.com',
icon: 'https://parcelvoy.com/providers/telnyx.svg',
}

static schema = ProviderSchema<TelnyxProviderParams, TelnyxDataParams>('telnyxTextProviderParams', {
type: 'object',
required: ['api_key', 'phone_number'],
properties: {
api_key: {
type: 'string',
title: 'API Key',
},
phone_number: { type: 'string' },
},
})

loadSetup(app: App): ProviderSetupMeta[] {
return [{
name: 'Inbound URL',
value: `${app.env.apiBaseUrl}/providers/${encodeHashid(this.id)}/${(this.constructor as any).namespace}/inbound`,
}]
}

async send(message: TextMessage): Promise<TextResponse> {
const { to, text } = message
const { phone_number: from } = this

const response = await fetch('https://api.telnyx.com/v2/messages', {
method: 'POST',
headers: {
Authorization: `Basic ${this.api_key}`,
'Content-Type': 'application/json',
'User-Agent': 'parcelvoy/v1 (+https://github.com/parcelvoy/platform)',
},
body: JSON.stringify({
from,
to,
text,
}),
})

const responseBody = await response.json()
if (response.ok) {
return {
message,
success: true,
response: responseBody.id,
}
} else {

// https://support.telnyx.com/en/articles/6505121-telnyx-messaging-error-codes
const error = responseBody.errors?.[0]
if (error.code === 40300) {
// Unable to send because recipient has unsubscribed
throw new UnsubscribeTextError(this.type, this.phone_number, responseBody.message)
} else if (responseBody.code === 40008) {
// Unable to send because region is not enabled
throw new UndeliverableTextError(this.type, this.phone_number, responseBody.message)
}
throw new TextError(this.type, this.phone_number, responseBody.message)
}
}

// https://www.twilio.com/docs/messaging/guides/webhook-request
parseInbound(inbound: any): InboundTextMessage {
const payload = inbound.data.payload
return {
to: payload.to,
from: payload.from.phone_number,
text: payload.text || '',
}
}

static controllers(): ProviderControllers {
const admin = createController('text', this)
return { admin, public: this.inbound(this.namespace) }
}
}
10 changes: 8 additions & 2 deletions apps/platform/src/providers/text/TextProvider.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
import Router from '@koa/router'
import { loadTextChannel } from '.'
import { unsubscribeSms } from '../../subscriptions/SubscriptionService'
import { toggleChannelSubscriptions } from '../../subscriptions/SubscriptionService'
import Provider, { ProviderGroup } from '../Provider'
import { InboundTextMessage, TextMessage, TextResponse } from './TextMessage'
import { Context } from 'koa'
import { getUserFromPhone } from '../../users/UserRepository'
import { getProject } from '../../projects/ProjectService'
import { EventPostJob } from '../../jobs'
import { SubscriptionState } from '../../subscriptions/Subscription'

export type TextProviderName = 'nexmo' | 'plivo' | 'twilio' | 'logger'

Expand Down Expand Up @@ -38,7 +39,12 @@ export abstract class TextProvider extends Provider {

// If the message includes the word STOP unsubscribe immediately
if (message.text.toLowerCase().includes('stop')) {
await unsubscribeSms(project.id, user)
await toggleChannelSubscriptions(project.id, user, 'text')

// If the message includes the word START, re-enable
// SMS messages for the user
} else if (message.text.toLowerCase().includes('start')) {
await toggleChannelSubscriptions(project.id, user, 'text', SubscriptionState.subscribed)

// If the message includes the word HELP, send the help message
} else if (message.text.toLowerCase().includes('help') && project.text_help_message) {
Expand Down
2 changes: 2 additions & 0 deletions apps/platform/src/providers/text/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import HttpSMSTextProvider from './HttpSMSProvider'
import LoggerTextProvider from './LoggerTextProvider'
import NexmoTextProvider from './NexmoTextProvider'
import PlivoTextProvider from './PlivoTextProvider'
import TelnyxTextProvider from './TelnyxTextProvider'
import TextChannel from './TextChannel'
import { TextProvider, TextProviderName } from './TextProvider'
import TwilioTextProvider from './TwilioTextProvider'
Expand All @@ -11,6 +12,7 @@ type TextProviderDerived = { new (): TextProvider } & typeof TextProvider
export const typeMap: Record<string, TextProviderDerived> = {
nexmo: NexmoTextProvider,
plivo: PlivoTextProvider,
telnyx: TelnyxTextProvider,
twilio: TwilioTextProvider,
httpsms: HttpSMSTextProvider,
logger: LoggerTextProvider,
Expand Down
18 changes: 9 additions & 9 deletions apps/platform/src/subscriptions/SubscriptionService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,15 +64,8 @@ export const updateSubscription = async (id: number, params: Partial<Subscriptio
return await Subscription.updateAndFetch(id, params)
}

export const subscriptionForChannel = async (channel: ChannelType, projectId: number): Promise<Subscription | undefined> => {
return await Subscription.first(qb => qb.where('channel', channel).where('project_id', projectId))
}

export const unsubscribeSms = async (projectId: number, user: User) => {
const subscription = await subscriptionForChannel('text', projectId)
if (user && subscription) {
unsubscribe(user.id, subscription.id)
}
export const subscriptionsForChannel = async (channel: ChannelType, projectId: number): Promise<Subscription[]> => {
return await Subscription.all(qb => qb.where('channel', channel).where('project_id', projectId))
}

export const toggleSubscription = async (userId: number, subscriptionId: number, state = SubscriptionState.unsubscribed): Promise<void> => {
Expand Down Expand Up @@ -122,6 +115,13 @@ export const toggleSubscription = async (userId: number, subscriptionId: number,
}).queue()
}

export const toggleChannelSubscriptions = async (projectId: number, user: User, channel: ChannelType, state = SubscriptionState.unsubscribed) => {
const subscriptions = await subscriptionsForChannel(channel, projectId)
for (const subscription of subscriptions) {
await toggleSubscription(user.id, subscription.id, state)
}
}

export const unsubscribe = async (userId: number, subscriptionId: number): Promise<void> => {
await toggleSubscription(userId, subscriptionId, SubscriptionState.unsubscribed)
}
Expand Down
25 changes: 25 additions & 0 deletions docs/docs/providers/telnyx.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
# Telnyx
## Setup
Start by creating a new account at [https://telnyx.com](https://telnyx.com). Once your account is created, the following steps will get your account linked to Parcelvoy.

## Outbound
All you need for outbound messages is a phone number that supports SMS.

If you already have a phone number, jump to step four.
1. Go to `Real-Time Communications -> Numbers -> Buy Numbers`
2. From here, you can pick the search criteria you care about for a number. Just make sure the number selected supports SMS (Parcelvoy will not work without it)
3. Purchase the number and copy it down.
4. Next, hit the `Home` button in the top left hand corner of the Telnyx dashboard and copy the `API Key` down.
5. Open a new window and go to your Parcelvoy project settings
6. Navigate to `Integrations` and click the `Add Integration` button.
7. Pick Telnyx from the list of integrations and enter the `API Key` and `Phone Number` from Telnyx.
8. Hit save to create the provider.

You are now setup to send SMS messages using Telnyx. Depending on your needs, you may need to get your number approved, etc but that is outside of this scope.

There is one more step however to make it fully functioning and that is to setup inbound messages so that Parcelvoy is notified of unsubscribes.

## Inbound
Setting up inbound messaging is important to comply with carrier rules and regulations regarding unsubscribing from communications. By default Telnyx automatically manages [opt-outs (unsubscribes)](https://support.telnyx.com/en/articles/1270091-sms-opt-out-keywords-and-stop-words), you just have to listen for the inbound webhook to then register that event in Parcelvoy. An additional benefit to setting up inbound messaging is that you can use the created events to trigger journeys.

To setup inbound SMS for Telnyx, please follow the [instructions on their website](https://support.telnyx.com/en/articles/4348981-receiving-sms-on-your-telnyx-number).
2 changes: 1 addition & 1 deletion docs/docs/providers/twilio.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ You are now setup to send SMS messages using Twilio. There is one more step howe
Setting up inbound messaging is important to comply with carrier rules and regulations regarding unsubscribing from communications. By default Twilio automatically manages [opt-outs (unsubscribes)](https://support.twilio.com/hc/en-us/articles/360034798533-Getting-Started-with-Advanced-Opt-Out-for-Messaging-Services), you just have to listen for the inbound webhook to then register that event in Parcelvoy. An additional benefit to setting up inbound messaging is that you can use the created events to trigger journeys.

To setup inbound SMS for Twilio, do the following:
1. In Twilip, navigate to `Develop -> Phone Numbers -> Manage -> Active Numbers`.
1. In Twilio, navigate to `Develop -> Phone Numbers -> Manage -> Active Numbers`.
2. Pick the phone number you are using internally.
3. Scroll down to the `Messaging` section.
4. On the line item `A Message Comes In` set the type to `Webhook`, the method to `HTTP POST` and then copy the Inbound URL from your provider into that field.
Expand Down
Loading