mirror of
https://fast.feibisi.com/https://github.com/parcelvoy/platform.git
synced 2025-08-28 11:46:02 +08:00
Adds Telnyx SMS provider, enables SMS resubscribe (#570)
This commit is contained in:
parent
3867511dce
commit
f4b78be484
6 changed files with 152 additions and 12 deletions
107
apps/platform/src/providers/text/TelnyxTextProvider.ts
Normal file
107
apps/platform/src/providers/text/TelnyxTextProvider.ts
Normal file
|
@ -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) }
|
||||
}
|
||||
}
|
|
@ -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'
|
||||
|
||||
|
@ -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) {
|
||||
|
|
|
@ -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'
|
||||
|
@ -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,
|
||||
|
|
|
@ -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> => {
|
||||
|
@ -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)
|
||||
}
|
||||
|
|
25
docs/docs/providers/telnyx.md
Normal file
25
docs/docs/providers/telnyx.md
Normal file
|
@ -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).
|
|
@ -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.
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue