Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion integrations/sunco/integration.definition.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import { events } from './definitions'

export default new IntegrationDefinition({
name: 'sunco',
version: '1.3.0',
version: '1.4.0',
title: 'Sunshine Conversations',
description: 'Give your bot access to a powerful omnichannel messaging platform.',
icon: 'icon.svg',
Expand Down
23 changes: 23 additions & 0 deletions integrations/sunco/src/actions/get-or-create-conversation.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { createClient } from '../sunshine-api'
import * as bp from '.botpress'

export const getOrCreateConversation: bp.IntegrationProps['actions']['getOrCreateConversation'] = async ({
client,
input,
ctx,
}) => {
const suncoClient = createClient(ctx.configuration.keyId, ctx.configuration.keySecret)
const suncoConversation = await suncoClient.conversations.getConversation(
ctx.configuration.appId,
input.conversation.id
)

const { conversation } = await client.getOrCreateConversation({
channel: 'channel',
tags: { id: `${suncoConversation.conversation?.id}` },
})

return {
conversationId: conversation.id,
}
}
19 changes: 19 additions & 0 deletions integrations/sunco/src/actions/get-or-create-user.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { createClient } from '../sunshine-api'
import * as bp from '.botpress'

export const getOrCreateUser: bp.IntegrationProps['actions']['getOrCreateUser'] = async ({ client, input, ctx }) => {
const suncoClient = createClient(ctx.configuration.keyId, ctx.configuration.keySecret)
const suncoUser = await suncoClient.users.getUser(ctx.configuration.appId, input.user.id)
const suncoProfile = suncoUser.user?.profile

const name = input.name ?? [suncoProfile?.givenName, suncoProfile?.surname].join(' ').trim()
const { user } = await client.getOrCreateUser({
tags: { id: `${suncoUser.user?.id}` },
name,
pictureUrl: input.pictureUrl ?? suncoProfile?.avatarUrl,
})

return {
userId: user.id,
}
}
11 changes: 11 additions & 0 deletions integrations/sunco/src/actions/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { getOrCreateConversation } from './get-or-create-conversation'
import { getOrCreateUser } from './get-or-create-user'
import { startTypingIndicator, stopTypingIndicator } from './typing-indicator'
import * as bp from '.botpress'

export const actions = {
startTypingIndicator,
stopTypingIndicator,
getOrCreateUser,
getOrCreateConversation,
} satisfies bp.IntegrationProps['actions']
59 changes: 59 additions & 0 deletions integrations/sunco/src/actions/typing-indicator.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import { createClient } from '../sunshine-api'
import { getConversationId } from '../util'
import * as bp from '.botpress'

type SendActivityProps = Pick<bp.AnyMessageProps, 'ctx' | 'client'> & {
conversationId: string
typingStatus?: 'start' | 'stop'
markAsRead?: boolean
}

async function sendActivity({ client, ctx, conversationId, typingStatus, markAsRead }: SendActivityProps) {
const { conversation } = await client.getConversation({ id: conversationId })
const suncoConversationId = getConversationId(conversation)
const { appId, keyId, keySecret } = ctx.configuration
const suncoClient = createClient(keyId, keySecret)
if (markAsRead) {
await suncoClient.activities.postActivity(appId, suncoConversationId, {
type: 'conversation:read',
author: { type: 'business' },
})
}
if (typingStatus) {
await suncoClient.activities.postActivity(appId, suncoConversationId, {
type: `typing:${typingStatus}`,
author: { type: 'business' },
})
}
}

export const startTypingIndicator: bp.IntegrationProps['actions']['startTypingIndicator'] = async ({
client,
ctx,
input,
}) => {
const { conversationId } = input
await sendActivity({
client,
ctx,
conversationId,
typingStatus: 'start',
markAsRead: true,
})
return {}
}

export const stopTypingIndicator: bp.IntegrationProps['actions']['stopTypingIndicator'] = async ({
client,
ctx,
input,
}) => {
const { conversationId } = input
await sendActivity({
client,
ctx,
conversationId,
typingStatus: 'stop',
})
return {}
}
144 changes: 144 additions & 0 deletions integrations/sunco/src/channels.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
import { RuntimeError } from '@botpress/client'
import { Action, CarouselItem, MessageContent, PostMessageRequest, createClient } from './sunshine-api'
import { Carousel, Choice } from './types'
import { getConversationId } from './util'
import * as bp from '.botpress'

export const channels = {
channel: {
messages: {
text: async (props) => {
await sendMessage(props, { type: 'text', text: props.payload.text })
},
image: async (props) => {
await sendMessage(props, { type: 'image', mediaUrl: props.payload.imageUrl })
},
markdown: async (props) => {
await sendMessage(props, { type: 'text', text: props.payload.markdown })
},
audio: async (props) => {
await sendMessage(props, { type: 'file', mediaUrl: props.payload.audioUrl })
},
video: async (props) => {
await sendMessage(props, { type: 'file', mediaUrl: props.payload.videoUrl })
},
file: async (props) => {
try {
await sendMessage(props, { type: 'file', mediaUrl: props.payload.fileUrl })
} catch (e) {
const err = e as any
// 400 errors can be sent if file has unsupported type
// See: https://docs.smooch.io/guide/validating-files/#rejections
if (err.status === 400 && err.response?.text) {
console.info(err.response.text)
}
throw e
}
},
location: async (props) => {
await sendMessage(props, {
type: 'location',
coordinates: {
lat: props.payload.latitude,
long: props.payload.longitude,
},
})
},
carousel: async (props) => {
await sendCarousel(props, props.payload)
},
card: async (props) => {
await sendCarousel(props, { items: [props.payload] })
},
dropdown: async (props) => {
await sendMessage(props, renderChoiceMessage(props.payload))
},
choice: async (props) => {
await sendMessage(props, renderChoiceMessage(props.payload))
},
bloc: () => {
throw new RuntimeError('Not implemented')
},
},
},
} satisfies bp.IntegrationProps['channels']

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

function renderChoiceMessage(payload: Choice): MessageContent {
return {
type: 'text',
text: payload.text,
actions: payload.options.map((r) => ({ type: 'reply' as const, text: r.label, payload: r.value })),
}
}

type SendMessageProps = Pick<bp.AnyMessageProps, 'ctx' | 'conversation' | 'ack'>

async function sendMessage({ conversation, ctx, ack }: SendMessageProps, payload: MessageContent) {
const client = createClient(ctx.configuration.keyId, ctx.configuration.keySecret)

const data: PostMessageRequest = {
author: { type: 'business' },
content: payload,
}

const { messages } = await client.messages.postMessage(ctx.configuration.appId, getConversationId(conversation), data)

const message = messages?.[0]

if (!message) {
throw new Error('Message not sent')
}

await ack({ tags: { id: message.id } })

if (messages.length > 1) {
console.warn('More than one message was sent')
}
}

const sendCarousel = async (props: SendMessageProps, payload: Carousel) => {
const items: CarouselItem[] = []

for (const card of payload.items) {
const actions: Action[] = []
for (const button of card.actions) {
if (button.action === 'url') {
actions.push({
type: 'link',
text: button.label,
uri: button.value,
})
} else if (button.action === 'postback') {
actions.push({
type: 'postback',
text: button.label,
payload: `${POSTBACK_PREFIX}${button.value}`,
})
} else if (button.action === 'say') {
actions.push({
type: 'postback',
text: button.label,
payload: `${SAY_PREFIX}${button.label}`,
})
}
}

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

items.push({ title: card.title, description: card.subtitle, mediaUrl: card.imageUrl, actions })
}

await sendMessage(props, {
type: 'carousel',
items,
})
}
Loading
Loading