Customization!
Add ability to control page title and color scheme. Also, partially implements manual triggering of cronjobs. Fixes #18master
parent
1c34b3f013
commit
6495e1c8ef
109
public/main.js
109
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();
|
||||
|
||||
|
|
|
@ -12,6 +12,12 @@ button, input, select {
|
|||
font-size: 100%;
|
||||
}
|
||||
|
||||
input[type="color"] {
|
||||
border: none;
|
||||
padding: 0px;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
a {
|
||||
color: #ddd;
|
||||
}
|
||||
|
|
137
src/app.ts
137
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);
|
||||
|
|
22
src/cron.ts
22
src/cron.ts
|
@ -9,6 +9,24 @@ interface CronJob {
|
|||
(streamer: twitch.StreamerUserIdTokenPair): Promise<void>
|
||||
}
|
||||
|
||||
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}
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
<head>
|
||||
<link rel=stylesheet href=/style.css />
|
||||
<link rel=stylesheet href=/colors.css />
|
||||
<title><%= it.pageTitle.replace('{username}',it.streamerName) %></title>
|
||||
<title><%~ it.pageTitle.replace('{username}',it.streamerName) %></title>
|
||||
<script>
|
||||
window.loggedIn = <%= it.loggedIn %>;
|
||||
window.validStates = <%~ it.validStates %>;
|
||||
|
@ -18,6 +18,9 @@
|
|||
<div id="nav-requests"><a href="/">Requests</a></div>
|
||||
<%- if (it.loggedIn) { -%>
|
||||
<div id="nav-addrequest"><a href="#" onclick="openAddRequestModal()">Add Request</a></div>
|
||||
<%- if (it.isStreamer) { -%>
|
||||
<div id="nav-streamersettings"><a href="#" onclick="openStreamerSettingsModal()">Streamer Settings</a></div>
|
||||
<%- } %>
|
||||
<div id="nav-userpic"><img src="<%= it.userProfilePicture %>" /></div>
|
||||
<div id="nav-username"><%= it.userName %></div>
|
||||
<div id="nav-logout"><a href="/logout">Logout</a></div>
|
||||
|
@ -39,11 +42,11 @@
|
|||
</div>
|
||||
<div id="modalBackground">
|
||||
<div class="modal" id="messageModal">
|
||||
<div class="modalClose"><a href="#" onclick="closeMessageModal()">×</a></div>
|
||||
<div class="modalClose"><a href="#" onclick="closeAllModals()">×</a></div>
|
||||
<span id="messageModalText"></span>
|
||||
</div>
|
||||
<div class="modal" id="addRequestModal">
|
||||
<div class="modalClose"><a href="#" onclick="closeAddRequestModal()">×</a></div>
|
||||
<div class="modalClose"><a href="#" onclick="closeAllModals()">×</a></div>
|
||||
<h1>Add Request</h1>
|
||||
<div class="error" id="addRequestError"></div>
|
||||
<span id="addRequestInputContainer">
|
||||
|
@ -53,7 +56,7 @@
|
|||
Currently, only Youtube links are accepted.
|
||||
</div>
|
||||
<div class="modal" id="updateRequestModal">
|
||||
<div class="modalClose"><a href="#" onclick="closeAddRequestModal()">×</a></div>
|
||||
<div class="modalClose"><a href="#" onclick="closeAllModals()">×</a></div>
|
||||
<h2>Update Request</h2>
|
||||
<div class="error" id="updateRequestError"></div>
|
||||
<br>
|
||||
|
@ -84,7 +87,7 @@
|
|||
</div>
|
||||
<br>
|
||||
<div>
|
||||
<a id="updateMetadataLink" href="#" onclick="updateRequestMetadata(
|
||||
<a href="#" onclick="updateRequestMetadata(
|
||||
document.getElementById('updateRequestUrl').innerText
|
||||
)">Update Request Metadata</a>
|
||||
<br>
|
||||
|
@ -112,6 +115,43 @@
|
|||
<button onclick="closeDeleteRequestModal()">No</button>
|
||||
<button onclick="deleteRequest(document.getElementById('updateRequestUrl').innerText)">Yes</button>
|
||||
</div>
|
||||
<div class="modal" id="streamerSettingsModal">
|
||||
<div class="modalClose"><a href="#" onclick="closeAllModals()">×</a></div>
|
||||
<h2>Streamer Settings</h2>
|
||||
<div class="error" id="streamerSettingsError"></div>
|
||||
<br>
|
||||
<div>
|
||||
<h3 style='margin-bottom: 0.5em'>Customization</h3>
|
||||
<p>
|
||||
Page Title:
|
||||
<input type="text" id="pageTitle" style="width: 15em" value="<%~ it.pageTitle %>"></input>
|
||||
<button onclick="updatePageTitle(document.getElementById('pageTitle').value)">Submit</button>
|
||||
<button onclick="updatePageTitle('{username}\'s Learn Request Queue')">Reset</button>
|
||||
</p>
|
||||
<p>
|
||||
<h3>Colors:</h3>
|
||||
<p>Click a color to change it</p>
|
||||
<b>Background:</b><br>
|
||||
Primary: <input type="color" id="color-bg-primary" value="<%= it.colors.bg.primary %>"></input><br>
|
||||
Table: <input type="color" id="color-bg-table" value="<%= it.colors.bg.table %>"></input><br>
|
||||
Navbar: <input type="color" id="color-bg-navbar" value="<%= it.colors.bg.navbar %>"></input><br>
|
||||
Error: <input type="color" id="color-bg-error" value="<%= it.colors.bg.error %>"></input><br>
|
||||
<br>
|
||||
<b>Foreground:</b><br>
|
||||
Primary: <input type="color" id="color-fg-primary" value="<%= it.colors.fg.primary %>"></input><br>
|
||||
Link Hover: <input type="color" id="color-fg-ahover" value="<%= it.colors.fg.ahover %>"></input><br>
|
||||
Title: <input type="color" id="color-fg-title" value="<%= it.colors.fg.title %>"></input><br>
|
||||
<br>
|
||||
<button onclick="updateColors(getColorObject())">Submit</button>
|
||||
<button onclick="updateColors({bg:{error: '#ff0000',table: '#282828',navbar: '#666666',primary: '#444444'},fg: { title: '#eeeeee', ahover: '#ffffff', primary: '#dddddd'}})">Reset</button>
|
||||
</p>
|
||||
</div>
|
||||
<hr>
|
||||
<div>
|
||||
<h3 style='margin-bottom: 0.5em'>Batch Jobs</h3>
|
||||
<a href="#" onclick="cronRequest('processBans')">Force Refresh Banned Users (NYI)</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
|
|
Loading…
Reference in New Issue