- Count: - - View requests in any state +
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 diff --git a/db/00-version.sql b/db/00-version.sql index 074ae7b..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,5); +INSERT INTO version (major,minor) VALUES (0,8); 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/15-streamer.sql b/db/15-streamer.sql index 53068c8..b39604c 100644 --- a/db/15-streamer.sql +++ b/db/15-streamer.sql @@ -3,4 +3,4 @@ CREATE TABLE streamer ( tokenPair json, PRIMARY KEY (userid), FOREIGN KEY (userid) REFERENCES users(userid) -) +); 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/50-bans.sql b/db/50-bans.sql index 979ae21..20ecfa1 100644 --- a/db/50-bans.sql +++ b/db/50-bans.sql @@ -1,4 +1,4 @@ CREATE TABLE bans ( userid integer, PRIMARY KEY (userid) -) +); diff --git a/db/50-follows.sql b/db/50-follows.sql index c511c43..63e1200 100644 --- a/db/50-follows.sql +++ b/db/50-follows.sql @@ -1,4 +1,4 @@ CREATE TABLE follows ( userid integer, PRIMARY KEY (userid) -) +); diff --git a/db/50-subscriptions.sql b/db/50-subscriptions.sql index 4271623..7e23cf1 100644 --- a/db/50-subscriptions.sql +++ b/db/50-subscriptions.sql @@ -1,4 +1,4 @@ CREATE TABLE subscriptions ( userid integer, PRIMARY KEY (userid) -) +); diff --git a/db/90-views.sql b/db/90-views.sql index b0d3f72..a31ec16 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); $$; /* @@ -62,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/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.5-v0.6.sql b/db/upgrade/v0.5-v0.6.sql new file mode 100644 index 0000000..a43869c --- /dev/null +++ b/db/upgrade/v0.5-v0.6.sql @@ -0,0 +1,5 @@ +BEGIN; + +UPDATE version SET minor = 6; + +COMMIT; diff --git a/db/upgrade/v0.6-v0.7.sql b/db/upgrade/v0.6-v0.7.sql new file mode 100644 index 0000000..0939227 --- /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; diff --git a/db/upgrade/v0.7-v0.8.sql b/db/upgrade/v0.7-v0.8.sql new file mode 100644 index 0000000..af83dae --- /dev/null +++ b/db/upgrade/v0.7-v0.8.sql @@ -0,0 +1,35 @@ +BEGIN; + +UPDATE version SET minor = 8; + +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); + + +COMMIT; diff --git a/public/main.js b/public/main.js index 73559b2..e9fee5d 100644 --- a/public/main.js +++ b/public/main.js @@ -1,29 +1,52 @@ var requestsDiv = document.getElementById("requests"); -var cronJobs = ['processBans'] +var cronJobs = ['processBans']; +var currentPage = 1; +var totalPages = 1; +var count = document.getElementById("count").value; +var sortBy = document.getElementById("sortBy").value; +var sortDir = "desc"; -function getRequests(count,allRequests) { - var reqUrl; - if (allRequests) { - reqUrl = "/api/getAllRequests"; - } else { - reqUrl = "/api/getRequests"; - } - reqUrl += `?count=${count}`; +function getRequests(offset,allRequests) { + if (allRequests) var reqUrl = "/api/getAllRequests"; + else var reqUrl = "/api/getRequests"; + reqUrl += `?count=${count}&offset=${offset}&sort=${sortBy}&sortDirection=${sortDir}`; fetch(reqUrl) .then(response => response.json()) .then(requests => { - window.requests = requests; - buildTable(); + buildTable(requests); }); } -function buildTable() { - var requestsDivHTML = '
Song | Requester | Score | '; +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 = '
---|
Song Title | Requester | Score | '; requestsDivHTML += 'State'; if (window.loggedIn) requestsDivHTML += ' | Vote'; if (window.isStreamer) requestsDivHTML += ' | Update | ' requestsDivHTML += "
---|---|---|---|---|---|
${request.title} | \${request.imageurl ? ` | \
${request.score} | `; @@ -45,12 +68,14 @@ function buildTable() { } function updateTable() { - allRequests = document.getElementById("allRequests").checked; - getRequests(document.getElementById("count").value,allRequests); + var offset = (currentPage - 1) * count; + var allRequests = document.getElementById("allRequests").checked; + getRequests(offset,allRequests); } 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 = ""; @@ -66,6 +91,11 @@ function applyUrlTransforms(url) { } } +function goToPage(page) { + currentPage = parseInt(page,10); + updateTable(); +} + function getColorObject() { return { bg: { @@ -206,7 +236,7 @@ function validateAndSubmitRequest() { updateTable(); document.getElementById("addRequestUrl").value = ""; response.text().then((message) => { - closeAddRequestModal(); + closeAllModals(); showMessage(message); }); }); @@ -348,5 +378,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 5ff0029..ccea461 100644 --- a/public/style.css +++ b/public/style.css @@ -111,6 +111,20 @@ div#nav-userpic { text-align: right; } +.tableSettings { + display: flex; + align-items: center; + justify-content: space-between; +} + +.tableSettings > span { + margin: auto; +} + +#tableSettingsTop { + margin-bottom: 10px; +} + #modalBackground { display: none; position: fixed; @@ -123,6 +137,7 @@ div#nav-userpic { height: 100%; background-color: #444; background-color: #444a; + margin: 5px; } .modal { @@ -196,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 3e931ce..583a56e 100644 --- a/src/app.ts +++ b/src/app.ts @@ -42,31 +42,85 @@ 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 sortDirection = ( request.query.sortDirection == "asc" ? "ASC" : "DESC" ); + var inverseSortDirection = ( sortDirection == "ASC" ? "DESC" : "ASC" ); + switch (request.query.sort) { + case undefined: // Default sort by newest + case "timestamp": + var requestSort = `reqTimestamp ${sortDirection}`; + break; + 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; + }; + var requestsTotal = await requests.getRequestsTotal(); if (request.session.user) { - requests.getRequestsVoted(requestCount,request.session.user.id).then((val: Array