Skip to content

Commit

Permalink
Merge pull request #620 from techmatters/CHI-2639-telegeram_poc
Browse files Browse the repository at this point in the history
CHI-2639: Telegram Custom Channel (POC)
  • Loading branch information
stephenhand authored Jun 6, 2024
2 parents 0bbb63b + aeb5582 commit 50ba9f6
Show file tree
Hide file tree
Showing 7 changed files with 361 additions and 35 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,11 @@ runs:
with:
ssm_parameter: "/development/line/${{inputs.account-sid}}/channel_access_token"
env_variable_name: "LINE_CHANNEL_ACCESS_TOKEN"
# Line environment variables
- name: Set helpline Line Studio Flow SID
uses: "marvinpinto/action-inject-ssm-secrets@latest"
with:
ssm_parameter: "/development/twilio/${{inputs.account-sid}}/line_studio_flow_sid"
env_variable_name: "LINE_STUDIO_FLOW_SID"
- name: Set helpline Line Flex Messaging Mode (Programmable Chat or Conversations)
uses: "marvinpinto/action-inject-ssm-secrets@latest"
with:
Expand All @@ -117,6 +121,37 @@ runs:
with:
ssm_parameter: "/development/twilio/${{inputs.account-sid}}/modica_flex_flow_sid"
env_variable_name: "MODICA_FLEX_FLOW_SID"


# Telegram environment variables
- name: Set Telegram Flex Bot Token
uses: "marvinpinto/action-inject-ssm-secrets@latest"
with:
ssm_parameter: "/development/telegram/${{inputs.account-sid}}/flex_bot_token"
env_variable_name: "TELEGRAM_FLEX_BOT_TOKEN"
- name: Set Telegram Bot Api Secret Token
uses: "marvinpinto/action-inject-ssm-secrets@latest"
with:
ssm_parameter: "/development/telegram/${{inputs.account-sid}}/bot_api_secret_token"
env_variable_name: "TELEGRAM_BOT_API_SECRET_TOKEN"
- name: Set helpline Telegram Studio Flow SID
uses: "marvinpinto/action-inject-ssm-secrets@latest"
with:
ssm_parameter: "/development/twilio/${{inputs.account-sid}}/telegram_studio_flow_sid"
env_variable_name: "TELEGRAM_STUDIO_FLOW_SID"

- name: Set helpline serverless URL
uses: "marvinpinto/action-inject-ssm-secrets@latest"
with:
ssm_parameter: "/development/serverless/${{inputs.account-sid}}/base_url"
env_variable_name: "SERVERLESS_BASE_URL"
- name: Set Telegram flex bot webhook
shell: bash
run: |
curl --request POST \
--header "Content-Type: application/json" \
--url "https://api.telegram.org/bot${{ env.TELEGRAM_FLEX_BOT_TOKEN }}/setWebhook" \
--data '{ "url": "${{ env.SERVERLESS_BASE_URL }}/webhooks/telegram/TelegramToFlex", "secret_token": "${{ env.TELEGRAM_BOT_API_SECRET_TOKEN }}" }'
# Append environment variables
- name: Add IWF_API_USERNAME
run: echo "IWF_API_USERNAME=${{ env.IWF_API_USERNAME }}" >> .env
Expand Down Expand Up @@ -155,6 +190,9 @@ runs:
- name: Add LINE_CHANNEL_ACCESS_TOKEN
run: echo "LINE_CHANNEL_ACCESS_TOKEN=${{ env.LINE_CHANNEL_ACCESS_TOKEN }}" >> .env
shell: bash
- name: Add LINE_STUDIO_FLOW_SID
run: echo "LINE_STUDIO_FLOW_SID=${{ env.LINE_STUDIO_FLOW_SID }}" >> .env
shell: bash
- name: Add LINE_TWILIO_MESSAGING_MODE
run: echo "LINE_TWILIO_MESSAGING_MODE=${{ env.LINE_TWILIO_MESSAGING_MODE }}" >> .env
shell: bash
Expand All @@ -167,3 +205,12 @@ runs:
- name: Add MODICA_FLEX_FLOW_SID
run: echo "MODICA_FLEX_FLOW_SID=${{ env.MODICA_FLEX_FLOW_SID }}" >> .env
shell: bash
- name: Add TELEGRAM_FLEX_BOT_TOKEN
run: echo "TELEGRAM_FLEX_BOT_TOKEN=${{ env.TELEGRAM_FLEX_BOT_TOKEN }}" >> .env
shell: bash
- name: Add TELEGRAM_BOT_API_SECRET_TOKEN
run: echo "TELEGRAM_BOT_API_SECRET_TOKEN=${{ env.TELEGRAM_BOT_API_SECRET_TOKEN }}" >> .env
shell: bash
- name: Add TELEGRAM_STUDIO_FLOW_SID
run: echo "TELEGRAM_STUDIO_FLOW_SID=${{ env.TELEGRAM_STUDIO_FLOW_SID }}" >> .env
shell: bash
41 changes: 30 additions & 11 deletions functions/helpers/customChannels/customChannelToFlex.private.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,8 +59,12 @@ export const findExistingConversation = async (
const existing = conversations.find((conversation) =>
['active', 'inactive'].includes(conversation.conversationState),
);
console.log(`Found existing conversation for ${identity}`, existing?.conversationSid, existing);
return existing !== undefined ? (existing.conversationSid as ConversationSid) : undefined;
if (existing) {
console.log(`Found existing conversation for ${identity}`, existing.conversationSid, existing);
return existing.conversationSid as ConversationSid;
}
console.log(`No existing conversation found for ${identity}`);
return undefined;
};

/**
Expand Down Expand Up @@ -181,6 +185,7 @@ export enum AseloCustomChannels {
Instagram = 'instagram',
Line = 'line',
Modica = 'modica',
Telegram = 'telegram',
}

export const isAseloCustomChannel = (s: unknown): s is AseloCustomChannels =>
Expand All @@ -201,11 +206,11 @@ type CreateFlexChannelParams = {
type CreateFlexConversationParams = {
studioFlowSid: string;
channelType: AseloCustomChannels; // The chat channel being used
twilioNumber: string; // The target Twilio number (usually have the shape <channel>:<id>, e.g. twitter:1234567)
uniqueUserName: string; // Unique identifier for this user
senderScreenName: string; // Friendly info to show to show in the Flex UI (like Twitter handle)
onMessageSentWebhookUrl: string; // The url that must be used as the onMessageSent event webhook.
conversationFriendlyName: string; // A name for the Flex conversation (typically same as uniqueUserName)
twilioNumber: string; // The target Twilio number (usually have the shape <channel>:<id>, e.g. twitter:1234567)
};

/**
Expand Down Expand Up @@ -333,7 +338,7 @@ const createConversation = async (
...channelAttributes,
channel_type: channelType,
channelType,
senderScreenName, // TODO: in Twitter this is "twitterUserHandle". Rework that in the UI when we use this
senderScreenName,
twilioNumber,
}),
});
Expand All @@ -355,6 +360,13 @@ const createConversation = async (
filters: ['onMessageAdded'],
},
});

console.log('conversation webhooks:');
(await conversationContext.webhooks.list()).forEach((wh) => {
Object.entries(wh).forEach(([key, value]) => {
console.log(`${key}:`, value);
});
});
} catch (err) {
return { conversationSid, error: err as Error };
}
Expand All @@ -370,12 +382,12 @@ type SendMessageToFlexParams = CreateFlexChannelParams & {
subscribedExternalId: string; // The id in the external chat system of the user that is subscribed to the webhook
};

type SendConversationMessageToFlexParams = CreateFlexConversationParams & {
syncServiceSid: string; // The Sync Service sid where user channel maps are stored
type SendConversationMessageToFlexParams = Omit<CreateFlexConversationParams, 'twilioNumber'> & {
messageText: string; // The body of the message to send
senderExternalId: string; // The id in the external chat system of the user sending the message - accountSid if not provided
messageAttributes?: string; // [optional] The message attributes
senderExternalId: string; // The id in the external chat system of the user sending the message
subscribedExternalId: string; // The id in the external chat system of the user that is subscribed to the webhook
customSubscribedExternalId?: string; // The id in the external chat system of the user that is subscribed to the webhook
customTwilioNumber?: string; // The target Twilio number (usually have the shape <channel>:<id>, e.g. twitter:1234567) - will be <channnel>:<accountSid> if not provided
};

/**
Expand Down Expand Up @@ -473,21 +485,23 @@ export const sendMessageToFlex = async (
* (e.g. if the message is sent by Twitter user 1234567, the uniqueUserName will be 'twitter:1234567')
*/
export const sendConversationMessageToFlex = async (
context: Context,
context: Context<{ ACCOUNT_SID: string }>,
{
studioFlowSid,
channelType,
twilioNumber,
customTwilioNumber,
uniqueUserName,
senderScreenName,
onMessageSentWebhookUrl,
messageText,
messageAttributes = undefined,
senderExternalId,
subscribedExternalId,
customSubscribedExternalId,
conversationFriendlyName,
}: SendConversationMessageToFlexParams,
): Promise<{ status: 'ignored' } | { status: 'sent'; response: any }> => {
const subscribedExternalId = customSubscribedExternalId || context.ACCOUNT_SID;
const twilioNumber = customTwilioNumber || `${channelType}:${subscribedExternalId}`;
// Do not send messages that were sent by the receiverId (account subscribed to the webhook), as they were either sent from Flex or from the specific UI of the chat system
console.log('=== sendConversationMessageToFlex ===');
if (senderExternalId === subscribedExternalId) {
Expand Down Expand Up @@ -524,6 +538,11 @@ export const sendConversationMessageToFlex = async (
messageAttributes,
});

console.log('sendConversationMessageToFlex response:');
Object.entries(response).forEach(([key, value]) => {
console.log(`${key}:`, value);
});

return { status: 'sent', response };
};

Expand Down
39 changes: 21 additions & 18 deletions functions/helpers/customChannels/flexToCustomChannel.private.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,12 +33,19 @@ export type ConversationWebhookEvent = {
Source: string;
};

export type ExternalSendResult = {
ok: boolean;
meta: Record<string, string>;
body: any;
resultCode: number;
};

export type WebhookEvent = ConversationWebhookEvent | ProgrammableChatWebhookEvent;

type Params<T extends WebhookEvent> = {
type Params<T extends WebhookEvent, TResponse = any> = {
event: T;
recipientId: string;
sendExternalMessage: (recipientId: string, messageText: string) => Promise<any>;
sendExternalMessage: (recipientId: string, messageText: string) => Promise<TResponse>;
};

export const isConversationWebhookEvent = (
Expand Down Expand Up @@ -79,34 +86,30 @@ export const redirectMessageToExternalChat = async (
};

export const redirectConversationMessageToExternalChat = async (
context: Context<{ CHAT_SERVICE_SID: string }>,
{ event, recipientId, sendExternalMessage }: Params<ConversationWebhookEvent>,
context: Context,
{ event, recipientId, sendExternalMessage }: Params<ConversationWebhookEvent, ExternalSendResult>,
): Promise<RedirectResult> => {
const { Body, ConversationSid, EventType, ParticipantSid, Source } = event;

let shouldSend = false;
if (Source === 'SDK') {
const response = await sendExternalMessage(recipientId, Body);
return { status: 'sent', response };
}

if (Source === 'API' && EventType === 'onMessageAdded') {
shouldSend = true;
} else if (Source === 'API' && EventType === 'onMessageAdded') {
const client = context.getTwilioClient();
const conversation = await client.conversations.conversations(ConversationSid).fetch();
const { attributes } = conversation;
console.log('conversation properties');
Object.entries(conversation).forEach(([key, value]) => {
console.log(key, value);
});

const { participantSid } = JSON.parse(attributes);

// Redirect bot, system or third participant, but not self
if (participantSid && participantSid !== ParticipantSid) {
const response = await sendExternalMessage(recipientId, Body);
shouldSend = participantSid && participantSid !== ParticipantSid;
}
if (shouldSend) {
const response = await sendExternalMessage(recipientId, Body);
if (response.ok) {
return { status: 'sent', response };
}
console.log(`Failed to send message: ${response.resultCode}`, response.body, response.meta);
throw new Error(`Failed to send message: ${response.resultCode}`);
}

// This ignores self messages and not supported sources
return { status: 'ignored' };
};
Expand Down
5 changes: 3 additions & 2 deletions functions/webhooks/line/FlexToLine.protected.ts
Original file line number Diff line number Diff line change
Expand Up @@ -69,9 +69,10 @@ const sendLineMessage =
});

return {
status: response.status,
ok: response.ok,
resultCode: response.status,
body: await response.json(),
headers: Object.fromEntries(Object.entries(response.headers)),
meta: Object.fromEntries(Object.entries(response.headers)),
};
};

Expand Down
5 changes: 2 additions & 3 deletions functions/webhooks/line/LineToFlex.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ import crypto from 'crypto';
import { ChannelToFlex } from '../../helpers/customChannels/customChannelToFlex.private';

type EnvVars = {
ACCOUNT_SID: string;
CHAT_SERVICE_SID: string;
SYNC_SERVICE_SID: string;
LINE_FLEX_FLOW_SID: string;
Expand Down Expand Up @@ -150,16 +151,14 @@ export const handler = async (
// eslint-disable-next-line no-await-in-loop
result = await channelToFlex.sendConversationMessageToFlex(context, {
studioFlowSid: context.LINE_STUDIO_FLOW_SID,
syncServiceSid: context.SYNC_SERVICE_SID,
conversationFriendlyName: chatFriendlyName,
channelType,
twilioNumber,
uniqueUserName,
senderScreenName,
onMessageSentWebhookUrl,
messageText,
senderExternalId,
subscribedExternalId,
customSubscribedExternalId: subscribedExternalId,
});
} else {
// eslint-disable-next-line no-await-in-loop
Expand Down
Loading

0 comments on commit 50ba9f6

Please sign in to comment.