Creating a new Adapter
Prerequisite
This guide assumes that you have a complete understanding of what an adapter is and its functionality in the system. Please go through this documentation about Adapters if you haven't.
note
Please go through supported adapters, to ensure that the type of adapter you're trying to create doesn't already exist.
Selecting a Channel
The first step of this guide is to select a channel
for which an adapter needs to be created. A channel means any client facing platform with which a user may interact with a bots in the system. Examples channels include, Whatsapp, Telegram, Slack, Discord, Email, etc.
Selecting a Provider
The next step of enabling communication on a specific channel is selecting a provider
. A provider may be an external/third party provider like Gupshup for Whatsapp, or the platform may itself provide REST APIs to communicate with it's own platform, like in the case of Telegram Bot APIs. Either way, you would need to get hold of a documentation that describes how to use the APIs of that provider.
Selecting Adapter Type
An adapter may implement one of the following interfaces
- IChatProvider (for chat platforms)
- IEmailProvider (for email platforms)
- ISmsProvider (for sms platforms)
- XMessageProvider (for any generic platform)
tip
Although adapters can implement any one of the above interfaces, XMessageProvider
are preferred over other types, since XMessage
is the internal language spec of the system and is highly structured and extensible.
Implementing the Adapter
1. The sendMessage
function (For One Way Communication)
Every provider "must" define the sendMessage
function which is responsible for sending a message to the corresponding channel by calling the actual APIs for a provider. The function accepts a single argument of a specific type based on what interface is implemented and the function should interpret the data provided as an argument and call the corresponding API with relevant data.
2. The convertMessageToXMsg
function (For Two Way Communication)
This is an optional function that a provider may define if two way communication is desired (Message can travel from User to Bot). The sole responsibility of this function is to convert the raw data sent by a Webhook/API into an XMessage that the system understands. This function accepts a single argument, that is, the raw data sent by a Webhook, which may vary in type according to the channel and provider service. The function must return an XMessage that contains the entire data sent by the Webhook.
Here is an example of how this function can be implemented. In this specific example, a telegram webhook message data is being converted into an XMessage
.
async convertMessageToXMsg(msg: TelegramUpdateMessage): Promise<XMessage> {
let messageType = MessageType.TEXT;
let text = msg.message.text;
if (text.startsWith('\\register')) {
text = text.replace('\\register ', '').trim();
messageType = MessageType.REGISTRATION;
}
const xmessage: XMessage = {
to: {
userID: "admin",
bot: true,
},
from: {
userID: `${msg.message.from.id}`,
bot: false,
},
channelURI: "Telegram",
providerURI: "Telegram",
messageState: MessageState.REPLIED,
messageId: {
channelMessageId: `${msg.update_id}`,
Id: uuid4(),
},
messageType: messageType,
timestamp: msg.message.date,
payload: {
text: text,
},
};
return xmessage;
}
3. Adding the Adapter to Factory
Every newly created adapter needs to be added to the Factory package to be made available for other services.
An adapter name is derived by concatinating the name of the provider
+ channel
it's created for.
Inside the adapter.factory.ts
, an adapter's name needs to be added to one of the corresponding groups according to the interface being used.
- emailTypes
- smsTypes
- chatTypes
- xmessageTypes
The AdapterFactory
class contains the getAdapter
method which returns the exact object instance based on the type provided.
Here is how you would add an adapter with provider XProvider
and channel XChannel
.
static getAdapter(
consumerData: GenericAdapterConfig
): IEmailProvider | ISmsProvider | IChatProvider | IPushProvider | XMessageProvider | undefined {
switch (consumerData.type) {
// other adapters
...
case 'XProviderXChannel':
return new MyAdapterClass(consumerData.config);
default:
return undefined;
}
}
Creating IChatProvider Type Adapter
Start by implementing the IChatProvider
interface.
- For one way communication define the
sendMessage
function. Here is an example which uses discord webhook API to send messages.
async sendMessage(data: IChatOptions): Promise<ISendMessageSuccessResponse> {
const url = new URL(data.webhookUrl);
url.searchParams.set('wait', 'true');
const response = await this.axiosInstance.post(url.toString(), {
content: data.content,
});
return {
id: response.data.id,
date: response.data.timestamp,
};
}
For two way communication define the
convertMessageToXMsg
function. See example here.Add your adapter to the factory package, like shown here.
Creating ISmsProvider Type Adapter
Start by implementing the ISmsProvider
interface.
- For one way communication define the
sendMessage
function. Here is an example which illustrates the usage of twilio provider to send SMS.
async sendMessage(
options: ISmsOptions
): Promise<ISendMessageSuccessResponse> {
const twilioResponse = await this.twilioClient.messages.create({
body: options.content,
to: options.to,
from: options.from || this.config.from,
});
return {
id: twilioResponse.sid,
date: twilioResponse.dateCreated.toISOString(),
};
}
For two way communication define the
convertMessageToXMsg
function. See example here.Add your adapter to the factory package, like shown here.
Creating IEmailProvider Type Adapter
Start by implementing the IEmailProvider
interface.
- For one way communication define the
sendMessage
function. Here is an example which uses Mailgun to send emails.
async sendMessage(
emailOptions: IEmailOptions
): Promise<ISendMessageSuccessResponse> {
const mailgunMessageData: Partial<MailgunMessageData> = {
from: emailOptions.from || this.config.from,
to: emailOptions.to,
subject: emailOptions.subject,
html: emailOptions.html,
cc: emailOptions.cc?.join(','),
bcc: emailOptions.bcc?.join(','),
attachment: emailOptions.attachments?.map((attachment) => {
return {
data: attachment.file,
filename: attachment.name,
};
}),
};
if (emailOptions.replyTo) {
mailgunMessageData['h:Reply-To'] = emailOptions.replyTo;
}
const response = await this.mailgunClient.messages.create(
this.config.domain,
mailgunMessageData as MailgunMessageData
);
return {
id: response.id,
date: new Date().toISOString(),
};
}
For two way communication define the
convertMessageToXMsg
function. See example here.Add your adapter to the factory package, like shown here.
Creating XMessageProvider Type Adapter
Start by implementing the XMessageProvider
interface.
- For one way communication define the
sendMessage
function. Here is an example which uses Gupshup to send Whatsapp messages.
async sendMessage (xMsg: XMessage) {
if (!this.providerConfig) {
console.error("Configuration not set for adapter!");
return;
}
try {
if (
(this.providerConfig.usernameHSM && this.providerConfig.passwordHSM) ||
(this.providerConfig.username2Way && this.providerConfig.password2Way)
) {
let text: string = xMsg.payload.text || '';
let builder = this.getURIBuilder();
let plainText: boolean = true;
const stylingTag: StylingTag | undefined =
xMsg.payload.stylingTag !== null
? xMsg.payload.stylingTag
: undefined;
builder = this.setBuilderCredentialsAndMethod(
builder,
MethodType.SIMPLEMESSAGE,
this.providerConfig.username2Way,
this.providerConfig.password2Way,
);
builder.set('send_to', '91' + xMsg.to.userID);
builder.set('phone_number', '91' + xMsg.to.userID);
builder.set('msg_type', xMsg.messageType);
builder.set('channel', 'Whatsapp');
if (xMsg.messageId.channelMessageId) {
builder.set('msg_id', xMsg.messageId.channelMessageId);
}
if (xMsg.payload.media && xMsg.payload.media.url) {
const media: MessageMedia = xMsg.payload.media;
builder.set('method', MethodType.MEDIAMESSAGE);
builder.set(
'msg_type',
this.getMessageTypeByMediaCategory(media.category)
);
builder.set('media_url', media.url);
builder.set('caption', media.text);
builder.set('isHSM', 'false');
plainText = false;
}
if (plainText) {
text += this.renderMessageChoices(xMsg.payload.buttonChoices || []);
builder.set('msg', text);
}
const expanded = new URL(
`https://media.smsgupshup.com/GatewayAPI/rest?${builder}`
);
try {
const response: GSWhatsappOutBoundResponse = await this.sendOutboundMessage(
expanded.toString()
);
if (response !== null && response.response.status === 'success') {
xMsg.messageId = MessageId.builder()
.setChannelMessageId(response.response.id)
.build();
xMsg.messageState = MessageState.SENT;
return xMsg;
} else {
console.error(
'Gupshup Whatsapp Message not sent: ',
response.response.details
);
xMsg.messageState = MessageState.NOT_SENT;
return xMsg;
}
} catch (error) {
console.error('Error in Send GS Whatsapp Outbound Message', error);
}
} else {
console.error('Credentials not found');
xMsg.messageState = MessageState.NOT_SENT;
return xMsg;
}
} catch (error) {
console.error('Error in processing outbound message', error);
throw error;
}
};
private sendOutboundMessage = (url: string): Promise<GSWhatsappOutBoundResponse> => {
return new Promise<GSWhatsappOutBoundResponse>((resolve, reject) => {
axios.create()
.get<GSWhatsappOutBoundResponse>(url)
.then((response: AxiosResponse<GSWhatsappOutBoundResponse>) => {
resolve(response.data);
})
.catch((error) => {
reject(error);
});
});
}
note
Even though the XMessageProvider
Type may seem complex, the example here just illustrates the extensibility of the XMessage
spec, the implementation may be much simpler if you're only trying to support few message types.