Skip to content

Commit

Permalink
feat(channels): channel validation (#437)
Browse files Browse the repository at this point in the history
* feat(channels): channel validation

* working draft

* slack validation

* messenger validation

* twilio validation

* smooch validation

* reword

* fix build

* fix tests

* fix test

* little refact

* some refactoring

* fix test

* fix

* fix

* fix

* wtf

* only perform updates when all channels have been verified

* fix twilio regex

* pr comments

* remove todo

* vonage

* teams
  • Loading branch information
samuelmasse committed Apr 12, 2022
1 parent 3eb0e52 commit 23a6990
Show file tree
Hide file tree
Showing 23 changed files with 346 additions and 89 deletions.
11 changes: 11 additions & 0 deletions packages/channels/src/base/channel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ export interface Channel {
get kvs(): Kvs | undefined
set kvs(kvs: Kvs | undefined)
setup(router: Router, logger?: Logger): Promise<void>
test(scope: string, config: any): Promise<boolean>
start(scope: string, config: any): Promise<void>
initialize(scope: string): Promise<void>
send(scope: string, endpoint: any, content: any): Promise<void>
Expand Down Expand Up @@ -80,6 +81,10 @@ export abstract class ChannelTemplate<
return this.service.start(scope, config)
}

async test(scope: string, config: TConfig) {
return this.service.test(scope, config)
}

async initialize(scope: string) {
return this.service.initialize(scope)
}
Expand Down Expand Up @@ -120,6 +125,12 @@ export abstract class ChannelTemplate<
}
}

export class ChannelTestError extends Error {
constructor(message: string, public readonly channel: string, public readonly field: string) {
super(message)
}
}

export interface ChannelEvents {
message: MessageEvent
proactive: ProactiveEvent
Expand Down
52 changes: 38 additions & 14 deletions packages/channels/src/base/service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ export abstract class ChannelService<
TState extends ChannelState<TConfig>
> extends Emitter<{
start: ChannelStartEvent
test: ChannelTestEvent
initialize: ChannelInitializeEvent
send: ChannelSendEvent
proactive: ChannelProactiveEvent
Expand All @@ -38,15 +39,20 @@ export abstract class ChannelService<
async setup() {}

async start(scope: string, config: TConfig) {
const state = await this.create(scope, config)
await this.addState(scope, config)
await this.emit('start', { scope })
}

if (this.manager) {
this.manager.set(scope, state)
} else {
this.states[scope] = state
async test(scope: string, config: TConfig) {
try {
await this.addState(scope, config)
await this.emit('test', { scope })
return true
} catch (e) {
throw e
} finally {
await this.clearState(scope)
}

await this.emit('start', { scope })
}

async initialize(scope: string) {
Expand Down Expand Up @@ -85,13 +91,7 @@ export abstract class ChannelService<

async stop(scope: string) {
await this.emit('stop', { scope })
await this.destroy(scope, this.get(scope))

if (this.manager) {
this.manager.del(scope)
} else {
delete this.states[scope]
}
await this.clearState(scope)
}

async destroy(scope: string, state: TState): Promise<void> {}
Expand Down Expand Up @@ -130,6 +130,26 @@ export abstract class ChannelService<
}
}

protected async addState(scope: string, config: TConfig) {
const state = await this.create(scope, config)

if (this.manager) {
this.manager.set(scope, state)
} else {
this.states[scope] = state
}
}

protected async clearState(scope: string) {
await this.destroy(scope, this.get(scope))

if (this.manager) {
this.manager.del(scope)
} else {
delete this.states[scope]
}
}

protected getIndexCacheKey(scope: string, identity: string, sender: string) {
return `${scope}_${identity}_${sender}`
}
Expand All @@ -139,6 +159,10 @@ export interface ChannelStartEvent {
scope: string
}

export interface ChannelTestEvent {
scope: string
}

export interface ChannelInitializeEvent {
scope: string
}
Expand Down
4 changes: 4 additions & 0 deletions packages/channels/src/messenger/messenger.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,3 +38,7 @@ export interface MessengerButton {
payload?: string
url?: string
}

export interface MessengerPageInfo {
id: string
}
43 changes: 40 additions & 3 deletions packages/channels/src/messenger/stream.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,18 @@
import axios from 'axios'
import { Endpoint } from '..'
import { ChannelTestError, Endpoint } from '..'
import { ChannelContext } from '../base/context'
import { CardToCarouselRenderer } from '../base/renderers/card'
import { DropdownToChoicesRenderer } from '../base/renderers/dropdown'
import { ChannelReceiveEvent } from '../base/service'
import { ChannelReceiveEvent, ChannelTestEvent } from '../base/service'
import { ChannelStream } from '../base/stream'
import { MessengerContext } from './context'
import { MessengerPageInfo } from './messenger'
import { MessengerRenderers } from './renderers'
import { MessengerSenders } from './senders'
import { MessengerService } from './service'

const GRAPH_URL = 'https://graph.facebook.com/v12.0'

export class MessengerStream extends ChannelStream<MessengerService, MessengerContext> {
get renderers() {
return [new CardToCarouselRenderer(), new DropdownToChoicesRenderer(), ...MessengerRenderers]
Expand All @@ -23,6 +26,28 @@ export class MessengerStream extends ChannelStream<MessengerService, MessengerCo
await super.setup()

this.service.on('receive', this.handleReceive.bind(this))
this.service.on('test', this.handleTest.bind(this))
}

private async handleTest({ scope }: ChannelTestEvent) {
const { config } = this.service.get(scope)

let info: MessengerPageInfo
try {
info = await this.fetchPageInfo(scope)
} catch {
throw new ChannelTestError('unable to reach messenger with the provided access token', 'messenger', 'accessToken')
}

if (info.id !== config.pageId) {
throw new ChannelTestError('page id does not match provided access token', 'messenger', 'pageId')
}

try {
await this.fetchAppInfo(scope)
} catch {
throw new ChannelTestError('app id does not match provided access token', 'messenger', 'appId')
}
}

protected async handleReceive({ scope, endpoint }: ChannelReceiveEvent) {
Expand All @@ -41,7 +66,7 @@ export class MessengerStream extends ChannelStream<MessengerService, MessengerCo
const { config } = this.service.get(scope)

await axios.post(
'https://graph.facebook.com/v12.0/me/messages',
`${GRAPH_URL}/me/messages`,
{
...data,
recipient: {
Expand All @@ -52,6 +77,18 @@ export class MessengerStream extends ChannelStream<MessengerService, MessengerCo
)
}

private async fetchPageInfo(scope: string): Promise<{ name: string; id: string }> {
const { config } = this.service.get(scope)

return (await axios.get(`${GRAPH_URL}/me`, { params: { access_token: config.accessToken } })).data
}

private async fetchAppInfo(scope: string): Promise<any> {
const { config } = this.service.get(scope)

return (await axios.get(`${GRAPH_URL}/${config.appId}`, { params: { access_token: config.accessToken } })).data
}

protected async getContext(base: ChannelContext<any>): Promise<MessengerContext> {
return {
...base,
Expand Down
21 changes: 21 additions & 0 deletions packages/channels/src/slack/stream.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
import { ChannelTestError } from '..'
import { ChannelContext } from '../base/context'
import { CardToCarouselRenderer } from '../base/renderers/card'
import { ChannelSender } from '../base/sender'
import { TypingSender } from '../base/senders/typing'
import { ChannelTestEvent } from '../base/service'
import { ChannelStream } from '../base/stream'
import { SlackContext } from './context'
import { SlackRenderers } from './renderers'
Expand All @@ -17,6 +19,25 @@ export class SlackStream extends ChannelStream<SlackService, SlackContext> {
return [new TypingSender(), ...SlackSenders]
}

async setup() {
await super.setup()
this.service.on('test', this.handleTest.bind(this))
}

private async handleTest({ scope }: ChannelTestEvent) {
const { app, config } = this.service.get(scope)

try {
await app.client.auth.test()
} catch {
throw new ChannelTestError(
'unable to authenticate slack request with the provided bot token',
'slack',
'botToken'
)
}
}

protected async getContext(base: ChannelContext<any>): Promise<SlackContext> {
return {
...base,
Expand Down
4 changes: 3 additions & 1 deletion packages/channels/src/smooch/service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ export interface SmoochState extends ChannelState<SmoochConfig> {
smooch: {
messages: any
activity: any
apps: any
}
}

Expand All @@ -20,7 +21,8 @@ export class SmoochService extends ChannelService<SmoochConfig, SmoochState> {
config,
smooch: {
messages: new SunshineConversationsClient.MessagesApi(client),
activity: new SunshineConversationsClient.ActivitiesApi(client)
activity: new SunshineConversationsClient.ActivitiesApi(client),
apps: new SunshineConversationsClient.AppsApi(client)
}
}
}
Expand Down
21 changes: 21 additions & 0 deletions packages/channels/src/smooch/stream.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import { ChannelTestError } from '../base/channel'
import { ChannelContext } from '../base/context'
import { CardToCarouselRenderer } from '../base/renderers/card'
import { DropdownToChoicesRenderer } from '../base/renderers/dropdown'
import { ChannelTestEvent } from '../base/service'
import { ChannelStream } from '../base/stream'
import { SmoochContext } from './context'
import { SmoochRenderers } from './renderers'
Expand All @@ -16,6 +18,25 @@ export class SmoochStream extends ChannelStream<SmoochService, SmoochContext> {
return SmoochSenders
}

async setup() {
await super.setup()
this.service.on('test', this.handleTest.bind(this))
}

private async handleTest({ scope }: ChannelTestEvent) {
const { smooch, config } = this.service.get(scope)

try {
await smooch.apps.getApp(config.appId)
} catch {
throw new ChannelTestError(
'unable to reach smooch with the provided key secret, key id and app id combination',
'smooch',
'keySecret'
)
}
}

protected async getContext(base: ChannelContext<any>): Promise<SmoochContext> {
return {
...base,
Expand Down
29 changes: 29 additions & 0 deletions packages/channels/src/teams/stream.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
import axios from 'axios'
import { URLSearchParams } from 'url'
import { ChannelTestError } from '../base/channel'
import { ChannelContext } from '../base/context'
import { CardToCarouselRenderer } from '../base/renderers/card'
import { ChannelTestEvent } from '../base/service'
import { ChannelStream } from '../base/stream'
import { TeamsContext } from './context'
import { TeamsRenderers } from './renderers'
Expand All @@ -15,6 +19,31 @@ export class TeamsStream extends ChannelStream<TeamsService, TeamsContext> {
return TeamsSenders
}

async setup() {
await super.setup()
this.service.on('test', this.handleTest.bind(this))
}

private async handleTest({ scope }: ChannelTestEvent) {
const { config } = this.service.get(scope)

try {
const params = new URLSearchParams({
grant_type: 'client_credentials',
client_id: config.appId,
client_secret: config.appPassword,
scope: 'https://api.botframework.com/.default'
})
await axios.post('https://login.microsoftonline.com/botframework.com/oauth2/v2.0/token', params.toString())
} catch (e) {
throw new ChannelTestError(
'unable to reach teams using the provided app id and app password combination',
'teams',
'appPassword'
)
}
}

protected async getContext(base: ChannelContext<any>): Promise<TeamsContext> {
return {
...base,
Expand Down
17 changes: 17 additions & 0 deletions packages/channels/src/telegram/stream.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import { ChannelTestError } from '..'
import { ChannelContext } from '../base/context'
import { CardToCarouselRenderer } from '../base/renderers/card'
import { DropdownToChoicesRenderer } from '../base/renderers/dropdown'
import { ChannelTestEvent } from '../base/service'
import { ChannelStream } from '../base/stream'
import { TelegramContext } from './context'
import { TelegramRenderers } from './renderers'
Expand All @@ -16,6 +18,21 @@ export class TelegramStream extends ChannelStream<TelegramService, TelegramConte
return TelegramSenders
}

async setup() {
await super.setup()
this.service.on('test', this.handleTest.bind(this))
}

private async handleTest({ scope }: ChannelTestEvent) {
const { telegraf } = this.service.get(scope)

try {
await telegraf.telegram.getWebhookInfo()
} catch {
throw new ChannelTestError('unable to reach telegram with the provided bot token', 'telegram', 'botToken')
}
}

protected async getContext(base: ChannelContext<any>): Promise<TelegramContext> {
return {
...base,
Expand Down
2 changes: 1 addition & 1 deletion packages/channels/src/twilio/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,6 @@ export interface TwilioConfig extends ChannelConfig {
}

export const TwilioConfigSchema = {
accountSID: Joi.string().required(),
accountSID: Joi.string().regex(/^AC.*/).required(),
authToken: Joi.string().required()
}
Loading

0 comments on commit 23a6990

Please sign in to comment.