From 4607eaf30791f14b7624c46940fa4d08e3b02220 Mon Sep 17 00:00:00 2001 From: Dessa Simpson Date: Wed, 24 Feb 2021 20:28:02 -0700 Subject: [PATCH 01/18] Move ordering from views to queries.ts MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Turns out views aren't guaranteed to keep their order ¯\_(ツ)_/¯ Fixes #24 --- db/90-views.sql | 6 ++---- src/queries.ts | 12 +++++++----- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/db/90-views.sql b/db/90-views.sql index b0d3f72..14896b0 100644 --- a/db/90-views.sql +++ b/db/90-views.sql @@ -2,8 +2,7 @@ CREATE OR REPLACE VIEW requests_vw AS SELECT url,COALESCE(videoTitle,url) AS title,displayName AS requester,imageUrl,state,score,reqTimestamp FROM requests JOIN requestMetadata USING (url) JOIN scores USING (url) - JOIN users ON requests.requester = users.userid - ORDER BY score DESC, reqTimestamp ASC; + JOIN users ON requests.requester = users.userid; CREATE OR REPLACE FUNCTION get_requests_voted(votinguserid int) RETURNS TABLE ( @@ -21,8 +20,7 @@ CREATE OR REPLACE FUNCTION get_requests_voted(votinguserid int) SELECT url,title,requester,imageUrl,state,score,reqTimestamp, (CASE WHEN votes.userid IS NULL THEN FALSE ELSE TRUE END) AS voted FROM requests_vw - LEFT JOIN votes ON (requests_vw.url = votes.requesturl AND votes.userid = votinguserid) - ORDER BY score DESC, reqTimestamp ASC; + LEFT JOIN votes ON (requests_vw.url = votes.requesturl AND votes.userid = votinguserid); $$; /* diff --git a/src/queries.ts b/src/queries.ts index 5c7c7fd..9b1712d 100644 --- a/src/queries.ts +++ b/src/queries.ts @@ -57,24 +57,26 @@ export const getRequests = { name: "getRequests", text: "SELECT * FROM requests_vw \ JOIN states ON requests_vw.state = states.state \ - WHERE active LIMIT $1" + WHERE active ORDER BY score DESC, reqTimestamp ASC LIMIT $1" } export const getRequestsVoted = { name: "getRequestsVoted", text: "SELECT * FROM get_requests_voted($2) \ JOIN states ON get_requests_voted.state = states.state \ - WHERE active LIMIT $1" + WHERE active ORDER BY score DESC, reqTimestamp ASC LIMIT $1" } export const getAllRequests = { name: "getAllRequests", - text: "SELECT * FROM requests_vw LIMIT $1" + text: "SELECT * FROM requests_vw \ + ORDER BY score DESC, reqTimestamp ASC LIMIT $1" } export const getAllRequestsVoted = { name: "getAllRequestsVoted", - text: "SELECT * FROM get_requests_voted($2) LIMIT $1" + text: "SELECT * FROM get_requests_voted($2) \ + ORDER BY score DESC, reqTimestamp ASC LIMIT $1" } export const checkRequestExists = { @@ -137,7 +139,7 @@ export const getAndLockCronJob = { text: `SELECT * FROM cron WHERE (lastSuccess + runInterval) < now() AND jobName = $1 - FOR UPDATE SKIP LOCKED;` + FOR UPDATE SKIP LOCKED` } export const updateCronJobLastSuccess = { From c634e763f39302bfc7f8059748b3b71a062385bb Mon Sep 17 00:00:00 2001 From: Dessa Simpson Date: Wed, 24 Feb 2021 20:45:50 -0700 Subject: [PATCH 02/18] Bump version to v0.5.1 --- src/version.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/version.ts b/src/version.ts index d149032..e071e95 100644 --- a/src/version.ts +++ b/src/version.ts @@ -6,7 +6,7 @@ import db from "./db"; var versionMajor = 0; var versionMinor = 5; -var versionPatch = 0; +var versionPatch = 1; export function getVersion() { return `${versionMajor}.${versionMinor}.${versionPatch}` From 84a22ccffd055e68ce04ec63d2972353e87da937 Mon Sep 17 00:00:00 2001 From: Dessa Simpson Date: Thu, 25 Feb 2021 19:52:57 -0700 Subject: [PATCH 03/18] Add pagination Note: Requests API endpoints /getRequests and /getRequestsAll have breaking changes (the response schema changed entirely). Fixes #26 --- public/main.js | 57 +++++++++++++++++++++++++++++++++++------------- public/style.css | 5 +++++ src/app.ts | 43 +++++++++++++++++++++++++----------- src/queries.ts | 27 ++++++++++++++++------- src/requests.ts | 26 +++++++++++++++------- views/main.eta | 34 +++++++++++++++++++++-------- 6 files changed, 139 insertions(+), 53 deletions(-) diff --git a/public/main.js b/public/main.js index 73559b2..3b2da71 100644 --- a/public/main.js +++ b/public/main.js @@ -1,29 +1,49 @@ var requestsDiv = document.getElementById("requests"); -var cronJobs = ['processBans'] +var cronJobs = ['processBans']; +var currentPage = 1; +var totalPages = 1; -function getRequests(count,allRequests) { - var reqUrl; - if (allRequests) { - reqUrl = "/api/getAllRequests"; - } else { - reqUrl = "/api/getRequests"; - } - reqUrl += `?count=${count}`; +function getRequests(count,offset,allRequests) { + if (allRequests) var reqUrl = "/api/getAllRequests"; + else var reqUrl = "/api/getRequests"; + reqUrl += `?count=${count}&offset=${offset}`; fetch(reqUrl) .then(response => response.json()) .then(requests => { - window.requests = requests; - buildTable(); + buildTable(requests); }); } -function buildTable() { +function buildTable(requests) { + totalPages = Math.ceil(requests.total/document.getElementById("count").value); + document.getElementById("totalPages").innerText = totalPages; + if (currentPage <= 1) { + currentPage = 1; + document.getElementById("pageBtnFirst").disabled = true; + document.getElementById("pageBtnPrev").disabled = true; + } else { + document.getElementById("pageBtnFirst").disabled = false; + document.getElementById("pageBtnPrev").disabled = false; + } + if (currentPage >= totalPages) { + currentPage = totalPages; + document.getElementById("pageBtnLast").disabled = true; + document.getElementById("pageBtnNext").disabled = true; + } else { + document.getElementById("pageBtnLast").disabled = false; + document.getElementById("pageBtnNext").disabled = false; + } + document.getElementById("page").innerHTML = ""; + for (i = 1; i <= totalPages; i++) { + document.getElementById("page").innerHTML += ``; + } + document.getElementById("page").value = currentPage; var requestsDivHTML = ''; requestsDivHTML += '' requestsDivHTML += ""; - for (request of requests) { + for (request of requests.requests) { requestsDivHTML += `\ \ `; @@ -45,8 +65,10 @@ function buildTable() { } function updateTable() { - allRequests = document.getElementById("allRequests").checked; - getRequests(document.getElementById("count").value,allRequests); + var count = document.getElementById("count").value; + var offset = (currentPage - 1) * count; + var allRequests = document.getElementById("allRequests").checked; + getRequests(count,offset,allRequests); } function applyUrlTransforms(url) { @@ -66,6 +88,11 @@ function applyUrlTransforms(url) { } } +function goToPage(page) { + currentPage = parseInt(page,10); + updateTable(); +} + function getColorObject() { return { bg: { diff --git a/public/style.css b/public/style.css index 5ff0029..6be863d 100644 --- a/public/style.css +++ b/public/style.css @@ -111,6 +111,11 @@ div#nav-userpic { text-align: right; } +#tableSettings { + display: flex; + justify-content: space-between; +} + #modalBackground { display: none; position: fixed; diff --git a/src/app.ts b/src/app.ts index 3e931ce..efaa358 100644 --- a/src/app.ts +++ b/src/app.ts @@ -42,31 +42,47 @@ app.use(session({ // API app.get("/api/getRequests", async (request, response) => { - if (!request.session) { - throw new Error ("Missing request.session") - } - var requestCount = ( request.query.count ? parseInt(request.query.count as string, 10) : 5 ); + if (!request.session) throw new Error ("Missing request.session"); await validateApiToken(request.session); + var requestCount = ( request.query.count ? parseInt(request.query.count as string, 10) : 5 ); + var requestOffset = ( request.query.offset ? parseInt(request.query.offset as string, 10) : 0 ); + var requestsTotal = await requests.getRequestsTotal(); if (request.session.user) { - requests.getRequestsVoted(requestCount,request.session.user.id).then((val: Array) => response.send(val)) + requests.getRequestsVoted(requestCount,requestOffset,request.session.user.id) + .then((val: Array) => response.send({ + total: requestsTotal, + requests: val + })) .catch((e: any) => errorHandler(request,response,e)); } else { - requests.getRequests(requestCount).then((val: Array) => response.send(val)) + requests.getRequests(requestCount,requestOffset) + .then((val: Array) => response.send({ + total: requestsTotal, + requests: val + })) .catch((e: any) => errorHandler(request,response,e)); } }); app.get("/api/getAllRequests", async (request, response) => { - if (!request.session) { - throw new Error ("Missing request.session") - } - var requestCount = ( request.query.count ? parseInt(request.query.count as string, 10) : 5 ); + if (!request.session) throw new Error ("Missing request.session"); await validateApiToken(request.session); + var requestCount = ( request.query.count ? parseInt(request.query.count as string, 10) : 5 ); + var requestOffset = ( request.query.offset ? parseInt(request.query.offset as string, 10) : 0 ); + var requestsTotal = await requests.getAllRequestsTotal(); if (request.session.user) { - requests.getAllRequestsVoted(requestCount,request.session.user.id).then((val: Array) => response.send(val)) + requests.getAllRequestsVoted(requestCount,requestOffset,request.session.user.id) + .then((val: Array) => response.send({ + total: requestsTotal, + requests: val + })) .catch((e: any) => errorHandler(request,response,e)); } else { - requests.getAllRequests(requestCount).then((val: Array) => response.send(val)) + requests.getAllRequests(requestCount,requestOffset) + .then((val: Array) => response.send({ + total: requestsTotal, + requests: val + })) .catch((e: any) => errorHandler(request,response,e)); } }); @@ -79,7 +95,8 @@ app.post("/api/addRequest", async (request, response) => { response.send("Session expired; please log in again"); return; } - var banned = await db.query(Object.assign(queries.checkBan, { values: [request.session.user.id] })).then((result: pg.QueryResult) => result.rowCount > 0); + var banned = await db.query(Object.assign(queries.checkBan, { values: [request.session.user.id] })) + .then((result: pg.QueryResult) => result.rowCount > 0); if (banned) { response.status(401); response.send("You are banned; you may not add new requests."); diff --git a/src/queries.ts b/src/queries.ts index 9b1712d..b30e482 100644 --- a/src/queries.ts +++ b/src/queries.ts @@ -56,27 +56,38 @@ export const updateVotePoints = { export const getRequests = { name: "getRequests", text: "SELECT * FROM requests_vw \ - JOIN states ON requests_vw.state = states.state \ - WHERE active ORDER BY score DESC, reqTimestamp ASC LIMIT $1" + JOIN states ON requests_vw.state = states.state WHERE active \ + ORDER BY score DESC, reqTimestamp ASC LIMIT $1 OFFSET $2" } export const getRequestsVoted = { name: "getRequestsVoted", - text: "SELECT * FROM get_requests_voted($2) \ - JOIN states ON get_requests_voted.state = states.state \ - WHERE active ORDER BY score DESC, reqTimestamp ASC LIMIT $1" + text: "SELECT * FROM get_requests_voted($3) \ + JOIN states ON get_requests_voted.state = states.state WHERE active \ + ORDER BY score DESC, reqTimestamp ASC LIMIT $1 OFFSET $2" +} + +export const getRequestsTotal = { + name: "getRequestsTotal", + text: "SELECT COUNT(*) FROM requests_vw \ + JOIN states ON requests_vw.state = states.state WHERE active" } export const getAllRequests = { name: "getAllRequests", text: "SELECT * FROM requests_vw \ - ORDER BY score DESC, reqTimestamp ASC LIMIT $1" + ORDER BY score DESC, reqTimestamp ASC LIMIT $1 OFFSET $2" } export const getAllRequestsVoted = { name: "getAllRequestsVoted", - text: "SELECT * FROM get_requests_voted($2) \ - ORDER BY score DESC, reqTimestamp ASC LIMIT $1" + text: "SELECT * FROM get_requests_voted($3) \ + ORDER BY score DESC, reqTimestamp ASC LIMIT $1 OFFSET $2" +} + +export const getAllRequestsTotal = { + name: "getAllRequestsTotal", + text: "SELECT COUNT(*) FROM requests_vw" } export const checkRequestExists = { diff --git a/src/requests.ts b/src/requests.ts index d23611e..ef3e1e4 100644 --- a/src/requests.ts +++ b/src/requests.ts @@ -4,30 +4,40 @@ import { log, LogLevel } from "./logging" import pg from "pg"; import db from "./db"; -export async function getRequests(count: number) { - var query = Object.assign(queries.getRequests, { values: [count] }); +export async function getRequests(count: number, offset: number) { + var query = Object.assign(queries.getRequests, { values: [count,offset] }); return db.query(query) .then((result: pg.QueryResult) => result.rows); }; -export async function getAllRequests(count: number) { - var query = Object.assign(queries.getAllRequests, { values: [count] }); +export async function getRequestsVoted(count: number, offset: number, user: number) { + var query = Object.assign(queries.getRequestsVoted, { values: [count,offset,user] }); return db.query(query) .then((result: pg.QueryResult) => result.rows); }; -export async function getRequestsVoted(count: number, user: number) { - var query = Object.assign(queries.getRequestsVoted, { values: [count,user] }); +export async function getRequestsTotal() { + return db.query(queries.getRequestsTotal) + .then((result: pg.QueryResult) => result.rows[0]["count"]); +}; + +export async function getAllRequests(count: number, offset: number) { + var query = Object.assign(queries.getAllRequests, { values: [count,offset] }); return db.query(query) .then((result: pg.QueryResult) => result.rows); }; -export async function getAllRequestsVoted(count: number,user: number) { - var query = Object.assign(queries.getAllRequestsVoted, { values: [count,user] }); +export async function getAllRequestsVoted(count: number, offset: number, user: number) { + var query = Object.assign(queries.getAllRequestsVoted, { values: [count,offset,user] }); return db.query(query) .then((result: pg.QueryResult) => result.rows); }; +export async function getAllRequestsTotal() { + return db.query(queries.getAllRequestsTotal) + .then((result: pg.QueryResult) => result.rows[0]["count"]); +}; + const validUrlRegexes = [ /^https:\/\/www\.youtube\.com\/watch\?v=[a-zA-Z0-9_-]{11}$/ ]; diff --git a/views/main.eta b/views/main.eta index f17520c..9f986d1 100644 --- a/views/main.eta +++ b/views/main.eta @@ -30,15 +30,31 @@

- Count: - - View requests in any state +
+ + Count: + + + + + + + of ? + + + + + View learned and rejected requests + +
SongRequesterScoreState'; if (window.loggedIn) requestsDivHTML += 'Vote'; if (window.isStreamer) requestsDivHTML += 'Update
${request.imageurl ? `` : ''}${request.requester}${request.score}
'; + var requestsDivHTML = '
SongRequesterScore
'; requestsDivHTML += '' @@ -65,10 +68,9 @@ function buildTable(requests) { } function updateTable() { - var count = document.getElementById("count").value; var offset = (currentPage - 1) * count; var allRequests = document.getElementById("allRequests").checked; - getRequests(count,offset,allRequests); + getRequests(offset,allRequests); } function applyUrlTransforms(url) { @@ -375,5 +377,16 @@ for(state of validStates) { var opt = document.createElement("option"); opt.text = state; opt.value = state; - updateRequestStateSelect.add(opt) + updateRequestStateSelect.add(opt); +} + +function toggleSortDir() { + if (window.sortDir == "desc") { + document.getElementById("sortDir").innerText = "↑"; + window.sortDir = "asc"; + } else { + document.getElementById("sortDir").innerText = "↓"; + window.sortDir = "desc"; + } + updateTable(); } diff --git a/public/style.css b/public/style.css index 6be863d..ccea461 100644 --- a/public/style.css +++ b/public/style.css @@ -111,11 +111,20 @@ div#nav-userpic { text-align: right; } -#tableSettings { +.tableSettings { display: flex; + align-items: center; justify-content: space-between; } +.tableSettings > span { + margin: auto; +} + +#tableSettingsTop { + margin-bottom: 10px; +} + #modalBackground { display: none; position: fixed; @@ -128,6 +137,7 @@ div#nav-userpic { height: 100%; background-color: #444; background-color: #444a; + margin: 5px; } .modal { @@ -201,3 +211,7 @@ div#nav-userpic { display: flex; justify-content: space-between; } + +#sortDir { + padding: 0; +} diff --git a/src/app.ts b/src/app.ts index ee577a3..583a56e 100644 --- a/src/app.ts +++ b/src/app.ts @@ -47,23 +47,26 @@ app.get("/api/getRequests", async (request, response) => { var requestCount = ( request.query.count ? parseInt(request.query.count as string, 10) : 5 ); var requestOffset = ( request.query.offset ? parseInt(request.query.offset as string, 10) : 0 ); var sortDirection = ( request.query.sortDirection == "asc" ? "ASC" : "DESC" ); + var inverseSortDirection = ( sortDirection == "ASC" ? "DESC" : "ASC" ); switch (request.query.sort) { - case undefined: - case "score": - var requestSort = `score ${sortDirection}, reqTimestamp ASC`; - break; + case undefined: // Default sort by newest case "timestamp": var requestSort = `reqTimestamp ${sortDirection}`; break; - case "alpha": + case "score": + var requestSort = `score ${sortDirection}, reqTimestamp ${inverseSortDirection}`; + break; + case "title": var requestSort = `title ${sortDirection}` break; + case "requester": + var requestSort = `requester ${sortDirection}, title ${sortDirection}` + break; default: response.status(400); response.send("Invalid sort"); return; }; - console.log('foo') var requestsTotal = await requests.getRequestsTotal(); if (request.session.user) { requests.getRequestsVoted(requestCount,requestOffset,requestSort,request.session.user.id) diff --git a/views/main.eta b/views/main.eta index 9f986d1..ce7d520 100644 --- a/views/main.eta +++ b/views/main.eta @@ -29,11 +29,10 @@ <%- } %>
-

-
+
Count: - @@ -41,6 +40,22 @@ + + Sort by: + + + + + View all requests + +
+

+
@@ -51,9 +66,6 @@ - - View learned and rejected requests -
From 632dc73ee9dc85bbab87882e008ded92fb244fbd Mon Sep 17 00:00:00 2001 From: Dessa Simpson Date: Sun, 25 Apr 2021 20:42:20 -0700 Subject: [PATCH 10/18] Add database logic to clear out requests that have 0 votes Also, clean up some db constraints. Fixes #29 --- db/20-requests.sql | 2 +- db/30-votes.sql | 4 ++-- db/95-procedures.sql | 8 ++++++++ db/upgrade/v0.6-v0.7.sql | 24 ++++++++++++++++++++++++ 4 files changed, 35 insertions(+), 3 deletions(-) create mode 100644 db/upgrade/v0.6-v0.7.sql diff --git a/db/20-requests.sql b/db/20-requests.sql index c3f3cf5..b63c359 100644 --- a/db/20-requests.sql +++ b/db/20-requests.sql @@ -1,5 +1,5 @@ CREATE TABLE requests ( - url varchar NOT NULL UNIQUE, + url varchar NOT NULL, requester int NOT NULL, state varchar NOT NULL DEFAULT 'Requested', reqTimestamp timestamptz NOT NULL DEFAULT NOW(), diff --git a/db/30-votes.sql b/db/30-votes.sql index 0b878ba..8da76f0 100644 --- a/db/30-votes.sql +++ b/db/30-votes.sql @@ -1,6 +1,6 @@ CREATE TABLE votes ( - requestUrl varchar, - userId int, + requestUrl varchar NOT NULL, + userId int NOT NULL, PRIMARY KEY (requestUrl,userId), FOREIGN KEY (requestUrl) REFERENCES requests(url) ON DELETE CASCADE, FOREIGN KEY (userId) REFERENCES users(userId) ON DELETE CASCADE diff --git a/db/95-procedures.sql b/db/95-procedures.sql index 28ec802..650f119 100644 --- a/db/95-procedures.sql +++ b/db/95-procedures.sql @@ -16,6 +16,13 @@ CREATE OR REPLACE PROCEDURE add_request(url varchar,requester int) CALL update_scores(); $$; +CREATE OR REPLACE PROCEDURE clear_zero_votes() + LANGUAGE SQL + AS $$ + DELETE FROM requests WHERE NOT EXISTS + (SELECT FROM votes WHERE requests.url = votes.requesturl); + $$; + CREATE OR REPLACE PROCEDURE add_vote(url varchar,voteuser int) LANGUAGE SQL AS $$ @@ -28,6 +35,7 @@ CREATE OR REPLACE PROCEDURE delete_vote(url varchar,voteuser int) AS $$ DELETE FROM votes WHERE requesturl = url AND userid = voteuser; CALL update_scores(); + CALL clear_zero_votes(); $$; CREATE OR REPLACE PROCEDURE update_request_score_modifier(updateurl varchar, scoreDiff int) diff --git a/db/upgrade/v0.6-v0.7.sql b/db/upgrade/v0.6-v0.7.sql new file mode 100644 index 0000000..4e0d89a --- /dev/null +++ b/db/upgrade/v0.6-v0.7.sql @@ -0,0 +1,24 @@ +BEGIN; + +UPDATE version SET minor = 7; + +ALTER TABLE votes + ALTER COLUMN requestUrl SET NOT NULL + ALTER COLUMN userId SET NOT NULL; + +CREATE OR REPLACE PROCEDURE clear_zero_votes() + LANGUAGE SQL + AS $$ + DELETE FROM requests WHERE NOT EXISTS + (SELECT FROM votes WHERE requests.url = votes.requesturl); + $$; + +CREATE OR REPLACE PROCEDURE delete_vote(url varchar,voteuser int) + LANGUAGE SQL + AS $$ + DELETE FROM votes WHERE requesturl = url AND userid = voteuser; + CALL update_scores(); + CALL clear_zero_votes(); + $$; + +COMMIT; From 347a6d232415874a2bb6f81ff8173628d303b165 Mon Sep 17 00:00:00 2001 From: Dessa Simpson Date: Sun, 25 Apr 2021 22:09:16 -0700 Subject: [PATCH 11/18] Add comma in v0.6-v0.7.sql --- db/upgrade/v0.6-v0.7.sql | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/db/upgrade/v0.6-v0.7.sql b/db/upgrade/v0.6-v0.7.sql index 4e0d89a..0939227 100644 --- a/db/upgrade/v0.6-v0.7.sql +++ b/db/upgrade/v0.6-v0.7.sql @@ -3,7 +3,7 @@ BEGIN; UPDATE version SET minor = 7; ALTER TABLE votes - ALTER COLUMN requestUrl SET NOT NULL + ALTER COLUMN requestUrl SET NOT NULL, ALTER COLUMN userId SET NOT NULL; CREATE OR REPLACE PROCEDURE clear_zero_votes() From fc09fd4dd426446de1b930594c06e73d5fc21092 Mon Sep 17 00:00:00 2001 From: Dessa Simpson Date: Sun, 25 Apr 2021 22:15:38 -0700 Subject: [PATCH 12/18] Bump version to v0.7 --- db/00-version.sql | 2 +- src/version.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/db/00-version.sql b/db/00-version.sql index 02983fb..23be6fa 100644 --- a/db/00-version.sql +++ b/db/00-version.sql @@ -8,4 +8,4 @@ CREATE OR REPLACE FUNCTION get_version() RETURNS VARCHAR AS $$SELECT major || '.' || minor FROM version $$ LANGUAGE SQL; -INSERT INTO version (major,minor) VALUES (0,6); +INSERT INTO version (major,minor) VALUES (0,7); diff --git a/src/version.ts b/src/version.ts index e062152..2751ddf 100644 --- a/src/version.ts +++ b/src/version.ts @@ -5,7 +5,7 @@ import pg from "pg"; import db from "./db"; var versionMajor = 0; -var versionMinor = 6; +var versionMinor = 7; var versionPatch = 0; export function getVersion() { From 891ccfae92ca3e1ec776874edab7da108ffa398f Mon Sep 17 00:00:00 2001 From: Dessa Simpson Date: Mon, 26 Apr 2021 05:16:43 +0000 Subject: [PATCH 13/18] Update 'RELEASE.md' --- RELEASE.md | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/RELEASE.md b/RELEASE.md index f1c186f..c60d7c9 100644 --- a/RELEASE.md +++ b/RELEASE.md @@ -12,9 +12,19 @@ If the answer to any of the above is yes, then the release MUST be a major or mi - [ ] Add a commit which adds a database upgrade script at `db/upgrade/[oldversion]-[newversion].sql` - ALWAYS use a transaction for the entirety of this file - At minimum, DB version must be bumped -- [ ] Add a commit which bumps the version in `app/version.ts` and `db/00-version.sql` + - Test the upgrade script as follows: + ``` + git checkout [previous release tag] + docker-compose down + docker-compose up + psql -h 0 -U postgres < db/upgrade/v[previous]-v[current].sql + ``` + Update the version in `src/version.ts` and verify the app works as expected. +- [ ] Add a commit which bumps the version in `src/version.ts` and `db/00-version.sql`, entitled `Bump version to vMAJOR.MINOR` - [ ] Tag the latest commit with `vMAJOR.MINOR` +- [ ] Write release notes ## Patch Releases -- [ ] Add a commit which bumps the patch level in `app/version.ts` +- [ ] Add a commit which bumps the patch level in `src/version.ts`, entitled `Bump version to vMAJOR.MINOR.PATCH` - [ ] Tag the latest commit with `vMAJOR.MINOR.PATCH` +- [ ] Write release notes \ No newline at end of file From 7897fe6f3dc2a1080a024c9a47689c68414e0f48 Mon Sep 17 00:00:00 2001 From: Dessa Simpson Date: Sun, 6 Jun 2021 23:11:30 -0700 Subject: [PATCH 14/18] Add ratelimits Fixes #10 --- db/05-config.sql | 16 +++++++++------- db/90-views.sql | 19 +++++++++++++++++++ db/upgrade/v0.7-v0.8.sql | 34 ++++++++++++++++++++++++++++++++++ src/queries.ts | 5 +++++ src/requests.ts | 22 ++++++++++++++++++---- 5 files changed, 85 insertions(+), 11 deletions(-) create mode 100644 db/upgrade/v0.7-v0.8.sql diff --git a/db/05-config.sql b/db/05-config.sql index 1c4b65a..2e830ab 100644 --- a/db/05-config.sql +++ b/db/05-config.sql @@ -1,12 +1,14 @@ CREATE TABLE config ( rowlock bool DEFAULT TRUE UNIQUE NOT NULL CHECK (rowlock = TRUE), - normaluservotepoints int NOT NULL, - followervotepoints int NOT NULL, - subscribervotepoints int NOT NULL, - title varchar NOT NULL, - colors jsonb NOT NULL, + normaluservotepoints int NOT NULL DEFAULT 10, + followervotepoints int NOT NULL DEFAULT 50, + subscribervotepoints int NOT NULL DEFAULT 100, + normaluserratelimit int NOT NULL DEFAULT 1, + followerratelimit int NOT NULL DEFAULT 2, + subscriberratelimit int NOT NULL DEFAULT 3, + title varchar NOT NULL DEFAULT '{username}''s Learn Request Queue', + colors jsonb NOT NULL DEFAULT '{"bg": {"primary": "#444444","table": "#282828","navbar": "#666666","error": "#ff0000"},"fg": {"primary": "#dddddd","ahover": "#ffffff","title": "#eeeeee"}}', PRIMARY KEY (rowLock) ); -INSERT INTO config (normalUserVotePoints,followerVotePoints,subscriberVotePoints,title,colors) - VALUES (10,50,100,'{username}''s Learn Request Queue','{"bg": {"primary": "#444444","table": "#282828","navbar": "#666666","error": "#ff0000"},"fg": {"primary": "#dddddd","ahover": "#ffffff","title": "#eeeeee"}}'); +INSERT INTO config (rowlock) VALUES (true); diff --git a/db/90-views.sql b/db/90-views.sql index 14896b0..a31ec16 100644 --- a/db/90-views.sql +++ b/db/90-views.sql @@ -60,3 +60,22 @@ CREATE OR REPLACE VIEW streamer_user_vw AS SELECT users.userid as userid, users.displayname as displayname, users.imageurl as imageurl FROM streamer LEFT JOIN users ON streamer.userid = users.userid; + +CREATE OR REPLACE VIEW ratelimit_vw AS + SELECT users.userid,COALESCE(count,0),ratelimit.reqcount AS max,COALESCE(count,0) >= ratelimit.reqcount AS status + FROM users + LEFT JOIN (SELECT requester,COUNT(url) + FROM requests + WHERE reqtimestamp > (now() - '24 hours'::interval) + GROUP BY requests.requester + ) AS requests ON users.userid = requests.requester + LEFT JOIN follows ON requests.requester = follows.userid + LEFT JOIN subscriptions ON requests.requester = subscriptions.userid + CROSS JOIN config + CROSS JOIN LATERAL (VALUES ( + CASE + WHEN follows.userid IS NULL AND subscriptions.userid IS NULL THEN config.normaluserratelimit + WHEN follows.userid IS NOT NULL AND subscriptions.userid IS NULL THEN config.followerratelimit + WHEN subscriptions.userid IS NOT NULL THEN config.subscriberratelimit + END + )) AS ratelimit(reqcount); diff --git a/db/upgrade/v0.7-v0.8.sql b/db/upgrade/v0.7-v0.8.sql new file mode 100644 index 0000000..c5d5ab7 --- /dev/null +++ b/db/upgrade/v0.7-v0.8.sql @@ -0,0 +1,34 @@ +BEGIN; + +ALTER TABLE config + ALTER COLUMN normaluservotepoints SET DEFAULT 10, + ALTER COLUMN followervotepoints SET DEFAULT 50, + ALTER COLUMN subscribervotepoints SET DEFAULT 100, + ALTER COLUMN title SET DEFAULT '{username}''s Learn Request Queue', + ALTER COLUMN colors SET DEFAULT '{"bg": {"primary": "#444444","table": "#282828","navbar": "#666666","error": "#ff0000"},"fg": {"primary": "#dddddd","ahover": "#ffffff","title": "#eeeeee"}}', + ADD COLUMN normaluserratelimit int NOT NULL DEFAULT 1, + ADD COLUMN followerratelimit int NOT NULL DEFAULT 2, + ADD COLUMN subscriberratelimit int NOT NULL DEFAULT 3; + +CREATE OR REPLACE VIEW ratelimit_vw AS + SELECT users.userid,COALESCE(count,0),ratelimit.reqcount AS max,COALESCE(count,0) >= ratelimit.reqcount AS status + FROM users + LEFT JOIN (SELECT requester,COUNT(url) + FROM requests + WHERE reqtimestamp > (now() - '24 hours'::interval) + GROUP BY requests.requester + ) AS requests ON users.userid = requests.requester + LEFT JOIN follows ON requests.requester = follows.userid + LEFT JOIN subscriptions ON requests.requester = subscriptions.userid + CROSS JOIN config + CROSS JOIN LATERAL (VALUES ( + CASE + WHEN follows.userid IS NULL AND subscriptions.userid IS NULL THEN config.normaluserratelimit + WHEN follows.userid IS NOT NULL AND subscriptions.userid IS NULL THEN config.followerratelimit + WHEN subscriptions.userid IS NOT NULL THEN config.subscriberratelimit + END + )) AS ratelimit(reqcount); + + +ROLLBACK; +--COMMIT; diff --git a/src/queries.ts b/src/queries.ts index 09c615b..8fa54e6 100644 --- a/src/queries.ts +++ b/src/queries.ts @@ -130,3 +130,8 @@ export const updateCronJobLastSuccess = { name: "updateCronJobLastSuccess", text: "UPDATE cron SET lastSuccess = now() WHERE jobName = $1" } + +export const getRateLimitStatus = { + name: "getRateLimitStatus", + text: "SELECT * FROM ratelimit_vw WHERE userid = $1" +} diff --git a/src/requests.ts b/src/requests.ts index 0033526..1a35094 100644 --- a/src/requests.ts +++ b/src/requests.ts @@ -87,6 +87,7 @@ async function retrieveYoutubeMetadata(url: string) { } export async function addRequest(url: string, requester: string): Promise<[number,string]> { + // Check that URL is of an accepted format var validUrl = false; for (var regex of validUrlRegexes) { if (regex.test(url)) { @@ -95,11 +96,24 @@ export async function addRequest(url: string, requester: string): Promise<[numbe } } if (!validUrl) return [400, "Invalid song URL."]; - var result = await checkRequestExists(url) - if (result) { - console.log(result); - return [200,`Song already requested by ${result.rows[0].requester}. State: ${result.rows[0].state}`] + + // Check whether the URL has already been requested + var existsResult = await checkRequestExists(url) + if (existsResult) { + console.log(existsResult); + return [200,`Song already requested by ${existsResult.rows[0].requester}. State: ${existsResult.rows[0].state}`] } + + // Check whether the user has hit their rate limit + var rateLimitQuery = Object.assign(queries.getRateLimitStatus, { values: [requester] }); + var rateLimitResult = (await db.query(rateLimitQuery)).rows[0]; + if (rateLimitResult.status) { + return [429,`You have reached your maximum of ${rateLimitResult.max} requests per day. + Please try again later.\n + Tip: Removing one of your requests from the past 24 hours by retracting your vote will allow you to replace it with another.`]; + } + + // Add the request var query = Object.assign(queries.addRequest, { values: [url,requester] }); return db.query(query) .then(async () => { From 8b49d51e08e947a99588608f7d27a34cea177af7 Mon Sep 17 00:00:00 2001 From: Dessa Simpson Date: Sun, 6 Jun 2021 23:20:47 -0700 Subject: [PATCH 15/18] Trim whitespace from URL on client side Fixes #32 --- public/main.js | 1 + 1 file changed, 1 insertion(+) diff --git a/public/main.js b/public/main.js index a5fc0dd..e9fee5d 100644 --- a/public/main.js +++ b/public/main.js @@ -75,6 +75,7 @@ function updateTable() { function applyUrlTransforms(url) { console.log("Begin applyUrlTransforms:" + url); + url = url.trim(); if (url.match(/^https?:\/\/(www\.)?youtu(\.be|be\.com)\//)) { // Youtube console.log("Youtube"); var videoid = ""; From 90f672b2888d8c72bf819f49ca861b817f4f50ee Mon Sep 17 00:00:00 2001 From: Dessa Simpson Date: Sun, 6 Jun 2021 23:32:53 -0700 Subject: [PATCH 16/18] Bump version to v0.8 --- db/00-version.sql | 2 +- db/upgrade/v0.7-v0.8.sql | 5 +++-- src/version.ts | 2 +- 3 files changed, 5 insertions(+), 4 deletions(-) diff --git a/db/00-version.sql b/db/00-version.sql index 23be6fa..6686897 100644 --- a/db/00-version.sql +++ b/db/00-version.sql @@ -8,4 +8,4 @@ CREATE OR REPLACE FUNCTION get_version() RETURNS VARCHAR AS $$SELECT major || '.' || minor FROM version $$ LANGUAGE SQL; -INSERT INTO version (major,minor) VALUES (0,7); +INSERT INTO version (major,minor) VALUES (0,8); diff --git a/db/upgrade/v0.7-v0.8.sql b/db/upgrade/v0.7-v0.8.sql index c5d5ab7..af83dae 100644 --- a/db/upgrade/v0.7-v0.8.sql +++ b/db/upgrade/v0.7-v0.8.sql @@ -1,5 +1,7 @@ BEGIN; +UPDATE version SET minor = 8; + ALTER TABLE config ALTER COLUMN normaluservotepoints SET DEFAULT 10, ALTER COLUMN followervotepoints SET DEFAULT 50, @@ -30,5 +32,4 @@ CREATE OR REPLACE VIEW ratelimit_vw AS )) AS ratelimit(reqcount); -ROLLBACK; ---COMMIT; +COMMIT; diff --git a/src/version.ts b/src/version.ts index 2751ddf..58b3b13 100644 --- a/src/version.ts +++ b/src/version.ts @@ -5,7 +5,7 @@ import pg from "pg"; import db from "./db"; var versionMajor = 0; -var versionMinor = 7; +var versionMinor = 8; var versionPatch = 0; export function getVersion() { From ac953b0bb5384ab1d852047d5699672681a78f34 Mon Sep 17 00:00:00 2001 From: Dessa Simpson Date: Sun, 27 Feb 2022 17:16:48 -0700 Subject: [PATCH 17/18] Uncomment debug lines --- src/cronjobs/processBans.ts | 8 ++++---- src/cronjobs/processSubscriptions.ts | 8 ++++---- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/src/cronjobs/processBans.ts b/src/cronjobs/processBans.ts index 2316612..a47a2fa 100644 --- a/src/cronjobs/processBans.ts +++ b/src/cronjobs/processBans.ts @@ -9,8 +9,8 @@ export async function processBans(streamer: twitch.StreamerUserIdTokenPair) { await dbconn.query("DELETE FROM bans"); var response = await twitch.streamerApiRequest(streamer, `/moderation/banned?broadcaster_id=${streamer.userid}&first=100`); - //log(LogLevel.DEBUG,"Ban API response:") - //log(LogLevel.DEBUG,JSON.stringify(response,null,2)); + log(LogLevel.DEBUG,"Ban API response:") + log(LogLevel.DEBUG,JSON.stringify(response,null,2)); while (true) { var insertBanQuery = "INSERT INTO bans (userid) VALUES "; var banRow = 0; // Used for $1, $2, etc. in parameterized query @@ -37,8 +37,8 @@ export async function processBans(streamer: twitch.StreamerUserIdTokenPair) { var oldFirstUserid = response.data[0].user_id; response = await twitch.streamerApiRequest(streamer, `/moderation/banned?broadcaster_id=${streamer.userid}&after=${response.pagination.cursor}&first=100`); - //log(LogLevel.DEBUG,"Ban API response:"); - //log(LogLevel.DEBUG,JSON.stringify(response,null,2)); + log(LogLevel.DEBUG,"Ban API response:"); + log(LogLevel.DEBUG,JSON.stringify(response,null,2)); // Work around broken api endpoint giving a cursor referring to the // current page, causing an infinite loop if (oldFirstUserid == response.data[0].user_id) break; diff --git a/src/cronjobs/processSubscriptions.ts b/src/cronjobs/processSubscriptions.ts index 3e250df..980e69b 100644 --- a/src/cronjobs/processSubscriptions.ts +++ b/src/cronjobs/processSubscriptions.ts @@ -9,8 +9,8 @@ export async function processSubscriptions(streamer: twitch.StreamerUserIdTokenP await dbconn.query("DELETE FROM subscriptions"); var response = await twitch.streamerApiRequest(streamer, `/subscriptions?broadcaster_id=${streamer.userid}&first=100`); - //log(LogLevel.DEBUG,"Subscriptions API response:") - //log(LogLevel.DEBUG,JSON.stringify(response,null,2)); + log(LogLevel.DEBUG,"Subscriptions API response:") + log(LogLevel.DEBUG,JSON.stringify(response,null,2)); while (true) { var insertSubscriptionQuery = "INSERT INTO subscriptions (userid) VALUES "; var subscriptionRow = 0; // Used for $1, $2, etc. in parameterized query @@ -36,8 +36,8 @@ export async function processSubscriptions(streamer: twitch.StreamerUserIdTokenP response = await twitch.streamerApiRequest(streamer, `/subscriptions?broadcaster_id=${streamer.userid}&after=${response.pagination.cursor}&first=100`); if (oldFirstUserid == response.data[0].user_id) break; - //log(LogLevel.DEBUG,"Subscription API response:"); - //log(LogLevel.DEBUG,JSON.stringify(response,null,2)); + log(LogLevel.DEBUG,"Subscription API response:"); + log(LogLevel.DEBUG,JSON.stringify(response,null,2)); } else { break; } From dd423bdb344bea065f421c97329323ca97cf3675 Mon Sep 17 00:00:00 2001 From: Dessa Simpson Date: Mon, 28 Feb 2022 09:34:18 -0700 Subject: [PATCH 18/18] Fix empty subscriptions page crash Fix issue where when Twitch returns an empty page of subscribers, the app crashes due to trying to get response.data[0].user_id where response.data[0] is undefined Also adds some debug logging --- src/cronjobs/processSubscriptions.ts | 3 ++- src/twitch.ts | 1 + 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/src/cronjobs/processSubscriptions.ts b/src/cronjobs/processSubscriptions.ts index 980e69b..5c53a10 100644 --- a/src/cronjobs/processSubscriptions.ts +++ b/src/cronjobs/processSubscriptions.ts @@ -35,9 +35,9 @@ export async function processSubscriptions(streamer: twitch.StreamerUserIdTokenP var oldFirstUserid = response.data[0].user_id; response = await twitch.streamerApiRequest(streamer, `/subscriptions?broadcaster_id=${streamer.userid}&after=${response.pagination.cursor}&first=100`); - if (oldFirstUserid == response.data[0].user_id) break; log(LogLevel.DEBUG,"Subscription API response:"); log(LogLevel.DEBUG,JSON.stringify(response,null,2)); + if (response.data.length === 0 || oldFirstUserid === response.data[0].user_id) break; } else { break; } @@ -45,6 +45,7 @@ export async function processSubscriptions(streamer: twitch.StreamerUserIdTokenP await dbconn.query("CALL update_scores()"); await dbconn.query('COMMIT'); } catch (e) { + log(LogLevel.ERROR,"cronjobs.processSubscriptions: Exception: " + e); log(LogLevel.ERROR,"cronjobs.processSubscriptions: Exception thrown; rolling back"); await dbconn.query('ROLLBACK'); throw(e); diff --git a/src/twitch.ts b/src/twitch.ts index 0569bee..494bc1a 100644 --- a/src/twitch.ts +++ b/src/twitch.ts @@ -53,6 +53,7 @@ export async function apiRequest(tokens: TokenPair, endpoint: string): Promise < return fetch("https://api.twitch.tv/helix" + endpoint, { headers: headers }) .then(async (res: FetchResponse) => { if (res.status == 200) { + log(LogLevel.DEBUG,"twitch.apiRequest: Request returned 200 for " + endpoint); return res.json(); } else { log(LogLevel.WARNING,"twitch.apiRequest: Failed API request (pre-refresh):");
Song TitleRequesterScoreState'; if (window.loggedIn) requestsDivHTML += 'Vote'; if (window.isStreamer) requestsDivHTML += 'Update