Skip to content

Commit

Permalink
feat(teams): teams v1 (#336)
Browse files Browse the repository at this point in the history
* init

* copy old code

* remove tenantId

* fix image

* dropdown

* quick_reply

* postback say something

* better carousel

* audio video

* d

* file

* location

* better image

* fix

* refact

* integrate in server
  • Loading branch information
samuelmasse committed Feb 3, 2022
1 parent 4671560 commit 3720261
Show file tree
Hide file tree
Showing 26 changed files with 495 additions and 3 deletions.
4 changes: 3 additions & 1 deletion packages/channels/example/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ 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 { TeamsChannel } from '../src/teams/channel'
import { TelegramChannel } from '../src/telegram/channel'
import { TwilioChannel } from '../src/twilio/channel'
import payloads from './payloads.json'
Expand All @@ -16,8 +17,9 @@ export class App {
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('teams', new TeamsChannel())
await this.setupChannel('telegram', new TelegramChannel())
await this.setupChannel('twilio', new TwilioChannel())
}

async setupChannel(name: string, channel: Channel) {
Expand Down
2 changes: 2 additions & 0 deletions packages/channels/example/payloads.json
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,8 @@
{
"type": "dropdown",
"message": "Pick something :",
"buttonText": "buuutontext",
"placeholderText": "plaaaceholder",
"options": [
{ "label": "Mars", "value": "planet_mars" },
{ "label": "Venus", "value": "planet_venus" },
Expand Down
1 change: 1 addition & 0 deletions packages/channels/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
"dependencies": {
"@slack/bolt": "^3.9.0",
"axios": "^0.25.0",
"botbuilder": "^4.15.0",
"cli-color": "^2.0.1",
"express": "^4.17.2",
"joi": "^17.6.0",
Expand Down
3 changes: 2 additions & 1 deletion packages/channels/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,6 @@ export * from './base/endpoint'
export * from './messenger/channel'
export * from './slack/channel'
export * from './smooch/channel'
export * from './twilio/channel'
export * from './teams/channel'
export * from './telegram/channel'
export * from './twilio/channel'
29 changes: 29 additions & 0 deletions packages/channels/src/teams/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
### Sending

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

### Receiving

| Channels | Teams |
| ------------- | :---: |
| Text ||
| Quick Reply ||
| Postback ||
| Say Something ||
| Voice ||
| Image ||
| File ||
| Audio ||
| Video ||
| Location ||
76 changes: 76 additions & 0 deletions packages/channels/src/teams/api.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
import { ActivityTypes, TurnContext } from 'botbuilder'
import { Response } from 'express'
import { ChannelApi, ChannelApiManager, ChannelApiRequest } from '../base/api'
import { POSTBACK_PREFIX, SAY_PREFIX } from './renderers/carousel'
import { QUICK_REPLY_PREFIX } from './renderers/choices'
import { TeamsService } from './service'

export class TeamsApi extends ChannelApi<TeamsService> {
async setup(router: ChannelApiManager) {
router.post('/teams', this.handleRequest.bind(this))
}

private async handleRequest(req: ChannelApiRequest, res: Response) {
const { adapter } = this.service.get(req.scope)

await adapter.processActivity(req, res, async (turnContext) => {
try {
if (this.isProactive(turnContext)) {
await this.handleProactive(req.scope, turnContext)
} else {
await this.handleMessage(req.scope, turnContext)
}
} catch (e) {
this.service.logger?.error(e, 'Error occurred processing teams activity')
}
})
}

private async handleMessage(scope: string, turnContext: TurnContext) {
const { activity } = turnContext
const convoRef = TurnContext.getConversationReference(activity)

await this.service.setRef(scope, convoRef.conversation!.id, convoRef)

const endpoint = { identity: '*', sender: activity.from.id, thread: convoRef.conversation!.id }
const text: string | undefined = activity.value?.text || activity.text

if (!text) {
return
}

if (text.startsWith(QUICK_REPLY_PREFIX)) {
const [_prefix, payload, title] = text.split('::')
await this.service.receive(scope, endpoint, { type: 'quick_reply', text: title, payload })
} else if (text.startsWith(SAY_PREFIX)) {
await this.service.receive(scope, endpoint, {
type: 'say_something',
text: text.replace(SAY_PREFIX, '')
})
} else if (text.startsWith(POSTBACK_PREFIX)) {
await this.service.receive(scope, endpoint, {
type: 'postback',
payload: text.replace(POSTBACK_PREFIX, '')
})
} else {
await this.service.receive(scope, endpoint, { type: 'text', text })
}
}

private isProactive(turnContext: TurnContext): boolean {
const { activity } = turnContext

return (
activity.type === ActivityTypes.ConversationUpdate &&
(activity.membersAdded || []).some((member) => member.id === activity.recipient.id)
)
}

private async handleProactive(scope: string, turnContext: TurnContext): Promise<void> {
const { activity } = turnContext
const convoRef = TurnContext.getConversationReference(activity)

await this.service.setRef(scope, convoRef.conversation!.id, convoRef)
await this.service.proactive(scope, { identity: '*', sender: activity.from.id, thread: convoRef.conversation!.id })
}
}
23 changes: 23 additions & 0 deletions packages/channels/src/teams/channel.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { ChannelTemplate } from '../base/channel'
import { TeamsApi } from './api'
import { TeamsConfig, TeamsConfigSchema } from './config'
import { TeamsService } from './service'
import { TeamsStream } from './stream'

export class TeamsChannel extends ChannelTemplate<TeamsConfig, TeamsService, TeamsApi, TeamsStream> {
get meta() {
return {
id: '39525c14-738f-4db8-b73b-1f0edb36ad7c',
name: 'teams',
version: '1.0.0',
schema: TeamsConfigSchema,
initiable: false,
lazy: true
}
}

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

export interface TeamsConfig extends ChannelConfig {
appId: string
appPassword: string
}

export const TeamsConfigSchema = {
appId: Joi.string().required(),
appPassword: Joi.string().required()
}
8 changes: 8 additions & 0 deletions packages/channels/src/teams/context.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { Activity, ConversationReference } from 'botbuilder'
import { ChannelContext } from '../base/context'
import { TeamsState } from './service'

export type TeamsContext = ChannelContext<TeamsState> & {
messages: Partial<Activity>[]
convoRef: Partial<ConversationReference>
}
11 changes: 11 additions & 0 deletions packages/channels/src/teams/renderers/audio.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { AudioRenderer } from '../../base/renderers/audio'
import { AudioContent } from '../../content/types'
import { TeamsContext } from '../context'

export class TeamsAudioRenderer extends AudioRenderer {
renderAudio(context: TeamsContext, payload: AudioContent) {
context.messages.push({
text: `${payload.title ? `${payload.title} ` : ''}${payload.audio}`
})
}
}
64 changes: 64 additions & 0 deletions packages/channels/src/teams/renderers/carousel.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import { Attachment, AttachmentLayoutTypes, CardAction, CardFactory } from 'botbuilder'
import { CarouselContext, CarouselRenderer } from '../../base/renderers/carousel'
import { ActionOpenURL, ActionPostback, ActionSaySomething, CardContent, CarouselContent } from '../../content/types'
import { TeamsContext } from '../context'

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

type Context = CarouselContext<TeamsContext> & {
attachements: Attachment[]
actions: CardAction[]
}

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

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

renderButtonUrl(context: Context, button: ActionOpenURL) {
context.actions.push({
type: 'openUrl',
value: button.url,
title: button.title
})
}

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

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

endRenderCard(context: Context, card: CardContent) {
context.attachements.push(
CardFactory.heroCard(card.title, CardFactory.images([card.image!]), CardFactory.actions(context.actions), {
subtitle: card.subtitle
})
)
}

endRender(context: Context, carousel: CarouselContent) {
context.channel.messages.push({
type: 'message',
attachments: context.attachements,
attachmentLayout: AttachmentLayoutTypes.Carousel
})
}
}
37 changes: 37 additions & 0 deletions packages/channels/src/teams/renderers/choices.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import { CardFactory } from 'botbuilder'
import { ChoicesRenderer } from '../../base/renderers/choices'
import { ChoiceContent } from '../../content/types'
import { TeamsContext } from '../context'

export const QUICK_REPLY_PREFIX = 'quick_reply::'

export class TeamsChoicesRenderer extends ChoicesRenderer {
renderChoice(context: TeamsContext, payload: ChoiceContent) {
if (!context.messages.length) {
context.messages.push({})
}

if (!context.messages[0].attachments) {
context.messages[0].attachments = []
}

context.messages[0].text = undefined
context.messages[0].attachments.push(
CardFactory.heroCard(
payload.text,
undefined,
CardFactory.actions(
payload.choices.map((x) => {
return {
type: 'messageBack',
title: x.title,
displayText: x.title,
value: `${QUICK_REPLY_PREFIX}${x.value}::${x.title}`,
text: `${QUICK_REPLY_PREFIX}${x.value}::${x.title}`
}
})
)
)
)
}
}
54 changes: 54 additions & 0 deletions packages/channels/src/teams/renderers/dropdown.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import { ActivityTypes, CardFactory } from 'botbuilder'
import { ChannelRenderer } from '../../base/renderer'
import { TeamsContext } from '../context'
import { QUICK_REPLY_PREFIX } from './choices'

export class TeamsDropdownRenderer implements ChannelRenderer<TeamsContext> {
get priority(): number {
return 0
}

handles(context: TeamsContext): boolean {
return context.payload.options?.length > 0
}

render(context: TeamsContext) {
const payload = context.payload // TODO: as sdk.DropdownContent

context.messages.push({
attachments: [
CardFactory.adaptiveCard({
$schema: 'http://adaptivecards.io/schemas/adaptive-card.json',
type: 'AdaptiveCard',
version: '1.2',
body: [
{
type: 'TextBlock',
size: 'Medium',
weight: 'Bolder',
text: payload.message
},
{
type: 'Input.ChoiceSet',
choices: payload.options.map((opt: any, idx: any) => ({
title: opt.label,
id: `choice-${idx}`,
value: `${QUICK_REPLY_PREFIX}${opt.value}::${opt.label}`
})),
id: 'text',
placeholder: payload.placeholderText,
wrap: true
}
],
actions: [
{
type: 'Action.Submit',
title: payload.buttonText,
id: 'btnSubmit'
}
]
})
]
})
}
}
11 changes: 11 additions & 0 deletions packages/channels/src/teams/renderers/file.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { FileRenderer } from '../../base/renderers/file'
import { FileContent } from '../../content/types'
import { TeamsContext } from '../context'

export class TeamsFileRenderer extends FileRenderer {
renderFile(context: TeamsContext, payload: FileContent) {
context.messages.push({
text: `${payload.title ? `${payload.title} ` : ''}${payload.file}`
})
}
}
12 changes: 12 additions & 0 deletions packages/channels/src/teams/renderers/image.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { CardFactory } from 'botbuilder'
import { ImageRenderer } from '../../base/renderers/image'
import { ImageContent } from '../../content/types'
import { TeamsContext } from '../context'

export class TeamsImageRenderer extends ImageRenderer {
renderImage(context: TeamsContext, payload: ImageContent) {
context.messages.push({
attachments: [CardFactory.heroCard(payload.title!, CardFactory.images([payload.image]))]
})
}
}

0 comments on commit 3720261

Please sign in to comment.