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>
This commit is contained in:
commit
e604c7e9c9
27 changed files with 4454 additions and 0 deletions
14
.dockerignore
Normal file
14
.dockerignore
Normal file
|
|
@ -0,0 +1,14 @@
|
||||||
|
node_modules
|
||||||
|
logs
|
||||||
|
uploads
|
||||||
|
|
||||||
|
# Config is mounted at runtime (-v ./config:/usr/src/app/config). Never bake it
|
||||||
|
# into the image, especially secrets like .env / botTokens.json / token.json.
|
||||||
|
.env
|
||||||
|
config
|
||||||
|
|
||||||
|
# Misc
|
||||||
|
testData.json
|
||||||
|
.DS_Store
|
||||||
|
*.log
|
||||||
|
.git
|
||||||
26
.env.example
Normal file
26
.env.example
Normal file
|
|
@ -0,0 +1,26 @@
|
||||||
|
# --- Server ----------------------------------------------------------------
|
||||||
|
SERVER_PORT=1450
|
||||||
|
|
||||||
|
# --- OAuth callback URL template ------------------------------------------
|
||||||
|
# The `:app` placeholder is replaced per-request with the bot's appName.
|
||||||
|
# IMPORTANT: every bot's resolved URL must be registered as an allowed
|
||||||
|
# redirect URI in the Webex integration portal
|
||||||
|
# (developer.webex.com -> My Apps -> <integration> -> Redirect URIs).
|
||||||
|
#
|
||||||
|
# Example: with the template below and bots `novi` and `techupdates`, register:
|
||||||
|
# https://bot.example.com/CollabCentral/novi/oauth
|
||||||
|
# https://bot.example.com/CollabCentral/techupdates/oauth
|
||||||
|
#
|
||||||
|
# When you add a new bot, register its redirect URI before users try to log in.
|
||||||
|
OAUTH_CALLBACK_URL_TEMPLATE=https://bot.example.com/CollabCentral/:app/oauth
|
||||||
|
|
||||||
|
# --- Webex integration (single integration used by all bots' OAuth) -------
|
||||||
|
WEBEX_INTEGRATION_CLIENT_ID=your-integration-client-id
|
||||||
|
WEBEX_INTEGRATION_CLIENT_SECRET=your-integration-client-secret
|
||||||
|
|
||||||
|
# --- Webex service account (used for group/people lookups) ----------------
|
||||||
|
WEBEX_SERVICE_ACCOUNT_CLIENT_ID=your-service-account-client-id
|
||||||
|
WEBEX_SERVICE_ACCOUNT_CLIENT_SECRET=your-service-account-client-secret
|
||||||
|
|
||||||
|
# --- Google Translate -----------------------------------------------------
|
||||||
|
GOOGLE_TRANSLATE_API_KEY=your-google-translate-api-key
|
||||||
19
.gitignore
vendored
Normal file
19
.gitignore
vendored
Normal file
|
|
@ -0,0 +1,19 @@
|
||||||
|
node_modules/
|
||||||
|
|
||||||
|
# Secrets
|
||||||
|
.env
|
||||||
|
config/botTokens.json
|
||||||
|
config/token.json
|
||||||
|
|
||||||
|
# Runtime data
|
||||||
|
config/jobs.json
|
||||||
|
config/jobs copy.json
|
||||||
|
config/userPrefs.json
|
||||||
|
uploads/*
|
||||||
|
!uploads/.gitkeep
|
||||||
|
|
||||||
|
# Misc
|
||||||
|
testData.json
|
||||||
|
.DS_Store
|
||||||
|
*.log
|
||||||
|
logs/
|
||||||
19
Dockerfile
Normal file
19
Dockerfile
Normal file
|
|
@ -0,0 +1,19 @@
|
||||||
|
FROM node:20
|
||||||
|
# Create app directory
|
||||||
|
WORKDIR /usr/src/app
|
||||||
|
|
||||||
|
# Install app dependencies
|
||||||
|
# A wildcard is used to ensure both package.json AND package-lock.json are copied
|
||||||
|
# where available (npm@5+)
|
||||||
|
COPY package*.json ./
|
||||||
|
|
||||||
|
RUN npm install
|
||||||
|
# If you are building your code for production
|
||||||
|
# RUN npm ci --only=production
|
||||||
|
|
||||||
|
# Bundle app source
|
||||||
|
COPY . .
|
||||||
|
|
||||||
|
EXPOSE 1450
|
||||||
|
|
||||||
|
CMD [ "node", "index.js" ]
|
||||||
10
config/botTokens.example.json
Normal file
10
config/botTokens.example.json
Normal file
|
|
@ -0,0 +1,10 @@
|
||||||
|
{
|
||||||
|
"novi": {
|
||||||
|
"token": "REPLACE-WITH-NOVI-BOT-TOKEN",
|
||||||
|
"enabled": true
|
||||||
|
},
|
||||||
|
"techupdates": {
|
||||||
|
"token": "REPLACE-WITH-TECHUPDATES-BOT-TOKEN",
|
||||||
|
"enabled": false
|
||||||
|
}
|
||||||
|
}
|
||||||
265
config/config.json
Normal file
265
config/config.json
Normal file
|
|
@ -0,0 +1,265 @@
|
||||||
|
{
|
||||||
|
"server": {
|
||||||
|
"name": "CollabCentral",
|
||||||
|
"port": "1450"
|
||||||
|
},
|
||||||
|
"languages": [],
|
||||||
|
"webex": {
|
||||||
|
"bot": {
|
||||||
|
"novi": {
|
||||||
|
"label": "Novi Communicator",
|
||||||
|
"authorized": {
|
||||||
|
"Y2lzY29zcGFyazovL3VzL1BFT1BMRS80NzAyNDlhNC1kNjFjLTQzNmMtYTE1My1kOGUzZTExMmI4MDU": {
|
||||||
|
"id": "Y2lzY29zcGFyazovL3VzL1BFT1BMRS80NzAyNDlhNC1kNjFjLTQzNmMtYTE1My1kOGUzZTExMmI4MDU",
|
||||||
|
"displayName": "Joe McQueen",
|
||||||
|
"email": "mcqueenj@ae.com",
|
||||||
|
"avatar": "https://avatar-prod-us-east-2.webexcontent.com/Avtr~V1~db9cdc31-8f8e-40b4-916c-8d9e9c92e70d/V1~667e3673b56f4ddc39139c73da9140dd631eb821a756524ead12e37e3a61afc7~9c5b6e767cff46e3af35a003cea38f25~1600",
|
||||||
|
"groups": [
|
||||||
|
{
|
||||||
|
"name": "Group: Audio Visual",
|
||||||
|
"alias": "AV Team",
|
||||||
|
"id": "Y2lzY29zcGFyazovL3VzL1NDSU1fR1JPVVAvZTQ3NmY3MjktZWViYi00MDI3LWFlMTctNzc1YWI1ZDgzYmNhOmRiOWNkYzMxLThmOGUtNDBiNC05MTZjLThkOWU5YzkyZTcwZA"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Group: South Side Campus",
|
||||||
|
"alias": "SSW Campus",
|
||||||
|
"id": "Y2lzY29zcGFyazovL3VzL1NDSU1fR1JPVVAvYjAxOWM5MzgtNTZhNi00NTQxLTk0NjgtYjFmOWEwM2I5NTNlOmRiOWNkYzMxLThmOGUtNDBiNC05MTZjLThkOWU5YzkyZTcwZA"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Group: IT Warrendale",
|
||||||
|
"alias": "Warrendale",
|
||||||
|
"id": "Y2lzY29zcGFyazovL3VzL1NDSU1fR1JPVVAvMjkwN2RlZGUtZjJmNi00OTdjLTkwMjItZTRmOGIzNzM3MTFhOmRiOWNkYzMxLThmOGUtNDBiNC05MTZjLThkOWU5YzkyZTcwZA"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Group: New York Design Office Personnel",
|
||||||
|
"alias": "NY Design Center",
|
||||||
|
"id": "Y2lzY29zcGFyazovL3VzL1NDSU1fR1JPVVAvNjUzOGQzYmYtY2UzOS00Y2ZlLWFmZWUtNGViYTM0OGQ3Y2FjOmRiOWNkYzMxLThmOGUtNDBiNC05MTZjLThkOWU5YzkyZTcwZA"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Group: 34th Street Office Personnel",
|
||||||
|
"alias": "34th Street",
|
||||||
|
"id": "Y2lzY29zcGFyazovL3VzL1NDSU1fR1JPVVAvZGVmOTA0MDMtMTBlZS00YzRlLWIzYWItYzNjZDlkODhmMTQ1OmRiOWNkYzMxLThmOGUtNDBiNC05MTZjLThkOWU5YzkyZTcwZA"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Group: San Francisco Personnel",
|
||||||
|
"alias": "San Francisco Office",
|
||||||
|
"id": "Y2lzY29zcGFyazovL3VzL1NDSU1fR1JPVVAvNjJjYTcxYTktYWYzYy00YjQ4LWIxZjUtMjc1NDEwNGYwMWJlOmRiOWNkYzMxLThmOGUtNDBiNC05MTZjLThkOWU5YzkyZTcwZA"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Group: Ottawa Campus",
|
||||||
|
"alias": "Ottawa Campus",
|
||||||
|
"id": "Y2lzY29zcGFyazovL3VzL1NDSU1fR1JPVVAvZWIyYWI1ZDgtYzk2Ny00MmUwLTg0OWQtZGE5OTYzMDdkNDhhOmRiOWNkYzMxLThmOGUtNDBiNC05MTZjLThkOWU5YzkyZTcwZA"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Group: Hazleton Campus",
|
||||||
|
"alias": "Hazleton Campus",
|
||||||
|
"id": "Y2lzY29zcGFyazovL3VzL1NDSU1fR1JPVVAvMDc3NTI1NGEtZTIxMS00ZTQ1LTgxNjktNjYxYTAzOGYxNTE3OmRiOWNkYzMxLThmOGUtNDBiNC05MTZjLThkOWU5YzkyZTcwZA"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Group: Canada Distribution Center",
|
||||||
|
"alias": "Canada DC",
|
||||||
|
"id": "Y2lzY29zcGFyazovL3VzL1NDSU1fR1JPVVAvZmUwMDk1MjUtMzEyZC00YzMxLTgwMzItNWEyODIwODRiYTVjOmRiOWNkYzMxLThmOGUtNDBiNC05MTZjLThkOWU5YzkyZTcwZA"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Group: Mexico Office Personnel",
|
||||||
|
"alias": "Mexico Office",
|
||||||
|
"id": "Y2lzY29zcGFyazovL3VzL1NDSU1fR1JPVVAvNjhmYTJiZGUtNjI2My00NWEwLTljMDMtNjMyMGI0NmVlODE0OmRiOWNkYzMxLThmOGUtNDBiNC05MTZjLThkOWU5YzkyZTcwZA"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Group: Hong Kong Office",
|
||||||
|
"alias": "Hong Kong Office",
|
||||||
|
"id": "Y2lzY29zcGFyazovL3VzL1NDSU1fR1JPVVAvMjdlM2QzZGEtZjI4Ny00NDg3LWI3NzUtM2JmZTA3MzNhNzZhOmRiOWNkYzMxLThmOGUtNDBiNC05MTZjLThkOWU5YzkyZTcwZA"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Group: Hazleton Mgmt",
|
||||||
|
"alias": "Hazleton Mgmt",
|
||||||
|
"id": "Y2lzY29zcGFyazovL3VzL1NDSU1fR1JPVVAvYTEzODI3OTgtOWY5Zi00YmY1LTlkMmQtODNhZTk2OGY0MjU3OmRiOWNkYzMxLThmOGUtNDBiNC05MTZjLThkOWU5YzkyZTcwZA"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Group: Ottawa Campus Mgmt",
|
||||||
|
"alias": "Ottawa Campus Mgmt",
|
||||||
|
"id": "Y2lzY29zcGFyazovL3VzL1NDSU1fR1JPVVAvZDYwNzFhYmMtYzNjZC00ZDdkLWFiYTktOGY0NDRiZTViODQ3OmRiOWNkYzMxLThmOGUtNDBiNC05MTZjLThkOWU5YzkyZTcwZA"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Group: MDC DC Mgmt",
|
||||||
|
"alias": "Canada DC Mgmt",
|
||||||
|
"id": "Y2lzY29zcGFyazovL3VzL1NDSU1fR1JPVVAvOTk2ZWRmNzYtNjdlYS00MjdkLWEzNGEtMTBjNjJhYmRkMDM5OmRiOWNkYzMxLThmOGUtNDBiNC05MTZjLThkOWU5YzkyZTcwZA"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"Y2lzY29zcGFyazovL3VzL1BFT1BMRS84MWUzMzExMi1jYWZlLTQ0NWYtOGE5OS01MDI3Mzg4ZDQwNGM": {
|
||||||
|
"id": "Y2lzY29zcGFyazovL3VzL1BFT1BMRS84MWUzMzExMi1jYWZlLTQ0NWYtOGE5OS01MDI3Mzg4ZDQwNGM",
|
||||||
|
"displayName": "Megan Reinecker",
|
||||||
|
"email": "reineckerm@ae.com",
|
||||||
|
"avatar": "https://avatar-prod-us-east-2.webexcontent.com/Avtr~V1~db9cdc31-8f8e-40b4-916c-8d9e9c92e70d/V1~81e33112-cafe-445f-8a99-5027388d404c~c5234572b50e49ff884c45b3ea0e3fea~1600",
|
||||||
|
"groups": [
|
||||||
|
{
|
||||||
|
"name": "Group: South Side Campus",
|
||||||
|
"alias": "SSW Campus",
|
||||||
|
"id": "Y2lzY29zcGFyazovL3VzL1NDSU1fR1JPVVAvYjAxOWM5MzgtNTZhNi00NTQxLTk0NjgtYjFmOWEwM2I5NTNlOmRiOWNkYzMxLThmOGUtNDBiNC05MTZjLThkOWU5YzkyZTcwZA"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Group: IT Warrendale",
|
||||||
|
"alias": "Warrendale",
|
||||||
|
"id": "Y2lzY29zcGFyazovL3VzL1NDSU1fR1JPVVAvMjkwN2RlZGUtZjJmNi00OTdjLTkwMjItZTRmOGIzNzM3MTFhOmRiOWNkYzMxLThmOGUtNDBiNC05MTZjLThkOWU5YzkyZTcwZA"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Group: New York Design Office Personnel",
|
||||||
|
"alias": "NY Design Center",
|
||||||
|
"id": "Y2lzY29zcGFyazovL3VzL1NDSU1fR1JPVVAvNjUzOGQzYmYtY2UzOS00Y2ZlLWFmZWUtNGViYTM0OGQ3Y2FjOmRiOWNkYzMxLThmOGUtNDBiNC05MTZjLThkOWU5YzkyZTcwZA"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Group: 34th Street Office Personnel",
|
||||||
|
"alias": "34th Street",
|
||||||
|
"id": "Y2lzY29zcGFyazovL3VzL1NDSU1fR1JPVVAvZGVmOTA0MDMtMTBlZS00YzRlLWIzYWItYzNjZDlkODhmMTQ1OmRiOWNkYzMxLThmOGUtNDBiNC05MTZjLThkOWU5YzkyZTcwZA"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Group: San Francisco Personnel",
|
||||||
|
"alias": "San Francisco Office",
|
||||||
|
"id": "Y2lzY29zcGFyazovL3VzL1NDSU1fR1JPVVAvNjJjYTcxYTktYWYzYy00YjQ4LWIxZjUtMjc1NDEwNGYwMWJlOmRiOWNkYzMxLThmOGUtNDBiNC05MTZjLThkOWU5YzkyZTcwZA"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Group: Ottawa Campus",
|
||||||
|
"alias": "Ottawa Campus",
|
||||||
|
"id": "Y2lzY29zcGFyazovL3VzL1NDSU1fR1JPVVAvZWIyYWI1ZDgtYzk2Ny00MmUwLTg0OWQtZGE5OTYzMDdkNDhhOmRiOWNkYzMxLThmOGUtNDBiNC05MTZjLThkOWU5YzkyZTcwZA"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Group: Hazleton Campus",
|
||||||
|
"alias": "Hazleton Campus",
|
||||||
|
"id": "Y2lzY29zcGFyazovL3VzL1NDSU1fR1JPVVAvMDc3NTI1NGEtZTIxMS00ZTQ1LTgxNjktNjYxYTAzOGYxNTE3OmRiOWNkYzMxLThmOGUtNDBiNC05MTZjLThkOWU5YzkyZTcwZA"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Group: Canada Distribution Center",
|
||||||
|
"alias": "Canada DC",
|
||||||
|
"id": "Y2lzY29zcGFyazovL3VzL1NDSU1fR1JPVVAvZmUwMDk1MjUtMzEyZC00YzMxLTgwMzItNWEyODIwODRiYTVjOmRiOWNkYzMxLThmOGUtNDBiNC05MTZjLThkOWU5YzkyZTcwZA"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Group: Mexico Office Personnel",
|
||||||
|
"alias": "Mexico Office",
|
||||||
|
"id": "Y2lzY29zcGFyazovL3VzL1NDSU1fR1JPVVAvNjhmYTJiZGUtNjI2My00NWEwLTljMDMtNjMyMGI0NmVlODE0OmRiOWNkYzMxLThmOGUtNDBiNC05MTZjLThkOWU5YzkyZTcwZA"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Group: Hong Kong Office",
|
||||||
|
"alias": "Hong Kong Office",
|
||||||
|
"id": "Y2lzY29zcGFyazovL3VzL1NDSU1fR1JPVVAvMjdlM2QzZGEtZjI4Ny00NDg3LWI3NzUtM2JmZTA3MzNhNzZhOmRiOWNkYzMxLThmOGUtNDBiNC05MTZjLThkOWU5YzkyZTcwZA"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Group: Hazleton Mgmt",
|
||||||
|
"alias": "Hazleton Mgmt",
|
||||||
|
"id": "Y2lzY29zcGFyazovL3VzL1NDSU1fR1JPVVAvYTEzODI3OTgtOWY5Zi00YmY1LTlkMmQtODNhZTk2OGY0MjU3OmRiOWNkYzMxLThmOGUtNDBiNC05MTZjLThkOWU5YzkyZTcwZA"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Group: Ottawa Campus Mgmt",
|
||||||
|
"alias": "Ottawa Campus Mgmt",
|
||||||
|
"id": "Y2lzY29zcGFyazovL3VzL1NDSU1fR1JPVVAvZDYwNzFhYmMtYzNjZC00ZDdkLWFiYTktOGY0NDRiZTViODQ3OmRiOWNkYzMxLThmOGUtNDBiNC05MTZjLThkOWU5YzkyZTcwZA"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Group: MDC DC Mgmt",
|
||||||
|
"alias": "Canada DC Mgmt",
|
||||||
|
"id": "Y2lzY29zcGFyazovL3VzL1NDSU1fR1JPVVAvOTk2ZWRmNzYtNjdlYS00MjdkLWEzNGEtMTBjNjJhYmRkMDM5OmRiOWNkYzMxLThmOGUtNDBiNC05MTZjLThkOWU5YzkyZTcwZA"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"Y2lzY29zcGFyazovL3VzL1BFT1BMRS80NmZkYWEzYy03YTNkLTRjY2QtYWU4Ny0xYzIzNmIyNGE0Zjg": {
|
||||||
|
"id": "Y2lzY29zcGFyazovL3VzL1BFT1BMRS80NmZkYWEzYy03YTNkLTRjY2QtYWU4Ny0xYzIzNmIyNGE0Zjg",
|
||||||
|
"displayName": "Jayna Catalina",
|
||||||
|
"email": "catalinaj@ae.com",
|
||||||
|
"avatar": "https://avatar-prod-us-east-2.webexcontent.com/Avtr~V1~db9cdc31-8f8e-40b4-916c-8d9e9c92e70d/V1~cf803b9485e536eece6e2f2e541f8ab8691867a0765656a0ebb28ba4b74d7bca~8afff234e4f045ad9fa92af6f8d082c0~1600",
|
||||||
|
"groups": [
|
||||||
|
{
|
||||||
|
"name": "Group: South Side Campus",
|
||||||
|
"alias": "SSW Campus",
|
||||||
|
"id": "Y2lzY29zcGFyazovL3VzL1NDSU1fR1JPVVAvYjAxOWM5MzgtNTZhNi00NTQxLTk0NjgtYjFmOWEwM2I5NTNlOmRiOWNkYzMxLThmOGUtNDBiNC05MTZjLThkOWU5YzkyZTcwZA"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Group: IT Warrendale",
|
||||||
|
"alias": "Warrendale",
|
||||||
|
"id": "Y2lzY29zcGFyazovL3VzL1NDSU1fR1JPVVAvMjkwN2RlZGUtZjJmNi00OTdjLTkwMjItZTRmOGIzNzM3MTFhOmRiOWNkYzMxLThmOGUtNDBiNC05MTZjLThkOWU5YzkyZTcwZA"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Group: New York Design Office Personnel",
|
||||||
|
"alias": "NY Design Center",
|
||||||
|
"id": "Y2lzY29zcGFyazovL3VzL1NDSU1fR1JPVVAvNjUzOGQzYmYtY2UzOS00Y2ZlLWFmZWUtNGViYTM0OGQ3Y2FjOmRiOWNkYzMxLThmOGUtNDBiNC05MTZjLThkOWU5YzkyZTcwZA"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Group: 34th Street Office Personnel",
|
||||||
|
"alias": "34th Street",
|
||||||
|
"id": "Y2lzY29zcGFyazovL3VzL1NDSU1fR1JPVVAvZGVmOTA0MDMtMTBlZS00YzRlLWIzYWItYzNjZDlkODhmMTQ1OmRiOWNkYzMxLThmOGUtNDBiNC05MTZjLThkOWU5YzkyZTcwZA"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Group: San Francisco Personnel",
|
||||||
|
"alias": "San Francisco Office",
|
||||||
|
"id": "Y2lzY29zcGFyazovL3VzL1NDSU1fR1JPVVAvNjJjYTcxYTktYWYzYy00YjQ4LWIxZjUtMjc1NDEwNGYwMWJlOmRiOWNkYzMxLThmOGUtNDBiNC05MTZjLThkOWU5YzkyZTcwZA"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Group: Ottawa Campus",
|
||||||
|
"alias": "Ottawa Campus",
|
||||||
|
"id": "Y2lzY29zcGFyazovL3VzL1NDSU1fR1JPVVAvZWIyYWI1ZDgtYzk2Ny00MmUwLTg0OWQtZGE5OTYzMDdkNDhhOmRiOWNkYzMxLThmOGUtNDBiNC05MTZjLThkOWU5YzkyZTcwZA"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Group: Hazleton Campus",
|
||||||
|
"alias": "Hazleton Campus",
|
||||||
|
"id": "Y2lzY29zcGFyazovL3VzL1NDSU1fR1JPVVAvMDc3NTI1NGEtZTIxMS00ZTQ1LTgxNjktNjYxYTAzOGYxNTE3OmRiOWNkYzMxLThmOGUtNDBiNC05MTZjLThkOWU5YzkyZTcwZA"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Group: Canada Distribution Center",
|
||||||
|
"alias": "Canada DC",
|
||||||
|
"id": "Y2lzY29zcGFyazovL3VzL1NDSU1fR1JPVVAvZmUwMDk1MjUtMzEyZC00YzMxLTgwMzItNWEyODIwODRiYTVjOmRiOWNkYzMxLThmOGUtNDBiNC05MTZjLThkOWU5YzkyZTcwZA"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Group: Mexico Office Personnel",
|
||||||
|
"alias": "Mexico Office",
|
||||||
|
"id": "Y2lzY29zcGFyazovL3VzL1NDSU1fR1JPVVAvNjhmYTJiZGUtNjI2My00NWEwLTljMDMtNjMyMGI0NmVlODE0OmRiOWNkYzMxLThmOGUtNDBiNC05MTZjLThkOWU5YzkyZTcwZA"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Group: Hong Kong Office",
|
||||||
|
"alias": "Hong Kong Office",
|
||||||
|
"id": "Y2lzY29zcGFyazovL3VzL1NDSU1fR1JPVVAvMjdlM2QzZGEtZjI4Ny00NDg3LWI3NzUtM2JmZTA3MzNhNzZhOmRiOWNkYzMxLThmOGUtNDBiNC05MTZjLThkOWU5YzkyZTcwZA"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Group: Hazleton Mgmt",
|
||||||
|
"alias": "Hazleton Mgmt",
|
||||||
|
"id": "Y2lzY29zcGFyazovL3VzL1NDSU1fR1JPVVAvYTEzODI3OTgtOWY5Zi00YmY1LTlkMmQtODNhZTk2OGY0MjU3OmRiOWNkYzMxLThmOGUtNDBiNC05MTZjLThkOWU5YzkyZTcwZA"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Group: Ottawa Campus Mgmt",
|
||||||
|
"alias": "Ottawa Campus Mgmt",
|
||||||
|
"id": "Y2lzY29zcGFyazovL3VzL1NDSU1fR1JPVVAvZDYwNzFhYmMtYzNjZC00ZDdkLWFiYTktOGY0NDRiZTViODQ3OmRiOWNkYzMxLThmOGUtNDBiNC05MTZjLThkOWU5YzkyZTcwZA"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Group: MDC DC Mgmt",
|
||||||
|
"alias": "Canada DC Mgmt",
|
||||||
|
"id": "Y2lzY29zcGFyazovL3VzL1NDSU1fR1JPVVAvOTk2ZWRmNzYtNjdlYS00MjdkLWEzNGEtMTBjNjJhYmRkMDM5OmRiOWNkYzMxLThmOGUtNDBiNC05MTZjLThkOWU5YzkyZTcwZA"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"techupdates": {
|
||||||
|
"label": "Technology Updates",
|
||||||
|
"authorized": {
|
||||||
|
"Y2lzY29zcGFyazovL3VzL1BFT1BMRS80NzAyNDlhNC1kNjFjLTQzNmMtYTE1My1kOGUzZTExMmI4MDU": {
|
||||||
|
"id": "Y2lzY29zcGFyazovL3VzL1BFT1BMRS80NzAyNDlhNC1kNjFjLTQzNmMtYTE1My1kOGUzZTExMmI4MDU",
|
||||||
|
"displayName": "Joe McQueen",
|
||||||
|
"email": "mcqueenj@ae.com",
|
||||||
|
"avatar": "https://avatar-prod-us-east-2.webexcontent.com/Avtr~V1~db9cdc31-8f8e-40b4-916c-8d9e9c92e70d/V1~667e3673b56f4ddc39139c73da9140dd631eb821a756524ead12e37e3a61afc7~9c5b6e767cff46e3af35a003cea38f25~1600",
|
||||||
|
"groups": [
|
||||||
|
{
|
||||||
|
"name": "Group: Audio Visual",
|
||||||
|
"alias": "AV Team",
|
||||||
|
"id": "Y2lzY29zcGFyazovL3VzL1NDSU1fR1JPVVAvZTQ3NmY3MjktZWViYi00MDI3LWFlMTctNzc1YWI1ZDgzYmNhOmRiOWNkYzMxLThmOGUtNDBiNC05MTZjLThkOWU5YzkyZTcwZA"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"integration": {
|
||||||
|
"integrationId": "Y2lzY29zcGFyazovL3VzL0FQUExJQ0FUSU9OL0MzZTQ5NDUwZGQ0ZGMxNmY0OWQxNjEyYTVjNGRiZWNhZjY2M2I1ODdmZWEyOGYzYjMwY2QwOGQzNThkNTNjMDQ1",
|
||||||
|
"integrationName": "CollabCentral"
|
||||||
|
},
|
||||||
|
"serviceAccount": {
|
||||||
|
"appName": "CollabCentral"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
18
config/languages.json
Normal file
18
config/languages.json
Normal file
|
|
@ -0,0 +1,18 @@
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"name": "spanish",
|
||||||
|
"key": "es"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "french",
|
||||||
|
"key": "fr"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "japanese",
|
||||||
|
"key": "ja"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "chinese",
|
||||||
|
"key": "zh"
|
||||||
|
}
|
||||||
|
]
|
||||||
BIN
html/DirectoryConnector.zip
Normal file
BIN
html/DirectoryConnector.zip
Normal file
Binary file not shown.
BIN
html/aeoPropsBot.png
Normal file
BIN
html/aeoPropsBot.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 67 KiB |
57
html/auth.js
Normal file
57
html/auth.js
Normal file
|
|
@ -0,0 +1,57 @@
|
||||||
|
//console.log(window.location.pathname);
|
||||||
|
var pathArray = window.location.pathname.split('/');
|
||||||
|
//console.log(pathArray);
|
||||||
|
//console.log(pathArray);
|
||||||
|
var appName = pathArray[2];
|
||||||
|
//console.log("appName=" + appName);
|
||||||
|
/*
|
||||||
|
if (appName == "novi") {
|
||||||
|
setCookie("appLabel", "Novi Communicator");
|
||||||
|
} else if (appName == "techupdates") {
|
||||||
|
setCookie("appLabel", "Technology Updates");
|
||||||
|
} else {
|
||||||
|
setCookie("appLabel", "Unknown");
|
||||||
|
}
|
||||||
|
setCookie("appName",appName,1);
|
||||||
|
*/
|
||||||
|
|
||||||
|
if (getCookie("id")) {
|
||||||
|
console.log("Found ID Cookie: " + getCookie("id"));
|
||||||
|
window.location.href = "/CollabCentral/" + appName + "/sendMessage.html";
|
||||||
|
} else {
|
||||||
|
console.log("No ID Cookie found.");
|
||||||
|
|
||||||
|
var randomNumber = Math.random().toString();
|
||||||
|
randomNumber = randomNumber.substring(2, randomNumber.length);
|
||||||
|
fetch('/CollabCentral/' + appName + '/authUrl')
|
||||||
|
.then(res => res.text())
|
||||||
|
.then((res) => {
|
||||||
|
window.location.href = res + randomNumber;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
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 "";
|
||||||
|
}
|
||||||
|
|
||||||
|
function setCookie(name, value, days) {
|
||||||
|
var expires = "";
|
||||||
|
if (days) {
|
||||||
|
var date = new Date();
|
||||||
|
date.setTime(date.getTime() + (days * 24 * 60 * 60 * 1000));
|
||||||
|
expires = "; expires=" + date.toUTCString();
|
||||||
|
}
|
||||||
|
document.cookie = name + "=" + (value || "") + expires + "; path=/";
|
||||||
|
}
|
||||||
5
html/index.html
Normal file
5
html/index.html
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
<html>
|
||||||
|
<body>
|
||||||
|
<script src="auth.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
165
html/jobDetail.html
Normal file
165
html/jobDetail.html
Normal file
|
|
@ -0,0 +1,165 @@
|
||||||
|
<html>
|
||||||
|
|
||||||
|
<head>
|
||||||
|
<title>CollabCentral</title>
|
||||||
|
<link id="appName" rel="icon" type="image/x-icon" style="border-radius: 50%;" href="favicon.ico">
|
||||||
|
<style>
|
||||||
|
* { box-sizing: border-box; }
|
||||||
|
|
||||||
|
body {
|
||||||
|
background-color: #FFFFFF;
|
||||||
|
color: #2F3941;
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Helvetica, Arial, sans-serif;
|
||||||
|
font-size: 15px;
|
||||||
|
line-height: 1.5;
|
||||||
|
-webkit-font-smoothing: antialiased;
|
||||||
|
padding: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header {
|
||||||
|
display: block;
|
||||||
|
background-color: tan;
|
||||||
|
width: 90%;
|
||||||
|
height: auto;
|
||||||
|
text-align: center;
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
#appInfo {
|
||||||
|
display: flex;
|
||||||
|
text-align: center;
|
||||||
|
align-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.name {
|
||||||
|
display: block;
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
|
||||||
|
.container {
|
||||||
|
max-width: 1100px;
|
||||||
|
margin: 1em auto;
|
||||||
|
padding: 0 1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
h2.jobTitle {
|
||||||
|
margin: 0.5em 0 0.2em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.subtitle {
|
||||||
|
color: #6c757d;
|
||||||
|
margin: 0 0 1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.summary-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: max-content 1fr;
|
||||||
|
gap: 0.45em 1.5em;
|
||||||
|
margin: 1em 0 1.5em;
|
||||||
|
padding: 1em 1.2em;
|
||||||
|
background: #f8f9fa;
|
||||||
|
border: 1px solid #e9ecef;
|
||||||
|
border-radius: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.summary-grid dt {
|
||||||
|
font-weight: 600;
|
||||||
|
color: #555;
|
||||||
|
}
|
||||||
|
|
||||||
|
.summary-grid dd { margin: 0; }
|
||||||
|
|
||||||
|
.section-title {
|
||||||
|
font-size: 1.05em;
|
||||||
|
font-weight: 700;
|
||||||
|
margin: 1.5em 0 0.4em;
|
||||||
|
color: #2F3941;
|
||||||
|
}
|
||||||
|
|
||||||
|
.message-preview {
|
||||||
|
border: 1px solid #ddd;
|
||||||
|
border-radius: 6px;
|
||||||
|
padding: 1em 1.2em;
|
||||||
|
background: #fafafa;
|
||||||
|
margin: 0.4em 0 1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.message-preview img { max-width: 100%; height: auto; }
|
||||||
|
|
||||||
|
.pill {
|
||||||
|
display: inline-block;
|
||||||
|
padding: 0.15em 0.7em;
|
||||||
|
border-radius: 999px;
|
||||||
|
font-size: 0.85em;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pill.ok { background: #d4edda; color: #155724; }
|
||||||
|
.pill.err { background: #f8d7da; color: #721c24; }
|
||||||
|
.pill.pending { background: #fff3cd; color: #856404; }
|
||||||
|
|
||||||
|
td { white-space: pre-wrap !important; word-wrap: break-word; }
|
||||||
|
|
||||||
|
.hidden { display: none; }
|
||||||
|
|
||||||
|
.panel-empty {
|
||||||
|
padding: 2em;
|
||||||
|
max-width: 600px;
|
||||||
|
margin: 2em auto;
|
||||||
|
text-align: center;
|
||||||
|
border: 1px solid #ddd;
|
||||||
|
border-radius: 8px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
<script src="https://code.jquery.com/jquery-1.11.2.min.js"></script>
|
||||||
|
<link rel="stylesheet" href="https://cdn.datatables.net/1.13.6/css/jquery.dataTables.css" />
|
||||||
|
<script src="https://cdn.datatables.net/1.13.6/js/jquery.dataTables.js"></script>
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<div class="header">
|
||||||
|
<div id="appInfo">
|
||||||
|
<img id="appIcon" style="border-radius: 50%; padding: 10pt;">
|
||||||
|
<div id="appLabel" style="font-size: 40pt;margin: auto;"></div>
|
||||||
|
</div>
|
||||||
|
<div style="text-align: center;"><a id="jobLink" href=''>Back to Jobs</a></div>
|
||||||
|
<div id="name" style="text-align: right;"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="unauthorizedPanel" class="panel-empty hidden">
|
||||||
|
<h2>You don't have access to this bot</h2>
|
||||||
|
<p>Your account isn't on the authorized list. Contact the bot's administrator.</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="notFoundPanel" class="panel-empty hidden">
|
||||||
|
<h2>Job not found</h2>
|
||||||
|
<p>The job you're looking for doesn't exist, has been purged, or belongs to a different bot.</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="detailPanel" class="container hidden">
|
||||||
|
<h2 class="jobTitle" id="jobTitle"></h2>
|
||||||
|
<p class="subtitle" id="subtitle"></p>
|
||||||
|
|
||||||
|
<dl class="summary-grid" id="summaryGrid"></dl>
|
||||||
|
|
||||||
|
<div class="section-title">Message</div>
|
||||||
|
<div class="message-preview" id="messagePreview"></div>
|
||||||
|
|
||||||
|
<div class="section-title">Recipients (<span id="recipientCount">0</span>)</div>
|
||||||
|
<table id="recipients" style="width: 100%;">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Name</th>
|
||||||
|
<th>Email</th>
|
||||||
|
<th>Status</th>
|
||||||
|
<th>Send Time</th>
|
||||||
|
<th>Error</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script src="jobDetail.js"></script>
|
||||||
|
</body>
|
||||||
|
|
||||||
|
</html>
|
||||||
179
html/jobDetail.js
Normal file
179
html/jobDetail.js
Normal file
|
|
@ -0,0 +1,179 @@
|
||||||
|
var pathArray = window.location.pathname.split('/');
|
||||||
|
var appName = pathArray[2];
|
||||||
|
|
||||||
|
var urlParams = new URLSearchParams(window.location.search);
|
||||||
|
var jobId = urlParams.get('jobId');
|
||||||
|
|
||||||
|
if (!getCookie("id")) {
|
||||||
|
var randomNumber = Math.random().toString().substring(2);
|
||||||
|
fetch('/CollabCentral/' + appName + '/authUrl')
|
||||||
|
.then(res => res.text())
|
||||||
|
.then(res => { window.location.href = res + randomNumber; });
|
||||||
|
} else {
|
||||||
|
bootstrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
function bootstrap() {
|
||||||
|
fetch('/CollabCentral/' + appName + '/info')
|
||||||
|
.then(res => res.json())
|
||||||
|
.then(info => {
|
||||||
|
document.title = info.label + " — Job #" + (jobId || "?");
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
loadJobDetail();
|
||||||
|
})
|
||||||
|
.catch(err => console.error("Failed to load /info:", err));
|
||||||
|
}
|
||||||
|
|
||||||
|
function loadJobDetail() {
|
||||||
|
if (!jobId) {
|
||||||
|
document.getElementById("notFoundPanel").classList.remove("hidden");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
fetch('/CollabCentral/' + appName + '/jobs/detail/' + encodeURIComponent(jobId))
|
||||||
|
.then(res => {
|
||||||
|
if (res.status === 404) {
|
||||||
|
document.getElementById("notFoundPanel").classList.remove("hidden");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
if (!res.ok) throw new Error("HTTP " + res.status);
|
||||||
|
return res.json();
|
||||||
|
})
|
||||||
|
.then(job => {
|
||||||
|
if (!job) return;
|
||||||
|
renderJob(job);
|
||||||
|
document.getElementById("detailPanel").classList.remove("hidden");
|
||||||
|
})
|
||||||
|
.catch(err => {
|
||||||
|
console.error("Failed to load job detail:", err);
|
||||||
|
document.getElementById("notFoundPanel").classList.remove("hidden");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderJob(job) {
|
||||||
|
document.getElementById("jobTitle").textContent = "Job #" + (job.jobId || "?");
|
||||||
|
var subtitle = "Sent by " + (job.senderDisplayName || "unknown");
|
||||||
|
if (job.startTime) subtitle += " · started " + formatDate(job.startTime);
|
||||||
|
document.getElementById("subtitle").textContent = subtitle;
|
||||||
|
|
||||||
|
var dl = document.getElementById("summaryGrid");
|
||||||
|
dl.innerHTML = "";
|
||||||
|
var stats = job.stats || {};
|
||||||
|
var rows = [
|
||||||
|
["Job ID", job.jobId],
|
||||||
|
["Submitted by", job.senderDisplayName],
|
||||||
|
["Scheduled for", job.scheduledFor ? formatDate(job.scheduledFor) : "Sent immediately"],
|
||||||
|
["Started", formatDate(job.startTime)],
|
||||||
|
["Completed", job.endTime ? formatDate(job.endTime) : "In progress"],
|
||||||
|
["Total duration", stats.totalTime],
|
||||||
|
["Webex time", stats.webexTime],
|
||||||
|
["Average per message", stats.averageTime],
|
||||||
|
["Recipients", (job.memberList || []).length],
|
||||||
|
["Delivered", formatSuccess(stats)]
|
||||||
|
];
|
||||||
|
for (var i = 0; i < rows.length; i++) {
|
||||||
|
var key = rows[i][0], val = rows[i][1];
|
||||||
|
if (val === undefined || val === null || val === "") continue;
|
||||||
|
var dt = document.createElement("dt"); dt.textContent = key;
|
||||||
|
var dd = document.createElement("dd"); dd.textContent = val;
|
||||||
|
dl.appendChild(dt); dl.appendChild(dd);
|
||||||
|
}
|
||||||
|
|
||||||
|
var preview = document.getElementById("messagePreview");
|
||||||
|
var msg = (job.message && job.message.english) || job.message || {};
|
||||||
|
if (msg.html) {
|
||||||
|
preview.innerHTML = msg.html;
|
||||||
|
} else if (msg.markdown) {
|
||||||
|
preview.textContent = msg.markdown;
|
||||||
|
} else if (msg.text) {
|
||||||
|
preview.textContent = msg.text;
|
||||||
|
} else if (typeof msg.raw === "string") {
|
||||||
|
preview.textContent = msg.raw;
|
||||||
|
} else {
|
||||||
|
preview.innerHTML = "<em>No message content available.</em>";
|
||||||
|
}
|
||||||
|
|
||||||
|
var members = job.memberList || [];
|
||||||
|
document.getElementById("recipientCount").textContent = members.length;
|
||||||
|
|
||||||
|
$('#recipients').dataTable({
|
||||||
|
data: members.map(formatRecipientRow),
|
||||||
|
columns: [
|
||||||
|
{ data: 'displayName' },
|
||||||
|
{ data: 'email' },
|
||||||
|
{ data: 'status' },
|
||||||
|
{ data: 'time' },
|
||||||
|
{ data: 'error' }
|
||||||
|
],
|
||||||
|
pageLength: 50,
|
||||||
|
lengthMenu: [25, 50, 100, 250],
|
||||||
|
order: [[2, 'asc']],
|
||||||
|
searching: true,
|
||||||
|
info: true,
|
||||||
|
paging: true
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatRecipientRow(m) {
|
||||||
|
var result = m.results || null;
|
||||||
|
var status;
|
||||||
|
var email = "—";
|
||||||
|
var err = "";
|
||||||
|
|
||||||
|
if (!result) {
|
||||||
|
status = '<span class="pill pending">Pending</span>';
|
||||||
|
} else if (result.id && !result.message && !result.errors) {
|
||||||
|
status = '<span class="pill ok">Delivered</span>';
|
||||||
|
email = result.toPersonEmail || email;
|
||||||
|
} else {
|
||||||
|
status = '<span class="pill err">Failed</span>';
|
||||||
|
email = result.toPersonEmail || email;
|
||||||
|
if (result.message) err = result.message;
|
||||||
|
else if (result.errors && result.errors.length) {
|
||||||
|
err = result.errors.map(function (e) { return e.description || e.code || JSON.stringify(e); }).join("; ");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
displayName: m.displayName || "—",
|
||||||
|
email: email,
|
||||||
|
status: status,
|
||||||
|
time: m.msgTime ? (m.msgTime / 1000).toFixed(2) + " s" : "—",
|
||||||
|
error: err
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatSuccess(stats) {
|
||||||
|
if (!stats || stats.succeededMsgs === undefined) return null;
|
||||||
|
var pct = (typeof stats.succeededMsgPct === "number") ? stats.succeededMsgPct.toFixed(1) : stats.succeededMsgPct;
|
||||||
|
return stats.succeededMsgs + " / " + stats.totalMsgs + " (" + pct + "%)";
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatDate(d) {
|
||||||
|
if (!d) return null;
|
||||||
|
try { return new Date(d).toLocaleString(); } catch (e) { return d; }
|
||||||
|
}
|
||||||
|
|
||||||
|
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 "";
|
||||||
|
}
|
||||||
13
html/js/virtual-select.min.css
vendored
Normal file
13
html/js/virtual-select.min.css
vendored
Normal file
File diff suppressed because one or more lines are too long
5
html/js/virtual-select.min.js
vendored
Normal file
5
html/js/virtual-select.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
126
html/monitorJobs.html
Normal file
126
html/monitorJobs.html
Normal file
|
|
@ -0,0 +1,126 @@
|
||||||
|
<html>
|
||||||
|
|
||||||
|
<head>
|
||||||
|
<title>CollabCentral</title>
|
||||||
|
<style>
|
||||||
|
* {
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
background-color: #FFFFFF;
|
||||||
|
color: #2F3941;
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Helvetica, Arial, sans-serif;
|
||||||
|
font-size: 15px;
|
||||||
|
line-height: 1.5;
|
||||||
|
-webkit-font-smoothing: antialiased;
|
||||||
|
padding: 10px;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
#table {
|
||||||
|
width: 80%;
|
||||||
|
}
|
||||||
|
td {
|
||||||
|
white-space: pre-wrap !important;
|
||||||
|
word-wrap:break-word;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header {
|
||||||
|
display: block;
|
||||||
|
background-color: tan;
|
||||||
|
width: 80%;
|
||||||
|
height: auto;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.name {
|
||||||
|
display: block;
|
||||||
|
text-align: right;
|
||||||
|
align-content: right;
|
||||||
|
}
|
||||||
|
|
||||||
|
#appInfo {
|
||||||
|
display: flex;
|
||||||
|
text-align: center;
|
||||||
|
align-content: center;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
.hidden {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
<script src="https://code.jquery.com/jquery-1.11.2.min.js"></script>
|
||||||
|
<link rel="stylesheet" href="https://cdn.datatables.net/1.13.6/css/jquery.dataTables.css" />
|
||||||
|
<script src="https://cdn.datatables.net/1.13.6/js/jquery.dataTables.js"></script>
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<link id="appName" rel="icon" type="image/x-icon" style="border-radius: 50%;" href="favicon.ico">
|
||||||
|
|
||||||
|
<div class="header">
|
||||||
|
<div id="appInfo">
|
||||||
|
<img id="appIcon" style="border-radius: 50%; padding: 10pt;">
|
||||||
|
<div id="appLabel" style="font-size: 40pt;margin: auto;"></div>
|
||||||
|
</div>
|
||||||
|
<div style="text-align: center;"><a id="jobLink" href=''>Send Message</a></div>
|
||||||
|
<div id="name" style="text-align: right;"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="unauthorizedPanel" class="hidden" style="padding: 2em; max-width: 600px; margin: 2em auto; text-align: center; border: 1px solid #ddd; border-radius: 8px;">
|
||||||
|
<h2>You don't have access to this bot</h2>
|
||||||
|
<p>Your account isn't on the authorized list for this bot. If you believe this is a mistake, contact the bot's administrator.</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="jobsPanel" class="hidden">
|
||||||
|
<h3>Running Jobs</h3>
|
||||||
|
<div class="table"></div>
|
||||||
|
<table id="runningJobs" class="table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Submitter</th>
|
||||||
|
<th>App</th>
|
||||||
|
<th>Start Time</th>
|
||||||
|
<th>Total Recipients</th>
|
||||||
|
<th>Completed Recipients</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
</table>
|
||||||
|
<h3>Scheduled Jobs</h3>
|
||||||
|
<table id="scheduledJobs">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Submitter</th>
|
||||||
|
<th>App</th>
|
||||||
|
<th>Message</th>
|
||||||
|
<th>Recipients</th>
|
||||||
|
<th>Scheduled for</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
<h3>Completed Jobs</h3>
|
||||||
|
<table id="completedJobs">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>jobId</th>
|
||||||
|
<th>Submitter</th>
|
||||||
|
<th>App</th>
|
||||||
|
<th>Message</th>
|
||||||
|
<th>Start Time</th>
|
||||||
|
<th>End Time</th>
|
||||||
|
<th>Total Time</th>
|
||||||
|
<th>Webex Time</th>
|
||||||
|
<th>Average Message Time</th>
|
||||||
|
<th>Messages Delivered</th>
|
||||||
|
<th>Total Messages</th>
|
||||||
|
<th>Success Rate</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
<script src="monitorJobs.js"></script>
|
||||||
|
</body>
|
||||||
|
|
||||||
|
</html>
|
||||||
132
html/monitorJobs.js
Normal file
132
html/monitorJobs.js
Normal file
|
|
@ -0,0 +1,132 @@
|
||||||
|
var pathArray = window.location.pathname.split('/');
|
||||||
|
var appName = pathArray[2];
|
||||||
|
|
||||||
|
if (getCookie("id")) {
|
||||||
|
console.log("Found ID Cookie: " + getCookie("id"));
|
||||||
|
|
||||||
|
// Fetch bot metadata + authorization state, then render the page.
|
||||||
|
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 + "/sendMessage.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("jobsPanel").classList.remove("hidden");
|
||||||
|
initJobTables();
|
||||||
|
})
|
||||||
|
.catch(err => console.error("Failed to load /info:", err));
|
||||||
|
} else {
|
||||||
|
console.log("No ID Cookie found.");
|
||||||
|
|
||||||
|
var randomNumber = Math.random().toString();
|
||||||
|
randomNumber = randomNumber.substring(2, randomNumber.length);
|
||||||
|
fetch('/CollabCentral/' + appName + '/authUrl')
|
||||||
|
.then(res => res.text())
|
||||||
|
.then((res) => {
|
||||||
|
window.location.href = res + randomNumber;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function initJobTables() {
|
||||||
|
$('#runningJobs').dataTable({
|
||||||
|
ajax: {
|
||||||
|
url: '/CollabCentral/' + appName + '/jobs/list/running',
|
||||||
|
dataSrc: ''
|
||||||
|
},
|
||||||
|
columns: [
|
||||||
|
{ data: 'senderDisplayName' },
|
||||||
|
{ data: 'appName' },
|
||||||
|
{ data: 'startTime' },
|
||||||
|
{ data: 'totalRecipients' },
|
||||||
|
{
|
||||||
|
data: 'completedRecipients',
|
||||||
|
render: function (val, type, row) {
|
||||||
|
if (type !== 'display' || row.jobId === undefined || row.jobId === null) return val;
|
||||||
|
return val + ' <a href="/CollabCentral/' + appName + '/jobDetail.html?jobId=' + encodeURIComponent(row.jobId) + '" style="margin-left: 0.5em; font-size: 0.85em;">view</a>';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
searching: false,
|
||||||
|
info: false,
|
||||||
|
ordering: false,
|
||||||
|
paging: false
|
||||||
|
});
|
||||||
|
|
||||||
|
$('#scheduledJobs').dataTable({
|
||||||
|
ajax: {
|
||||||
|
url: '/CollabCentral/' + appName + '/jobs/list/scheduled',
|
||||||
|
dataSrc: ''
|
||||||
|
},
|
||||||
|
columns: [
|
||||||
|
{ data: 'senderDisplayName' },
|
||||||
|
{ data: 'appName' },
|
||||||
|
{ data: 'message' },
|
||||||
|
{ data: 'totalRecipients' },
|
||||||
|
{ data: 'scheduledFor' }
|
||||||
|
],
|
||||||
|
searching: false,
|
||||||
|
info: false,
|
||||||
|
ordering: false,
|
||||||
|
paging: false
|
||||||
|
});
|
||||||
|
|
||||||
|
$('#completedJobs').dataTable({
|
||||||
|
ajax: {
|
||||||
|
url: '/CollabCentral/' + appName + '/jobs/list/completed',
|
||||||
|
dataSrc: ''
|
||||||
|
},
|
||||||
|
columns: [
|
||||||
|
{
|
||||||
|
data: 'jobId',
|
||||||
|
render: function (jobId) {
|
||||||
|
if (jobId === undefined || jobId === null) return '';
|
||||||
|
return '<a href="/CollabCentral/' + appName + '/jobDetail.html?jobId=' + encodeURIComponent(jobId) + '">#' + jobId + '</a>';
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ data: 'senderDisplayName' },
|
||||||
|
{ data: 'appName' },
|
||||||
|
{ data: 'message.english.html' },
|
||||||
|
{ data: 'startTime' },
|
||||||
|
{ data: 'endTime' },
|
||||||
|
{ data: 'stats.totalTime' },
|
||||||
|
{ data: 'stats.webexTime' },
|
||||||
|
{ data: 'stats.averageTime' },
|
||||||
|
{ data: 'stats.succeededMsgs' },
|
||||||
|
{ data: 'stats.totalMsgs' },
|
||||||
|
{ data: 'stats.succeededMsgPct' }
|
||||||
|
],
|
||||||
|
searching: false,
|
||||||
|
info: false,
|
||||||
|
ordering: false,
|
||||||
|
paging: false
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
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 "";
|
||||||
|
}
|
||||||
BIN
html/novi.ico
Normal file
BIN
html/novi.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 33 KiB |
BIN
html/novi.png
Normal file
BIN
html/novi.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 33 KiB |
308
html/sendMessage.html
Normal file
308
html/sendMessage.html
Normal file
|
|
@ -0,0 +1,308 @@
|
||||||
|
<html>
|
||||||
|
|
||||||
|
<head>
|
||||||
|
<title>CollabCentral</title>
|
||||||
|
<link id="appName" rel="icon" type="image/x-icon" style="border-radius: 50%;" href="favicon.ico">
|
||||||
|
<script>
|
||||||
|
let cookie = document.cookie;
|
||||||
|
if (cookie) {
|
||||||
|
} else {
|
||||||
|
window.location.href = "index.html";
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
* {
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
input {
|
||||||
|
width: 60%;
|
||||||
|
padding: 12px 20px;
|
||||||
|
margin: 8px 0;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
display: inline-block;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-field input {
|
||||||
|
border: 1px solid #87929D;
|
||||||
|
border-radius: 4px;
|
||||||
|
padding: 10px;
|
||||||
|
width: 75%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-field label {
|
||||||
|
display: block;
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
|
||||||
|
label {
|
||||||
|
margin-top: 10px;
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
background-color: #FFFFFF;
|
||||||
|
color: #2F3941;
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Helvetica, Arial, sans-serif;
|
||||||
|
font-size: 15px;
|
||||||
|
line-height: 1.5;
|
||||||
|
-webkit-font-smoothing: antialiased;
|
||||||
|
padding: 10px;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
.header {
|
||||||
|
display: block;
|
||||||
|
background-color: tan;
|
||||||
|
width: 75%;
|
||||||
|
height: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
form {
|
||||||
|
display: block;
|
||||||
|
margin-top: 0em;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
#file-ip-1-preview {
|
||||||
|
width: auto;
|
||||||
|
height: auto;
|
||||||
|
padding: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
.groups {
|
||||||
|
width: 1000px;
|
||||||
|
height: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
#my-text-area {
|
||||||
|
height: 250px;
|
||||||
|
width: 75%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.EasyMDEContainer {
|
||||||
|
display: block;
|
||||||
|
width: 75%;
|
||||||
|
border: 1px solid #87929D;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.submit {
|
||||||
|
width: 75%;
|
||||||
|
height: auto;
|
||||||
|
margin: 10px;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 0.4rem;
|
||||||
|
width: 450px;
|
||||||
|
padding: 1.3rem;
|
||||||
|
min-height: 250px;
|
||||||
|
position: absolute;
|
||||||
|
top: 20%;
|
||||||
|
background-color: white;
|
||||||
|
border: 1px solid #ddd;
|
||||||
|
border-radius: 15px;
|
||||||
|
z-index: 2;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal .flex {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal input {
|
||||||
|
padding: 0.7rem 1rem;
|
||||||
|
border: 1px solid #ddd;
|
||||||
|
border-radius: 5px;
|
||||||
|
font-size: 0.9em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal p {
|
||||||
|
font-size: 0.9rem;
|
||||||
|
color: #777;
|
||||||
|
margin: 0.4rem 0 0.2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.submit button {
|
||||||
|
display: inline-block;
|
||||||
|
padding: 0.8rem 1.4rem;
|
||||||
|
font-weight: 700;
|
||||||
|
background-color: black;
|
||||||
|
color: white;
|
||||||
|
border-radius: 5px;
|
||||||
|
text-align: center;
|
||||||
|
font-size: 1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn {
|
||||||
|
display: inline-block;
|
||||||
|
padding: 0.8rem 1.4rem;
|
||||||
|
font-weight: 700;
|
||||||
|
background-color: black;
|
||||||
|
color: white;
|
||||||
|
border-radius: 5px;
|
||||||
|
text-align: center;
|
||||||
|
font-size: 1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-open {
|
||||||
|
position: absolute;
|
||||||
|
bottom: 150px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-close {
|
||||||
|
transform: translate(10px, -20px);
|
||||||
|
padding: 0.5rem 0.7rem;
|
||||||
|
background: #eee;
|
||||||
|
border-radius: 50%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.overlay {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
bottom: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
background: rgba(0, 0, 0, 0.5);
|
||||||
|
backdrop-filter: blur(3px);
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hidden {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.name {
|
||||||
|
display: block;
|
||||||
|
text-align: right;
|
||||||
|
align-content: right;
|
||||||
|
}
|
||||||
|
|
||||||
|
#appInfo {
|
||||||
|
display: flex;
|
||||||
|
text-align: center;
|
||||||
|
align-content: center;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
#multi_option {
|
||||||
|
max-width: 100%;
|
||||||
|
width: 350px;
|
||||||
|
}
|
||||||
|
|
||||||
|
vscomp-toggle-button {
|
||||||
|
padding: 10px 30px 10px 10px;
|
||||||
|
border-radius: 5px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/easymde/dist/easymde.min.css">
|
||||||
|
<link rel="stylesheet" href="js/virtual-select.min.css" />
|
||||||
|
|
||||||
|
<script src="js/virtual-select.min.js"></script>
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/easymde/dist/easymde.min.js"></script>
|
||||||
|
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
|
||||||
|
|
||||||
|
<div class="header">
|
||||||
|
<div id="appInfo">
|
||||||
|
<img id="appIcon" style="border-radius: 50%; padding: 10pt;">
|
||||||
|
<div id="appLabel" style="font-size: 40pt;margin: auto;"></div>
|
||||||
|
</div>
|
||||||
|
<div style="text-align: center;"><a id="jobLink" href=''>Monitor Jobs</a></div>
|
||||||
|
<div id="name" style="text-align: right;"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
<div id="unauthorizedPanel" class="hidden" style="padding: 2em; max-width: 600px; margin: 2em auto; text-align: center; border: 1px solid #ddd; border-radius: 8px;">
|
||||||
|
<h2>You don't have access to this bot</h2>
|
||||||
|
<p>Your account isn't on the authorized list for this bot. If you believe this is a mistake, contact the bot's administrator.</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="formPanel" class="form-input hidden">
|
||||||
|
<form action="" id="sendMessage" enctype="multipart/form-data">
|
||||||
|
<div class="form-field">
|
||||||
|
|
||||||
|
<img id="file-ip-1-preview">
|
||||||
|
<label for="uploadImage">Upload Image</label>
|
||||||
|
<input type="file" id="uploadImage" accept="image/*" name="uploadImage"
|
||||||
|
onchange="showImagePreview(event);">
|
||||||
|
</div>
|
||||||
|
<label for="my-text-area">Message</label>
|
||||||
|
<textarea id="my-text-area"></textarea>
|
||||||
|
|
||||||
|
<div class="form-field">
|
||||||
|
<div class="groups">
|
||||||
|
<label for="favGroups">Favorite Groups:</label>
|
||||||
|
<select id="favGroups" multiple name="favGroups" placeholder="Favorite Groups"
|
||||||
|
data-silent-initial-value-set="false">
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="groups">
|
||||||
|
<label>New Groups:</label>
|
||||||
|
<select id="newGroups" multiple name="newGroups" placeholder="New Groups"
|
||||||
|
data-silent-initial-value-set="false">
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="people">
|
||||||
|
<label for="uploadCSV">Upload CSV:</label>
|
||||||
|
<input type="file" id="uploadCSV" name="uploadCSV" accept="text/csv">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="form-field">
|
||||||
|
<div class="when">
|
||||||
|
<label for="meeting-time">Schedule for:</label>
|
||||||
|
<input type="datetime-local" id="scheduledFor" name="scheduledFor" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="submit">
|
||||||
|
<button id="submit" type="submit">Submit</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</form>
|
||||||
|
|
||||||
|
|
||||||
|
<section class="modal hidden">
|
||||||
|
<div class="flex">
|
||||||
|
<button class="btn-close" onclick="continueEditing()">X</button>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h3>Ready to send?</h3>
|
||||||
|
<p id="modalMessage">
|
||||||
|
This message will be sent to XX people.<br>
|
||||||
|
Ready to send?
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button class="btn" onclick="continueEditing()">Continue Editing</button>
|
||||||
|
<button class="btn" onclick="submit()">Submit</button>
|
||||||
|
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<div class="overlay hidden"></div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
<script src="sendMessage.js"></script>
|
||||||
|
|
||||||
|
|
||||||
|
</body>
|
||||||
|
|
||||||
|
</html>
|
||||||
222
html/sendMessage.js
Normal file
222
html/sendMessage.js
Normal file
|
|
@ -0,0 +1,222 @@
|
||||||
|
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 = "";
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
BIN
html/techupdates.ico
Normal file
BIN
html/techupdates.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 15 KiB |
BIN
html/techupdates.png
Normal file
BIN
html/techupdates.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 102 KiB |
1500
package-lock.json
generated
Normal file
1500
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load diff
25
package.json
Normal file
25
package.json
Normal file
|
|
@ -0,0 +1,25 @@
|
||||||
|
{
|
||||||
|
"name": "collabcentral",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"description": "Micro-service for sending messages to the organization.",
|
||||||
|
"main": "index.js",
|
||||||
|
"scripts": {
|
||||||
|
"start": "node --env-file=.env index.js",
|
||||||
|
"test": "echo \"Error: no test specified\" && exit 1"
|
||||||
|
},
|
||||||
|
"author": "Joseph B. McQueen",
|
||||||
|
"license": "ISC",
|
||||||
|
"type": "module",
|
||||||
|
"dependencies": {
|
||||||
|
"body-parser": "^1.20.2",
|
||||||
|
"cookie-parser": "^1.4.6",
|
||||||
|
"express": "^4.18.2",
|
||||||
|
"form-data": "^4.0.0",
|
||||||
|
"multer": "^1.4.5-lts.1",
|
||||||
|
"node-cron": "^3.0.2",
|
||||||
|
"node-fetch": "^3.3.2",
|
||||||
|
"p-queue": "^7.4.1",
|
||||||
|
"sharp": "^0.32.6",
|
||||||
|
"uuid": "^9.0.1"
|
||||||
|
}
|
||||||
|
}
|
||||||
0
uploads/.gitkeep
Normal file
0
uploads/.gitkeep
Normal file
Loading…
Reference in a new issue