diff --git a/public/main.js b/public/main.js index d4cf22c..6087780 100644 --- a/public/main.js +++ b/public/main.js @@ -1,4 +1,5 @@ var requestsDiv = document.getElementById("requests"); +var cronJobs = ['processBans'] function getRequests(count,allRequests) { var reqUrl; @@ -65,6 +66,22 @@ function applyUrlTransforms(url) { } } +function getColorObject() { + return { + bg: { + primary: document.getElementById('color-bg-primary').value, + table: document.getElementById('color-bg-table').value, + navbar: document.getElementById('color-bg-navbar').value, + error: document.getElementById('color-bg-error').value, + }, + fg: { + primary: document.getElementById('color-fg-primary').value, + ahover: document.getElementById('color-fg-ahover').value, + title: document.getElementById('color-fg-title').value, + } + } +} + function addRequestErr(msg) { document.getElementById('addRequestError').style.display = "inline-block"; document.getElementById('addRequestError').innerText = msg; @@ -85,33 +102,45 @@ function updateRequestErrReset() { document.getElementById('updateRequestError').innerText = ""; } -function showMessage(msg) { - document.getElementById("messageModalText").innerText = msg; +function streamerSettingsErr(msg) { + document.getElementById('streamerSettingsError').style.display = "inline-block"; + document.getElementById('streamerSettingsError').innerText = msg; +} + +function streamerSettingsErrReset() { + document.getElementById('streamerSettingsError').style.display = "none"; + document.getElementById('streamerSettingsError').innerText = ""; +} + +// Hides all modals in preparation to show a one or to close out of +// all modals and return to the page. Does NOT hide modalBackground. +function hideModals() { + document.getElementById("messageModal").style.display = "none"; document.getElementById("addRequestModal").style.display = "none"; document.getElementById("updateRequestModal").style.display = "none"; document.getElementById("deleteRequestModal").style.display = "none"; + document.getElementById("streamerSettingsModal").style.display = "none"; +} + +function closeAllModals() { + hideModals(); + document.getElementById("modalBackground").style.display = "none"; +} + +function showMessage(msg) { + hideModals(); + document.getElementById("messageModalText").innerText = msg; document.getElementById("modalBackground").style.display = "flex"; document.getElementById("messageModal").style.display = "block"; } -function closeMessageModal() { - document.getElementById("modalBackground").style.display = "none"; - document.getElementById("messageModal").style.display = "none"; -} - function openAddRequestModal() { + hideModals(); document.getElementById("modalBackground").style.display = "flex"; - document.getElementById("updateRequestModal").style.display = "none"; - document.getElementById("messageModal").style.display = "none"; - document.getElementById("addRequestModal").style.display = "block"; -} - -function closeAddRequestModal() { - document.getElementById("modalBackground").style.display = "none"; - document.getElementById("addRequestModal").style.display = "none"; } function openUpdateRequestModal(tr) { + hideModals(); var url = tr.getElementsByClassName('request-link')[0].firstChild.href; var score = tr.getElementsByClassName('request-score')[0].innerText; var state = tr.getElementsByClassName('request-state')[0].innerText; @@ -120,18 +149,12 @@ function openUpdateRequestModal(tr) { document.getElementById("updateRequestModalCurrentScore").innerText = score; document.querySelector(`#updateRequestStateSelect [value="${state}"]`).selected = true; document.getElementById("scoreModifierInput").value = 0; - document.getElementById("messageModal").style.display = "none"; - document.getElementById("addRequestModal").style.display = "none"; document.getElementById("modalBackground").style.display = "flex"; document.getElementById("updateRequestModal").style.display = "block"; } -function closeUpdateRequestModal() { - document.getElementById("modalBackground").style.display = "none"; - document.getElementById("updateRequestModal").style.display = "none"; -} - function openDeleteRequestModal(url) { + hideModals(); document.getElementById("updateRequestUrl").href = url; document.getElementById("updateRequestUrl").innerText = url; document.getElementById("messageModal").style.display = "none"; @@ -141,17 +164,21 @@ function openDeleteRequestModal(url) { document.getElementById("deleteRequestModal").style.display = "block"; } +// Returns to update request modal function closeDeleteRequestModal() { + hideModals(); document.getElementById("deleteRequestModal").style.display = "none"; document.getElementById("updateRequestModal").style.display = "block"; } -function closeAllModals() { - document.getElementById("messageModal").style.display = "none"; - document.getElementById("addRequestModal").style.display = "none"; - document.getElementById("updateRequestModal").style.display = "none"; - document.getElementById("deleteRequestModal").style.display = "none"; - document.getElementById("modalBackground").style.display = "none"; +function openStreamerSettingsModal() { + hideModals(); + document.getElementById("modalBackground").style.display = "flex"; + document.getElementById("streamerSettingsModal").style.display = "block"; +} + +function cronRequest(job) { + if (!cronJobs.includes(job)) throw new Error("Request for invalid job"); } const validUrlRegexes = [ @@ -262,7 +289,33 @@ function deleteRequest(url) { }); } +function updatePageTitle(pageTitle) { + streamerSettingsErrReset(); + fetch("/api/updatePageTitle", { method: 'POST', body: new URLSearchParams({ + pageTitle: pageTitle + })}) + .then(response => { + if (response.ok) { + location.reload(); + } else { + response.text().then(streamerSettingsErr); + } + }); +} +function updateColors(colors) { + streamerSettingsErrReset(); + fetch("/api/updateColors", { method: 'POST', headers: { + 'Content-Type': 'application/json' + }, body: JSON.stringify(colors)}) + .then(response => { + if (response.ok) { + location.reload(); + } else { + response.text().then(streamerSettingsErr); + } + }); +} updateTable(); diff --git a/public/style.css b/public/style.css index 7f4cbba..c25812e 100644 --- a/public/style.css +++ b/public/style.css @@ -12,6 +12,12 @@ button, input, select { font-size: 100%; } +input[type="color"] { + border: none; + padding: 0px; + vertical-align: middle; +} + a { color: #ddd; } diff --git a/src/app.ts b/src/app.ts index 3bdc436..a7d191f 100644 --- a/src/app.ts +++ b/src/app.ts @@ -30,6 +30,7 @@ const app = express(); app.use(version.checkVersionMiddleware); app.use(express.static('public')); app.use(express.urlencoded({extended: false})); +app.use(express.json()); app.use(session({ secret: config.sessionSecret, saveUninitialized: false, @@ -189,6 +190,101 @@ app.post("/api/updateRequestScoreModifier", async (request, response) => { .catch((e: any) => errorHandler(request,response,e)); }); +app.post("/api/updatePageTitle", async (request, response) => { + if (request.session) await validateApiToken(request.session); + if (!request.session || !request.session.user) { + response.status(401); + response.send("Session expired; please log in again"); + return; + } + var streamerid = await db.query(queries.getStreamerId).then((result: pg.QueryResult) => result.rows[0]['userid']); + if (request.session.user.id != streamerid) { + response.status(401); + response.send("You are not the streamer"); + return; + } + if (!request.body.pageTitle) { + response.status(400); + response.send("Missing pageTitle"); + return; + } + var pageTitle = request.body.pageTitle as string; + response.type('text/plain'); + pageTitle = pageTitle.replace(/[&<>"']/g, function(m) { + switch (m) { + case '&': + return '&'; + case '<': + return '<'; + case '>': + return '>'; + case '"': + return '"'; + case "'": + return '''; + default: + return ''; + } + }); + await db.query(Object.assign(queries.updatePageTitle,{ values: [pageTitle] })) + .catch((e: any) => errorHandler(request,response,e)); + response.status(200); + response.send('Successfully updated page title'); +}); + +app.post("/api/updateColors", async (request, response) => { + if (request.session) await validateApiToken(request.session); + if (!request.session || !request.session.user) { + response.status(401); + response.send("Session expired; please log in again"); + return; + } + var streamerid = await db.query(queries.getStreamerId).then((result: pg.QueryResult) => result.rows[0]['userid']); + if (request.session.user.id != streamerid) { + response.status(401); + response.send("You are not the streamer"); + return; + } + if (!request.body.bg) { + response.status(400); + response.send("Missing bg"); + return; + } + if (!request.body.fg) { + response.status(400); + response.send("Missing fg"); + return; + } + type Colors = { [key: string]: { [key: string]: string} } + var colors: Colors = { bg: {}, fg: {} }; + console.log(JSON.stringify(request.body,null,2)); + for (var color of ['primary','table','navbar','error']) { + var setcolor = request.body.bg[color]; + if (/^#[0-9a-fA-F]{6}$/.test(setcolor)) { + colors.bg[color] = setcolor + } else { + response.status(400); + response.send(`Color 'bg.${color}' missing or invalid`) + return; + } + } + for (var color of ['primary','ahover','title']) { + var setcolor = request.body.fg[color]; + if (/^#[0-9a-fA-F]{6}$/.test(setcolor)) { + colors.fg[color] = setcolor + } else { + response.status(400); + response.send(`Color 'fg.${color}' missing or invalid`) + return; + } + } + response.type('text/plain'); + await db.query(Object.assign(queries.updateColors,{ values: [JSON.stringify(colors)] })) + .catch((e: any) => errorHandler(request,response,e)); + response.status(200); + response.send('Successfully updated colors'); +}); + app.post("/api/deleteRequest", async (request, response) => { if (request.session) await validateApiToken(request.session); if (!request.session || !request.session.user) { @@ -266,6 +362,37 @@ app.post("/api/deleteVote", async (request,response) => { .catch((e: any) => errorHandler(request,response,e)); }); +app.get("/api/cronRequest", async (request, response) => { + if (request.session) await validateApiToken(request.session); + if (!request.session || !request.session.user) { + response.status(401); + response.send("Session expired; please log in again"); + return; + } + var streamerid = await db.query(queries.getStreamerId).then((result: pg.QueryResult) => result.rows[0]['userid']); + if (request.session.user.id != streamerid) { + response.status(401); + response.send("You are not the streamer"); + return; + } + if (!request.query.job) { + response.status(400); + response.send("Missing job"); + return; + } + var job = request.body.job as string; + try { + cron.validateJob(job) + } catch (e) { + response.status(400); + response.send("Invalid job") + return; + } + response.type('text/plain'); + cron.request(job).catch((e: any) => errorHandler(request,response,e)); +}); + + // Twitch callback app.get("/callback", async (request, response) => { if (request.query.error) { @@ -302,7 +429,7 @@ app.get("/callback", async (request, response) => { app.get("/", async (request, response) => { if (request.session) await validateApiToken(request.session); var streamerInfo = await db.query(queries.getStreamerInfo).then((result: pg.QueryResult) => result.rows[0]); - var config = await db.query(queries.getConfig).then((result: pg.QueryResult) => result.rows[0]); + var streamerConfig = await db.query(queries.getConfig).then((result: pg.QueryResult) => result.rows[0]); if (typeof streamerInfo == 'undefined') { response.redirect(307, `https://id.twitch.tv/oauth2/authorize?client_id=${config.twitchClientId}&redirect_uri=${config.urlPrefix}/callback&response_type=code&scope=channel:read:subscriptions moderation:read`); return; @@ -312,7 +439,7 @@ app.get("/", async (request, response) => { loggedIn: false, clientId: config.twitchClientId, urlPrefix: config.urlPrefix, - pageTitle: config.title, + pageTitle: streamerConfig.title, streamerName: streamerInfo['displayname'], streamerProfilePicture: streamerInfo['imageurl'] }); @@ -324,9 +451,10 @@ app.get("/", async (request, response) => { userProfilePicture: request.session.user.profile_image_url, validStates: validStates, isStreamer: streamerInfo['userid'] == request.session.user.id, - pageTitle: config.title, + pageTitle: streamerConfig.title, streamerName: streamerInfo['displayname'], - streamerProfilePicture: streamerInfo['imageurl'] + streamerProfilePicture: streamerInfo['imageurl'], + colors: streamerConfig.colors }); } }); @@ -334,7 +462,6 @@ app.get("/", async (request, response) => { app.get("/colors.css", async (_request, response) => { var streamerInfo = await db.query(queries.getStreamerInfo).then((result: pg.QueryResult) => result.rows[0]); var colors = await db.query(queries.getConfig).then((result: pg.QueryResult) => result.rows[0]['colors']); - console.log(colors); if (typeof streamerInfo == 'undefined') return; response.contentType("text/css"); response.render('colors.eta', colors); diff --git a/src/cron.ts b/src/cron.ts index 256835e..fef3758 100644 --- a/src/cron.ts +++ b/src/cron.ts @@ -9,6 +9,24 @@ interface CronJob { (streamer: twitch.StreamerUserIdTokenPair): Promise } +function validateJob(job: string) { + if (!Object.keys(cronjobs).includes(job)) throw new Error("Invalid cronjob " + job); +} + +async function runJob(job: string) { + validateJob(job); + var streamer = await db.query(queries.getStreamerIdToken).then( + (result: pg.QueryResult) => result.rows[0]); + return await (cronjobs as { [key: string]: CronJob })[job](streamer); +} + +// Run a specific job on request of streamer +async function request(job: string) { + validateJob(job); + // TODO: Rate limiting + await runJob(job); +} + async function run() { // If instance is not yet set up, end processing at this point var streamer = await db.query(queries.getStreamerIdToken).then( @@ -42,11 +60,11 @@ async function run() { log(LogLevel.ERROR,`cron: Job ${job} exception message: ${e}`) } finally { log(LogLevel.DEBUG,`cron: Job ${job} hit finally; releasing dbconn`); - await dbconn.release(); + dbconn.release(); } log(LogLevel.INFO,"cron: Finished job " + job); } log(LogLevel.INFO,"End cron run") } -export = {run} +export = {run,validateJob,request} diff --git a/src/queries.ts b/src/queries.ts index b9eeeb7..407dbc0 100644 --- a/src/queries.ts +++ b/src/queries.ts @@ -37,6 +37,16 @@ export const updateStreamer = { ON CONFLICT (userid) DO UPDATE SET tokenPair = $2" } +export const updatePageTitle = { + name: "updatePageTitle", + text: "UPDATE config SET title = $1" +} + +export const updateColors = { + name: "updateColors", + text: "UPDATE config SET colors = $1" +} + // Request-related queries export const getRequests = { name: "getRequests", diff --git a/views/main.eta b/views/main.eta index c04023e..ea7fd32 100644 --- a/views/main.eta +++ b/views/main.eta @@ -3,7 +3,7 @@ - <%= it.pageTitle.replace('{username}',it.streamerName) %> + <%~ it.pageTitle.replace('{username}',it.streamerName) %>