#!/usr/bin/env node /* ***************************************************************** \\\\\\\\\\\\\\\\\\\\\\\\\\\\\ ||||||||||||||||||||||||||||| | "||,,,,. "|" .,,,,||" ____ | .d8888b. .d8888b. ( ) _____ /|\ o8' o '8o o8'o `8o | | ( ) ||| o8. .8o o8. .8o | | _______ | | `Y8888P' `Y8888P' | |( )| | ,||''|| \ / ||''||, | || || | ,|| ||, \ / .|| ||, | || || | || || ` || || | || || | ,|| '|| ||' ||, | || || | _ || '|| ||' || | || || || \ || |; ;| || | || || || ' || ,| |, || | || || || ' ||, ,|| ||, ,|| | || || || | ||, ,||| |||, ,|| | || || || | '||,,||||,...,||||,,|| | || || ||___|_ `|||..."|||"...|||' (____)(_______)(_____)|____| |%%%%%%%%WWWW%%%%%%WWWW%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%| `""""""""""3$F""""#$F""""""""""""""""""""""""""""""""""""""""' @$.... '$B d$$$$$$$$$$: ```````````` Basebot by Joe McQueen - 2020 Purpose: Template for bots To do: */ //Webex Framework APIs import fs from 'fs'; import path from 'path'; import framework from 'webex-node-bot-framework'; import express from 'express'; import bodyParser from 'body-parser'; import fetch from 'node-fetch'; import cron from 'node-cron'; var config = JSON.parse(fs.readFileSync('./config/config.json')); var schedule = JSON.parse(fs.readFileSync('./config/schedule.json')); var authorizedMembers = JSON.parse(fs.readFileSync('./config/authorized.json')); var app = express(); app.use(bodyParser.json()); app.use(bodyParser.urlencoded({ extended: false })); //Main function to run on startUp var server = app.listen(config.server.port, function () { logger("startup", config.server.name + " running on port " + config.server.port + ".") }); app.get('/status', function (req, res) { res.status(200).send({ "status": `Alive and kicking. ${authorizedMembers.length} total names in authorizedMembers.` }); }) app.post('/members', function (req, res) { saveJSON(req.body, ('./config/authorized.json')) res.status(200).send({ "status": "Authorized users updated." }); authorizedMembers = JSON.parse(fs.readFileSync('./config/authorized.json')); logger(`POST /members`, `Authorized users updated.`) }) /*app.get('/codes', (req, res) => res.json(schedule)) app.get('/code', function (req, res) { var nowDate = Date.now(); //console.log("Now:" + nowDate); for (var codes in schedule) { //console.log(code); var startDate = Date.parse(schedule[codes].start); var endDate = Date.parse(schedule[codes].end); //console.log("Code: " + code + " Start: " + startDate + " End: " + endDate ); if ((nowDate.valueOf() <= endDate.valueOf() && nowDate.valueOf() >= startDate.valueOf())) { //console.log(startDate + "\t" + nowDate + "\t" + endDate); var influxPoint = 'discount,type=api count=1'; //sendInfluxPoint(influxPoint); buildCard(schedule[codes]) .then(discountCode => { res.send(discountCode) }) } } });*/ // init framework var Framework = new framework(config.auth.webex.bot); Framework.start(); console.log("Starting framework, please wait..."); Framework.on("initialized", function () { console.log("framework is all fired up! [Press CTRL-C to quit]"); }); //framework.on('log', (msg) => { // console.log(msg); //}); Framework.on('membershipRulesAction', (type, event, bot, id, ...args) => { console.log(`Framework membershipRulesAction of type:${type}, event:${event} occurred in space "${bot.room.title}".`); try { switch (type) { case ('state-change'): console.log(`Membership Rules forced a "${event}" event`); break; case ('event-swallowed'): if (event === 'spawn') { if (args.length >= 2) { let actorId = args[0]; let membershipRuleChange = args[1]; let email = membershipRuleChange.membership.personEmail; if (membershipRuleChange && membershipRuleChange.membershipRule === "restrictedToEmailDomains") { console.log(`spawn swallowed. Dissallowed member: ${email}`); } else { console.log(`spawn swallowed. Space membership is missing one of ` + 'the users specified in the `guideEmails` framework config parameter'); } } } if ((event === 'memberExits') || (event === 'memberEnters')) { let member = args[0]; console.log(`Ignored ${event} for ${member.personEmail} in disallowed space.`); } break; case ('hears-swallowed'): console.log(`Membership Rules swallowed a "${event}" event`); break; default: assert(true === false, `Got unexpected membershipsRules type: ${type}`); break; } } catch (e) { console.error(`Failed processing mebershipRulesAction event "${event}": ${e.message}`); } }); //Process incoming Webex Teams messages let responded = false; /* On mention with command ex User enters @botname help, the bot will write back in markdown */ Framework.hears(/help|what can i (do|say)|what (can|do) you do/i, function (bot, trigger) { console.log(`someone needs help! They asked ${trigger.text}`); responded = true; bot.say("markdown","Say the word 'code' or 'discount' and press enter.") }); Framework.hears(/discount|code/i, function (bot, trigger) { responded = true; //console.log(trigger); logger(`Framework.hears(/discount|code/i`, `${trigger.person.displayName} (${trigger.person.userName}) requested a code.`) var nowDate = Date.now(); if (authorizedMembers.includes(trigger.person.displayName.toLowerCase()) || authorizedMembers.includes(trigger.person.userName.toLowerCase())) { logger(`Framework.hears(/discount|code/i`, `${trigger.person.displayName} (${trigger.person.userName}) authorized.`) var found = false; for (var codes in schedule) { var startDate = Date.parse(schedule[codes].start); var endDate = Date.parse(schedule[codes].end); if ((nowDate.valueOf() <= endDate.valueOf() && nowDate.valueOf() >= startDate.valueOf())) { found = true; buildCard(schedule[codes]) .then(discountCode => { bot.sendCard(discountCode.card, discountCode.text) }) } } if (!found) { bot.dm("mcqueenj@ae.com", "markdown", "No code found") } let d = new Date(); let fileName = "requests-" + d.getFullYear() + "-" + (d.getMonth() + 1) + ".log"; let p = path.join('./logs/', fileName); var stampData = d.getHours() + ":" + d.getMinutes() + ":" + d.getSeconds() + " " + (d.getMonth() + 1) + "/" + d.getDate() + "/" + d.getFullYear(); fs.appendFileSync(p, stampData + ": " + trigger.person.displayName + "," + trigger.person.userName + "\n"); } else { logger(`Framework.hears(/discount|code/i`, `${trigger.person.displayName} (${trigger.person.userName}) failed authorization.`); bot.reply(trigger.message, 'You are not authorized to access discount codes. If you believe this is in error, please contact the support center.'); bot.dm('mcqueenj@ae.com', `${trigger.person.displayName} requested a discount code but was not authorized.`); } }); // Process a submitted card Framework.on('attachmentAction', function (bot, trigger) { responded = true; }); /* On mention with unexpected bot command Its a good practice is to gracefully handle unexpected input */ Framework.hears(/.*/, function (bot, trigger) { // This will fire for any input so only respond if we haven't already if (!responded) { console.log(`catch-all handler fired for user input: ${trigger.text}`); bot.say(`Sorry, I don't know how to answer that command: ${trigger.text}`) .then(() => sendHelp(bot)) .catch((e) => console.error(`Problem in the unexepected command hander: ${e.message}`)); } responded = false; }); function sendHelp(bot) { bot.sendCard(helpCard, "Discount provides the latest discount codes for employees of AE, Aerie, Tailgate, and Todd Snyder."); } // gracefully shutdown (ctrl-c) process.on('SIGINT', function () { Framework.debug('Stopping DiscountBot...'); server.close(); Framework.stop().then(function () { process.exit(); }); }); function buildCard(code) { return new Promise(async function (resolve, reject) { var discountCard = { "type": "AdaptiveCard", "$schema": "http://adaptivecards.io/schemas/adaptive-card.json", "version": "1.3", "body": [ { "type": "TextBlock", "text": "Associate Discount Code for " + code.month, "wrap": true, "size": "Medium", "horizontalAlignment": "Center", "weight": "Bolder" }, { "type": "Container", "items": [ { "type": "TextBlock", "text": "[AE & Aerie](https://www.ae.com)", "wrap": true, "weight": "Bolder", "size": "ExtraLarge", "separator": true, "horizontalAlignment": "Center", "spacing": "None" }, { "type": "TextBlock", "text": "US, Canada, & ROW", "wrap": true, "weight": "Lighter", "size": "Small", "separator": true, "horizontalAlignment": "Center", "spacing": "None" }, { "type": "TextBlock", "text": code.aeAerie.usrow.code, "wrap": true, "fontType": "Monospace", "size": "ExtraLarge", "weight": "Bolder", "horizontalAlignment": "Center", "spacing": "Padding" }, { "type": "TextBlock", "text": code.aeAerie.usrow.message, "wrap": true, "size": "Small", "color": "Light", "horizontalAlignment": "Center", "weight": "Lighter", "spacing": "Small" } ], "separator": true }, { "type": "Container", "items": [ { "type": "TextBlock", "text": "[AE & Aerie](https://www.ae.com/mx/es)", "wrap": true, "weight": "Bolder", "size": "ExtraLarge", "separator": true, "horizontalAlignment": "Center", "spacing": "None" }, { "type": "TextBlock", "text": "Mexico", "wrap": true, "weight": "Lighter", "size": "Small", "separator": true, "horizontalAlignment": "Center", "spacing": "None" }, { "type": "TextBlock", "text": code.aeAerie.mexico.code, "wrap": true, "fontType": "Monospace", "size": "ExtraLarge", "weight": "Bolder", "horizontalAlignment": "Center", "spacing": "Padding" }, { "type": "TextBlock", "text": code.aeAerie.mexico.message, "wrap": true, "size": "Small", "color": "Light", "horizontalAlignment": "Center", "weight": "Lighter", "spacing": "Small" } ], "separator": true }, { "type": "Container", "items": [ { "type": "TextBlock", "text": "[Todd Snyder](https://www.toddsnyder.com)", "wrap": true, "weight": "Bolder", "size": "ExtraLarge", "separator": true, "horizontalAlignment": "Center", "spacing": "None" }, { "type": "TextBlock", "text": code.toddSnyder.usrow.tstc.code, "wrap": true, "fontType": "Monospace", "size": "ExtraLarge", "weight": "Bolder", "horizontalAlignment": "Center", "spacing": "Padding" }, { "type": "TextBlock", "text": code.toddSnyder.usrow.tstc.message, "wrap": true, "size": "Small", "color": "Light", "horizontalAlignment": "Center", "weight": "Lighter", "spacing": "Small" }, { "type": "TextBlock", "text": code.toddSnyder.usrow.thirdparty.code, "wrap": true, "fontType": "Monospace", "size": "ExtraLarge", "weight": "Bolder", "horizontalAlignment": "Center", "spacing": "Padding" }, { "type": "TextBlock", "text": code.toddSnyder.usrow.thirdparty.message, "wrap": true, "size": "Small", "color": "Light", "horizontalAlignment": "Center", "weight": "Lighter", "spacing": "Small" } ], "separator": true }, { "type": "Container", "items": [ { "type": "TextBlock", "text": "[Unsubscribed](https://www.unsubscribed.com)", "wrap": true, "weight": "Bolder", "size": "ExtraLarge", "separator": true, "horizontalAlignment": "Center", "spacing": "None" }, { "type": "TextBlock", "text": code.unsubscribed.usrow.code, "wrap": true, "fontType": "Monospace", "weight": "Bolder", "horizontalAlignment": "Center", "size": "ExtraLarge", "spacing": "Padding" }, { "type": "TextBlock", "text": code.unsubscribed.usrow.message, "wrap": true, "weight": "Lighter", "size": "Small", "color": "Light", "horizontalAlignment": "Center", "spacing": "Small" }, { "type": "TextBlock", "text": code.unsubscribed.thirdparty.code, "wrap": true, "fontType": "Monospace", "weight": "Bolder", "horizontalAlignment": "Center", "size": "ExtraLarge", "spacing": "Padding" }, { "type": "TextBlock", "text": code.unsubscribed.thirdparty.message, "wrap": true, "weight": "Lighter", "size": "Small", "color": "Light", "horizontalAlignment": "Center", "spacing": "Small" }, { "type": "TextBlock", "text": code.unsubscribed.giftcard.code, "wrap": true, "fontType": "Monospace", "weight": "Bolder", "horizontalAlignment": "Center", "size": "ExtraLarge", "spacing": "Padding" }, { "type": "TextBlock", "text": code.unsubscribed.giftcard.message, "wrap": true, "weight": "Lighter", "size": "Small", "color": "Light", "horizontalAlignment": "Center", "spacing": "Small" } ], "separator": true }, { "type": "TextBlock", "text": "[Associate Discount Policies & Codes](https://onfirstup.com/AEO/AEO/contents/25437784) \n\nThe online discount is a benefit to all AEO Corporate and Distribution Center associates as well as Field leadership (RD, DTL, Regional Assistants, Field Visual, Field Auditors, Field Real Estate, and Field HR).", "wrap": true, "horizontalAlignment": "Center", "separator": true } ] } var discountText = "# Discount Code for " + code.month + "\n" + "## AE/Aerie US, Canada, & ROW [Website](https://www.ae.com/)" + "\n" + "`" + code.aeAerie.usrow.code + "`\n" + "_" + code.aeAerie.usrow.message + "_\n" + "## AE/Aerie Mexico [Website](https://www.ae.com/mx/es)\n" + "`" + code.aeAerie.mexico.code + "`\n" + "_" + code.aeAerie.mexico.message + "_\n" + "- - -\n" + "## Todd Snyder [Website](https://www.toddsnyder.com)\n" + "`" + code.toddSnyder.usrow.tstc.code + "`\n" + "_" + code.toddSnyder.usrow.tstc.message + "_\n" + "`" + code.toddSnyder.usrow.thirdparty.code + "`\n" + "_" + code.toddSnyder.usrow.thirdparty.message + "_\n" + "- - -\n" + "## Unsubscribed [Website](https://www.unsubscribed.com)\n" + "`" + code.unsubscribed.usrow.code + "`\n" + "_" + code.unsubscribed.usrow.message + "_\n" + "`" + code.unsubscribed.thirdparty.code + "`\n" + "_" + code.unsubscribed.thirdparty.message + "_\n" + "`" + code.unsubscribed.giftcard.code + "`\n" + "_" + code.unsubscribed.giftcard.message + "_\n\n" + "_hese employee discount codes cannot be combined with any other discounts or affiliate marketing links. The online discount is a benefit to all AEO Corporate and Distribution Center associates as well as Field leadership (RD, DTL, Regional Assistants, Field Visual, Field Auditors, Field Real Estate, and Field HR)._"; resolve({ "card": discountCard, "text": discountText }) }) } function logger(activeFunction, logLine) { var d = new Date(); console.log(d.toLocaleString() + " " + activeFunction + ": " + logLine); } function cleanOldFiles() { try { var files = fs.readdirSync(config.server.logging.directory); } catch (error) { logger('clearnOldFiles', `Error: ${error}`) } for (var file of files) { var fileStats = fs.statSync(config.server.logging.directory + file); if (new Date(fileStats.mtime).getTime() <= new Date().getTime() - (config.server.logging.retensionDays * 24 * 3600 * 1000)) { logger('cleanOldFiles', `Removing file '${config.server.logging.directory + file}'`) fs.unlinkSync(config.server.logging.directory + file); } } } function isJSONObject(obj) { return obj !== null && typeof obj === 'object' && obj.constructor === Object; } function refreshToken() { logger('refreshToken', `Checking token.`) if (new Date(new Date(config.serviceAccount.authorization.expiresOn) - 7200000) < new Date()) { logger(`refreshToken`, `Token needs refreshed.`) var url = "https://webexapis.com/v1/access_token"; var params = new URLSearchParams(); params.append('grant_type', 'refresh_token'); params.append('client_id', config.serviceAccount.authorization.clientId); params.append('client_secret', config.serviceAccount.authorization.clientSecret); params.append('refresh_token', config.serviceAccount.authorization.token.refresh_token); fetch(url, { method: 'post', body: params }) .then(res => { return res.json() }) .then(function (json) { console.log(JSON.stringify(json)) config.serviceAccount.authorization.created = new Date().toTimeString; config.serviceAccount.authorization.token = json; config.serviceAccount.authorization.expiresOn = new Date(Date.now() + (json.expires_in * 1000)); config.serviceAccount.authorization.refreshBy = new Date(Date.now() + (json.refresh_token_expires_in * 1000)); logger(`refreshToken`, `Token refreshed. Expires on ${config.serviceAccount.authorization.expiresOn.toLocaleString('en-US', { weekday: 'long', year: 'numeric', month: 'long', day: 'numeric', hour: '2-digit', minute: '2-digit', second: '2-digit' })}.`) saveJSON(config, './config/config.json') }) .catch(error => { logger(`refreshToken`, `Error: ${error}`) }); } else { logger(`refreshToken`, `Token is good until ${new Date(config.serviceAccount.authorization.expiresOn).toLocaleString('en-US', { weekday: 'long', year: 'numeric', month: 'long', day: 'numeric', hour: '2-digit', minute: '2-digit', second: '2-digit' })}.`) } } function saveJSON(jsonObject, configFile) { return new Promise(async function (resolve, reject) { const jsonData = JSON.stringify(jsonObject, null, 4); fs.writeFileSync(configFile, jsonData, (error) => { if (error) { reject(error) throw err; } else { logger("saveJSON", "Wrote " + configFile); } }) resolve(); }) }