From 253fc4b5cf086f9b0eaa1859259b813fc4074e9f Mon Sep 17 00:00:00 2001 From: Tobias Messner Date: Sun, 9 Oct 2022 23:18:17 +0200 Subject: [PATCH 1/3] Start work on automatically managing predictions Co-authored-by: Solomon Cammack --- server/.env.example | 4 + server/package.json | 2 + server/src/actions/manage-prediction.js | 142 ++++++ server/src/airtable-interface.js | 2 +- server/src/routes.js | 77 +++ server/src/types.js | 4 + server/yarn.lock | 473 +++++++++++++++++- .../website/dashboard/Predictions.vue | 35 ++ website/src/utils/dashboard.js | 8 + website/src/views/Dashboard.vue | 4 +- 10 files changed, 740 insertions(+), 11 deletions(-) create mode 100644 server/src/actions/manage-prediction.js create mode 100644 website/src/components/website/dashboard/Predictions.vue diff --git a/server/.env.example b/server/.env.example index a7e7a932..4f99bf43 100644 --- a/server/.env.example +++ b/server/.env.example @@ -5,6 +5,10 @@ DISCORD_TOKEN= DISCORD_CLIENT_ID= DISCORD_CLIENT_SECRET= +TWITCH_CLIENT_ID= +TWITCH_CLIENT_SECRET= +TWITCH_REDIRECT_URI= + STAFFAPPS_GUILD_ID= STAFFAPPS_CATEGORY_ID= STAFFAPPS_APPLICATION_CHANNEL_ID= diff --git a/server/package.json b/server/package.json index 7c29f78a..ae79ebfc 100644 --- a/server/package.json +++ b/server/package.json @@ -19,6 +19,8 @@ "nodemon": "^2.0.7" }, "dependencies": { + "@twurple/api": "^5.2.5", + "@twurple/auth": "^5.2.5", "airtable": "^0.10.1", "body-parser": "^1.19.0", "chalk": "^4.1.0", diff --git a/server/src/actions/manage-prediction.js b/server/src/actions/manage-prediction.js new file mode 100644 index 00000000..452cb82f --- /dev/null +++ b/server/src/actions/manage-prediction.js @@ -0,0 +1,142 @@ +const { ApiClient } = require("@twurple/api"); +const { StaticAuthProvider, refreshUserToken } = require("@twurple/auth"); + +const automaticPredictionTitleStartCharacter = "⬥"; + +function generatePredictionTitle(map) { + let title; + if (map.number) { + if (map.name) { + title = `Who will win map ${map.number} - ${map.name}?`; + } else { + title = `Who will win map ${map.number}?`; + } + + } else if (map.name) { + title = `Who will win ${map.name}?`; + } else { + title = "Who will win the map?"; + } + return `${automaticPredictionTitleStartCharacter} ${title}`; +} + +function getTargetPrediction(predictions, teams) { + return predictions.find(p => + ["ACTIVE", "LOCKED"].includes(p.status) && + p.outcomes.every(outcome => [...teams.map(t => t.name), "Draw"].includes(outcome.title)) && + p.title.startsWith(automaticPredictionTitleStartCharacter) + ); +} + +module.exports = { + key: "manage-prediction", + auth: ["client"], + requiredParams: ["predictionAction"], + optionalParams: ["autoLockAfter"], // TODO: this wont work yet + /*** + * @param {ActionSuccessCallback} success + * @param {ActionErrorCallback} error + * @param {PredictionAction} predictionAction + * @param {number?} autoLockAfter + * @param {ClientData} client + * @param {CacheGetFunction} get + * @param {SimpleUpdateRecord} updateRecord + * @returns {Promise} + */ + // eslint-disable-next-line no-empty-pattern + async handler(success, error, { predictionAction, autoLockAfter = 120 }, { client }, { get }) { + if (!(["create", "lock", "resolve", "cancel"].includes(predictionAction))) return error("Invalid action"); + console.log(predictionAction); + + const broadcast = await get(client?.broadcast?.[0]); + if (!broadcast) return error("No broadcast associated"); + if (!broadcast.channel) return error("No channel associated with broadcast"); + + const channel = await get(broadcast?.channel?.[0]); + if (!channel.twitch_refresh_token) return error("No twitch auth token associated with channel"); + if (!channel.channel_id || !channel.name || !channel.twitch_scopes) return error("Invalid channel data"); + // TODO: store this in Cache.auth somewhere + const { accessToken } = await refreshUserToken(process.env.TWITCH_CLIENT_ID, process.env.TWITCH_CLIENT_SECRET, channel.twitch_refresh_token); + + const authProvider = new StaticAuthProvider(process.env.TWITCH_CLIENT_ID, accessToken); + const api = new ApiClient({authProvider}); + + const match = await get(broadcast?.live_match?.[0]); + if (!match) return error("No match associated"); + + const team1 = await get(match?.teams?.[0]); + const team2 = await get(match?.teams?.[1]); + if (!team1 || !team2) return error("Did not find two teams!"); + + const maps = await Promise.all((match.maps || []).map(async m => { + let map = await get(m); + + if (map?.map?.[0]) { + let mapData = await get(map?.map?.[0]); + map.map = mapData; + } + + if (map?.winner?.[0]) { + let winner = await get(map?.winner?.[0]); + map.winner = winner; + } + + return map; + })); + if (maps.length === 0) return error("No maps associated with match"); + + const { data: predictions } = await api.predictions.getPredictions(channel.channel_id); + + + if (["create", "lock"].includes(predictionAction)) { + const currentMap = maps.filter(m => !m.dummy && !m.winner && !m.draw && !m.banner)[0]; + if (!currentMap) return error("No valid map to start a prediction for"); + + + const targetPrediction = getTargetPrediction(predictions, [team1, team2]); + console.log(targetPrediction); + + if (predictionAction === "create") { + if (targetPrediction) return error("Prediction already exists"); + const predictionTitle = generatePredictionTitle(currentMap); + + const responsePrediction = await api.predictions.createPrediction(channel.channel_id, { + title: predictionTitle, + outcomes: [team1.name, team2.name, "Draw"], + autoLockAfter: 120 + }); + console.log(responsePrediction); + return success(); // TODO: check responsePrediction for errors + } + + if (!targetPrediction) return error("Prediction does not exist"); + + if (predictionAction === "lock") { + const responsePrediction = await api.predictions.lockPrediction(channel.channel_id, targetPrediction.id); + console.log(responsePrediction); + } + + } else if (["resolve"].includes(predictionAction)) { + const lastMap = maps.filter(m => !m.dummy && !m.banner && (m.winner || m.draw)).pop(); + const targetPrediction = getTargetPrediction(predictions, [team1, team2]); + console.log(targetPrediction); + + if (lastMap.draw) { + const responsePrediction = await api.predictions.resolvePrediction(channel.channel_id, targetPrediction.id, targetPrediction.outcomes.find(o => o.title === "Draw").id); + console.log(responsePrediction); + } else { + const responsePrediction = await api.predictions.resolvePrediction(channel.channel_id, targetPrediction.id, targetPrediction.outcomes.find(o => o.title === lastMap.winner.name).id); + console.log(responsePrediction); + } + + } else if (["cancel"].includes(predictionAction)) { + const activePredictions = predictions.filter(p => ["ACTIVE", "LOCKED"].includes(p.status)); + for (const prediction of activePredictions) { + const responsePrediction = await api.predictions.cancelPrediction(channel.channel_id, prediction.id); + console.log(responsePrediction); + } + } + + return success(); + } +}; diff --git a/server/src/airtable-interface.js b/server/src/airtable-interface.js index 42823881..23bcec8f 100644 --- a/server/src/airtable-interface.js +++ b/server/src/airtable-interface.js @@ -82,7 +82,7 @@ function setRebuilding(isRebuilding) { // Starting with syncing Matches // const tables = ["Matches", "Teams", "Themes", "Events", "Players", "Player Relationships"]; -const tables = ["Broadcasts", "Clients", "Players", "Events", "Event Series", "Teams", "Ad Reads", "Ad Read Groups", "News", "Matches", "Themes", "Socials", "Accolades", "Player Relationships", "Brackets", "Live Guests", "Headlines", "Maps", "Map Data", "Heroes", "Log Files", "Tracks", "Track Groups", "Track Group Roles"]; +const tables = ["Broadcasts", "Clients", "Players", "Channels", "Events", "Event Series", "Teams", "Ad Reads", "Ad Read Groups", "News", "Matches", "Themes", "Socials", "Accolades", "Player Relationships", "Brackets", "Live Guests", "Headlines", "Maps", "Map Data", "Heroes", "Log Files", "Tracks", "Track Groups", "Track Group Roles"]; const staticTables = ["Redirects"]; function deAirtable(obj) { diff --git a/server/src/routes.js b/server/src/routes.js index a44c6b11..99cb496c 100644 --- a/server/src/routes.js +++ b/server/src/routes.js @@ -1,3 +1,9 @@ +const fetch = require("node-fetch"); +const { updateRecord, + createRecord +} = require("./action-utils"); +const { getTokenInfo } = require("@twurple/auth"); + function cleanID(id) { if (!id) return null; if (typeof id !== "string") return id.id || null; // no real id oops @@ -281,4 +287,75 @@ module.exports = ({ app, cors, Cache, io }) => { return res.status(500).send(e.message); } }); + + let states = {}; + + function createState() { + // return a uuid without a library + let uuid = ""; + for (let i = 0; i < 32; i++) { + uuid += Math.floor(Math.random() * 16).toString(16); + } + return uuid; + } + + app.get("/twitch_auth/:scopes", (req, res) => { + let state = createState(); + states[state] = req.params.scopes; + res.redirect(`https://id.twitch.tv/oauth2/authorize?client_id=${process.env.TWITCH_CLIENT_ID}&redirect_uri=${process.env.TWITCH_REDIRECT_URI}&response_type=code&scope=${req.params.scopes}&force_verify=true`); + }); + + + app.get("/twitch_callback", async(req, res) => { + try { + const response = await fetch("https://id.twitch.tv/oauth2/token", { + method: "POST", + headers: { + "Content-Type": "application/x-www-form-urlencoded", + }, + body: `client_id=${process.env.TWITCH_CLIENT_ID}&client_secret=${process.env.TWITCH_CLIENT_SECRET}&grant_type=authorization_code&code=${req.query.code}&redirect_uri=${process.env.TWITCH_REDIRECT_URI}` + }); + const data = await response.json(); + + // get user data + const tokenInfo = await getTokenInfo(data.access_token, process.env.TWITCH_CLIENT_ID); + + let scopes = states[req.query.state]; + if (scopes) delete states[req.query.state]; + + + // get or create channel in table + const channelIDs = (await Cache.get("Channels"))?.ids || []; + const channels = await Promise.all(channelIDs.map(id => Cache.get(id))); + console.log(channels); + const existingChannel = channels.find(c => c.channel_id === tokenInfo.userId); + + let airtableResponse; + // store into channels table with tokens + scopes + + if (existingChannel) { + airtableResponse = await updateRecord(Cache, "Channels", existingChannel.id, { + "Twitch Refresh Token": data.refresh_token, + "Twitch Scopes": tokenInfo.scopes.join(" "), + "Channel ID": tokenInfo.userId, + "Name": tokenInfo.userName + }); + } else { + airtableResponse = await createRecord(Cache, "Channels", [{ + "Twitch Refresh Token": data.refresh_token, + "Twitch Scopes": tokenInfo.scopes.join(" "), + "Channel ID": tokenInfo.userId, + "Name": tokenInfo.userName + }]); + } + + console.log(airtableResponse); + + + return res.send("okay thanks"); + } catch (e) { + console.error("[Twitch Auth] error", e); + } + }); + }; diff --git a/server/src/types.js b/server/src/types.js index 21d39b72..5e74360e 100644 --- a/server/src/types.js +++ b/server/src/types.js @@ -105,3 +105,7 @@ * @param {object} changes - New data to change * @returns {Promise} */ + +/** + * @typedef {'create'|'lock'|'resolve'|'cancel'} PredictionAction + */ diff --git a/server/yarn.lock b/server/yarn.lock index 4f4db2ce..1e0234b4 100644 --- a/server/yarn.lock +++ b/server/yarn.lock @@ -23,6 +23,72 @@ chalk "^2.0.0" js-tokens "^4.0.0" +"@d-fischer/cache-decorators@^3.0.0": + version "3.0.0" + resolved "https://registry.yarnpkg.com/@d-fischer/cache-decorators/-/cache-decorators-3.0.0.tgz#6456c3f8c74c4dd22d80faa7985d4efff6d2ff77" + integrity sha512-mYUCjrp5hJgimceC5bof3zzmElyxzW4ty+73IjY12wvxLAqsq0CbgLGspnJm6KgwEfGoeRnISZD4EXJidG3FvA== + dependencies: + "@d-fischer/shared-utils" "^3.0.1" + "@types/node" "^14.14.22" + tslib "^2.1.0" + +"@d-fischer/cross-fetch@^4.0.2": + version "4.1.0" + resolved "https://registry.yarnpkg.com/@d-fischer/cross-fetch/-/cross-fetch-4.1.0.tgz#6a25d3244c5014ccd97a7f1056f7f7b619aca6a3" + integrity sha512-HH87JacceXOANr5XuBuSIQmPRPOvOUPwU1JR0DLUT6A8jGcP0jq2YUAiFCyZ8VGwDbTjsD3CZFpWIp5o7mIFWQ== + dependencies: + node-fetch "2.6.7" + +"@d-fischer/detect-node@^3.0.1": + version "3.0.1" + resolved "https://registry.yarnpkg.com/@d-fischer/detect-node/-/detect-node-3.0.1.tgz#7b051a86611b0396ba205aabae805b18cc642a78" + integrity sha512-0Rf3XwTzuTh8+oPZW9SfxTIiL+26RRJ0BRPwj5oVjZFyFKmsj9RGfN2zuTRjOuA3FCK/jYm06HOhwNK+8Pfv8w== + +"@d-fischer/logger@^4.0.0": + version "4.2.0" + resolved "https://registry.yarnpkg.com/@d-fischer/logger/-/logger-4.2.0.tgz#6ba05043c3a49cef3ebf5bc6f7825566dca37d3c" + integrity sha512-Hgm/GfeZfv2UPcRY4uF4S7OtCCv6Xxx++q7FAXc1qMrInaowKrmpo6YdFwRfZ8z53gzXFhc6H8j1Ttm0046uTw== + dependencies: + "@d-fischer/shared-utils" "^3.2.0" + detect-node "^2.0.4" + tslib "^2.0.3" + +"@d-fischer/promise.allsettled@^2.0.2": + version "2.0.2" + resolved "https://registry.yarnpkg.com/@d-fischer/promise.allsettled/-/promise.allsettled-2.0.2.tgz#2f8d55dba8412f656d62885e723e9b591d45f71c" + integrity sha512-xY0vYDwJYFe22MS5ccQ50N4Mcc2nQ8J4eWE5Y354IxZwW32O5uTT6mmhFSuVF6ZrKvzHOCIrK+9WqOR6TI3tcA== + dependencies: + array.prototype.map "^1.0.3" + call-bind "^1.0.2" + define-properties "^1.1.3" + es-abstract "^1.18.0-next.2" + get-intrinsic "^1.0.2" + iterate-value "^1.0.2" + +"@d-fischer/qs@^7.0.2": + version "7.0.2" + resolved "https://registry.yarnpkg.com/@d-fischer/qs/-/qs-7.0.2.tgz#21942f51590e20954086bdc32fb3e608d3525659" + integrity sha512-yAu3xDooiL+ef84Jo8nLjDjWBRk7RXk163Y6aTvRB7FauYd3spQD/dWvgT7R4CrN54Juhrrc3dMY7mc+jZGurQ== + +"@d-fischer/rate-limiter@^0.6.1": + version "0.6.1" + resolved "https://registry.yarnpkg.com/@d-fischer/rate-limiter/-/rate-limiter-0.6.1.tgz#5134640e32831e22d190fc8a08b89dba38af3e87" + integrity sha512-SqsMxs77c/W9knmcX9ccy12ASq+E5UXzxU/ZIDAhZyll+2IqyikhgLQUu5n7bdxb3kox+TOSCIEFxDSSaGhcTA== + dependencies: + "@d-fischer/logger" "^4.0.0" + "@d-fischer/promise.allsettled" "^2.0.2" + "@d-fischer/shared-utils" "^3.2.0" + "@types/node" "^12.12.5" + tslib "^2.0.3" + +"@d-fischer/shared-utils@^3.0.1", "@d-fischer/shared-utils@^3.2.0": + version "3.3.0" + resolved "https://registry.yarnpkg.com/@d-fischer/shared-utils/-/shared-utils-3.3.0.tgz#07e4b1c3251fdb7232293d095939176a5529ecd9" + integrity sha512-k9kOsggH+xyFf4+p63TI1kbG1oQX2qHra3OY7LGg2+26JMgL0RD6Hss86QXB4bx0SFielmdA9OD35NETZ/E51g== + dependencies: + "@types/node" "^14.11.2" + tslib "^2.0.3" + "@discordjs/builders@^0.16.0": version "0.16.0" resolved "https://registry.yarnpkg.com/@discordjs/builders/-/builders-0.16.0.tgz#3201f57fa57c4dd77aebb480cf47da77b7ba2e8c" @@ -79,6 +145,51 @@ dependencies: defer-to-connect "^1.0.1" +"@twurple/api-call@^5.2.5": + version "5.2.5" + resolved "https://registry.yarnpkg.com/@twurple/api-call/-/api-call-5.2.5.tgz#415b291602431f444ffaee588a4d9af772c58e52" + integrity sha512-oPbkNnh6zT6twNFmt2FOJwK+KQjlJvWKZUcvKW/P/JGe9olIBfNIScVcmnAEr9olOvBoipnF/xLu0V0f0C/UeQ== + dependencies: + "@d-fischer/cross-fetch" "^4.0.2" + "@d-fischer/qs" "^7.0.2" + "@twurple/common" "^5.2.5" + "@types/node-fetch" "^2.5.7" + tslib "^2.0.3" + +"@twurple/api@^5.2.5": + version "5.2.5" + resolved "https://registry.yarnpkg.com/@twurple/api/-/api-5.2.5.tgz#1a820c2d509400ab06c27e17b639fc23f68f5009" + integrity sha512-EzIJToZOimXEaZt6uGZTj+6iyD+L2uUiHeSjfMdY+KwIbdxM99218YFSOA8l2RHRSBrNkt2rYEau45oFOP/RcA== + dependencies: + "@d-fischer/cache-decorators" "^3.0.0" + "@d-fischer/detect-node" "^3.0.1" + "@d-fischer/logger" "^4.0.0" + "@d-fischer/rate-limiter" "^0.6.1" + "@d-fischer/shared-utils" "^3.2.0" + "@twurple/api-call" "^5.2.5" + "@twurple/common" "^5.2.5" + tslib "^2.0.3" + +"@twurple/auth@^5.2.5": + version "5.2.5" + resolved "https://registry.yarnpkg.com/@twurple/auth/-/auth-5.2.5.tgz#3091111194a5ceebc1fa4ce6cd9d8f3219a4ab20" + integrity sha512-gu2D3FU87MtXhSnxmiHgTJrOig57ttx89YvamqBq9hT4QpId23Z5H0wUtvumL+G3NlyhcJ6+LOYvWiz8MFPRWA== + dependencies: + "@d-fischer/logger" "^4.0.0" + "@d-fischer/shared-utils" "^3.2.0" + "@twurple/api-call" "^5.2.5" + "@twurple/common" "^5.2.5" + tslib "^2.0.3" + +"@twurple/common@^5.2.5": + version "5.2.5" + resolved "https://registry.yarnpkg.com/@twurple/common/-/common-5.2.5.tgz#ceb97aa4df5e5e91e6ef3dedc52c3cf7c8b7ea54" + integrity sha512-pZe/IjdNfxiKtVw+3ntViq0gMnZrx8x+VB3aAzl/hB6bE10iUB0vV0qK7gCrvDJCnMDk0oGFLdl+nQOwkDHVpQ== + dependencies: + "@d-fischer/shared-utils" "^3.2.0" + klona "^2.0.4" + tslib "^2.0.3" + "@types/component-emitter@^1.2.10": version "1.2.10" resolved "https://registry.yarnpkg.com/@types/component-emitter/-/component-emitter-1.2.10.tgz#ef5b1589b9f16544642e473db5ea5639107ef3ea" @@ -94,7 +205,7 @@ resolved "https://registry.yarnpkg.com/@types/cors/-/cors-2.8.10.tgz#61cc8469849e5bcdd0c7044122265c39cec10cf4" integrity sha512-C7srjHiVG3Ey1nR6d511dtDkCEjxuN9W1HWAEjGq8kpcwmNM6JJkpC0xvabM7BXTG2wDq8Eu33iH9aQKa7IvLQ== -"@types/node-fetch@^2.6.2": +"@types/node-fetch@^2.5.7", "@types/node-fetch@^2.6.2": version "2.6.2" resolved "https://registry.yarnpkg.com/@types/node-fetch/-/node-fetch-2.6.2.tgz#d1a9c5fd049d9415dce61571557104dec3ec81da" integrity sha512-DHqhlq5jeESLy19TYhLakJ07kNumXWjcDdxXsLUMJZ6ue8VZJj4kLPQVE/2mdHh3xZziNF1xppu5lwmS53HR+A== @@ -112,6 +223,16 @@ resolved "https://registry.yarnpkg.com/@types/node/-/node-14.14.37.tgz#a3dd8da4eb84a996c36e331df98d82abd76b516e" integrity sha512-XYmBiy+ohOR4Lh5jE379fV2IU+6Jn4g5qASinhitfyO71b/sCo6MKsMLF5tc7Zf2CE8hViVQyYSobJNke8OvUw== +"@types/node@^12.12.5": + version "12.20.55" + resolved "https://registry.yarnpkg.com/@types/node/-/node-12.20.55.tgz#c329cbd434c42164f846b909bd6f85b5537f6240" + integrity sha512-J8xLz7q2OFulZ2cyGTLE1TbbZcjpno7FaN6zdJNrgAdrJ+DZzh/uFR6YrTb4C+nXakvud8Q4+rbhoIWlYQbUFQ== + +"@types/node@^14.11.2", "@types/node@^14.14.22": + version "14.18.31" + resolved "https://registry.yarnpkg.com/@types/node/-/node-14.18.31.tgz#4b873dea3122e71af4f77e65ec5841397ff254d3" + integrity sha512-vQAnaReSQkEDa8uwAyQby8bYGKu84R/deEc6mg5T8fX6gzCn8QW6rziSgsti1fNvsrswKUKPnVTi7uoB+u62Mw== + "@types/ws@^8.5.3": version "8.5.3" resolved "https://registry.yarnpkg.com/@types/ws/-/ws-8.5.3.tgz#7d25a1ffbecd3c4f2d35068d0b283c037003274d" @@ -264,6 +385,17 @@ array-flatten@1.1.1: resolved "https://registry.yarnpkg.com/array-flatten/-/array-flatten-1.1.1.tgz#9a5f699051b1e7073328f2a008968b64ea2955d2" integrity sha1-ml9pkFGx5wczKPKgCJaLZOopVdI= +array.prototype.map@^1.0.3: + version "1.0.4" + resolved "https://registry.yarnpkg.com/array.prototype.map/-/array.prototype.map-1.0.4.tgz#0d97b640cfdd036c1b41cfe706a5e699aa0711f2" + integrity sha512-Qds9QnX7A0qISY7JT5WuJO0NJPE9CMlC6JzHQfhpqAAQQzufVRoeH7EzUY5GcPTx72voG8LV/5eo+b8Qi8hmhA== + dependencies: + call-bind "^1.0.2" + define-properties "^1.1.3" + es-abstract "^1.19.0" + es-array-method-boxes-properly "^1.0.0" + is-string "^1.0.7" + astral-regex@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/astral-regex/-/astral-regex-2.0.0.tgz#483143c567aeed4785759c0865786dc77d7d2e31" @@ -379,7 +511,7 @@ cacheable-request@^6.0.0: normalize-url "^4.1.0" responselike "^1.0.2" -call-bind@^1.0.0: +call-bind@^1.0.0, call-bind@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/call-bind/-/call-bind-1.0.2.tgz#b1d4e89e688119c3c9a903ad30abb2f6a919be3c" integrity sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA== @@ -666,6 +798,14 @@ defer-to-connect@^1.0.1: resolved "https://registry.yarnpkg.com/defer-to-connect/-/defer-to-connect-1.1.3.tgz#331ae050c08dcf789f8c83a7b81f0ed94f4ac591" integrity sha512-0ISdNousHvZT2EiFlZeZAHBUvSxmKswVCEf8hW7KWgG4a8MVEu/3Vb6uWYozkjylyCxe0JBIiRB1jV45S70WVQ== +define-properties@^1.1.3, define-properties@^1.1.4: + version "1.1.4" + resolved "https://registry.yarnpkg.com/define-properties/-/define-properties-1.1.4.tgz#0b14d7bd7fbeb2f3572c3a7eda80ea5d57fb05b1" + integrity sha512-uckOqKcfaVvtBdsVkdPv3XjveQJsNQqmhXgRi8uhvWWuPYZCNlzT8qAyblUgNoXdHdjMTzAqeGjAoli8f+bzPA== + dependencies: + has-property-descriptors "^1.0.0" + object-keys "^1.1.1" + delayed-stream@~1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/delayed-stream/-/delayed-stream-1.0.0.tgz#df3ae199acadfb7d440aaae0b29e2272b24ec619" @@ -691,6 +831,11 @@ detect-libc@^2.0.0: resolved "https://registry.yarnpkg.com/detect-libc/-/detect-libc-2.0.1.tgz#e1897aa88fa6ad197862937fbc0441ef352ee0cd" integrity sha512-463v3ZeIrcWtdgIg6vI6XUncguvr2TnGl4SzDXinkt9mSLpBJKXT3mW6xT3VQdDN11+WVs29pgvivTc4Lp8v+w== +detect-node@^2.0.4: + version "2.1.0" + resolved "https://registry.yarnpkg.com/detect-node/-/detect-node-2.1.0.tgz#c9c70775a49c3d03bc2c06d9a73be550f978f8b1" + integrity sha512-T0NIuQpnTvFDATNuHN5roPwSBG83rFsuO+MXXH9/3N1eFbn4wcPjttvjMLEPWJ0RGUYgQE7cGgS3tNxbqCGM7g== + discord-api-types@^0.33.5: version "0.33.5" resolved "https://registry.yarnpkg.com/discord-api-types/-/discord-api-types-0.33.5.tgz#6548b70520f7b944c60984dca4ab58654d664a12" @@ -794,6 +939,64 @@ enquirer@^2.3.5: dependencies: ansi-colors "^4.1.1" +es-abstract@^1.18.0-next.2, es-abstract@^1.19.0, es-abstract@^1.19.5: + version "1.20.4" + resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.20.4.tgz#1d103f9f8d78d4cf0713edcd6d0ed1a46eed5861" + integrity sha512-0UtvRN79eMe2L+UNEF1BwRe364sj/DXhQ/k5FmivgoSdpM90b8Jc0mDzKMGo7QS0BVbOP/bTwBKNnDc9rNzaPA== + dependencies: + call-bind "^1.0.2" + es-to-primitive "^1.2.1" + function-bind "^1.1.1" + function.prototype.name "^1.1.5" + get-intrinsic "^1.1.3" + get-symbol-description "^1.0.0" + has "^1.0.3" + has-property-descriptors "^1.0.0" + has-symbols "^1.0.3" + internal-slot "^1.0.3" + is-callable "^1.2.7" + is-negative-zero "^2.0.2" + is-regex "^1.1.4" + is-shared-array-buffer "^1.0.2" + is-string "^1.0.7" + is-weakref "^1.0.2" + object-inspect "^1.12.2" + object-keys "^1.1.1" + object.assign "^4.1.4" + regexp.prototype.flags "^1.4.3" + safe-regex-test "^1.0.0" + string.prototype.trimend "^1.0.5" + string.prototype.trimstart "^1.0.5" + unbox-primitive "^1.0.2" + +es-array-method-boxes-properly@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/es-array-method-boxes-properly/-/es-array-method-boxes-properly-1.0.0.tgz#873f3e84418de4ee19c5be752990b2e44718d09e" + integrity sha512-wd6JXUmyHmt8T5a2xreUwKcGPq6f1f+WwIJkijUqiGcJz1qqnZgP6XIK+QyIWU5lT7imeNxUll48bziG+TSYcA== + +es-get-iterator@^1.0.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/es-get-iterator/-/es-get-iterator-1.1.2.tgz#9234c54aba713486d7ebde0220864af5e2b283f7" + integrity sha512-+DTO8GYwbMCwbywjimwZMHp8AuYXOS2JZFWoi2AlPOS3ebnII9w/NLpNZtA7A0YLaVDw+O7KFCeoIV7OPvM7hQ== + dependencies: + call-bind "^1.0.2" + get-intrinsic "^1.1.0" + has-symbols "^1.0.1" + is-arguments "^1.1.0" + is-map "^2.0.2" + is-set "^2.0.2" + is-string "^1.0.5" + isarray "^2.0.5" + +es-to-primitive@^1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/es-to-primitive/-/es-to-primitive-1.2.1.tgz#e55cd4c9cdc188bcefb03b366c736323fc5c898a" + integrity sha512-QCOllgZJtaUo9miYBcLChTUaHNjJF3PYs1VidD7AwiEj1kYxKeQTctLAezAOH5ZKRH0g2IgPn6KwB4IT8iRpvA== + dependencies: + is-callable "^1.1.4" + is-date-object "^1.0.1" + is-symbol "^1.0.2" + escape-goat@^2.0.0: version "2.1.1" resolved "https://registry.yarnpkg.com/escape-goat/-/escape-goat-2.1.1.tgz#1b2dc77003676c457ec760b2dc68edb648188675" @@ -1074,11 +1277,26 @@ function-bind@^1.1.1: resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.1.tgz#a56899d3ea3c9bab874bb9773b7c5ede92f4895d" integrity sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A== +function.prototype.name@^1.1.5: + version "1.1.5" + resolved "https://registry.yarnpkg.com/function.prototype.name/-/function.prototype.name-1.1.5.tgz#cce0505fe1ffb80503e6f9e46cc64e46a12a9621" + integrity sha512-uN7m/BzVKQnCUF/iW8jYea67v++2u7m5UgENbHRtdDVclOUP+FMPlCNdmk0h/ysGyo2tavMJEDqJAkJdRa1vMA== + dependencies: + call-bind "^1.0.2" + define-properties "^1.1.3" + es-abstract "^1.19.0" + functions-have-names "^1.2.2" + functional-red-black-tree@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/functional-red-black-tree/-/functional-red-black-tree-1.0.1.tgz#1b0ab3bd553b2a0d6399d29c0e3ea0b252078327" integrity sha1-GwqzvVU7Kg1jmdKcDj6gslIHgyc= +functions-have-names@^1.2.2: + version "1.2.3" + resolved "https://registry.yarnpkg.com/functions-have-names/-/functions-have-names-1.2.3.tgz#0404fe4ee2ba2f607f0e0ec3c80bae994133b834" + integrity sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ== + gauge@~2.7.3: version "2.7.4" resolved "https://registry.yarnpkg.com/gauge/-/gauge-2.7.4.tgz#2c03405c7538c39d7eb37b317022e325fb018bf7" @@ -1102,6 +1320,15 @@ get-intrinsic@^1.0.2: has "^1.0.3" has-symbols "^1.0.1" +get-intrinsic@^1.1.0, get-intrinsic@^1.1.1, get-intrinsic@^1.1.3: + version "1.1.3" + resolved "https://registry.yarnpkg.com/get-intrinsic/-/get-intrinsic-1.1.3.tgz#063c84329ad93e83893c7f4f243ef63ffa351385" + integrity sha512-QJVz1Tj7MS099PevUG5jvnt9tSkXN8K14dxQlikJuPt4uD9hHAHjLyLBiLR5zELelBdD9QNRAXZzsJx0WaDL9A== + dependencies: + function-bind "^1.1.1" + has "^1.0.3" + has-symbols "^1.0.3" + get-stream@^4.1.0: version "4.1.0" resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-4.1.0.tgz#c1b255575f3dc21d59bfc79cd3d2b46b1c3a54b5" @@ -1116,6 +1343,14 @@ get-stream@^5.1.0: dependencies: pump "^3.0.0" +get-symbol-description@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/get-symbol-description/-/get-symbol-description-1.0.0.tgz#7fdb81c900101fbd564dd5f1a30af5aadc1e58d6" + integrity sha512-2EmdH1YvIQiZpltCNgkuiUnyukzxM/R6NDJX31Ke3BG1Nq5b0S2PhX59UKi9vZpPDQVdqn+1IcaAwnzTT5vCjw== + dependencies: + call-bind "^1.0.2" + get-intrinsic "^1.1.1" + github-from-package@0.0.0: version "0.0.0" resolved "https://registry.yarnpkg.com/github-from-package/-/github-from-package-0.0.0.tgz#97fb5d96bfde8973313f20e8288ef9a167fa64ce" @@ -1183,6 +1418,11 @@ graceful-fs@^4.1.2: resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.6.tgz#ff040b2b0853b23c3d31027523706f1885d76bee" integrity sha512-nTnJ528pbqxYanhpDYsi4Rd8MAeaBA67+RZ10CM1m3bTAVFEDcd5AuA4a6W5YkGZ1iNXHzZz8T6TBKLeBuNriQ== +has-bigints@^1.0.1, has-bigints@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/has-bigints/-/has-bigints-1.0.2.tgz#0871bd3e3d51626f6ca0966668ba35d5602d6eaa" + integrity sha512-tSvCKtBr9lkF0Ex0aQiP9N+OpV4zi2r/Nee5VkRDbaqv35RLYMzbwQfFSZZH0kR+Rd6302UJZ2p/bJCEoR3VoQ== + has-flag@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-3.0.0.tgz#b5d454dc2199ae225699f3467e5a07f3b955bafd" @@ -1193,11 +1433,30 @@ has-flag@^4.0.0: resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-4.0.0.tgz#944771fd9c81c81265c4d6941860da06bb59479b" integrity sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ== +has-property-descriptors@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/has-property-descriptors/-/has-property-descriptors-1.0.0.tgz#610708600606d36961ed04c196193b6a607fa861" + integrity sha512-62DVLZGoiEBDHQyqG4w9xCuZ7eJEwNmJRWw2VY84Oedb7WFcA27fiEVe8oUQx9hAUJ4ekurquucTGwsyO1XGdQ== + dependencies: + get-intrinsic "^1.1.1" + has-symbols@^1.0.1: version "1.0.2" resolved "https://registry.yarnpkg.com/has-symbols/-/has-symbols-1.0.2.tgz#165d3070c00309752a1236a479331e3ac56f1423" integrity sha512-chXa79rL/UC2KlX17jo3vRGz0azaWEx5tGqZg5pO3NUyEJVB17dMruQlzCCOfUvElghKcm5194+BCRvi2Rv/Gw== +has-symbols@^1.0.2, has-symbols@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/has-symbols/-/has-symbols-1.0.3.tgz#bb7b2c4349251dce87b125f7bdf874aa7c8b39f8" + integrity sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A== + +has-tostringtag@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/has-tostringtag/-/has-tostringtag-1.0.0.tgz#7e133818a7d394734f941e73c3d3f9291e658b25" + integrity sha512-kFjcSNhnlGV1kyoGk7OXKSawH5JOb/LzUc5w9B02hOTO0dfFRjbHQKvg1d6cf3HbeUmtU9VbbV3qzZ2Teh97WQ== + dependencies: + has-symbols "^1.0.2" + has-unicode@^2.0.0: version "2.0.1" resolved "https://registry.yarnpkg.com/has-unicode/-/has-unicode-2.0.1.tgz#e0e6fe6a28cf51138855e086d1691e771de2a8b9" @@ -1310,16 +1569,40 @@ ini@~1.3.0: resolved "https://registry.yarnpkg.com/ini/-/ini-1.3.8.tgz#a29da425b48806f34767a4efce397269af28432c" integrity sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew== +internal-slot@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/internal-slot/-/internal-slot-1.0.3.tgz#7347e307deeea2faac2ac6205d4bc7d34967f59c" + integrity sha512-O0DB1JC/sPyZl7cIo78n5dR7eUSwwpYPiXRhTzNxZVAMUuB8vlnRFyLxdrVToks6XPLVnFfbzaVd5WLjhgg+vA== + dependencies: + get-intrinsic "^1.1.0" + has "^1.0.3" + side-channel "^1.0.4" + ipaddr.js@1.9.1: version "1.9.1" resolved "https://registry.yarnpkg.com/ipaddr.js/-/ipaddr.js-1.9.1.tgz#bff38543eeb8984825079ff3a2a8e6cbd46781b3" integrity sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g== +is-arguments@^1.1.0: + version "1.1.1" + resolved "https://registry.yarnpkg.com/is-arguments/-/is-arguments-1.1.1.tgz#15b3f88fda01f2a97fec84ca761a560f123efa9b" + integrity sha512-8Q7EARjzEnKpt/PCD7e1cgUS0a6X8u5tdSiMqXhojOdoV9TsMsiO+9VLC5vAmO8N7/GmXn7yjR8qnA6bVAEzfA== + dependencies: + call-bind "^1.0.2" + has-tostringtag "^1.0.0" + is-arrayish@^0.3.1: version "0.3.2" resolved "https://registry.yarnpkg.com/is-arrayish/-/is-arrayish-0.3.2.tgz#4574a2ae56f7ab206896fb431eaeed066fdf8f03" integrity sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ== +is-bigint@^1.0.1: + version "1.0.4" + resolved "https://registry.yarnpkg.com/is-bigint/-/is-bigint-1.0.4.tgz#08147a1875bc2b32005d41ccd8291dffc6691df3" + integrity sha512-zB9CruMamjym81i2JZ3UMn54PKGsQzsJeo6xvN3HJJ4CAsQNB6iRutp2To77OfCNuoxspsIhzaPoO1zyCEhFOg== + dependencies: + has-bigints "^1.0.1" + is-binary-path@~2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/is-binary-path/-/is-binary-path-2.1.0.tgz#ea1f7f3b80f064236e83470f86c09c254fb45b09" @@ -1334,6 +1617,11 @@ is-boolean-object@^1.1.0: dependencies: call-bind "^1.0.0" +is-callable@^1.1.4, is-callable@^1.2.7: + version "1.2.7" + resolved "https://registry.yarnpkg.com/is-callable/-/is-callable-1.2.7.tgz#3bc2a85ea742d9e36205dcacdd72ca1fdc51b055" + integrity sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA== + is-ci@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/is-ci/-/is-ci-2.0.0.tgz#6bc6334181810e04b5c22b3d589fdca55026404c" @@ -1341,6 +1629,13 @@ is-ci@^2.0.0: dependencies: ci-info "^2.0.0" +is-date-object@^1.0.1: + version "1.0.5" + resolved "https://registry.yarnpkg.com/is-date-object/-/is-date-object-1.0.5.tgz#0841d5536e724c25597bf6ea62e1bd38298df31f" + integrity sha512-9YQaSxsAiSwcvS33MBk3wTCVnWK+HhF8VZR2jRxehM16QcVOdHqPn4VPHmRK4lSr38n9JriurInLcP90xsYNfQ== + dependencies: + has-tostringtag "^1.0.0" + is-extglob@^2.1.1: version "2.1.1" resolved "https://registry.yarnpkg.com/is-extglob/-/is-extglob-2.1.1.tgz#a88c02535791f02ed37c76a1b9ea9773c833f8c2" @@ -1383,6 +1678,16 @@ is-interactive@^1.0.0: resolved "https://registry.yarnpkg.com/is-interactive/-/is-interactive-1.0.0.tgz#cea6e6ae5c870a7b0a0004070b7b587e0252912e" integrity sha512-2HvIEKRoqS62guEC+qBjpvRubdX910WCMuJTZ+I9yvqKU2/12eSL549HMwtabb4oupdj2sMP50k+XJfB/8JE6w== +is-map@^2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/is-map/-/is-map-2.0.2.tgz#00922db8c9bf73e81b7a335827bc2a43f2b91127" + integrity sha512-cOZFQQozTha1f4MxLFzlgKYPTyj26picdZTx82hbc/Xf4K/tZOOXSCkMvU4pKioRXGDLJRn0GM7Upe7kR721yg== + +is-negative-zero@^2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/is-negative-zero/-/is-negative-zero-2.0.2.tgz#7bf6f03a28003b8b3965de3ac26f664d765f3150" + integrity sha512-dqJvarLawXsFbNDeJW7zAz8ItJ9cd28YufuuFzh0G8pNHjJMnY08Dv7sYX2uF5UpQOwieAeOExEYAWWfu7ZZUA== + is-npm@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/is-npm/-/is-npm-4.0.0.tgz#c90dd8380696df87a7a6d823c20d0b12bbe3c84d" @@ -1408,11 +1713,45 @@ is-path-inside@^3.0.1: resolved "https://registry.yarnpkg.com/is-path-inside/-/is-path-inside-3.0.3.tgz#d231362e53a07ff2b0e0ea7fed049161ffd16283" integrity sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ== +is-regex@^1.1.4: + version "1.1.4" + resolved "https://registry.yarnpkg.com/is-regex/-/is-regex-1.1.4.tgz#eef5663cd59fa4c0ae339505323df6854bb15958" + integrity sha512-kvRdxDsxZjhzUX07ZnLydzS1TU/TJlTUHHY4YLL87e37oUA49DfkLqgy+VjFocowy29cKvcSiu+kIv728jTTVg== + dependencies: + call-bind "^1.0.2" + has-tostringtag "^1.0.0" + +is-set@^2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/is-set/-/is-set-2.0.2.tgz#90755fa4c2562dc1c5d4024760d6119b94ca18ec" + integrity sha512-+2cnTEZeY5z/iXGbLhPrOAaK/Mau5k5eXq9j14CpRTftq0pAJu2MwVRSZhyZWBzx3o6X795Lz6Bpb6R0GKf37g== + +is-shared-array-buffer@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/is-shared-array-buffer/-/is-shared-array-buffer-1.0.2.tgz#8f259c573b60b6a32d4058a1a07430c0a7344c79" + integrity sha512-sqN2UDu1/0y6uvXyStCOzyhAjCSlHceFoMKJW8W9EU9cvic/QdsZ0kEU93HEy3IUEFZIiH/3w+AH/UQbPHNdhA== + dependencies: + call-bind "^1.0.2" + is-string@^1.0.5: version "1.0.5" resolved "https://registry.yarnpkg.com/is-string/-/is-string-1.0.5.tgz#40493ed198ef3ff477b8c7f92f644ec82a5cd3a6" integrity sha512-buY6VNRjhQMiF1qWDouloZlQbRhDPCebwxSjxMjxgemYT46YMd2NR0/H+fBhEfWX4A/w9TBJ+ol+okqJKFE6vQ== +is-string@^1.0.7: + version "1.0.7" + resolved "https://registry.yarnpkg.com/is-string/-/is-string-1.0.7.tgz#0dd12bf2006f255bb58f695110eff7491eebc0fd" + integrity sha512-tE2UXzivje6ofPW7l23cjDOMa09gb7xlAqG6jG5ej6uPV32TlWP3NKPigtaGeHNu9fohccRYvIiZMfOOnOYUtg== + dependencies: + has-tostringtag "^1.0.0" + +is-symbol@^1.0.2, is-symbol@^1.0.3: + version "1.0.4" + resolved "https://registry.yarnpkg.com/is-symbol/-/is-symbol-1.0.4.tgz#a6dac93b635b063ca6872236de88910a57af139c" + integrity sha512-C/CPBqKWnvdcxqIARxyOh4v1UUEOCHpgDa0WYgpKDFMszcrPcffg5uhwSgPCLD2WWxmq6isisz87tzT01tuGhg== + dependencies: + has-symbols "^1.0.2" + is-typedarray@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/is-typedarray/-/is-typedarray-1.0.0.tgz#e479c80858df0c1b11ddda6940f96011fcda4a9a" @@ -1423,11 +1762,23 @@ is-unicode-supported@^0.1.0: resolved "https://registry.yarnpkg.com/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz#3f26c76a809593b52bfa2ecb5710ed2779b522a7" integrity sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw== +is-weakref@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/is-weakref/-/is-weakref-1.0.2.tgz#9529f383a9338205e89765e0392efc2f100f06f2" + integrity sha512-qctsuLZmIQ0+vSSMfoVvyFe2+GSEvnmZ2ezTup1SBse9+twCCeial6EEi3Nc2KFcf6+qz2FBPnjXsk8xhKSaPQ== + dependencies: + call-bind "^1.0.2" + is-yarn-global@^0.3.0: version "0.3.0" resolved "https://registry.yarnpkg.com/is-yarn-global/-/is-yarn-global-0.3.0.tgz#d502d3382590ea3004893746754c89139973e232" integrity sha512-VjSeb/lHmkoyd8ryPVIKvOCn4D1koMqY+vqyjjUfc3xyKtP4dYOxM44sZrnqQSzSds3xyOrUTLTC9LVCVgLngw== +isarray@^2.0.5: + version "2.0.5" + resolved "https://registry.yarnpkg.com/isarray/-/isarray-2.0.5.tgz#8af1e4c1221244cc62459faf38940d4e644a5723" + integrity sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw== + isarray@~1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/isarray/-/isarray-1.0.0.tgz#bb935d48582cba168c06834957a54a3e07124f11" @@ -1438,6 +1789,19 @@ isexe@^2.0.0: resolved "https://registry.yarnpkg.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10" integrity sha1-6PvzdNxVb/iUehDcsFctYz8s+hA= +iterate-iterator@^1.0.1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/iterate-iterator/-/iterate-iterator-1.0.2.tgz#551b804c9eaa15b847ea6a7cdc2f5bf1ec150f91" + integrity sha512-t91HubM4ZDQ70M9wqp+pcNpu8OyJ9UAtXntT/Bcsvp5tZMnz9vRa+IunKXeI8AnfZMTv0jNuVEmGeLSMjVvfPw== + +iterate-value@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/iterate-value/-/iterate-value-1.0.2.tgz#935115bd37d006a52046535ebc8d07e9c9337f57" + integrity sha512-A6fMAio4D2ot2r/TYzr4yUWrmwNdsN5xL7+HUiyACE4DXm+q8HtPcnFTp+NnW3k4N05tZ7FVYFFb2CR13NxyHQ== + dependencies: + es-get-iterator "^1.0.2" + iterate-iterator "^1.0.1" + js-tokens@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499" @@ -1478,6 +1842,11 @@ keyv@^3.0.0: dependencies: json-buffer "3.0.0" +klona@^2.0.4: + version "2.0.5" + resolved "https://registry.yarnpkg.com/klona/-/klona-2.0.5.tgz#d166574d90076395d9963aa7a928fabb8d76afbc" + integrity sha512-pJiBpiXMbt7dkzXe8Ghj/u4FfXOOa98fPW+bihOJ4SjnoijweJrNThJfd3ifXpXhREjpoF2mZVH1GfS9LV3kHQ== + latest-version@^5.0.0: version "5.1.0" resolved "https://registry.yarnpkg.com/latest-version/-/latest-version-5.1.0.tgz#119dfe908fe38d15dfa43ecd13fa12ec8832face" @@ -1673,18 +2042,18 @@ node-addon-api@^4.3.0: resolved "https://registry.yarnpkg.com/node-addon-api/-/node-addon-api-4.3.0.tgz#52a1a0b475193e0928e98e0426a0d1254782b77f" integrity sha512-73sE9+3UaLYYFmDsFZnqCInzPyh3MqIwZO9cw58yIqAZhONrrabrYyYe3TuIqtIiOuTXVhsGau8hcrhhwSsDIQ== -node-fetch@^2.6.1: - version "2.6.1" - resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.6.1.tgz#045bd323631f76ed2e2b55573394416b639a0052" - integrity sha512-V4aYg89jEoVRxRb2fJdAg8FHvI7cEyYdVAh94HH0UIK8oJxUfkjlDQN9RbMx+bEjP7+ggMiFRprSti032Oipxw== - -node-fetch@^2.6.7: +node-fetch@2.6.7, node-fetch@^2.6.7: version "2.6.7" resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.6.7.tgz#24de9fba827e3b4ae44dc8b20256a379160052ad" integrity sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ== dependencies: whatwg-url "^5.0.0" +node-fetch@^2.6.1: + version "2.6.1" + resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.6.1.tgz#045bd323631f76ed2e2b55573394416b639a0052" + integrity sha512-V4aYg89jEoVRxRb2fJdAg8FHvI7cEyYdVAh94HH0UIK8oJxUfkjlDQN9RbMx+bEjP7+ggMiFRprSti032Oipxw== + nodemon@^2.0.7: version "2.0.7" resolved "https://registry.yarnpkg.com/nodemon/-/nodemon-2.0.7.tgz#6f030a0a0ebe3ea1ba2a38f71bf9bab4841ced32" @@ -1738,6 +2107,26 @@ object-assign@^4, object-assign@^4.1.0: resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863" integrity sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM= +object-inspect@^1.12.2, object-inspect@^1.9.0: + version "1.12.2" + resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.12.2.tgz#c0641f26394532f28ab8d796ab954e43c009a8ea" + integrity sha512-z+cPxW0QGUp0mcqcsgQyLVRDoXFQbXOwBaqyF7VIgI4TWNQsDHrBpUQslRmIfAoYWdYzs6UlKJtB2XJpTaNSpQ== + +object-keys@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/object-keys/-/object-keys-1.1.1.tgz#1c47f272df277f3b1daf061677d9c82e2322c60e" + integrity sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA== + +object.assign@^4.1.4: + version "4.1.4" + resolved "https://registry.yarnpkg.com/object.assign/-/object.assign-4.1.4.tgz#9673c7c7c351ab8c4d0b516f4343ebf4dfb7799f" + integrity sha512-1mxKf0e58bvyjSCtKYY4sRe9itRk3PJpquJOjeIkz885CczcI4IvJJDLPS72oowuSh+pBxUFROpX+TU++hxhZQ== + dependencies: + call-bind "^1.0.2" + define-properties "^1.1.4" + has-symbols "^1.0.3" + object-keys "^1.1.1" + on-finished@~2.3.0: version "2.3.0" resolved "https://registry.yarnpkg.com/on-finished/-/on-finished-2.3.0.tgz#20f1336481b083cd75337992a16971aa2d906947" @@ -1964,6 +2353,15 @@ readdirp@~3.5.0: dependencies: picomatch "^2.2.1" +regexp.prototype.flags@^1.4.3: + version "1.4.3" + resolved "https://registry.yarnpkg.com/regexp.prototype.flags/-/regexp.prototype.flags-1.4.3.tgz#87cab30f80f66660181a3bb7bf5981a872b367ac" + integrity sha512-fjggEOO3slI6Wvgjwflkc4NFRCTZAu5CnNfBd5qOMYhWdn67nJBBu34/TkD++eeFmd8C9r9jfXJ27+nSiRkSUA== + dependencies: + call-bind "^1.0.2" + define-properties "^1.1.3" + functions-have-names "^1.2.2" + regexpp@^3.1.0: version "3.1.0" resolved "https://registry.yarnpkg.com/regexpp/-/regexpp-3.1.0.tgz#206d0ad0a5648cffbdb8ae46438f3dc51c9f78e2" @@ -2025,6 +2423,15 @@ safe-buffer@^5.0.1, safe-buffer@~5.2.0: resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.1.tgz#1eaf9fa9bdb1fdd4ec75f58f9cdb4e6b7827eec6" integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ== +safe-regex-test@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/safe-regex-test/-/safe-regex-test-1.0.0.tgz#793b874d524eb3640d1873aad03596db2d4f2295" + integrity sha512-JBUUzyOgEwXQY1NuPtvcj/qcBDbDmEvWufhlnXZIm75DEHp+afM1r1ujJpJsV/gSM4t59tpDyPi1sd6ZaPFfsA== + dependencies: + call-bind "^1.0.2" + get-intrinsic "^1.1.3" + is-regex "^1.1.4" + "safer-buffer@>= 2.1.2 < 3": version "2.1.2" resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a" @@ -2119,6 +2526,15 @@ shebang-regex@^3.0.0: resolved "https://registry.yarnpkg.com/shebang-regex/-/shebang-regex-3.0.0.tgz#ae16f1644d873ecad843b0307b143362d4c42172" integrity sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A== +side-channel@^1.0.4: + version "1.0.4" + resolved "https://registry.yarnpkg.com/side-channel/-/side-channel-1.0.4.tgz#efce5c8fdc104ee751b25c58d4290011fa5ea2cf" + integrity sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw== + dependencies: + call-bind "^1.0.0" + get-intrinsic "^1.0.2" + object-inspect "^1.9.0" + signal-exit@^3.0.0: version "3.0.7" resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.7.tgz#a9a1767f8af84155114eaabd73f99273c8f59ad9" @@ -2234,6 +2650,24 @@ string-width@^4.0.0, string-width@^4.1.0, string-width@^4.2.0: is-fullwidth-code-point "^3.0.0" strip-ansi "^6.0.0" +string.prototype.trimend@^1.0.5: + version "1.0.5" + resolved "https://registry.yarnpkg.com/string.prototype.trimend/-/string.prototype.trimend-1.0.5.tgz#914a65baaab25fbdd4ee291ca7dde57e869cb8d0" + integrity sha512-I7RGvmjV4pJ7O3kdf+LXFpVfdNOxtCW/2C8f6jNiW4+PQchwxkCDzlk1/7p+Wl4bqFIZeF47qAHXLuHHWKAxog== + dependencies: + call-bind "^1.0.2" + define-properties "^1.1.4" + es-abstract "^1.19.5" + +string.prototype.trimstart@^1.0.5: + version "1.0.5" + resolved "https://registry.yarnpkg.com/string.prototype.trimstart/-/string.prototype.trimstart-1.0.5.tgz#5466d93ba58cfa2134839f81d7f42437e8c01fef" + integrity sha512-THx16TJCGlsN0o6dl2o6ncWUsdgnLRSA23rRE5pyGBw/mLr3Ej/R2LaqCtgP8VNMGZsvMWnf9ooZPyY2bHvUFg== + dependencies: + call-bind "^1.0.2" + define-properties "^1.1.4" + es-abstract "^1.19.5" + string_decoder@^1.1.1: version "1.3.0" resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.3.0.tgz#42f114594a46cf1a8e30b0a84f56c78c3edac21e" @@ -2380,7 +2814,7 @@ ts-mixer@^6.0.1: resolved "https://registry.yarnpkg.com/ts-mixer/-/ts-mixer-6.0.1.tgz#7c2627fb98047eb5f3c7f2fee39d1521d18fe87a" integrity sha512-hvE+ZYXuINrx6Ei6D6hz+PTim0Uf++dYbK9FFifLNwQj+RwKquhQpn868yZsCtJYiclZF1u8l6WZxxKi+vv7Rg== -tslib@^2.4.0: +tslib@^2.0.3, tslib@^2.1.0, tslib@^2.4.0: version "2.4.0" resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.4.0.tgz#7cecaa7f073ce680a05847aa77be941098f36dc3" integrity sha512-d6xOpEDfsi2CZVlPQzGeux8XMwLT9hssAsaPYExaQMuYskwb+x1x7J371tWlbBdWHroy99KnVB6qIkUbs5X3UQ== @@ -2424,6 +2858,16 @@ typedarray-to-buffer@^3.1.5: dependencies: is-typedarray "^1.0.0" +unbox-primitive@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/unbox-primitive/-/unbox-primitive-1.0.2.tgz#29032021057d5e6cdbd08c5129c226dff8ed6f9e" + integrity sha512-61pPlCD9h51VoreyJ0BReideM3MDKMKnh6+V9L08331ipq6Q8OFXZYiqP6n/tbHx4s5I9uRhcye6BrbkizkBDw== + dependencies: + call-bind "^1.0.2" + has-bigints "^1.0.2" + has-symbols "^1.0.3" + which-boxed-primitive "^1.0.2" + undefsafe@^2.0.3: version "2.0.3" resolved "https://registry.yarnpkg.com/undefsafe/-/undefsafe-2.0.3.tgz#6b166e7094ad46313b2202da7ecc2cd7cc6e7aae" @@ -2516,6 +2960,17 @@ whatwg-url@^5.0.0: tr46 "~0.0.3" webidl-conversions "^3.0.0" +which-boxed-primitive@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/which-boxed-primitive/-/which-boxed-primitive-1.0.2.tgz#13757bc89b209b049fe5d86430e21cf40a89a8e6" + integrity sha512-bwZdv0AKLpplFY2KZRX6TvyuN7ojjr7lwkg6ml0roIy9YeuSr7JS372qlNW18UQYzgYK9ziGcerWqZOmEn9VNg== + dependencies: + is-bigint "^1.0.1" + is-boolean-object "^1.1.0" + is-number-object "^1.0.4" + is-string "^1.0.5" + is-symbol "^1.0.3" + which@^2.0.1: version "2.0.2" resolved "https://registry.yarnpkg.com/which/-/which-2.0.2.tgz#7c6a8dd0a636a0327e10b59c9286eee93f3f51b1" diff --git a/website/src/components/website/dashboard/Predictions.vue b/website/src/components/website/dashboard/Predictions.vue new file mode 100644 index 00000000..974591fc --- /dev/null +++ b/website/src/components/website/dashboard/Predictions.vue @@ -0,0 +1,35 @@ + + + + + diff --git a/website/src/utils/dashboard.js b/website/src/utils/dashboard.js index d3bb4556..675efee7 100644 --- a/website/src/utils/dashboard.js +++ b/website/src/utils/dashboard.js @@ -36,6 +36,14 @@ export async function updateMatchData(auth, match, updatedData) { }); } +export async function managePred(auth, client, predictionAction) { + if (!auth?.user) return { error: true, errorMessage: "Not authenticated" }; + return await authenticatedRequest(auth, "actions/manage-prediction", { + client: client.id || client, + predictionAction + }); +} + export async function updateMapData(auth, match, mapData) { if (!auth?.user) return { error: true, errorMessage: "Not authenticated" }; return await authenticatedRequest(auth, "actions/update-map-data", { diff --git a/website/src/views/Dashboard.vue b/website/src/views/Dashboard.vue index a7f2b9d9..c6a6dfa6 100644 --- a/website/src/views/Dashboard.vue +++ b/website/src/views/Dashboard.vue @@ -12,6 +12,7 @@
+ @@ -23,10 +24,11 @@ import MatchThumbnail from "@/components/website/match/MatchThumbnail"; import MatchEditor from "@/components/website/dashboard/MatchEditor"; import { BFormCheckbox } from "bootstrap-vue"; import { togglePlayerCams } from "@/utils/dashboard"; +import Predictions from "@/components/website/dashboard/Predictions"; export default { name: "Dashboard", - components: { MatchEditor, MatchThumbnail, BroadcastSwitcher, BFormCheckbox }, + components: { Predictions, MatchEditor, MatchThumbnail, BroadcastSwitcher, BFormCheckbox }, computed: { user() { if (!this.$root.auth.user?.airtableID) return {}; From 1c243813e8310fd0fe80962141f300551fb073d5 Mon Sep 17 00:00:00 2001 From: Tobias Messner Date: Fri, 14 Oct 2022 20:23:45 +0200 Subject: [PATCH 2/3] Changes by solca --- server/src/action-manager.js | 16 ++++++++++++---- server/src/action-utils.js | 5 +++-- server/src/actions/update-map-data.js | 2 +- 3 files changed, 16 insertions(+), 7 deletions(-) diff --git a/server/src/action-manager.js b/server/src/action-manager.js index 6da1a2d7..38816f49 100644 --- a/server/src/action-manager.js +++ b/server/src/action-manager.js @@ -56,6 +56,11 @@ async function load(expressApp, cors, Cache, io) { if (!authObjects.client) return res.status(403).send({ error: true, errorMessage: "No client data associated with this token" }); } + if (action.optionalParams) { + (action.optionalParams || []).forEach(key => { + params[key] = req.body[key] || null; + }); + } if (action.requiredParams) { (action.requiredParams || []).forEach(key => { params[key] = req.body[key]; @@ -65,10 +70,13 @@ async function load(expressApp, cors, Cache, io) { try { await action.handler( async (data) => res.send({ error: false, ...data }), - async (errorMessage, errorCode) => res.status(errorCode || 400).send({ - error: true, - errorMessage - }), + async (errorMessage, errorCode) => { + console.warn(`Error in action [${action.key}] ${errorCode} ${errorMessage}`); + res.status(errorCode || 400).send({ + error: true, + errorMessage + }); + }, params, authObjects, { diff --git a/server/src/action-utils.js b/server/src/action-utils.js index 2878fbbb..f68240c2 100644 --- a/server/src/action-utils.js +++ b/server/src/action-utils.js @@ -54,7 +54,7 @@ async function updateRecord(Cache, tableName, item, data) { await slmngg(tableName).update(item.id, data); } catch (e) { console.error("Airtable update failed", e); - return { error: true}; + return { error: true }; } } @@ -70,13 +70,14 @@ async function createRecord(Cache, tableName, records) { // TODO: think about how eager update would work try { - let newRecords = await slmngg(tableName).create(records); + let newRecords = await slmngg(tableName).create(records.map(recordData => ({ fields: recordData }))); newRecords.forEach(record => { Cache.set(cleanID(record.id), deAirtable(record.fields), { eager: true }); }); console.log(newRecords.length); console.log(newRecords); } catch (e) { + console.error("Airtable create failed", e); return { error: true }; } } diff --git a/server/src/actions/update-map-data.js b/server/src/actions/update-map-data.js index 15c8dd04..d4eece41 100644 --- a/server/src/actions/update-map-data.js +++ b/server/src/actions/update-map-data.js @@ -114,7 +114,7 @@ module.exports = { if (map.score_1) fieldData["Score 1"] = map.score_1; if (map.score_2) fieldData["Score 2"] = map.score_2; - return { fields: fieldData }; + return fieldData; }); if (recordCreations.length) { From a086a9284a64d39f1fc9e35db5c3d0be69d19443 Mon Sep 17 00:00:00 2001 From: Solomon Cammack Date: Fri, 14 Oct 2022 21:02:03 +0100 Subject: [PATCH 3/3] Update access token generation and storage, add title controls, improve UI. Co-authored-by: Tobias Messner --- server/src/action-manager.js | 2 + server/src/action-utils.js | 2 +- server/src/actions/manage-prediction.js | 26 ++++-- server/src/actions/set-title.js | 89 +++++++++++++++++++ server/src/cache.js | 32 ++++++- server/src/routes.js | 29 +++--- server/src/types.js | 16 ++++ .../website/dashboard/Predictions.vue | 10 ++- website/src/utils/dashboard.js | 10 ++- website/src/views/Dashboard.vue | 16 +++- 10 files changed, 196 insertions(+), 36 deletions(-) create mode 100644 server/src/actions/set-title.js diff --git a/server/src/action-manager.js b/server/src/action-manager.js index 38816f49..176faa47 100644 --- a/server/src/action-manager.js +++ b/server/src/action-manager.js @@ -83,6 +83,7 @@ async function load(expressApp, cors, Cache, io) { updateRecord: (tableName, item, data) => updateRecord(Cache, tableName, item, data), get: Cache.get, createRecord: (tableName, data) => createRecord(Cache, tableName, data), + auth: Cache.auth } ); } catch (e) { @@ -137,6 +138,7 @@ async function load(expressApp, cors, Cache, io) { updateRecord: (tableName, item, data) => updateRecord(Cache, tableName, item, data), get: Cache.get, createRecord: (tableName, data) => createRecord(Cache, tableName, data), + auth: Cache.auth } ); } catch (e) { diff --git a/server/src/action-utils.js b/server/src/action-utils.js index f68240c2..bac03da6 100644 --- a/server/src/action-utils.js +++ b/server/src/action-utils.js @@ -78,7 +78,7 @@ async function createRecord(Cache, tableName, records) { console.log(newRecords); } catch (e) { console.error("Airtable create failed", e); - return { error: true }; + return { error: true, errorMessage: e.message }; } } diff --git a/server/src/actions/manage-prediction.js b/server/src/actions/manage-prediction.js index 452cb82f..b2566077 100644 --- a/server/src/actions/manage-prediction.js +++ b/server/src/actions/manage-prediction.js @@ -32,7 +32,7 @@ module.exports = { key: "manage-prediction", auth: ["client"], requiredParams: ["predictionAction"], - optionalParams: ["autoLockAfter"], // TODO: this wont work yet + optionalParams: ["autoLockAfter"], /*** * @param {ActionSuccessCallback} success * @param {ActionErrorCallback} error @@ -40,11 +40,12 @@ module.exports = { * @param {number?} autoLockAfter * @param {ClientData} client * @param {CacheGetFunction} get + * @param {CacheAuthFunctions} auth * @param {SimpleUpdateRecord} updateRecord * @returns {Promise} */ // eslint-disable-next-line no-empty-pattern - async handler(success, error, { predictionAction, autoLockAfter = 120 }, { client }, { get }) { + async handler(success, error, { predictionAction, autoLockAfter = 120 }, { client }, { get, auth }) { if (!(["create", "lock", "resolve", "cancel"].includes(predictionAction))) return error("Invalid action"); console.log(predictionAction); @@ -52,15 +53,20 @@ module.exports = { if (!broadcast) return error("No broadcast associated"); if (!broadcast.channel) return error("No channel associated with broadcast"); - const channel = await get(broadcast?.channel?.[0]); + const channel = await auth.getChannel(broadcast?.channel?.[0]); if (!channel.twitch_refresh_token) return error("No twitch auth token associated with channel"); if (!channel.channel_id || !channel.name || !channel.twitch_scopes) return error("Invalid channel data"); - // TODO: store this in Cache.auth somewhere - const { accessToken } = await refreshUserToken(process.env.TWITCH_CLIENT_ID, process.env.TWITCH_CLIENT_SECRET, channel.twitch_refresh_token); + let scopes = channel.twitch_scopes.split(" "); + if (!["channel:manage:predictions", "channel:read:predictions"].every(scope => scopes.includes(scope))) return error("Token doesn't have the required scopes"); + + console.log(channel); + const accessToken = await auth.getTwitchAccessToken(channel); const authProvider = new StaticAuthProvider(process.env.TWITCH_CLIENT_ID, accessToken); const api = new ApiClient({authProvider}); + // TODO: move cancel action to here + const match = await get(broadcast?.live_match?.[0]); if (!match) return error("No match associated"); @@ -100,10 +106,16 @@ module.exports = { if (targetPrediction) return error("Prediction already exists"); const predictionTitle = generatePredictionTitle(currentMap); + let outcomes = [team1.name, team2.name]; + + if (!(currentMap && currentMap.map.type === "Control")) { + outcomes.push("Draw"); + } + const responsePrediction = await api.predictions.createPrediction(channel.channel_id, { title: predictionTitle, - outcomes: [team1.name, team2.name, "Draw"], - autoLockAfter: 120 + outcomes: outcomes, + autoLockAfter: autoLockAfter || 120 }); console.log(responsePrediction); return success(); // TODO: check responsePrediction for errors diff --git a/server/src/actions/set-title.js b/server/src/actions/set-title.js new file mode 100644 index 00000000..200d59ee --- /dev/null +++ b/server/src/actions/set-title.js @@ -0,0 +1,89 @@ +const { ApiClient } = require("@twurple/api"); +const { StaticAuthProvider } = require("@twurple/auth"); +module.exports = { + key: "set-title", + auth: ["client"], + /*** + * @param {ActionSuccessCallback} success + * @param {ActionErrorCallback} error + * @param {PredictionAction} predictionAction + * @param {number?} autoLockAfter + * @param {ClientData} client + * @param {CacheGetFunction} get + * @param {CacheAuthFunctions} auth + * @param {SimpleUpdateRecord} updateRecord + * @returns {Promise} + */ + // eslint-disable-next-line no-empty-pattern + async handler(success, error, { predictionAction, autoLockAfter = 120 }, { client }, { get, auth }) { + + const broadcast = await get(client?.broadcast?.[0]); + if (!broadcast) return error("No broadcast associated"); + if (!broadcast.channel) return error("No channel associated with broadcast"); + + const event = await get(broadcast.event?.[0]); + if (!event) return error("No event associated with broadcast"); + + + const channel = await auth.getChannel(broadcast?.channel?.[0]); + if (!channel.twitch_refresh_token) return error("No twitch auth token associated with channel"); + if (!channel.channel_id || !channel.name || !channel.twitch_scopes) return error("Invalid channel data"); + let scopes = channel.twitch_scopes.split(" "); + if (!["channel:manage:broadcast"].every(scope => scopes.includes(scope))) return error("Token doesn't have the required scopes"); + + const accessToken = await auth.getTwitchAccessToken(channel); + + const authProvider = new StaticAuthProvider(process.env.TWITCH_CLIENT_ID, accessToken); + const api = new ApiClient({authProvider}); + + + const match = await get(broadcast?.live_match?.[0]); + if (!match) return error("No match associated"); + + const team1 = await get(match?.teams?.[0]); + const team2 = await get(match?.teams?.[1]); + if (!team1 || !team2) return error("Did not find two teams!"); + + const formatOptions = { + "event": event.name, + "event_long": event.name, + "event_short": event.short, + "team_1_code": team1.code, + "team_1_name": team1.name, + "team_2_code": team2.code, + "team_2_name": team2.name, + "match_sub_event": match.sub_event, + "match_round": match.round, + "match_number": match.match_number, + }; + + let newTitle = broadcast.title_format; + + Object.entries(formatOptions).forEach(([key, val]) => { + newTitle = newTitle.replace(`{${key}}`, val); + }); + + const gameMap = { + "Overwatch": "Overwatch 2", + "Valorant": "VALORANT", + "League of Legends": "League of Legends" + }; + + if (event.game && gameMap[event.game]) { + const game = await api.games.getGameByName(gameMap[event.game]); + const channelInfo = api.channels.updateChannelInfo(channel.channel_id, { + title: newTitle, + gameId: game.id + }); + console.log(channelInfo); + } else { + const channelInfo = api.channels.updateChannelInfo(channel.channel_id, { + title: newTitle + }); + console.log(channelInfo); + } + + return success(); + // return response?.error ? error("Airtable error", 500) : success(); + } +}; diff --git a/server/src/cache.js b/server/src/cache.js index cf358e03..0f366fd7 100644 --- a/server/src/cache.js +++ b/server/src/cache.js @@ -1,5 +1,7 @@ const crypto = require("crypto"); - +const { accessTokenIsExpired, + refreshUserToken +} = require("@twurple/auth"); /* - Get and set data - Store data @@ -190,6 +192,11 @@ async function set(id, data, options) { // }); } + if (data?.__tableName === "Channels") { + auth.set(`channel_${id}`, data); + return; // not setting it on global requestble store + } + if (data?.__tableName === "Events") { // update antileak if (!data.antileak?.length) { @@ -304,12 +311,33 @@ async function getPlayer(discordID) { return players.get(discordID); } +async function getChannel(airtableID) { + return auth.get(`channel_${cleanID(airtableID)}`); +} + +async function getTwitchAccessToken(channel) { + // get stored access token, check if it's valid + // otherwise / or if no token, get from refresh token + let storedToken = auth.get(`twitch_access_token_${channel.channel_id}`); + + if (!storedToken || accessTokenIsExpired(storedToken)) { + // refresh token + let token = await refreshUserToken(process.env.TWITCH_CLIENT_ID, process.env.TWITCH_CLIENT_SECRET, channel.twitch_refresh_token); + auth.set(`twitch_access_token_${channel.channel_id}`, token); + return token; + + } + return storedToken; +} + module.exports = { set, get, setup, onUpdate, auth: { start: authStart, getData: getAuthenticatedData, - getPlayer + getPlayer, + getChannel, + getTwitchAccessToken } }; diff --git a/server/src/routes.js b/server/src/routes.js index 99cb496c..0d07a2de 100644 --- a/server/src/routes.js +++ b/server/src/routes.js @@ -2,7 +2,7 @@ const fetch = require("node-fetch"); const { updateRecord, createRecord } = require("./action-utils"); -const { getTokenInfo } = require("@twurple/auth"); +const { exchangeCode } = require("@twurple/auth"); function cleanID(id) { if (!id) return null; @@ -298,8 +298,13 @@ module.exports = ({ app, cors, Cache, io }) => { } return uuid; } + const TwitchEnvSet = ["TWITCH_REDIRECT_URI", "TWITCH_CLIENT_ID", "TWITCH_CLIENT_SECRET"].every(key => !!process.env[key]); + if (!TwitchEnvSet) { + console.error("Twitch authentication on the server is disabled. Set TWITCH_ keys in server/.env to enable it."); + } app.get("/twitch_auth/:scopes", (req, res) => { + if (!TwitchEnvSet) return res.status(503).send({ error: true, message: "Twitch authentication is disabled on the server." }); let state = createState(); states[state] = req.params.scopes; res.redirect(`https://id.twitch.tv/oauth2/authorize?client_id=${process.env.TWITCH_CLIENT_ID}&redirect_uri=${process.env.TWITCH_REDIRECT_URI}&response_type=code&scope=${req.params.scopes}&force_verify=true`); @@ -307,23 +312,13 @@ module.exports = ({ app, cors, Cache, io }) => { app.get("/twitch_callback", async(req, res) => { + if (!TwitchEnvSet) return res.status(503).send({ error: true, message: "Twitch authentication is disabled on the server." }); try { - const response = await fetch("https://id.twitch.tv/oauth2/token", { - method: "POST", - headers: { - "Content-Type": "application/x-www-form-urlencoded", - }, - body: `client_id=${process.env.TWITCH_CLIENT_ID}&client_secret=${process.env.TWITCH_CLIENT_SECRET}&grant_type=authorization_code&code=${req.query.code}&redirect_uri=${process.env.TWITCH_REDIRECT_URI}` - }); - const data = await response.json(); - - // get user data - const tokenInfo = await getTokenInfo(data.access_token, process.env.TWITCH_CLIENT_ID); + const tokenInfo = await exchangeCode(process.env.TWITCH_CLIENT_ID, process.env.TWITCH_CLIENT_SECRET, req.query.code, process.env.TWITCH_REDIRECT_URI); let scopes = states[req.query.state]; if (scopes) delete states[req.query.state]; - // get or create channel in table const channelIDs = (await Cache.get("Channels"))?.ids || []; const channels = await Promise.all(channelIDs.map(id => Cache.get(id))); @@ -335,26 +330,26 @@ module.exports = ({ app, cors, Cache, io }) => { if (existingChannel) { airtableResponse = await updateRecord(Cache, "Channels", existingChannel.id, { - "Twitch Refresh Token": data.refresh_token, + "Twitch Refresh Token": tokenInfo.accessToken, "Twitch Scopes": tokenInfo.scopes.join(" "), "Channel ID": tokenInfo.userId, "Name": tokenInfo.userName }); } else { airtableResponse = await createRecord(Cache, "Channels", [{ - "Twitch Refresh Token": data.refresh_token, + "Twitch Refresh Token": tokenInfo.accessToken, "Twitch Scopes": tokenInfo.scopes.join(" "), "Channel ID": tokenInfo.userId, "Name": tokenInfo.userName }]); } - console.log(airtableResponse); - + // console.log(airtableResponse); return res.send("okay thanks"); } catch (e) { console.error("[Twitch Auth] error", e); + res.status(400).send({ error: true, errorMessage: e.message}); } }); diff --git a/server/src/types.js b/server/src/types.js index 5e74360e..59c166e4 100644 --- a/server/src/types.js +++ b/server/src/types.js @@ -27,6 +27,22 @@ * @property {DirtyAirtableID} id */ +/** + * @typedef {Object} CacheAuthFunctions + * @property {CacheGetAuthFunction} getChannel - Get Twitch channel data from Airtable + * @property {function} getTwitchAccessToken - Get refreshed access token from Twitch + */ + +/** + * @typedef CacheGetAuthFunction + * @param {AnyAirtableID} airtableID - ID from Channels table + */ +/** + * @typedef TwitchGetOrRefreshToken + * @param {object} channel - Channels table object + * @param {string} object.channel_id - Twitch channel ID + * @param {string} object.twitch_refresh_token - Twitch refresh token + */ /** * @typedef {Object} DiscordUserData diff --git a/website/src/components/website/dashboard/Predictions.vue b/website/src/components/website/dashboard/Predictions.vue index 974591fc..961967cc 100644 --- a/website/src/components/website/dashboard/Predictions.vue +++ b/website/src/components/website/dashboard/Predictions.vue @@ -1,18 +1,20 @@