Skip to content

Commit

Permalink
feat(smooch): smooch v1 (#333)
Browse files Browse the repository at this point in the history
* smooch start

* text

* typing indicators

* image

* choices

* carousel

* postback

* readme

* more typings

* fix

* location

* readme

* file

* audio video

* yarn.lock

* add to server
  • Loading branch information
samuelmasse committed Feb 2, 2022
1 parent 2382f30 commit c10e228
Show file tree
Hide file tree
Showing 25 changed files with 644 additions and 16 deletions.
2 changes: 2 additions & 0 deletions packages/channels/example/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import Joi from 'joi'
import { Channel } from '../src/base/channel'
import { MessengerChannel } from '../src/messenger/channel'
import { SlackChannel } from '../src/slack/channel'
import { SmoochChannel } from '../src/smooch/channel'
import { TelegramChannel } from '../src/telegram/channel'
import { TwilioChannel } from '../src/twilio/channel'
import payloads from './payloads.json'
Expand All @@ -14,6 +15,7 @@ export class App {
async setup() {
await this.setupChannel('messenger', new MessengerChannel())
await this.setupChannel('slack', new SlackChannel())
await this.setupChannel('smooch', new SmoochChannel())
await this.setupChannel('twilio', new TwilioChannel())
await this.setupChannel('telegram', new TelegramChannel())
}
Expand Down
1 change: 1 addition & 0 deletions packages/channels/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@
"lru-cache": "^6.0.0",
"ms": "^2.1.3",
"raw-body": "^2.4.2",
"sunshine-conversations-client": "^9.5.9",
"telegraf": "^4.6.0",
"twilio": "^3.73.1",
"uuid": "^8.3.2",
Expand Down
1 change: 1 addition & 0 deletions packages/channels/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,6 @@ export * from './base/channel'
export * from './base/endpoint'
export * from './messenger/channel'
export * from './slack/channel'
export * from './smooch/channel'
export * from './twilio/channel'
export * from './telegram/channel'
29 changes: 29 additions & 0 deletions packages/channels/src/smooch/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
### Sending

| Channels | Smooch |
| -------- | :----: |
| Text ||
| Image ||
| Choice ||
| Dropdown ||
| Card ||
| Carousel ||
| File ||
| Audio ||
| Video ||
| Location ||

### Receiving

| Channels | Smooch |
| ------------- | :----: |
| Text ||
| Quick Reply | N/A |
| Postback ||
| Say Something ||
| Voice ||
| Image ||
| File ||
| Audio ||
| Video ||
| Location ||
70 changes: 70 additions & 0 deletions packages/channels/src/smooch/api.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import express, { Response } from 'express'
import { ChannelApi, ChannelApiManager, ChannelApiRequest } from '../base/api'
import { POSTBACK_PREFIX, SAY_PREFIX } from './renderers/carousel'
import { SmoochService } from './service'
import { SmoochEvent, SmoochMessageEvent, SmoochPostbackEvent, SmoochRequestBody } from './smooch'

export class SmoochApi extends ChannelApi<SmoochService> {
async setup(router: ChannelApiManager) {
router.use('/smooch', express.json())
router.post('/smooch', this.handleRequest.bind(this))
}

private async handleRequest(req: ChannelApiRequest, res: Response) {
const apiKey = req.headers['x-api-key'] as string | undefined
const { config } = this.service.get(req.scope)

if (apiKey !== config.webhookSecret) {
return res.sendStatus(403)
}

const body = req.body as SmoochRequestBody

for (const event of body.events) {
await this.handleEvent(req.scope, event)
}

res.sendStatus(200)
}

private async handleEvent(scope: string, event: SmoochEvent) {
if (event.type === 'conversation:message') {
return this.handleMessage(scope, event)
} else if (event.type === 'conversation:postback') {
return this.handlePostback(scope, event)
}
}

private async handleMessage(scope: string, event: SmoochMessageEvent) {
const { conversation, message } = event.payload

if (message.author.type === 'business') {
return
}

await this.service.receive(
scope,
{ identity: '*', sender: message.author.userId, thread: conversation.id },
{ type: 'text', text: message.content.text }
)
}

private async handlePostback(scope: string, event: SmoochPostbackEvent) {
const { conversation, postback, user } = event.payload
const { payload } = postback

const endpoint = { identity: '*', sender: user.id, thread: conversation.id }

if (payload.startsWith(POSTBACK_PREFIX)) {
await this.service.receive(scope, endpoint, {
type: 'postback',
payload: payload.replace(POSTBACK_PREFIX, '')
})
} else if (payload.startsWith(SAY_PREFIX)) {
await this.service.receive(scope, endpoint, {
type: 'say_something',
text: payload.replace(SAY_PREFIX, '')
})
}
}
}
23 changes: 23 additions & 0 deletions packages/channels/src/smooch/channel.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { ChannelTemplate } from '../base/channel'
import { SmoochApi } from './api'
import { SmoochConfig, SmoochConfigSchema } from './config'
import { SmoochService } from './service'
import { SmoochStream } from './stream'

export class SmoochChannel extends ChannelTemplate<SmoochConfig, SmoochService, SmoochApi, SmoochStream> {
get meta() {
return {
id: '82c7a7ee-f1c9-4fb6-8306-18f03d6aadc9',
name: 'smooch',
version: '1.0.0',
schema: SmoochConfigSchema,
initiable: true,
lazy: true
}
}

constructor() {
const service = new SmoochService()
super(service, new SmoochApi(service), new SmoochStream(service))
}
}
16 changes: 16 additions & 0 deletions packages/channels/src/smooch/config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import Joi from 'joi'
import { ChannelConfig } from '../base/config'

export interface SmoochConfig extends ChannelConfig {
appId: string
keyId: string
keySecret: string
webhookSecret: string
}

export const SmoochConfigSchema = {
appId: Joi.string().required(),
keyId: Joi.string().required(),
keySecret: Joi.string().required(),
webhookSecret: Joi.string().required()
}
7 changes: 7 additions & 0 deletions packages/channels/src/smooch/context.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { ChannelContext } from '../base/context'
import { SmoochState } from './service'
import { SmoochContent } from './smooch'

export type SmoochContext = ChannelContext<SmoochState> & {
messages: SmoochContent[]
}
9 changes: 9 additions & 0 deletions packages/channels/src/smooch/renderers/audio.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { AudioRenderer } from '../../base/renderers/audio'
import { AudioContent } from '../../content/types'
import { SmoochContext } from '../context'

export class SmoochAudioRenderer extends AudioRenderer {
renderAudio(context: SmoochContext, payload: AudioContent) {
context.messages.push({ type: 'file', text: payload.title, mediaUrl: payload.audio })
}
}
72 changes: 72 additions & 0 deletions packages/channels/src/smooch/renderers/carousel.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
import { CarouselContext, CarouselRenderer } from '../../base/renderers/carousel'
import { ActionOpenURL, ActionPostback, ActionSaySomething, CardContent, CarouselContent } from '../../content/types'
import { SmoochContext } from '../context'
import { SmoochAction, SmoochCard } from '../smooch'

export const POSTBACK_PREFIX = 'postback::'
export const SAY_PREFIX = 'say::'

type Context = CarouselContext<SmoochContext> & {
items: SmoochCard[]
actions: SmoochAction[]
}

export class SmoochCarouselRenderer extends CarouselRenderer {
startRender(context: Context, carousel: CarouselContent) {
context.items = []
}

startRenderCard(context: Context, card: CardContent) {
context.actions = []
}

renderButtonUrl(context: Context, button: ActionOpenURL) {
context.actions.push({
type: 'link',
text: button.title,
uri: button.url
})
}

renderButtonPostback(context: Context, button: ActionPostback) {
context.actions.push({
type: 'postback',
text: button.title,
payload: `${POSTBACK_PREFIX}${button.payload}`
})
}

renderButtonSay(context: Context, button: ActionSaySomething) {
context.actions.push({
type: 'postback',
text: button.title,
payload: `${SAY_PREFIX}${button.text}`
})
}

endRenderCard(context: Context, card: CardContent) {
if (context.actions.length === 0) {
context.actions.push({
type: 'postback',
text: card.title,
payload: card.title
})
}

const smoochCard: SmoochCard = {
title: card.title,
description: card.subtitle,
mediaUrl: card.image,
actions: context.actions
}

context.items.push(smoochCard)
}

endRender(context: Context, carousel: CarouselContent) {
context.channel.messages.push({
type: 'carousel',
items: context.items
})
}
}
10 changes: 10 additions & 0 deletions packages/channels/src/smooch/renderers/choices.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { ChoicesRenderer } from '../../base/renderers/choices'
import { ChoiceContent } from '../../content/types'
import { SmoochContext } from '../context'

export class SmoochChoicesRenderer extends ChoicesRenderer {
renderChoice(context: SmoochContext, payload: ChoiceContent): void {
const message = context.messages[0]
message.actions = payload.choices.map((r) => ({ type: 'reply', text: r.title, payload: r.value }))
}
}
9 changes: 9 additions & 0 deletions packages/channels/src/smooch/renderers/file.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { FileRenderer } from '../../base/renderers/file'
import { FileContent } from '../../content/types'
import { SmoochContext } from '../context'

export class SmoochFileRenderer extends FileRenderer {
renderFile(context: SmoochContext, payload: FileContent) {
context.messages.push({ type: 'file', text: payload.title, mediaUrl: payload.file })
}
}
9 changes: 9 additions & 0 deletions packages/channels/src/smooch/renderers/image.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { ImageRenderer } from '../../base/renderers/image'
import { ImageContent } from '../../content/types'
import { SmoochContext } from '../context'

export class SmoochImageRenderer extends ImageRenderer {
renderImage(context: SmoochContext, payload: ImageContent): void {
context.messages.push({ type: 'image', mediaUrl: payload.image, text: payload.title })
}
}
19 changes: 19 additions & 0 deletions packages/channels/src/smooch/renderers/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { SmoochAudioRenderer } from './audio'
import { SmoochCarouselRenderer } from './carousel'
import { SmoochChoicesRenderer } from './choices'
import { SmoochFileRenderer } from './file'
import { SmoochImageRenderer } from './image'
import { SmoochLocationRenderer } from './location'
import { SmoochTextRenderer } from './text'
import { SmoochVideoRenderer } from './video'

export const SmoochRenderers = [
new SmoochTextRenderer(),
new SmoochImageRenderer(),
new SmoochChoicesRenderer(),
new SmoochCarouselRenderer(),
new SmoochFileRenderer(),
new SmoochAudioRenderer(),
new SmoochVideoRenderer(),
new SmoochLocationRenderer()
]
19 changes: 19 additions & 0 deletions packages/channels/src/smooch/renderers/location.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { LocationRenderer } from '../../base/renderers/location'
import { LocationContent } from '../../content/types'
import { SmoochContext } from '../context'

export class SmoochLocationRenderer extends LocationRenderer {
renderLocation(context: SmoochContext, payload: LocationContent) {
context.messages.push({
type: 'location',
coordinates: {
lat: payload.latitude,
long: payload.longitude
},
location: {
address: payload.address,
name: payload.title
}
})
}
}
12 changes: 12 additions & 0 deletions packages/channels/src/smooch/renderers/text.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { TextRenderer } from '../../base/renderers/text'
import { TextContent } from '../../content/types'
import { SmoochContext } from '../context'

export class SmoochTextRenderer extends TextRenderer {
renderText(context: SmoochContext, payload: TextContent): void {
context.messages.push({
type: 'text',
text: payload.text
})
}
}
9 changes: 9 additions & 0 deletions packages/channels/src/smooch/renderers/video.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { VideoRenderer } from '../../base/renderers/video'
import { VideoContent } from '../../content/types'
import { SmoochContext } from '../context'

export class SmoochVideoRenderer extends VideoRenderer {
renderVideo(context: SmoochContext, payload: VideoContent) {
context.messages.push({ type: 'file', text: payload.title, mediaUrl: payload.video })
}
}
17 changes: 17 additions & 0 deletions packages/channels/src/smooch/senders/common.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { CommonSender } from '../../base/senders/common'
import { SmoochContext } from '../context'
const SunshineConversationsClient = require('sunshine-conversations-client')

export class SmoochCommonSender extends CommonSender {
async send(context: SmoochContext) {
for (const message of context.messages) {
const data = new SunshineConversationsClient.MessagePost()
data.author = {
type: 'business'
}
data.content = message

await context.state.smooch.messages.postMessage(context.state.config.appId, context.thread, data)
}
}
}
4 changes: 4 additions & 0 deletions packages/channels/src/smooch/senders/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
import { SmoochCommonSender } from './common'
import { SmoochTypingSender } from './typing'

export const SmoochSenders = [new SmoochTypingSender(), new SmoochCommonSender()]
15 changes: 15 additions & 0 deletions packages/channels/src/smooch/senders/typing.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { TypingSender } from '../../base/senders/typing'
import { SmoochContext } from '../context'
const SunshineConversationsClient = require('sunshine-conversations-client')

export class SmoochTypingSender extends TypingSender {
async sendIndicator(context: SmoochContext) {
const data = new SunshineConversationsClient.ActivityPost()
data.author = {
type: 'business'
}
data.type = 'typing:start'

await context.state.smooch.activity.postActivity(context.state.config.appId, context.thread, data)
}
}

0 comments on commit c10e228

Please sign in to comment.