Skip to content

Commit

Permalink
allow scheduling messages for broadcast
Browse files Browse the repository at this point in the history
  • Loading branch information
pwalsh committed Oct 11, 2022
1 parent f20d3ff commit ba32772
Show file tree
Hide file tree
Showing 21 changed files with 2,045 additions and 1,867 deletions.
2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
SHELL := /bin/bash
USER := govflow
NAME := govflow
VERSION := 0.0.116-alpha
VERSION := 0.0.117-alpha
COMPOSE := docker-compose -f docker-compose/docker-compose.yml --compatibility
REPOSITORY := $(USER)/$(NAME)
DOCKER_HOST := ghcr.io
Expand Down
2 changes: 1 addition & 1 deletion docs/broadcasting.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Broadcasting

Broadcasting refers to sendign messages out of the system via any supported communication channel.
Broadcasting refers to sending messages out of the system via any supported communication channel.

Primarily, GovFlow broadcasts messages around the lifecycle of a service request.

Expand Down
File renamed without changes.
6 changes: 3 additions & 3 deletions docs/inbound-sms.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,9 @@ At the level of the GovFlow instance (the server), we need one Twilio Number con

This number is used to emit SMS messages for Service Request submissions as a fallback, and irregardless of whether a given Jurisdiction has opted in to Inbound SMS ot Two-way SMS. Think if it like `webmaster@localhost` for SMS in GovFlow.

This number needs to be configured to send messages (default when you acquire a Twilio Number), and also it is highly recommended to provide an autoresponder from this number so if anyone attempts to respond back to an SMS, it is clear that it is a "no reply" number.
This number needs to be configured to send messages (default when you acquire a Twilio Number), and also it is highly recommended to provide an autoresponder from this number so if anyone attempts to respond back to an SMS, it is clear that it is a "no reply" number. This number will belong to a Twilio Messaging Service, you should name a your messaging service and associate all numbers with it. Set the messaging service as `TWILIO_MESSAGE_SERVICE_SID`.

The core configuration for Twilio is to set `TWILIO_ACCOUNT_SID ` and `TWILIO_AUTH_TOKEN` with valid credentials from a Twilio account, in order to interact with Twilio.
The core configuration for Twilio is to set `TWILIO_ACCOUNT_SID `, `TWILIO_MESSAGE_SERVICE_SID`, and `TWILIO_AUTH_TOKEN` with valid credentials from a Twilio account, in order to interact with Twilio.

### Set up an auto response with Studio Flow

Expand All @@ -29,7 +29,7 @@ The core configuration for Twilio is to set `TWILIO_ACCOUNT_SID ` and `TWILIO_AU

Ok, so now with the basics of what the GovFlow server needs done, we can move onto Jurisdiction-level Twilio Number configuration for inbound SMS.

As a reminder, rhe core configuration for Twilio is to set `TWILIO_ACCOUNT_SID ` and `TWILIO_AUTH_TOKEN` with valid credentials from a Twilio account.
As a reminder, the core configuration for Twilio is to set `TWILIO_ACCOUNT_SID `, `TWILIO_MESSAGE_SERVICE_SID`, and `TWILIO_AUTH_TOKEN` with valid credentials from a Twilio account.

### Create a general number for the Jursidiction

Expand Down
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@govflow/govflow",
"version": "0.0.116-alpha",
"version": "0.0.117-alpha",
"description": "An open, modular work order and workflow management system for local governments and resident satisfaction.",
"homepage": "https://github.com/govflow/govflow",
"main": "./index.js",
Expand Down
95 changes: 50 additions & 45 deletions src/cli/send-test-dispatch.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,49 +5,54 @@ import { createApp } from '../index';
import { DispatchConfigAttributes, TemplateConfigContextAttributes } from '../types';

(async () => {
// Ensure you unset process.env.COMMUNICATIONS_TO_CONSOLE to use the real backend.
const app = await createApp();
const {
sendGridApiKey,
sendGridFromEmail,
twilioAccountSid,
twilioAuthToken,
twilioFromPhone,
twilioStatusCallbackURL,
testToEmail,
testToPhone
} = app.config;
const { communicationRepository, emailStatusRepository } = app.repositories;
const dispatchConfig = {
channel: process.env.TEST_DISPATCH_CHANNEL,
sendGridApiKey: sendGridApiKey as string,
toEmail: testToEmail as string,
fromEmail: sendGridFromEmail as string,
twilioAccountSid: twilioAccountSid as string,
twilioAuthToken: twilioAuthToken as string,
twilioStatusCallbackURL: twilioStatusCallbackURL as string,
fromPhone: twilioFromPhone as string,
toPhone: testToPhone as string
} as DispatchConfigAttributes;
const templateConfig = {
name: 'service-request-new-public-user',
context: {
appName: 'Test Gov Flow Message Dispatch',
appRequestUrl: `https://example.com/`,
serviceRequestStatus: 'inbox',
serviceRequestPublicId: '1234',
jurisdictionName: 'Dummy Name',
jurisdictionEmail: 'dummy@example.com',
jurisdictionReplyToServiceRequestEnabled: false,
recipientName: 'Test Recipient Name',
messageType: 'core'
} as TemplateConfigContextAttributes
}
const record = await dispatchMessage(
dispatchConfig,
templateConfig,
communicationRepository,
emailStatusRepository
);
console.log(record);
// Ensure you unset process.env.COMMUNICATIONS_TO_CONSOLE to use the real backend.
const app = await createApp();
const {
sendGridApiKey,
sendGridFromEmail,
twilioAccountSid,
twilioMessagingServiceSid,
twilioAuthToken,
twilioFromPhone,
twilioStatusCallbackURL,
testToEmail,
testToPhone
} = app.config;
const { communicationRepository, emailStatusRepository } = app.repositories;
const dispatchConfig = {
channel: process.env.TEST_DISPATCH_CHANNEL,
sendGridApiKey: sendGridApiKey as string,
toEmail: testToEmail as string,
fromEmail: sendGridFromEmail as string,
twilioAccountSid: twilioAccountSid as string,
twilioMessagingServiceSid: twilioMessagingServiceSid as string,
twilioAuthToken: twilioAuthToken as string,
twilioStatusCallbackURL: twilioStatusCallbackURL as string,
fromPhone: twilioFromPhone as string,
toPhone: testToPhone as string,
// Add 15 mins
// sendAt: new Date(new Date().getTime() + (16 * 60 * 1000)),
sendAt: new Date(),
} as DispatchConfigAttributes;
const templateConfig = {
name: 'service-request-new-public-user',
context: {
appName: 'Test Gov Flow Message Dispatch',
appRequestUrl: `https://example.com/`,
serviceRequestStatus: 'inbox',
serviceRequestPublicId: '1234',
jurisdictionName: 'Dummy Name',
jurisdictionEmail: 'dummy@example.com',
jurisdictionReplyToServiceRequestEnabled: false,
recipientName: 'Test Recipient Name',
messageType: 'core'
} as TemplateConfigContextAttributes
}
const record = await dispatchMessage(
dispatchConfig,
templateConfig,
communicationRepository,
emailStatusRepository
);
console.log(record);
})();
26 changes: 14 additions & 12 deletions src/cli/send-test-email.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,16 +5,18 @@ import { sendEmail } from '../email';
import { AppConfig } from '../types';

(async () => {
const config = await initConfig();
const { sendGridApiKey, sendGridFromEmail, testToEmail } = config as AppConfig;
const response = await sendEmail(
sendGridApiKey as string,
testToEmail as string,
sendGridFromEmail as string,
sendGridFromEmail as string, // replyTo
'Test subject line',
'Test <strong>html</strong> body',
'Test text body',
);
console.log(response);
const config = await initConfig();
const { sendGridApiKey, sendGridFromEmail, testToEmail } = config as AppConfig;
const sendAt = new Date();
const response = await sendEmail(
sendGridApiKey as string,
testToEmail as string,
sendGridFromEmail as string,
sendGridFromEmail as string, // replyTo
'Test subject line',
'Test <strong>html</strong> body',
'Test text body',
sendAt,
);
console.log(response);
})();
38 changes: 21 additions & 17 deletions src/cli/send-test-sms.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,21 +5,25 @@ import { sendSms } from '../sms';
import { AppConfig } from '../types';

(async () => {
const config = await initConfig();
const {
twilioAccountSid,
twilioAuthToken,
twilioFromPhone,
testToPhone,
twilioStatusCallbackURL
} = config as AppConfig;
const response = await sendSms(
twilioAccountSid as string,
twilioAuthToken as string,
testToPhone as string,
twilioFromPhone as string,
'Test message body.',
twilioStatusCallbackURL,
);
console.log(response);
const config = await initConfig();
const {
twilioAccountSid,
twilioMessagingServiceSid,
twilioAuthToken,
twilioFromPhone,
testToPhone,
twilioStatusCallbackURL
} = config as AppConfig;
const sendAt = new Date();
const response = await sendSms(
twilioAccountSid as string,
twilioMessagingServiceSid as string,
twilioAuthToken as string,
testToPhone as string,
twilioFromPhone as string,
'Test message body.',
twilioStatusCallbackURL,
sendAt,
);
console.log(response);
})();
99 changes: 50 additions & 49 deletions src/config/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,64 +6,65 @@ import logger from '../logging';
import type { AppConfig } from '../types';

declare global {
// eslint-disable-next-line no-var
var appCustomConfig: Partial<AppConfig>
// eslint-disable-next-line no-var
var appCustomConfig: Partial<AppConfig>
}

dotenv.config();

const defaultConfig: Partial<AppConfig> = {
'env': process.env.NODE_ENV || 'development',
'databaseUrl': process.env.DATABASE_URL || '',
'sendGridApiKey': process.env.SENDGRID_API_KEY || '',
'sendGridFromEmail': process.env.SENDGRID_FROM_EMAIL || '',
'sendGridSignedWebhookVerificationKey': process.env.SENDGRID_SIGNED_WEBHOOK_VERIFICATION_KEY || '',
'twilioAccountSid': process.env.TWILIO_ACCOUNT_SID || '',
'twilioAuthToken': process.env.TWILIO_AUTH_TOKEN || '',
'twilioFromPhone': process.env.TWILIO_FROM_PHONE || '',
'twilioStatusCallbackURL': process.env.TWILIO_STATUS_CALLBACK_URL || '',
'sentryDsn': process.env.SENTRY_DSN || '',
'sentryEnvironment': process.env.SENTRY_ENVIRONMENT || '',
'testToEmail': process.env.TEST_TO_EMAIL || '',
'testToPhone': process.env.TEST_TO_PHONE || '',
'appPort': parseInt(process.env.APP_PORT || '3000', 10),
'appName': process.env.APP_NAME || 'Gov Flow',
'appClientUrl': process.env.APP_CLIENT_URL || '',
'appClientRequestsPath': process.env.APP_CLIENT_REQUESTS_PATH || '/requests',
'communicationsToConsole': process.env.COMMUNICATIONS_TO_CONSOLE || '',
'captchaEnabled': Boolean(parseInt(process.env.CAPTCHA_ENABLED || '1', 10)), // 0 or 1
'reCaptchaSecretKey': process.env.RECAPTCHA_SECRET_KEY || '',
'storageBucket': process.env.STORAGE_BUCKET || 'govflow_uploads',
'storageRegion': process.env.STORAGE_REGION || 'us-east-1',
'storageSSL': Boolean(parseInt(process.env.STORAGE_SSL || '1', 10)), // 0 or 1
'storagePort': parseInt(process.env.STORAGE_PORT || '9000', 10),
'storageEndpoint': process.env.STORAGE_ENDPOINT || '127.0.0.1',
'storageAccessKey': process.env.STORAGE_ACCESS_KEY || '',
'storageSecretKey': process.env.STORAGE_SECRET_KEY || '',
'storageSignedGetExpiry': parseInt(process.env.STORAGE_SIGNED_GET_EXPIRY || '1', 10) * 60, // minutes
'inboundEmailDomain': process.env.INBOUND_EMAIL_DOMAIN || 'inbound.example.com',
'env': process.env.NODE_ENV || 'development',
'databaseUrl': process.env.DATABASE_URL || '',
'sendGridApiKey': process.env.SENDGRID_API_KEY || '',
'sendGridFromEmail': process.env.SENDGRID_FROM_EMAIL || '',
'sendGridSignedWebhookVerificationKey': process.env.SENDGRID_SIGNED_WEBHOOK_VERIFICATION_KEY || '',
'twilioMessagingServiceSid': process.env.TWILIO_MESSAGE_SERVICE_SID || '',
'twilioAccountSid': process.env.TWILIO_ACCOUNT_SID || '',
'twilioAuthToken': process.env.TWILIO_AUTH_TOKEN || '',
'twilioFromPhone': process.env.TWILIO_FROM_PHONE || '',
'twilioStatusCallbackURL': process.env.TWILIO_STATUS_CALLBACK_URL || '',
'sentryDsn': process.env.SENTRY_DSN || '',
'sentryEnvironment': process.env.SENTRY_ENVIRONMENT || '',
'testToEmail': process.env.TEST_TO_EMAIL || '',
'testToPhone': process.env.TEST_TO_PHONE || '',
'appPort': parseInt(process.env.APP_PORT || '3000', 10),
'appName': process.env.APP_NAME || 'Gov Flow',
'appClientUrl': process.env.APP_CLIENT_URL || '',
'appClientRequestsPath': process.env.APP_CLIENT_REQUESTS_PATH || '/requests',
'communicationsToConsole': process.env.COMMUNICATIONS_TO_CONSOLE || '',
'captchaEnabled': Boolean(parseInt(process.env.CAPTCHA_ENABLED || '1', 10)), // 0 or 1
'reCaptchaSecretKey': process.env.RECAPTCHA_SECRET_KEY || '',
'storageBucket': process.env.STORAGE_BUCKET || 'govflow_uploads',
'storageRegion': process.env.STORAGE_REGION || 'us-east-1',
'storageSSL': Boolean(parseInt(process.env.STORAGE_SSL || '1', 10)), // 0 or 1
'storagePort': parseInt(process.env.STORAGE_PORT || '9000', 10),
'storageEndpoint': process.env.STORAGE_ENDPOINT || '127.0.0.1',
'storageAccessKey': process.env.STORAGE_ACCESS_KEY || '',
'storageSecretKey': process.env.STORAGE_SECRET_KEY || '',
'storageSignedGetExpiry': parseInt(process.env.STORAGE_SIGNED_GET_EXPIRY || '1', 10) * 60, // minutes
'inboundEmailDomain': process.env.INBOUND_EMAIL_DOMAIN || 'inbound.example.com',
}

async function resolveCustomConfig(): Promise<AppConfig> {
if (global.appCustomConfig) {
return global.appCustomConfig as AppConfig;
}
const customConfigModule = process.env.CONFIG_MODULE_PATH || '';
try {
const resolvedPath = path.resolve(customConfigModule);
const { config } = await import(resolvedPath);
return config;
} catch (error) {
logger.warn(`No custom config found at '${customConfigModule}'.`)
}
return {} as AppConfig;
if (global.appCustomConfig) {
return global.appCustomConfig as AppConfig;
}
const customConfigModule = process.env.CONFIG_MODULE_PATH || '';
try {
const resolvedPath = path.resolve(customConfigModule);
const { config } = await import(resolvedPath);
return config;
} catch (error) {
logger.warn(`No custom config found at '${customConfigModule}'.`)
}
return {} as AppConfig;
}

export async function initConfig(): Promise<AppConfig> {
const customConfig = await resolveCustomConfig();
const config = merge(defaultConfig, customConfig) as AppConfig;
config.database = initEngine(config.databaseUrl as string);
config.migrator = initMigrator(config.database, config.plugins?.migrations);
Object.freeze(config);
return config;
const customConfig = await resolveCustomConfig();
const config = merge(defaultConfig, customConfig) as AppConfig;
config.database = initEngine(config.databaseUrl as string);
config.migrator = initMigrator(config.database, config.plugins?.migrations);
Object.freeze(config);
return config;
}
Loading

0 comments on commit ba32772

Please sign in to comment.