Compare commits
7 Commits
Author | SHA1 | Date |
---|---|---|
Dessa Simpson | dd423bdb34 | |
Dessa Simpson | ac953b0bb5 | |
Dessa Simpson | 90f672b288 | |
Dessa Simpson | 8b49d51e08 | |
Dessa Simpson | 7897fe6f3d | |
Dessa Simpson | 6e26650077 | |
Dessa Simpson | 891ccfae92 |
14
RELEASE.md
14
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`
|
- [ ] 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
|
- ALWAYS use a transaction for the entirety of this file
|
||||||
- At minimum, DB version must be bumped
|
- 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`
|
- [ ] Tag the latest commit with `vMAJOR.MINOR`
|
||||||
|
- [ ] Write release notes
|
||||||
|
|
||||||
## Patch Releases
|
## 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`
|
- [ ] Tag the latest commit with `vMAJOR.MINOR.PATCH`
|
||||||
|
- [ ] Write release notes
|
|
@ -8,4 +8,4 @@ CREATE OR REPLACE FUNCTION get_version() RETURNS VARCHAR
|
||||||
AS $$SELECT major || '.' || minor FROM version $$
|
AS $$SELECT major || '.' || minor FROM version $$
|
||||||
LANGUAGE SQL;
|
LANGUAGE SQL;
|
||||||
|
|
||||||
INSERT INTO version (major,minor) VALUES (0,7);
|
INSERT INTO version (major,minor) VALUES (0,8);
|
||||||
|
|
|
@ -1,12 +1,14 @@
|
||||||
CREATE TABLE config (
|
CREATE TABLE config (
|
||||||
rowlock bool DEFAULT TRUE UNIQUE NOT NULL CHECK (rowlock = TRUE),
|
rowlock bool DEFAULT TRUE UNIQUE NOT NULL CHECK (rowlock = TRUE),
|
||||||
normaluservotepoints int NOT NULL,
|
normaluservotepoints int NOT NULL DEFAULT 10,
|
||||||
followervotepoints int NOT NULL,
|
followervotepoints int NOT NULL DEFAULT 50,
|
||||||
subscribervotepoints int NOT NULL,
|
subscribervotepoints int NOT NULL DEFAULT 100,
|
||||||
title varchar NOT NULL,
|
normaluserratelimit int NOT NULL DEFAULT 1,
|
||||||
colors jsonb NOT NULL,
|
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)
|
PRIMARY KEY (rowLock)
|
||||||
);
|
);
|
||||||
|
|
||||||
INSERT INTO config (normalUserVotePoints,followerVotePoints,subscriberVotePoints,title,colors)
|
INSERT INTO config (rowlock) VALUES (true);
|
||||||
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"}}');
|
|
||||||
|
|
|
@ -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
|
SELECT users.userid as userid, users.displayname as displayname, users.imageurl as imageurl FROM streamer
|
||||||
LEFT JOIN users
|
LEFT JOIN users
|
||||||
ON streamer.userid = users.userid;
|
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);
|
||||||
|
|
|
@ -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;
|
|
@ -75,6 +75,7 @@ function updateTable() {
|
||||||
|
|
||||||
function applyUrlTransforms(url) {
|
function applyUrlTransforms(url) {
|
||||||
console.log("Begin applyUrlTransforms:" + url);
|
console.log("Begin applyUrlTransforms:" + url);
|
||||||
|
url = url.trim();
|
||||||
if (url.match(/^https?:\/\/(www\.)?youtu(\.be|be\.com)\//)) { // Youtube
|
if (url.match(/^https?:\/\/(www\.)?youtu(\.be|be\.com)\//)) { // Youtube
|
||||||
console.log("Youtube");
|
console.log("Youtube");
|
||||||
var videoid = "";
|
var videoid = "";
|
||||||
|
|
|
@ -9,8 +9,8 @@ export async function processBans(streamer: twitch.StreamerUserIdTokenPair) {
|
||||||
await dbconn.query("DELETE FROM bans");
|
await dbconn.query("DELETE FROM bans");
|
||||||
var response = await twitch.streamerApiRequest(streamer,
|
var response = await twitch.streamerApiRequest(streamer,
|
||||||
`/moderation/banned?broadcaster_id=${streamer.userid}&first=100`);
|
`/moderation/banned?broadcaster_id=${streamer.userid}&first=100`);
|
||||||
//log(LogLevel.DEBUG,"Ban API response:")
|
log(LogLevel.DEBUG,"Ban API response:")
|
||||||
//log(LogLevel.DEBUG,JSON.stringify(response,null,2));
|
log(LogLevel.DEBUG,JSON.stringify(response,null,2));
|
||||||
while (true) {
|
while (true) {
|
||||||
var insertBanQuery = "INSERT INTO bans (userid) VALUES ";
|
var insertBanQuery = "INSERT INTO bans (userid) VALUES ";
|
||||||
var banRow = 0; // Used for $1, $2, etc. in parameterized query
|
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;
|
var oldFirstUserid = response.data[0].user_id;
|
||||||
response = await twitch.streamerApiRequest(streamer,
|
response = await twitch.streamerApiRequest(streamer,
|
||||||
`/moderation/banned?broadcaster_id=${streamer.userid}&after=${response.pagination.cursor}&first=100`);
|
`/moderation/banned?broadcaster_id=${streamer.userid}&after=${response.pagination.cursor}&first=100`);
|
||||||
//log(LogLevel.DEBUG,"Ban API response:");
|
log(LogLevel.DEBUG,"Ban API response:");
|
||||||
//log(LogLevel.DEBUG,JSON.stringify(response,null,2));
|
log(LogLevel.DEBUG,JSON.stringify(response,null,2));
|
||||||
// Work around broken api endpoint giving a cursor referring to the
|
// Work around broken api endpoint giving a cursor referring to the
|
||||||
// current page, causing an infinite loop
|
// current page, causing an infinite loop
|
||||||
if (oldFirstUserid == response.data[0].user_id) break;
|
if (oldFirstUserid == response.data[0].user_id) break;
|
||||||
|
|
|
@ -9,8 +9,8 @@ export async function processSubscriptions(streamer: twitch.StreamerUserIdTokenP
|
||||||
await dbconn.query("DELETE FROM subscriptions");
|
await dbconn.query("DELETE FROM subscriptions");
|
||||||
var response = await twitch.streamerApiRequest(streamer,
|
var response = await twitch.streamerApiRequest(streamer,
|
||||||
`/subscriptions?broadcaster_id=${streamer.userid}&first=100`);
|
`/subscriptions?broadcaster_id=${streamer.userid}&first=100`);
|
||||||
//log(LogLevel.DEBUG,"Subscriptions API response:")
|
log(LogLevel.DEBUG,"Subscriptions API response:")
|
||||||
//log(LogLevel.DEBUG,JSON.stringify(response,null,2));
|
log(LogLevel.DEBUG,JSON.stringify(response,null,2));
|
||||||
while (true) {
|
while (true) {
|
||||||
var insertSubscriptionQuery = "INSERT INTO subscriptions (userid) VALUES ";
|
var insertSubscriptionQuery = "INSERT INTO subscriptions (userid) VALUES ";
|
||||||
var subscriptionRow = 0; // Used for $1, $2, etc. in parameterized query
|
var subscriptionRow = 0; // Used for $1, $2, etc. in parameterized query
|
||||||
|
@ -35,9 +35,9 @@ export async function processSubscriptions(streamer: twitch.StreamerUserIdTokenP
|
||||||
var oldFirstUserid = response.data[0].user_id;
|
var oldFirstUserid = response.data[0].user_id;
|
||||||
response = await twitch.streamerApiRequest(streamer,
|
response = await twitch.streamerApiRequest(streamer,
|
||||||
`/subscriptions?broadcaster_id=${streamer.userid}&after=${response.pagination.cursor}&first=100`);
|
`/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,"Subscription API response:");
|
log(LogLevel.DEBUG,JSON.stringify(response,null,2));
|
||||||
//log(LogLevel.DEBUG,JSON.stringify(response,null,2));
|
if (response.data.length === 0 || oldFirstUserid === response.data[0].user_id) break;
|
||||||
} else {
|
} else {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
@ -45,6 +45,7 @@ export async function processSubscriptions(streamer: twitch.StreamerUserIdTokenP
|
||||||
await dbconn.query("CALL update_scores()");
|
await dbconn.query("CALL update_scores()");
|
||||||
await dbconn.query('COMMIT');
|
await dbconn.query('COMMIT');
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
log(LogLevel.ERROR,"cronjobs.processSubscriptions: Exception: " + e);
|
||||||
log(LogLevel.ERROR,"cronjobs.processSubscriptions: Exception thrown; rolling back");
|
log(LogLevel.ERROR,"cronjobs.processSubscriptions: Exception thrown; rolling back");
|
||||||
await dbconn.query('ROLLBACK');
|
await dbconn.query('ROLLBACK');
|
||||||
throw(e);
|
throw(e);
|
||||||
|
|
|
@ -130,3 +130,8 @@ export const updateCronJobLastSuccess = {
|
||||||
name: "updateCronJobLastSuccess",
|
name: "updateCronJobLastSuccess",
|
||||||
text: "UPDATE cron SET lastSuccess = now() WHERE jobName = $1"
|
text: "UPDATE cron SET lastSuccess = now() WHERE jobName = $1"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const getRateLimitStatus = {
|
||||||
|
name: "getRateLimitStatus",
|
||||||
|
text: "SELECT * FROM ratelimit_vw WHERE userid = $1"
|
||||||
|
}
|
||||||
|
|
|
@ -87,6 +87,7 @@ async function retrieveYoutubeMetadata(url: string) {
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function addRequest(url: string, requester: string): Promise<[number,string]> {
|
export async function addRequest(url: string, requester: string): Promise<[number,string]> {
|
||||||
|
// Check that URL is of an accepted format
|
||||||
var validUrl = false;
|
var validUrl = false;
|
||||||
for (var regex of validUrlRegexes) {
|
for (var regex of validUrlRegexes) {
|
||||||
if (regex.test(url)) {
|
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."];
|
if (!validUrl) return [400, "Invalid song URL."];
|
||||||
var result = await checkRequestExists(url)
|
|
||||||
if (result) {
|
// Check whether the URL has already been requested
|
||||||
console.log(result);
|
var existsResult = await checkRequestExists(url)
|
||||||
return [200,`Song already requested by ${result.rows[0].requester}. State: ${result.rows[0].state}`]
|
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] });
|
var query = Object.assign(queries.addRequest, { values: [url,requester] });
|
||||||
return db.query(query)
|
return db.query(query)
|
||||||
.then(async () => {
|
.then(async () => {
|
||||||
|
|
|
@ -53,6 +53,7 @@ export async function apiRequest(tokens: TokenPair, endpoint: string): Promise <
|
||||||
return fetch("https://api.twitch.tv/helix" + endpoint, { headers: headers })
|
return fetch("https://api.twitch.tv/helix" + endpoint, { headers: headers })
|
||||||
.then(async (res: FetchResponse) => {
|
.then(async (res: FetchResponse) => {
|
||||||
if (res.status == 200) {
|
if (res.status == 200) {
|
||||||
|
log(LogLevel.DEBUG,"twitch.apiRequest: Request returned 200 for " + endpoint);
|
||||||
return res.json();
|
return res.json();
|
||||||
} else {
|
} else {
|
||||||
log(LogLevel.WARNING,"twitch.apiRequest: Failed API request (pre-refresh):");
|
log(LogLevel.WARNING,"twitch.apiRequest: Failed API request (pre-refresh):");
|
||||||
|
|
|
@ -5,7 +5,7 @@ import pg from "pg";
|
||||||
import db from "./db";
|
import db from "./db";
|
||||||
|
|
||||||
var versionMajor = 0;
|
var versionMajor = 0;
|
||||||
var versionMinor = 7;
|
var versionMinor = 8;
|
||||||
var versionPatch = 0;
|
var versionPatch = 0;
|
||||||
|
|
||||||
export function getVersion() {
|
export function getVersion() {
|
||||||
|
|
Loading…
Reference in New Issue