speedybot
Version:
<p align="center"> <a href="https://github.com/valgaze/speedybot"> <img src="https://img.shields.io/npm/v/speedybot.svg" /> </a> <a href="https://github.com/valgaze/speedybot"> <img src="https://img.shields.io/npm/dm/speedybot.svg" /> </a>
952 lines • 33.4 kB
JavaScript
import { CONSTANTS, mainRequester } from "./index";
import { SpeedyCard, checkers, } from "./cards";
export class SpeedyBot {
_token;
makeRequest;
middlewares = [];
topMiddleware = null;
fallbackText = "Your client does not support Adaptive Cards";
errorHandler;
constructor(_token = "", makeRequest = mainRequester) {
this._token = _token;
this.makeRequest = makeRequest;
}
secrets = {};
addSecret(key, value) {
this.secrets[key] = value;
}
addSecrets(payload) {
Object.entries(payload).forEach(([k, v]) => this.addSecret(k, v));
}
getSecret(key) {
return this.secrets[key];
}
API = {
messages: "https://webexapis.com/v1/messages",
attachments: "https://webexapis.com/v1/attachment/actions",
user: {
self: "https://webexapis.com/v1/people/me",
getPersonDetails: "https://webexapis.com/v1/people",
},
rooms: "https://webexapis.com/v1/rooms",
roomDetails: "https://webexapis.com/v1/rooms",
webhooks: "https://webexapis.com/v1/webhooks",
};
setToken(token) {
this._token = token;
return this;
}
getToken() {
return this._token;
}
setFallbackText(t) {
this.fallbackText = t;
}
insertStepToFront(middleware) {
if (this.topMiddleware) {
this.middlewares.unshift(this.topMiddleware);
}
this.topMiddleware = middleware;
}
pickRandom(listOrMin, max) {
if (Array.isArray(listOrMin)) {
const items = listOrMin;
return items[Math.floor(Math.random() * items.length)];
}
else {
const min = listOrMin;
return Math.floor(Math.random() * (max - min + 1)) + min;
}
}
addStep(middleware) {
this.middlewares.push(middleware);
}
addStepSequence(middlewares) {
this.middlewares.push(...middlewares);
}
regex(reg, cb) {
this.addStep(async ($) => {
if ($.text) {
if (reg instanceof RegExp) {
const matched = reg.test($.text.toLowerCase());
if (matched) {
return await cb($);
}
}
}
return $.next;
});
}
onCamera(cb, formats = []) {
this.addStep(async ($) => {
if ($.file) {
const { extension } = $.file;
const photos = ["png", "jpg", "jpeg", ...formats];
if (photos.includes(extension)) {
return await cb($);
}
}
return $.next;
});
}
exact(keyword, cb) {
this.insertStepToFront(async ($) => {
if (typeof keyword === "string") {
if ($.text === keyword) {
return await cb($);
}
}
return $.next;
});
}
contains(keyword, cb) {
this.addStep(async ($) => {
if (typeof keyword === "string") {
if ($.text?.toLowerCase().includes(keyword.toLowerCase())) {
return await cb($);
}
}
else if (Array.isArray(keyword)) {
for (const item of keyword) {
if ($.text?.toLowerCase().includes(item.toLowerCase())) {
return await cb($);
}
}
}
return $.next;
});
}
onReject(e) {
if (this.errorHandler) {
this.errorHandler(e);
}
}
captureError(func) {
this.errorHandler = func;
}
async runMiddleware(env, startingCtx = {}) {
const { roomId } = env.data;
const botInst = this;
try {
const messageType = this.detectType(env);
let roomType = "";
if ("roomType" in env.data) {
roomType = env.data.roomType;
}
const { details, author: authorData } = await this.buildDetails(messageType, env);
const [authorEmail] = authorData.emails;
const [, authorDomain] = authorEmail.toLowerCase().split("@");
let filePayload = undefined;
let attachedData = undefined;
let textData;
let mentionedPeople = [];
let messageId = "";
let parentIdVal;
const proceed = await this.checkAuthor(authorData);
if (!proceed) {
return false;
}
if (messageType === "card") {
const { inputs, messageId: id } = details;
attachedData = inputs;
messageId = id;
const roomData = await this.getRoom(roomId);
roomType = roomData.type;
const chipToken = CONSTANTS.CHIP_LABEL;
const isChip = inputs[chipToken];
if (isChip) {
textData = inputs[chipToken];
attachedData = undefined;
}
const isDelete = inputs[CONSTANTS.submitToken] === CONSTANTS.action_delete;
if (isDelete) {
await this.deleteMessage(id);
return true;
}
}
if (messageType === "text" || messageType === "file") {
const { parentId, id, roomType: roomType_val, text = textData, files, mentionedPeople: mentionedPeople_val = [], } = details;
parentIdVal = parentId;
mentionedPeople = mentionedPeople_val;
attachedData = undefined;
messageId = id;
roomType = roomType_val;
textData = text ?? "";
textData =
roomType === "group"
? textData.split(" ").slice(1).join(" ") || ""
: textData;
if (files && files.length) {
const [targetUrl] = files;
try {
let fileData = await this.peekFile(targetUrl);
filePayload = {
...fileData,
async getData() {
const { data } = await botInst.getFile(targetUrl);
return data;
},
};
}
catch (e) {
const err = {
message: "There was an issue obtaining file data",
error: e,
e,
roomId,
};
this.onReject(err);
return false;
}
}
}
const $Magic = {
next: true,
end: false,
messageType,
id: messageId,
author: {
domain: authorDomain,
email: authorEmail,
id: authorData.id,
org: authorData.orgId,
name: authorData.displayName,
type: authorData.type,
profilePic: authorData.avatar,
},
text: textData,
msg: {
parentId: parentIdVal,
roomId,
roomType,
mentionedPeople: mentionedPeople.slice(1),
},
file: filePayload,
data: attachedData,
ctx: startingCtx,
buildDMLink(target, label) {
const url = `webexteams://im?email=${target}`;
return this.buildLink(url, label ?? url);
},
debug() {
return {
messageType: this.messageType,
text: this.text ?? "",
data: this.data,
messageId: this.id,
author: {
id: this.author.id,
name: this.author.name,
email: this.author.email,
},
hasFile: !!this.file,
msg: {
parentId: this.msg.parentId,
roomId: this.msg.roomId,
roomType: this.msg.roomType,
mentionedPeople: this.msg.mentionedPeople,
},
};
},
async thread(threadData) {
let [root, ...messages] = threadData;
const maxEntries = Math.min(messages.length, 5);
const { id: parentId, roomId } = await this.send(root);
for (let i = 0; i < maxEntries; i++) {
const msg = messages[i];
let msgObj = {
parentId,
};
if (typeof msg === "string") {
msgObj["markdown"] = msg;
msgObj["text"] = msg;
msgObj["roomId"] = roomId;
}
await botInst._send(msgObj);
}
return true;
},
send(msg) {
return botInst.sendTo(roomId, msg);
},
reply(msg) {
return botInst.replyTo(roomId, this.msg.parentId ?? messageId, msg);
},
edit(messageObj, msg) {
return botInst.editMessage(messageObj.roomId, messageObj.id, msg);
},
card(config) {
return botInst.card(config);
},
buildLink(target, label, noBold = false) {
let link = `[${label || target}](${target})`;
if (!noBold) {
link = `**${link}**`;
}
return link;
},
buildDataSnippet(data, type = "json") {
const msg = `
\`\`\`${type}
${type === "json" ? JSON.stringify(data, null, 2) : data}
\`\`\``;
return msg;
},
pickRandom: (listOrMin, max) => {
if (Array.isArray(listOrMin)) {
const items = listOrMin;
return items[Math.floor(Math.random() * items.length)];
}
else {
const min = listOrMin;
return Math.floor(Math.random() * (max - min + 1)) + min;
}
},
async sendFile(data, fileExtension) {
return botInst.sendFileTo(roomId, data, fileExtension);
},
async getFile(url, opts) {
const fileData = await botInst.peekFile(url);
const filePayload = {
...fileData,
async getData() {
const { data } = await botInst.getFile(url);
return data;
},
};
return filePayload;
},
fillTemplate(utterances, template) {
let payload;
if (typeof utterances !== "string") {
payload = this.pickRandom(utterances) || "";
}
else {
payload = utterances;
}
const replacer = (utterance, target, replacement) => {
if (!utterance.includes(`$[${target}]`)) {
return utterance;
}
return replacer(utterance.replace(`$[${target}]`, replacement), target, replacement);
};
for (const key in template) {
const val = template[key];
payload = replacer(payload, key, val);
}
return payload;
},
async clearScreen(repeatCount = 50) {
const newLine = "\n";
const repeatClamp = repeatCount > 7000 ? 5000 : repeatCount;
const clearScreen = `${newLine.repeat(repeatClamp)}`;
const requestOptions = {
method: "POST",
headers: {
Authorization: `Bearer ${botInst.getToken()}`,
"content-type": "application/json;charset=UTF-8",
},
body: JSON.stringify({
roomId,
text: clearScreen,
markdown: clearScreen,
}),
};
const res = await botInst.makeRequest(botInst.API.messages, {}, { rawInit: requestOptions });
return res;
},
};
if (this.topMiddleware) {
const result = await this.topMiddleware($Magic);
if (result === false || !result) {
return false;
}
}
for (const middleware of this.middlewares) {
const result = await middleware($Magic);
if (!result) {
break;
}
}
}
catch (e) {
const defaultMessage = `There was a catastrophic error ${e}`;
let payload = {
message: defaultMessage,
status: "00000",
e,
};
if (typeof e === "object" && e !== null) {
const { message = defaultMessage, status = "00000" } = e;
payload = { message, status, roomId, e };
}
this.onReject(payload);
}
return true;
}
resolveDestination(candidate) {
let target;
if (typeof candidate === "string") {
if (checkers.isEmail(candidate)) {
target = { toPersonEmail: candidate };
}
else {
target = { roomId: candidate };
}
}
else if (typeof candidate === "object" && "personId" in candidate) {
target = { toPersonId: candidate.personId };
}
return target;
}
async sendTo(emailOrRoomId, message) {
const target = this.resolveDestination(emailOrRoomId);
if (typeof message === "string") {
const payload = {
...target,
markdown: message,
};
return this._send(payload);
}
else if (typeof message === "object") {
const payload = {
...target,
markdown: this.fallbackText,
text: this.fallbackText,
attachments: [
{
contentType: "application/vnd.microsoft.card.adaptive",
content: "build" in message && typeof message.build === "function"
? message.build()
: message,
},
],
};
return this._send(payload);
}
throw new Error("Invalid message passed to sendTo");
}
async _send(payload) {
const res = (await this.makeRequest(this.API.messages, payload, {
method: "POST",
token: this.getToken(),
}));
const json = await res.json();
return json;
}
async validateToken(token) {
let valid = false;
try {
const res = await this.getSelf(token);
if (res.id) {
valid = true;
}
}
catch (e) {
valid = false;
}
return valid;
}
card(config) {
const card = new SpeedyCard();
const { title = "", subTitle = "", image = "", data = {}, chips = [], table = [], choices = [], backgroundImage = "", } = config ?? {};
if (backgroundImage) {
card.setBackgroundImage;
}
if (title) {
card.addTitle(title);
}
if (subTitle) {
card.addSubtitle(subTitle);
}
if (image) {
card.setBackgroundImage(image);
}
if (Object.keys(data).length) {
card.attachData(data);
}
if (chips.length) {
card.addChips(chips);
}
if (choices.length) {
card.addPickerDropdown(choices);
}
if (table) {
if (Array.isArray(table) && table.length) {
card.addTable(table);
}
else {
if (Object.entries(table).length) {
card.addTable(table);
}
}
}
return card;
}
stashCard(secret, message, unwrapLabel = CONSTANTS.unwrapLabel, destroyLabel = CONSTANTS.destroyLabel) {
return this.card({ title: message || "Info" })
.addDeleteButton(destroyLabel)
.addSubcard(this.card().addSubtitle(String(secret)), unwrapLabel);
}
errorCard(title, message) {
return this.card().addBlock(this.card().addTitle(title).addSubtitle(message), { backgroundColor: "Attention" });
}
successCard(title, message) {
return this.card().addBlock(this.card().addTitle(title).addSubtitle(message), { backgroundColor: "Good" });
}
async editMessage(roomId, messageId, message) {
const url = `https://webexapis.com/v1/messages/${messageId}`;
const editPayload = {
roomId,
markdown: message,
};
const res = (await this.makeRequest(url, editPayload, {
token: this.getToken(),
method: "PUT",
}));
const json = await res.json();
return json;
}
async replyTo(roomIdParam, param2, param3) {
let roomId;
let messageId;
let msg;
if (typeof roomIdParam === "object") {
roomId = roomIdParam.roomId;
messageId = roomIdParam.id;
msg = param2 || "";
}
else {
roomId = roomIdParam;
messageId = param2 || "";
msg = param3 || "";
}
const payload = {
markdown: msg,
text: msg,
parentId: messageId,
roomId,
};
return this._send(payload);
}
detectType(envelope) {
let type = "text";
if (envelope.resource === "messages") {
if ("files" in envelope.data && envelope.data.files?.length) {
const { files = [] } = envelope.data;
if (files.length) {
type = "file";
}
}
}
else if (envelope.resource === "attachmentActions") {
type = "card";
}
return type;
}
async deleteMessage(msgId) {
const url = `${this.API.messages}/${msgId}`;
const res = await this.makeRequest(url, {}, {
token: this.getToken(),
method: "DELETE",
});
return res;
}
async deleteWebhook(webhookId) {
const url = `${this.API.webhooks}/${webhookId}`;
await this.makeRequest(url, {}, {
token: this.getToken(),
method: "DELETE",
});
return;
}
async getWebhooks() {
const url = `${this.API.webhooks}`;
const res = (await this.makeRequest(url, {}, {
method: "GET",
token: this.getToken(),
}));
const json = await res.json();
return json;
}
async fetchWebhooks() {
const webhooks = (await this.getWebhooks()) || [];
const list = webhooks.items.map((webhook) => {
const { id, name, resource, targetUrl } = webhook;
return {
id,
name,
resource,
targetUrl,
};
});
return list;
}
Setup(url, secret) {
return Promise.all([
this.createFirehose(url, secret),
this.createAttachmentActionsWebhook(url, secret),
]).catch((e) => {
throw e;
});
}
async getRecentRooms(limit = 100) {
const url = `${this.API.rooms}?max=${limit}&sortBy=lastactivity`;
const res = (await this.makeRequest(url, {}, {
method: "GET",
token: this.getToken(),
}));
const json = await res.json();
return json.items.map((item) => {
const { type, title, id } = item;
return {
type,
title,
id,
};
});
}
async createAttachmentActionsWebhook(url, secret) {
const payload = {
resource: "attachmentActions",
event: "created",
targetUrl: url,
name: `${new Date().toISOString()}_attachmentActions`,
};
if (secret) {
payload.secret = secret;
}
return this.createWebhook(payload);
}
async createFirehose(url, secret) {
const payload = {
resource: "messages",
event: "created",
targetUrl: url,
name: `${new Date().toISOString()}_firehose`,
};
if (secret) {
payload.secret = secret;
}
return this.createWebhook(payload);
}
async createWebhook(payload) {
const url = `${this.API.webhooks}`;
const res = (await this.makeRequest(url, payload, {
method: "POST",
token: this.getToken(),
}));
const json = await res.json();
return json;
}
async checkAuthor(authorObj) {
const data = await this.getSelf();
const { id } = data;
return id !== authorObj.id;
}
async getSelf(token) {
const url = this.API?.user?.self;
const res = (await this.makeRequest(url, {}, {
method: "GET",
token: token ?? this.getToken(),
}));
const json = (await res.json());
return json;
}
async whoAmI() {
const selfData = await this.getSelf();
const { items: webhooks } = await this.getWebhooks();
return {
...selfData,
webhooks,
};
}
async buildDetails(type, envelope) {
const [author, data] = await Promise.all([
this.getPerson(envelope.data.personId),
this.getData(type, envelope),
]);
return {
author,
details: data,
};
}
async getPerson(personId) {
const url = `${this.API.user.getPersonDetails}/${personId}`;
const res = (await this.makeRequest(url, {}, {
method: "GET",
token: this.getToken(),
}));
const json = await res.json();
return json;
}
async getRoom(roomId) {
const url = `${this.API.roomDetails}/${roomId}`.replace(" ", "");
const res = (await this.makeRequest(url, {}, {
method: "GET",
token: this.getToken(),
}));
const json = await res.json();
return json;
}
async getData(type, envelope) {
let url = this.API.messages;
if (type === "card") {
url = this.API.attachments;
}
const { data } = envelope;
const { id } = data;
url = `${url}/${id}`;
const res = (await this.makeRequest(url, {}, {
method: "GET",
token: this.getToken(),
}));
const json = await res.json();
if (type === "card") {
return json;
}
if (type === "text") {
return json;
}
if (type === "file") {
return json;
}
return json;
}
generateFileName() {
return `${this.rando()}_${this.rando()}`;
}
rando() {
return Math.random().toString(36).slice(2);
}
handleExtension(input = "") {
const hasDot = input.indexOf(".") > -1;
let fileName = "";
const [prefix, ext] = input.split(".");
if (hasDot) {
if (!prefix || prefix === "*") {
fileName = `${this.generateFileName()}.${ext}`;
}
else {
fileName = input;
}
}
else {
fileName = `${this.generateFileName()}.${prefix}`;
}
return fileName;
}
async sendFileTo(destination, data, extensionOrFileName, textLabel = "", contentType = null) {
const target = this.resolveDestination(destination);
if (!extensionOrFileName) {
throw new Error(`$(bot).sendDataAsFile: Missing filename/extension parameter, ex "myfile.png" or "*.png"`);
}
let finalContentType = contentType;
if (!finalContentType) {
finalContentType = this.guessContentType(extensionOrFileName);
if (!finalContentType) {
throw new Error(`$(bot).sendDataAsFile: Missing 'content-type' parameter, ex "image/png"`);
}
}
const fullFileName = this.handleExtension(extensionOrFileName);
const formData = new FormData();
const isJSON = data && typeof data === "object" && finalContentType.includes("json");
formData.append("files", new Blob([isJSON ? JSON.stringify(data, null, 2) : data], {
type: finalContentType,
}), fullFileName);
const [entry] = Object.entries(target);
const [finalDestination, v] = entry;
formData.append(finalDestination, v);
formData.append("text", textLabel);
const requestOptions = {
method: "POST",
headers: {
Authorization: `Bearer ${this.getToken()}`,
},
body: formData,
};
const res = (await this.makeRequest(this.API.messages, {}, { rawInit: requestOptions }));
const json = await res.json();
return json;
}
async getFile(url, opts = {}) {
const res = (await this.makeRequest(url, {}, {
method: "GET",
token: this.getToken(),
}));
const { contentType: type, name: fileName, extension, bytes, } = this.extractFiledata(res);
const shouldProbablyBeArrayBuffer = (!type.includes("json") && !type.includes("text")) ||
type.includes("image");
let data = res;
if (opts.responseType === "arraybuffer" || shouldProbablyBeArrayBuffer) {
try {
data = await res.arrayBuffer();
}
catch (e) {
data = {};
}
}
else {
try {
if (type.includes("json")) {
data = (await res.json());
}
else {
data = await res.text();
}
}
catch (e) {
data = {};
}
}
const payload = {
url: url,
name: fileName,
extension,
contentType: type,
data,
bytes,
};
return payload;
}
extractFiledata(res) {
const type = res.headers.get("content-type");
const contentDispo = res.headers.get("content-disposition");
const contentLength = Number(res.headers.get("content-length"));
const fileName = contentDispo
?.split(";")[1]
.split("=")[1]
.replace(/\"/g, "");
const extension = fileName.split(".").pop() || "";
return {
bytes: contentLength,
contentType: type,
extension,
name: fileName,
};
}
async peekFile(url) {
const res = await this.makeRequest(url, {}, {
method: "HEAD",
token: this.getToken(),
});
return { url, ...this.extractFiledata(res) };
}
guessContentType(extensionOrFileName) {
const hasDot = extensionOrFileName.indexOf(".") > -1;
let extension = "";
const pieces = extensionOrFileName.split(".");
const hasMultipleDots = pieces.length > 2;
const [prefix, ext] = pieces;
if (hasDot) {
if (!prefix || prefix === "*") {
extension = ext;
}
if (hasMultipleDots) {
extension = pieces.pop();
}
}
else {
extension = prefix;
}
const mapping = {
doc: "application/msword",
docx: "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
xls: "application/vnd.ms-excel",
xlsx: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
ppt: "application/vnd.ms-powerpoint",
pptx: "application/vnd.openxmlformats-officedocument.presentationml.presentation",
pdf: "application/pdf",
jpg: "image/jpeg",
jpeg: "image/jpeg",
bmp: "image/bmp",
gif: "image/gif",
png: "image/png",
txt: "text/plain",
csv: "text/csv",
html: "text/html",
json: "application/json",
"*": "application/octet-stream",
mp3: "audio/mpeg",
mp4: "video/mp4",
mpeg: "video/mpeg",
mpkg: "application/vnd.apple.installer+xml",
vf: "application/json",
};
const res = mapping[extension] || null;
return res;
}
fuzzyMatch(candidate, options) {
const lowerCaseCandidate = candidate.toLowerCase();
return options.some((option) => option.toLowerCase().includes(lowerCaseCandidate));
}
convertToHash(arr) {
return arr.reduce((hash, item) => {
const [key, value] = item.split("=");
hash[key] = value;
return hash;
}, {});
}
appCard(appName, logoUrl, config = {}) {
return this.card().addHeader(appName, { iconURL: logoUrl, ...config });
}
async fetchData(url, retries = 3, onChunk) {
const delay = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
try {
const response = await fetch(url, {
method: "GET",
headers: {
Authorization: `Bearer ${this._token}`,
"Content-Type": "application/json",
},
});
if (!response.ok) {
if (response.status === 429 && retries > 0) {
const retryAfter = parseInt(response.headers.get("Retry-After") || "1", 10) * 1000;
await delay(retryAfter);
return await this.fetchData(url, retries - 1);
}
else {
const text = await response.text();
throw new Error(`Error fetching messages: ${response.statusText}, ${text}`);
}
}
const { items } = await response.json();
const data = items;
const linkHeader = response.headers.get("Link");
const hasNext = linkHeader ? linkHeader.includes(`rel="next"`) : false;
const nextURL = hasNext
? linkHeader.split(";")[0].replace("<", "").replace(">", "")
: null;
if (onChunk) {
await onChunk(data);
}
if (hasNext && nextURL) {
const nextPageData = await this.fetchData(nextURL, retries);
return data.concat(nextPageData);
}
return data;
}
catch (error) {
throw new Error(`Error fetching data: ${error}`);
}
}
buildQueryURL(target, options) {
const queryParams = [];
if (options) {
Object.entries(options).forEach(([key, value]) => {
if (value !== undefined && value !== null) {
queryParams.push(`${encodeURIComponent(key)}=${encodeURIComponent(value)}`);
}
});
}
const queryString = queryParams.length > 0 ? `?${queryParams.join("&")}` : "";
return `${target}${queryString}`;
}
async getAllRooms(options) {
const url = this.buildQueryURL(this.API.rooms, options);
const collection = await this.fetchData(url);
return collection;
}
}
//# sourceMappingURL=speedybot.js.map