collabcentral/html/sendMessage.js
Joseph B. McQueen e604c7e9c9 Initial commit: multi-bot CollabCentral
Extends the single-Novi codebase into a multi-bot mass-messenger where
each bot has its own token, avatar, label, and per-user authorization.

- Secrets moved out of config.json: per-bot tokens in gitignored
  config/botTokens.json (with enabled flag), service-account OAuth in
  gitignored config/token.json (rewritten by refresh cron), integration
  and Google keys in .env.
- Single Webex integration handles OAuth for all bots via a per-app
  redirect URI derived from OAUTH_CALLBACK_URL_TEMPLATE.
- New requireBot middleware and getBotConfig helper reject requests for
  unknown or disabled bots at the /CollabCentral/:app boundary.
- New /info endpoint plus dynamic frontend loading (sendMessage,
  monitorJobs) so pages self-describe per bot, including bot avatar
  fetched from Webex /people/me at startup.
- Job draft state keyed by cookieId + appName so each bot has its own
  building queue; job list/detail endpoints filter by appName so users
  only see jobs from bots they are authorized on.
- New jobDetail page for a readable per-job view; completed jobs are
  retained for 30 days by the cleanup cron.
- Completion adaptive cards use per-bot avatar and label.
- Miscellaneous fixes: off-by-two in the send loop, removed three dead
  send/process variants, added defensive init for jobs.* on load,
  dropped the deprecated crypto npm shim, and cleaned up stray logger
  labels and typos.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-01 17:53:07 -04:00

222 lines
No EOL
7.3 KiB
JavaScript

const listbox = document.querySelector('#list');
const modal = document.querySelector(".modal");
const overlay = document.querySelector(".overlay");
const openModalBtn = document.querySelector(".btn-open");
const closeModalBtn = document.querySelector(".btn-close");
var pathArray = window.location.pathname.split('/'); //Gets the path of the URL called
var appName = pathArray[2]; //Gets the appName from the path.
document.getElementById('sendMessage').onsubmit = function (event) {
event.preventDefault() // prevent form from posting without JS
var xhttp = new XMLHttpRequest(); // create new AJAX request
xhttp.onreadystatechange = function () {
if (this.readyState == this.DONE && this.status == 200) { // sucess from server
result = JSON.parse(xhttp.response);
console.log(xhttp.responseText)
console.log(xhttp.response);
var message = "I sent you a test message in your Webex Client for review.<br><br>";
message += "Recipients: " + result.memberList.length + "<br><br>";
if (document.getElementById("scheduledFor").value) {
message += "Scheduled for " + document.getElementById("scheduledFor").value;
} else {
message += "Message will be sent immediately.";
}
document.getElementById("modalMessage").innerHTML = message;
openModal();
} else { // errors occured
}
}
var formData = new FormData()
formData.append('uploadImage', document.getElementById('uploadImage').files[0]) // since inputs allow multi files submission, therefore files are in array
formData.append('message', message.value())
formData.append('uploadCSV', document.getElementById('uploadCSV').files[0]) // since inputs allow multi files submission, therefore files are in array
formData.append('appName', appName);
var selectedGroups = [];
for (var group of document.querySelector('#favGroups').getSelectedOptions()) {
selectedGroups.push(group.value);
}
for (var group of document.querySelector('#newGroups').getSelectedOptions()) {
selectedGroups.push(group.value);
}
formData.append('groups', selectedGroups);
formData.append('scheduledFor', document.getElementById("scheduledFor").value);
xhttp.open("POST", "/CollabCentral/" + appName + "/jobs/edit")
console.log(xhttp);
xhttp.send(formData)
}
function showImagePreview(event) {
if (event.target.files.length > 0) {
var src = URL.createObjectURL(event.target.files[0]);
var preview = document.getElementById("file-ip-1-preview");
preview.src = src;
preview.style.display = "block";
preview.style.maxHeight = "350px";
preview.style.maxWidth = "500px";
}
}
let id = getCookie("id");
// Fetch bot metadata + authorization state, then either render the form or
// show the "not authorized" panel. All bot-specific labels and icons come from
// /info so adding a new bot doesn't require any front-end changes.
fetch('/CollabCentral/' + appName + '/info')
.then(res => res.json())
.then(info => {
document.title = info.label;
document.getElementById("appLabel").innerHTML = info.label;
document.getElementById("name").innerHTML = getCookie("displayName") || "";
document.getElementById("appName").href = info.faviconUrl;
document.getElementById("jobLink").href = "/CollabCentral/" + appName + "/monitorJobs.html";
var appIcon = document.getElementById("appIcon");
appIcon.src = info.iconUrl;
appIcon.style.maxHeight = "150px";
appIcon.style.maxWidth = "150px";
if (!info.authorized) {
document.getElementById("unauthorizedPanel").classList.remove("hidden");
return;
}
document.getElementById("formPanel").classList.remove("hidden");
loadFavoriteGroups();
loadNewGroups();
})
.catch(err => {
console.error("Failed to load /info:", err);
});
function loadFavoriteGroups() {
var x = document.getElementById("favGroups");
fetch('/CollabCentral/' + appName + '/user/groups/list')
.then(res => res.json())
.then((res) => {
for (var group of res) {
var option = document.createElement("option");
option.text = group.name;
option.value = group.id;
x.add(option);
}
VirtualSelect.init({ ele: '#favGroups' });
});
}
function loadNewGroups() {
var y = document.getElementById("newGroups");
fetch('/CollabCentral/' + appName + '/user/groups/find')
.then(res => res.json())
.then((res) => {
for (var group of res) {
var option = document.createElement("option");
option.text = group.displayName;
option.value = group.id;
y.add(option);
}
VirtualSelect.init({ ele: '#newGroups' });
});
}
function getCookie(cname) {
let name = cname + "=";
let decodedCookie = decodeURIComponent(document.cookie);
let ca = decodedCookie.split(';');
for (let i = 0; i < ca.length; i++) {
let c = ca[i];
while (c.charAt(0) == ' ') {
c = c.substring(1);
}
if (c.indexOf(name) == 0) {
return c.substring(name.length, c.length);
}
}
return "";
}
const message = new EasyMDE({
element: document.getElementById('my-text-area'),
toolbar: ["bold", "italic", "|", "heading-1", "heading-2", "heading-3", "|", "code", "unordered-list", "ordered-list", "link", "quote", "|", "preview"],
maxHeight: "200px"
});
function submit() {
if (document.getElementById("scheduledFor").value) {
fetch("/CollabCentral/" + appName + "/jobs/schedule", {
method: "POST",
redirect: "follow"
})
.then(response => {
clearForm();
// HTTP 301 response
if (response.redirected) {
window.location.href = response.url;
}
})
.catch(function(err) {
console.info(err + " url: " + url);
});
} else {
console.log("RunNow selected.");
fetch("/CollabCentral/" + appName + "/jobs/runNow", {
method: "POST",
redirect: "follow"
})
.then(response => {
clearForm();
// HTTP 301 response
if (response.redirected) {
window.location.href = response.url;
}
})
.catch(function(err) {
console.info(err);
});
}
closeModal();
}
function continueEditing() {
closeModal();
}
const openModal = function () {
modal.classList.remove("hidden");
overlay.classList.remove("hidden");
};
const closeModal = function () {
modal.classList.add("hidden");
overlay.classList.add("hidden");
};
overlay.addEventListener("click", closeModal);
function clearForm() {
document.getElementById('uploadImage').innerHTML = "";
message.value("");
document.getElementById('uploadCSV').innerHTML = "";
document.getElementById('favGroups').selected = null;
document.getElementById('newGroups').selected = null;
document.getElementById('scheduledFor').innerHTML = "";
}