diff --git a/.github/actions/spelling/allow.txt b/.github/actions/spelling/allow.txt index 24555fd1..dfdbd479 100644 --- a/.github/actions/spelling/allow.txt +++ b/.github/actions/spelling/allow.txt @@ -1,11 +1,23 @@ +appid +apikey +apiname +appdetails +appids +appinfo deno gpgarmor github githubassets https +IPlayer +ISteam leetcode +Nie npx +personaname pgn +playerstats +rtime scm shas splatoon @@ -13,5 +25,13 @@ Splatnet ssh statink STATINK +steamcommunity +steamid +steamids +steampowered +timecreated ubuntu +unlocktime +userid yargsparser +webtoken diff --git a/.github/actions/spelling/excludes.txt b/.github/actions/spelling/excludes.txt index 51b24af1..7b180c74 100644 --- a/.github/actions/spelling/excludes.txt +++ b/.github/actions/spelling/excludes.txt @@ -55,3 +55,4 @@ ignore$ ^\Qsource/templates/terminal/partials/screenshot.ejs\E$ ^\Qtests/mocks/api/github/rest/emojis/get.mjs\E$ ^\Qtests/mocks/api/axios/get/lichess.mjs\E$ +^\Qtests/mocks/api/axios/get/steam.mjs\E$ \ No newline at end of file diff --git a/.github/readme/imgs/plugin_steam_userid.png b/.github/readme/imgs/plugin_steam_userid.png new file mode 100644 index 00000000..30ede968 Binary files /dev/null and b/.github/readme/imgs/plugin_steam_userid.png differ diff --git a/.github/readme/imgs/plugin_steam_webtoken.png b/.github/readme/imgs/plugin_steam_webtoken.png new file mode 100644 index 00000000..63e83717 Binary files /dev/null and b/.github/readme/imgs/plugin_steam_webtoken.png differ diff --git a/source/app/web/statics/embed/app.placeholder.js b/source/app/web/statics/embed/app.placeholder.js index f671abaa..da229afd 100644 --- a/source/app/web/statics/embed/app.placeholder.js +++ b/source/app/web/statics/embed/app.placeholder.js @@ -1035,6 +1035,104 @@ }, }) : null), + //Steam + ...(set.plugins.enabled.steam + ? ({ + steam: { + sections: options["anilist.sections"].split(",").map(x => x.trim()).filter(x => x), + player: { + level: faker.datatype.number(100), + avatar: "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mOcOnfpfwAGfgLYttYINwAAAABJRU5ErkJggg==", + created: 1366386002, + name: faker.internet.userName(), + }, + games: { + count: 2, + playtime: 89.23333333333333, + achievements: 0, + "most-played": [ + { + id: 524220, + name: "NieR:Automata™", + icon: + "data:image/jpg;base64,/9j/2wBDAAYEBQYFBAYGBQYHBwYIChAKCgkJChQODwwQFxQYGBcUFhYaHSUfGhsjHBYWICwgIyYnKSopGR8tMC0oMCUoKSj/2wBDAQcHBwoIChMKChMoGhYaKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCj/wAARCABAAEADASIAAhEBAxEB/8QAHAAAAgMBAQEBAAAAAAAAAAAABgcFCAkEAwEA/8QAMBAAAQMDAwEGBQQDAAAAAAAAAQIDBAAFEQYHITESExQiQVEjMkJhcQhSobEVgcH/xAAUAQEAAAAAAAAAAAAAAAAAAAAA/8QAFBEBAAAAAAAAAAAAAAAAAAAAAP/aAAwDAQACEQMRAD8ArPebk9cpIW+U8D0FcJUTj39q+Kxnjp6VN6N0zcdV3xi12lpTjzqgCoDIQPc0EKlZ6YJI6Yrvt1oudwIRAhPPqV+1NXb252F01pmHGfuDHjLoPMtazlGfbsmmoxZbXHwI9uhN4/aykH+qDNa5afvNsBE+3vsgj6k1EqKgeRhXTmtQXbTb3jh6BEcT7LZSo/yKXevNktKaqbkOGGIk5acNuMnsJSfwKCgXbUlWfqrpts12DLS8zgqB5yKJNydDXLQl9ct9xQe7J+G5jhQ9KEU/MMUH5XUZOR9qu1+l7Q8fS+jDfLk0lufLyVLWPlb6giqdaYgpueorfDI8rzoSRWhGpbZKe20MK15D6YiEoSnqSAKCMTuzZv8AMeFUy6Ifb7vx2fhAjqKYTD7chpDzCgttYCkqHQikM3fbEnbB6yqhpevThU34NKfi9vI82cUz9EKfs+lILd+eS24oAISRykY4BoC5RwCSQPzS9uu6tmgXZcRCHJEdo4elIPkb980R3iWLvZp8ayS21TO78uOopC27VtqsGjLzpm/2l1m8rU4UhxIKpBUrjBxQMHfPSkDX+3L8yEEPSmGy9GcQOSeOKoU60piQ404PM2opUPYjitDNl7XPg6Ebau6VBTqlLS2vqlB6CqQbsWtFn3AusZsdlJcLmPyTQRGkZibfqq2ynD5Wngo1ou5Pks6et8yE33iCyhS04z5SkVmecpWCnqORV7/0266Z1ZoliHJfQq5RPI40evYHAoCKdqvR0BwyViCLljPd9gBeag7rutF75pk2N98k+TBHP4rx3F2YiX68qvlqfVGuSQMIUSUEj7UAzdtNwo0pl+LLjPOtqzgNelAwY+58W3zA2rT8lhx35jkDipuJrLRV5mI8aICbik5Sh5sKXn84pSx9ttwrhOW7cJUdBUAAe66Ue6G2WhWq6ovF+f8AG3JGCgtkpQD9x60DMtEp+WHHFoCI2MN4GOKz73lnouW410eRyAstjHuCauzvNrGLonQ81/vEty3GyiMgcEqrPSXJXMmyJLxy68srJ+5OaDxX81EOhNWXHR1/YudsdUlTagVIB4UPYioi4wZEJ/upCCleM1yhJ59KC/8AtzvRpnV8NhLstuFcSACw6rlSvXFM5JT84UCFDg54rLZpx1hxLsdS2lJ6KScEf7ootm4OqbatCmLvJV2eneOFQ/ug0jzzyefzQDr3dfTOjWHRMnNPTUDiM2rzVR27bjaruR7Ui7SEk8ZbWU/9oXlyZEt0uS3nH3T9S1FR/mgLd0df3LXl9cmTVqEYKIaazwkenFBaeo9K+dk56V7w4zsqQGWkFS1dAKD/2Q==", + playtime: 44.88333333333333, + played: 1582407120, + description: "NieR: Automata tells the story of androids 2B, 9S and A2 and their battle to reclaim the machine-driven dystopia overrun by powerful machines.", + genres: [ + "Action", + "RPG", + ], + achievements: [ + { + icon: + "data:image/jpg;base64,/9j/2wBDAAYEBQYFBAYGBQYHBwYIChAKCgkJChQODwwQFxQYGBcUFhYaHSUfGhsjHBYWICwgIyYnKSopGR8tMC0oMCUoKSj/2wBDAQcHBwoIChMKChMoGhYaKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCj/wAARCAAgACADASIAAhEBAxEB/8QAGAAAAwEBAAAAAAAAAAAAAAAAAAUGBwP/xAAmEAABAgQDCQAAAAAAAAAAAAABAAIDBAUGBxIUFiMxNVNhc5Gx/8QAFgEBAQEAAAAAAAAAAAAAAAAAAAME/8QAFBEBAAAAAAAAAAAAAAAAAAAAAP/aAAwDAQACEQMRAD8A6XVcM5QqlppTK+G4GJvSXEEngOyT7d1TpwPRRiVz5niH0qTWNVWbd1TpwPRTi1bhnK7UtNN5WQ2gRN0S0kg8D2Wdqsw158/xH6EBiVz5niH0qTWiXVb05XalqZTKyG0GHvQWkkHiOyT7CVTqQPZQSarMNefP8R+hGwlU6kD2U4tW3pyhVLUzeV8NwEPdAuIJPE9kH//Z", + achieved: true, + unlocked: 1565976624, + name: "Transcendent Being", + description: "", + id: "ACH_BAD_END", + }, + { + icon: + "data:image/jpg;base64,/9j/2wBDAAYEBQYFBAYGBQYHBwYIChAKCgkJChQODwwQFxQYGBcUFhYaHSUfGhsjHBYWICwgIyYnKSopGR8tMC0oMCUoKSj/2wBDAQcHBwoIChMKChMoGhYaKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCj/wAARCAAgACADASIAAhEBAxEB/8QAGAABAQEBAQAAAAAAAAAAAAAABQQDAAL/xAAtEAABAwMCBAQGAwAAAAAAAAABAgMEAAURBiISITFBExQjMlFhcYGRsRXB0f/EABcBAQEBAQAAAAAAAAAAAAAAAAABAwT/xAAUEQEAAAAAAAAAAAAAAAAAAAAA/9oADAMBAAIRAxEAPwC3UF7lWeeY8bc2rLnqKJIJPT6VhE1BdpCgAhtORy2qJ/ArxqprxtRIB7IH7pixMJZfZWOQPEB+DXG1FSL/AHhj3soB64IINU6evMm8TvLytqEYX6aikkg9D8vlSmomEPcK8hWEYIznuaE0uz4N/cx3R/dQdqdt3+cDkfgUtLZ2LOAr4DPaqbOiDeZrcW4w5cd2Mkq3HiQVBW4Ajl7eX3OOlb3W2yLjMMiPxJSklO9JByDjP0q+2tzIiAlyO27j4k1QfdotvsD7zjEOa9Hkoy64yBhrh6de2P1U2mXmpN2W8wh5Dakjh8UYKh3OKcuKZUpspbitNkjBOT/tHWu3yLfN8xJClpWQnYkqIyeX2oP/2Q==", + achieved: true, + unlocked: 1565976316, + name: "A Round by the Pond", + description: "20 different kinds of fish caught.", + id: "ACH_FISHING", + }, + ], + rate: { + total: 47, + achieved: 47, + }, + }, + ], + "recently-played": [ + { + id: 1113560, + name: "NieR Replicant ver.1.22474487139...", + icon: + "data:image/jpg;base64,/9j/2wBDAAYEBQYFBAYGBQYHBwYIChAKCgkJChQODwwQFxQYGBcUFhYaHSUfGhsjHBYWICwgIyYnKSopGR8tMC0oMCUoKSj/2wBDAQcHBwoIChMKChMoGhYaKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCj/wAARCABAAEADASIAAhEBAxEB/8QAHAAAAgIDAQEAAAAAAAAAAAAABgcDBQEECAIA/8QAMBAAAQMDAgUCBgEFAQAAAAAAAQIDBAAFERIhBhMxQVEHYRQiIzJCcRUIJFJigcH/xAAYAQADAQEAAAAAAAAAAAAAAAABAgMABP/EAB8RAAICAwACAwAAAAAAAAAAAAABAhEDEiEEQSIxYf/aAAwDAQACEQMRAD8A6m228CgT1PmN/DtMDd1s6z+jRfebg3abVJnSN2mEa1fqubeKOOBxNcBYoTwTcpaylxfX6Z+0DwaWT9FsMbezNluY5e3Jdpt0hoMqRh94jIx4HvV9ZbVHs0NMa3r5YTuvmb6vcV9Bsjdkt6GorJ0gfMvuVd81aWpUfWEqBKldSqpHV+lhHZCmNTbqSfyFYdjoQgKdSS0fzHQVOuGIriXUk8rqfet55DcVkTYg5jB+9pW9YWynjKkR5BcjFL7H+o3o1tPEbbiEtyxy1dM0K3S2amhMthUjI1AZ2J8YrUtt6DTyBcGg2UnAKhso0U6BKCmi09dLkm3cEvLdUUtuZSrfqKQ/oPbbchM6+SAVzHCUoKznAB2xTj/qVZMn08faQkqdVnTjzQF6dQmWOD4fKQOcjdxI/VNJ0LiXxDNy5FlvXo5jRPzHskeTUciNFmR0vwZCQ71Tg7ZoUeW6J5nWtZkxSdDzIOdGKEo/FlxmcQqZiwHmilWFJ7Y84qd2X1G1br4/8O7BeSEuDrqGc/qpLZc+RJMaUfpObJBPehlm6KU+0uc1yXBsVEdRUvEENu5RNKlrQtr6jC0Kxk1vsDjQfEKjRgw25g5K0at+teNUG5RXTIbCHEjC09/2KHrRc0XaAwlSyHUfTO++1ef5Q2+4FqajY7ax0Ioi6tBd6o2r+QjxtassqUQR2G1IK4sXPh65MXC3zELjh0pcjAbhI710h6gsvOWlHJISkElRPakTxE6yxqclIPwh+Vah296M+MGDsFZZw1CU4LjDQYsh0AOxldFjyBWxHgMc9UlLaQ6vZeBUMN2JCjMhb6dKkhSD3welbLMqDJePwMlLjgGVpT2qTtnSlqZlRfiHmFLwW0KyoeR4qJUpLbHORlbTSjgf+VsPvKDiUspKlDqmhuXJ+HYfLSgVJJOj/I+KMX6Fkm+klhuTFr4mXHQomPJwUgncKO5ovvqorrzSJK0pc/An8vakxeHC1Li3SM5qTHXrUQe/cVf8cX1f8FAfOdThyhwHocU9E2unQ/EUkXO3PvNqKYUdOvmdnD4pK8ZRFOfGW6WOWmUyChRGwB3FP3iRtL1qctkZCUuSRpAA2T7mgj1UtammLNISxzQkhp9SR9oCetPJeyGGdcOd/TG4G7zXbBc1KE2EsqjLUdl74A99qawZgWNouTHWoh/LVjKjSC9WZbNt4nactWuNNbOoqScZHagGVcrlOK3JEyS7k5IU4SKDx7O0Os+q1as6Ru/FYkkpgILLWcKWo/cPY0u+LuNmYrzTMJfMcZOsqB2UT2pdyOILm+xyVu4RpCfl2OKqMkryrJPcmtHHTthyeTaqKDnh3idtiW9FkkqgSDqyo50LO5/5RFd7wzcOCp0DX/cxAXU7/iemKUmQVbZx4qQOuDOHFZIwd+o8GncVdkFmdas//9k=", + playtime: 44.35, + played: 1625611102, + description: "The upgraded prequel of NieR:Automata. A kind young man sets out with Grimoire Weiss, a strange talking book, to search for the "Sealed verses" in order to save his sister Yonah, who fell terminally ill to the Black Scrawl.", + genres: [ + "Action", + "Adventure", + "RPG", + ], + achievements: [ + { + icon: + "data:image/jpg;base64,/9j/2wBDAAYEBQYFBAYGBQYHBwYIChAKCgkJChQODwwQFxQYGBcUFhYaHSUfGhsjHBYWICwgIyYnKSopGR8tMC0oMCUoKSj/2wBDAQcHBwoIChMKChMoGhYaKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCj/wAARCAAgACADASIAAhEBAxEB/8QAGAAAAwEBAAAAAAAAAAAAAAAAAwUGBAL/xAAsEAACAgEDAwMDAwUAAAAAAAABAgMEBQAGERIhMSJBUQdhkRMUoRVDUnGz/8QAFQEBAQAAAAAAAAAAAAAAAAAAAAH/xAAYEQEBAQEBAAAAAAAAAAAAAAABACERMf/aAAwDAQACEQMRAD8AsL+R3Jkd+5LEYnIrSr1Yo2QGFGDcqpJLEE88t4+2rbA08hXQDJ5N70jEDkxIgX/XSBzoWMxpr5vK3XPa08ZUD2CoF/PY6lZs7uaDaW9LmTr16NnHGT9nJGvVwvSCOxPq4DDhu3f21fcJ5a89nb8eKwVqKZ68mTtCoqxlAFLu3Qx6lbn0qPHzrNHlM5S3thcdcumercezDIpVCrGOIOGUhVIIblSD8ffSjIY3LZPZGzYzXS3LjpoLd4GRY1SJULBT1Hu3Qy/Y9/GqLH4dpc7jrP8ASWoVcfLI9UGRWLCVGMh4UkKvUV4HnudXCk+x+XisZ3IYz+9VCNz8hlB/jn+dSP1ZuGrs/dNeM8SW3pRIB5P6pCH/AJnXGWo7mxu/psvhKAvU7CKsimRF6V4UMOGYEN6fPfzphujZ1rPSLPDk7UD8wSiKZ+tVkicsvt7Bm4+CffUOCMYONyd/IboyG344QsVXJdVtyhCrVijjWNAf8pGX8Bu3fVVdz9SvuOhhnk5u3EeQIO5CgE8n8HSXBbZtbeny08Vy1dSy5siL9YiWWXjv1sex5+dI8Bh9yZL6hRbg3Jj4aaVYmihCOpBUhgPDE8+s8nto8Zt//9k=", + achieved: true, + unlocked: 1625610706, + name: "e8 a8 98 e6 86 b6 e3 82 b5 e3 83 bc e3 83 90 e3 83 bc", + description: "", + id: "ACHIEVEMENT_0230", + }, + { + icon: + "data:image/jpg;base64,/9j/2wBDAAYEBQYFBAYGBQYHBwYIChAKCgkJChQODwwQFxQYGBcUFhYaHSUfGhsjHBYWICwgIyYnKSopGR8tMC0oMCUoKSj/2wBDAQcHBwoIChMKChMoGhYaKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCj/wAARCAAgACADASIAAhEBAxEB/8QAGAABAAMBAAAAAAAAAAAAAAAABgQFBwP/xAAsEAACAQQBAwIFBAMAAAAAAAABAgMEBQYRABIhMSJBBxMUUaEVIzJhUnGC/8QAFgEBAQEAAAAAAAAAAAAAAAAAAQAC/8QAGhEAAwEBAQEAAAAAAAAAAAAAAAERIUExMv/aAAwDAQACEQMRAD8AXZJe7vT5DVJLe1sVvR1hgeeCN4JGIH8pvV0MdghXC/0TxvY6S4UsJ/Urm9fIwBBMSIF/10gb5Gp7LG9wvclZFHPTXAorQyKHR0EYUgg+ffgDNbZk2BWSeuw27O1hh0Xt9SonajUt5iZgT0bIBU9wD7+z9Yi8HGaV60ENKTkMVmknf5MRljV1lc6+4Ovbv4G+/CtFeMnt3xHtljvVfFU09XHI5VIkA0EYqdgAhtr48a4dseE5ZltTb8lya4W2qi6FqKairY2eKRWXYUqmgi999tkHR0eaHHZK2qyS23m809ujq6cMiCjeR+gFWB6mYAMNHQAUd/f244sDWW1vu8VRfbhbN/vUoRt/cMoP43+eTLxQpdLPX2+TulVTyQn/AKUgfnXM9yCgyy3ZrV3fG6GOtiqIgvTI6BV9KgjTMp36B3/vifFbvks88aX/AB40miCZ4qiNl2D/AIhiRzM6hp0+Gc/1Hw7xqQb39BErb9mUdJ/IPJtZfqSmyKgszODW1aNIqDyFAJ2fsPSeVCxX+wYvFQ2e3U1wrIzN8tvniNEBlZkBDaJPSwHkDY88O4xaMpuWd02QZVb4KN6WBoUETp0nYYDQDMd+o8YnWV4f/9k=", + achieved: true, + unlocked: 1625607419, + name: "Daredevil", + description: "", + id: "ACHIEVEMENT_0460", + }, + ], + rate: { + total: 47, + achieved: 44, + }, + }, + ], + }, + }, + }) + : null), //LeetCode ...(set.plugins.enabled.leetcode ? ({ diff --git a/source/plugins/community/splatoon/s3si/index.ts b/source/plugins/community/splatoon/s3si/index.ts index 203a64e1..f2b37097 100644 --- a/source/plugins/community/splatoon/s3si/index.ts +++ b/source/plugins/community/splatoon/s3si/index.ts @@ -12,9 +12,9 @@ class APIError extends Error { } } const AGENT_NAME = "s3si.ts"; -const S3SI_VERSION = "0.2.4"; -const NSOAPP_VERSION = "2.4.0"; -const WEB_VIEW_VERSION = "2.0.0-bd36a652"; +const S3SI_VERSION = "0.3.1"; +const NSOAPP_VERSION = "2.5.0"; +const WEB_VIEW_VERSION = "3.0.0-2857bc50"; const S3SI_LINK = "https://github.com/spacemeowx2/s3si.ts"; const USERAGENT = `${AGENT_NAME}/${S3SI_VERSION} (${S3SI_LINK})`; const DEFAULT_APP_USER_AGENT = "Mozilla/5.0 (Linux; Android 11; Pixel 5) " + "AppleWebKit/537.36 (KHTML, like Gecko) " + "Chrome/94.0.4606.61 Mobile Safari/537.36"; @@ -29,7 +29,7 @@ const SPLATNET3_STATINK_MAP = { LOFT: "yagura", GOAL: "hoko", CLAM: "asari", - TRI_COLOR: "nawabari" + TRI_COLOR: "tricolor" }, RESULT: { WIN: "win", @@ -54,15 +54,6 @@ const SPLATNET3_STATINK_MAP = { 7: "giant_tornado", 8: "mudmouth_eruption" }, - COOP_UNIFORM_MAP: { - 1: "orange", - 2: "green", - 3: "yellow", - 4: "pink", - 5: "blue", - 6: "black", - 7: "white" - }, COOP_SPECIAL_MAP: { "bd327d1b64372dedefd32adb28bea62a5b6152d93aada5d9fc4f669a1955d6d4": "nicedama", "463eedc60013608666b260c79ac8c352f9795c3d0cce074d3fbbdbd2c054a56d": "hopsonar", @@ -748,19 +739,19 @@ function parse(args, { "--": doubleDash = false , alias ={} , boolean: __boolean } const aliases = {}; if (alias !== undefined) { - for(const key1 in alias){ - const val = getForce(alias, key1); + for(const key in alias){ + const val = getForce(alias, key); if (typeof val === "string") { - aliases[key1] = [ + aliases[key] = [ val ]; } else { - aliases[key1] = val; + aliases[key] = val; } - for (const alias1 of getForce(aliases, key1)){ - aliases[alias1] = [ - key1 - ].concat(aliases[key1].filter((y)=>alias1 !== y)); + for (const alias of getForce(aliases, key)){ + aliases[alias] = [ + key + ].concat(aliases[key].filter((y)=>alias !== y)); } } } @@ -768,11 +759,11 @@ function parse(args, { "--": doubleDash = false , alias ={} , boolean: __boolean const stringArgs = typeof string === "string" ? [ string ] : string; - for (const key2 of stringArgs.filter(Boolean)){ - flags.strings[key2] = true; - const alias2 = get(aliases, key2); - if (alias2) { - for (const al of alias2){ + for (const key of stringArgs.filter(Boolean)){ + flags.strings[key] = true; + const alias = get(aliases, key); + if (alias) { + for (const al of alias){ flags.strings[al] = true; } } @@ -782,12 +773,12 @@ function parse(args, { "--": doubleDash = false , alias ={} , boolean: __boolean const collectArgs = typeof collect === "string" ? [ collect ] : collect; - for (const key3 of collectArgs.filter(Boolean)){ - flags.collect[key3] = true; - const alias3 = get(aliases, key3); - if (alias3) { - for (const al1 of alias3){ - flags.collect[al1] = true; + for (const key of collectArgs.filter(Boolean)){ + flags.collect[key] = true; + const alias = get(aliases, key); + if (alias) { + for (const al of alias){ + flags.collect[al] = true; } } } @@ -796,12 +787,12 @@ function parse(args, { "--": doubleDash = false , alias ={} , boolean: __boolean const negatableArgs = typeof negatable === "string" ? [ negatable ] : negatable; - for (const key4 of negatableArgs.filter(Boolean)){ - flags.negatable[key4] = true; - const alias4 = get(aliases, key4); - if (alias4) { - for (const al2 of alias4){ - flags.negatable[al2] = true; + for (const key of negatableArgs.filter(Boolean)){ + flags.negatable[key] = true; + const alias = get(aliases, key); + if (alias) { + for (const al of alias){ + flags.negatable[al] = true; } } } @@ -864,47 +855,47 @@ function parse(args, { "--": doubleDash = false , alias ={} , boolean: __boolean if (/^--.+=/.test(arg)) { const m = arg.match(/^--([^=]+)=(.*)$/s); assert(m != null); - const [, key5, value] = m; - if (flags.bools[key5]) { + const [, key, value] = m; + if (flags.bools[key]) { const booleanValue = value !== "false"; - setArg(key5, booleanValue, arg); + setArg(key, booleanValue, arg); } else { - setArg(key5, value, arg); + setArg(key, value, arg); } } else if (/^--no-.+/.test(arg) && get(flags.negatable, arg.replace(/^--no-/, ""))) { - const m1 = arg.match(/^--no-(.+)/); - assert(m1 != null); - setArg(m1[1], false, arg, false); + const m = arg.match(/^--no-(.+)/); + assert(m != null); + setArg(m[1], false, arg, false); } else if (/^--.+/.test(arg)) { - const m2 = arg.match(/^--(.+)/); - assert(m2 != null); - const [, key6] = m2; + const m = arg.match(/^--(.+)/); + assert(m != null); + const [, key] = m; const next = args[i + 1]; - if (next !== undefined && !/^-/.test(next) && !get(flags.bools, key6) && !flags.allBools && (get(aliases, key6) ? !aliasIsBoolean(key6) : true)) { - setArg(key6, next, arg); + if (next !== undefined && !/^-/.test(next) && !get(flags.bools, key) && !flags.allBools && (get(aliases, key) ? !aliasIsBoolean(key) : true)) { + setArg(key, next, arg); i++; } else if (/^(true|false)$/.test(next)) { - setArg(key6, next === "true", arg); + setArg(key, next === "true", arg); i++; } else { - setArg(key6, get(flags.strings, key6) ? "" : true, arg); + setArg(key, get(flags.strings, key) ? "" : true, arg); } } else if (/^-[^-]+/.test(arg)) { const letters = arg.slice(1, -1).split(""); let broken = false; for(let j = 0; j < letters.length; j++){ - const next1 = arg.slice(j + 2); - if (next1 === "-") { - setArg(letters[j], next1, arg); + const next = arg.slice(j + 2); + if (next === "-") { + setArg(letters[j], next, arg); continue; } - if (/[A-Za-z]/.test(letters[j]) && /=/.test(next1)) { - setArg(letters[j], next1.split(/=(.+)/)[1], arg); + if (/[A-Za-z]/.test(letters[j]) && /=/.test(next)) { + setArg(letters[j], next.split(/=(.+)/)[1], arg); broken = true; break; } - if (/[A-Za-z]/.test(letters[j]) && /-?\d+(\.\d*)?(e-?\d+)?$/.test(next1)) { - setArg(letters[j], next1, arg); + if (/[A-Za-z]/.test(letters[j]) && /-?\d+(\.\d*)?(e-?\d+)?$/.test(next)) { + setArg(letters[j], next, arg); broken = true; break; } @@ -916,16 +907,16 @@ function parse(args, { "--": doubleDash = false , alias ={} , boolean: __boolean setArg(letters[j], get(flags.strings, letters[j]) ? "" : true, arg); } } - const [key7] = arg.slice(-1); - if (!broken && key7 !== "-") { - if (args[i + 1] && !/^(-|--)[^-]/.test(args[i + 1]) && !get(flags.bools, key7) && (get(aliases, key7) ? !aliasIsBoolean(key7) : true)) { - setArg(key7, args[i + 1], arg); + const [key] = arg.slice(-1); + if (!broken && key !== "-") { + if (args[i + 1] && !/^(-|--)[^-]/.test(args[i + 1]) && !get(flags.bools, key) && (get(aliases, key) ? !aliasIsBoolean(key) : true)) { + setArg(key, args[i + 1], arg); i++; } else if (args[i + 1] && /^(true|false)$/.test(args[i + 1])) { - setArg(key7, args[i + 1] === "true", arg); + setArg(key, args[i + 1] === "true", arg); i++; } else { - setArg(key7, get(flags.strings, key7) ? "" : true, arg); + setArg(key, get(flags.strings, key) ? "" : true, arg); } } } else { @@ -938,35 +929,35 @@ function parse(args, { "--": doubleDash = false , alias ={} , boolean: __boolean } } } - for (const [key8, value1] of Object.entries(defaults)){ - if (!hasKey(argv, key8.split("."))) { - setKey(argv, key8, value1); - if (aliases[key8]) { - for (const x of aliases[key8]){ - setKey(argv, x, value1); + for (const [key, value] of Object.entries(defaults)){ + if (!hasKey(argv, key.split("."))) { + setKey(argv, key, value); + if (aliases[key]) { + for (const x of aliases[key]){ + setKey(argv, x, value); } } } } - for (const key9 of Object.keys(flags.bools)){ - if (!hasKey(argv, key9.split("."))) { - const value2 = get(flags.collect, key9) ? [] : false; - setKey(argv, key9, value2, false); + for (const key of Object.keys(flags.bools)){ + if (!hasKey(argv, key.split("."))) { + const value = get(flags.collect, key) ? [] : false; + setKey(argv, key, value, false); } } - for (const key10 of Object.keys(flags.strings)){ - if (!hasKey(argv, key10.split(".")) && get(flags.collect, key10)) { - setKey(argv, key10, [], false); + for (const key of Object.keys(flags.strings)){ + if (!hasKey(argv, key.split(".")) && get(flags.collect, key)) { + setKey(argv, key, [], false); } } if (doubleDash) { argv["--"] = []; - for (const key11 of notFlags){ - argv["--"].push(key11); + for (const key of notFlags){ + argv["--"].push(key); } } else { - for (const key12 of notFlags){ - argv._.push(key12); + for (const key of notFlags){ + argv._.push(key); } } return argv; @@ -1011,9 +1002,9 @@ class BytesList { chunk.start += diff; } let offset = 0; - for (const chunk1 of this.#chunks){ - chunk1.offset = offset; - offset += chunk1.end - chunk1.start; + for (const chunk of this.#chunks){ + chunk.offset = offset; + offset += chunk.end - chunk.start; } this.#len = offset; } @@ -1099,9 +1090,9 @@ function concat(...buf) { } const output = new Uint8Array(length); let index = 0; - for (const b1 of buf){ - output.set(b1, index); - index += b1.length; + for (const b of buf){ + output.set(b, index); + index += b.length; } return output; } @@ -1324,10 +1315,10 @@ class BufReader { if (p.byteLength === 0) return rr; if (this.#r === this.#w) { if (p.byteLength >= this.#buf.byteLength) { - const rr1 = await this.#rd.read(p); - const nread = rr1 ?? 0; + const rr = await this.#rd.read(p); + const nread = rr ?? 0; assert(nread >= 0, "negative read"); - return rr1; + return rr; } this.#r = 0; this.#w = 0; @@ -2120,14 +2111,14 @@ function utf8DecodeJs(bytes, inputOffset, byteLength) { const byte2 = bytes[offset++] & 0x3f; units.push((byte1 & 0x1f) << 6 | byte2); } else if ((byte1 & 0xf0) === 0xe0) { - const byte21 = bytes[offset++] & 0x3f; + const byte2 = bytes[offset++] & 0x3f; const byte3 = bytes[offset++] & 0x3f; - units.push((byte1 & 0x1f) << 12 | byte21 << 6 | byte3); + units.push((byte1 & 0x1f) << 12 | byte2 << 6 | byte3); } else if ((byte1 & 0xf8) === 0xf0) { - const byte22 = bytes[offset++] & 0x3f; - const byte31 = bytes[offset++] & 0x3f; + const byte2 = bytes[offset++] & 0x3f; + const byte3 = bytes[offset++] & 0x3f; const byte4 = bytes[offset++] & 0x3f; - let unit = (byte1 & 0x07) << 0x12 | byte22 << 0x0c | byte31 << 0x06 | byte4; + let unit = (byte1 & 0x07) << 0x12 | byte2 << 0x0c | byte3 << 0x06 | byte4; if (unit > 0xffff) { unit -= 0x10000; units.push(unit >>> 10 & 0x3ff | 0xd800); @@ -2195,18 +2186,18 @@ function encodeTimeSpecToTimestamp({ sec , nsec }) { } else { const secHigh = sec / 0x100000000; const secLow = sec & 0xffffffff; - const rv1 = new Uint8Array(8); - const view1 = new DataView(rv1.buffer); - view1.setUint32(0, nsec << 2 | secHigh & 0x3); - view1.setUint32(4, secLow); - return rv1; + const rv = new Uint8Array(8); + const view = new DataView(rv.buffer); + view.setUint32(0, nsec << 2 | secHigh & 0x3); + view.setUint32(4, secLow); + return rv; } } else { - const rv2 = new Uint8Array(12); - const view2 = new DataView(rv2.buffer); - view2.setUint32(0, nsec); - setInt64(view2, 4, sec); - return rv2; + const rv = new Uint8Array(12); + const view = new DataView(rv.buffer); + view.setUint32(0, nsec); + setInt64(view, 4, sec); + return rv; } } function encodeDateToTimeSpec(date) { @@ -2242,20 +2233,20 @@ function decodeTimestampToTimeSpec(data) { { const nsec30AndSecHigh2 = view.getUint32(0); const secLow32 = view.getUint32(4); - const sec1 = (nsec30AndSecHigh2 & 0x3) * 0x100000000 + secLow32; - const nsec1 = nsec30AndSecHigh2 >>> 2; + const sec = (nsec30AndSecHigh2 & 0x3) * 0x100000000 + secLow32; + const nsec = nsec30AndSecHigh2 >>> 2; return { - sec: sec1, - nsec: nsec1 + sec, + nsec }; } case 12: { - const sec2 = getInt64(view, 4); - const nsec2 = view.getUint32(0); + const sec = getInt64(view, 4); + const nsec = view.getUint32(0); return { - sec: sec2, - nsec: nsec2 + sec, + nsec }; } default: @@ -2302,13 +2293,13 @@ class ExtensionCodec { } } } - for(let i1 = 0; i1 < this.encoders.length; i1++){ - const encoder1 = this.encoders[i1]; - if (encoder1 != null) { - const data1 = encoder1(object, context); - if (data1 != null) { - const type1 = i1; - return new ExtData(type1, data1); + for(let i = 0; i < this.encoders.length; i++){ + const encoder = this.encoders[i]; + if (encoder != null) { + const data = encoder(object, context); + if (data != null) { + const type = i; + return new ExtData(type, data); } } } @@ -2482,11 +2473,11 @@ class Encoder { utf8EncodeTE(object, this.bytes, this.pos); this.pos += byteLength; } else { - const byteLength1 = utf8Count(object); - this.ensureBufferSizeToWrite(maxHeaderSize + byteLength1); - this.writeStringHeader(byteLength1); + const byteLength = utf8Count(object); + this.ensureBufferSizeToWrite(maxHeaderSize + byteLength); + this.writeStringHeader(byteLength); utf8EncodeJs(object, this.bytes, this.pos); - this.pos += byteLength1; + this.pos += byteLength; } } encodeObject(object, depth) { @@ -2896,9 +2887,9 @@ class Decoder { object = {}; } } else if (headByte < 0xa0) { - const size1 = headByte - 0x90; - if (size1 !== 0) { - this.pushArrayState(size1); + const size = headByte - 0x90; + if (size !== 0) { + this.pushArrayState(size); this.complete(); continue DECODE; } else { @@ -2935,59 +2926,59 @@ class Decoder { } else if (headByte === 0xd3) { object = this.readI64(); } else if (headByte === 0xd9) { - const byteLength1 = this.lookU8(); - object = this.decodeUtf8String(byteLength1, 1); + const byteLength = this.lookU8(); + object = this.decodeUtf8String(byteLength, 1); } else if (headByte === 0xda) { - const byteLength2 = this.lookU16(); - object = this.decodeUtf8String(byteLength2, 2); + const byteLength = this.lookU16(); + object = this.decodeUtf8String(byteLength, 2); } else if (headByte === 0xdb) { - const byteLength3 = this.lookU32(); - object = this.decodeUtf8String(byteLength3, 4); + const byteLength = this.lookU32(); + object = this.decodeUtf8String(byteLength, 4); } else if (headByte === 0xdc) { - const size2 = this.readU16(); - if (size2 !== 0) { - this.pushArrayState(size2); + const size = this.readU16(); + if (size !== 0) { + this.pushArrayState(size); this.complete(); continue DECODE; } else { object = []; } } else if (headByte === 0xdd) { - const size3 = this.readU32(); - if (size3 !== 0) { - this.pushArrayState(size3); + const size = this.readU32(); + if (size !== 0) { + this.pushArrayState(size); this.complete(); continue DECODE; } else { object = []; } } else if (headByte === 0xde) { - const size4 = this.readU16(); - if (size4 !== 0) { - this.pushMapState(size4); + const size = this.readU16(); + if (size !== 0) { + this.pushMapState(size); this.complete(); continue DECODE; } else { object = {}; } } else if (headByte === 0xdf) { - const size5 = this.readU32(); - if (size5 !== 0) { - this.pushMapState(size5); + const size = this.readU32(); + if (size !== 0) { + this.pushMapState(size); this.complete(); continue DECODE; } else { object = {}; } } else if (headByte === 0xc4) { - const size6 = this.lookU8(); - object = this.decodeBinary(size6, 1); + const size = this.lookU8(); + object = this.decodeBinary(size, 1); } else if (headByte === 0xc5) { - const size7 = this.lookU16(); - object = this.decodeBinary(size7, 2); + const size = this.lookU16(); + object = this.decodeBinary(size, 2); } else if (headByte === 0xc6) { - const size8 = this.lookU32(); - object = this.decodeBinary(size8, 4); + const size = this.lookU32(); + object = this.decodeBinary(size, 4); } else if (headByte === 0xd4) { object = this.decodeExtension(1, 0); } else if (headByte === 0xd5) { @@ -2999,14 +2990,14 @@ class Decoder { } else if (headByte === 0xd8) { object = this.decodeExtension(16, 0); } else if (headByte === 0xc7) { - const size9 = this.lookU8(); - object = this.decodeExtension(size9, 1); + const size = this.lookU8(); + object = this.decodeExtension(size, 1); } else if (headByte === 0xc8) { - const size10 = this.lookU16(); - object = this.decodeExtension(size10, 2); + const size = this.lookU16(); + object = this.decodeExtension(size, 2); } else if (headByte === 0xc9) { - const size11 = this.lookU32(); - object = this.decodeExtension(size11, 4); + const size = this.lookU32(); + object = this.decodeExtension(size, 4); } else { throw new Error(`Unrecognized type byte: ${prettyByte(headByte)}`); } @@ -4458,7 +4449,7 @@ function globToRegExp(glob, { extended =true , globstar: globstarOption = true , else if (value == "lower") segment += "a-z"; else if (value == "print") segment += "\x20-\x7E"; else if (value == "punct") { - segment += "!\"#$%&'()*+,\\-./:;<=>?@[\\\\\\]^_‘{|}~"; + segment += "!\"#$%&'()*+,\\-./:;<=>?@[\\\\\\]^_ÔÇÿ{|}~"; } else if (value == "space") segment += "\\s\v"; else if (value == "upper") segment += "A-Z"; else if (value == "word") segment += "\\w"; @@ -5051,12 +5042,12 @@ function parseHistoryDetailId(id) { uuid }; } else if (coopRE.test(plainText)) { - const [, uid1, timestamp1, uuid1] = plainText.match(coopRE); + const [, uid, timestamp, uuid] = plainText.match(coopRE); return { type: "CoopHistoryDetail", - uid: uid1, - timestamp: timestamp1, - uuid: uuid1 + uid, + timestamp, + uuid }; } else { throw new Error(`Invalid ID: ${plainText}`); @@ -5081,55 +5072,75 @@ function urlSimplify(url) { return url; } } -async function loginManually({ newFetcher , prompts: { promptLogin } }) { +async function loginSteps({ newFetcher }, step2) { const fetch = newFetcher(); - const state = urlBase64Encode(random(36)); - const authCodeVerifier = urlBase64Encode(random(32)); - const authCvHash = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(authCodeVerifier)); - const authCodeChallenge = urlBase64Encode(authCvHash); - const body = { - "state": state, - "redirect_uri": "npf71b963c1b7b6d119://auth", - "client_id": "71b963c1b7b6d119", - "scope": "openid user user.birthday user.mii user.screenName", - "response_type": "session_token_code", - "session_token_code_challenge": authCodeChallenge, - "session_token_code_challenge_method": "S256", - "theme": "login_form" - }; - const url = "https://accounts.nintendo.com/connect/1.0.0/authorize?" + new URLSearchParams(body); - const res = await fetch.get({ - url, - headers: { - "Host": "accounts.nintendo.com", - "Connection": "keep-alive", - "Cache-Control": "max-age=0", - "Upgrade-Insecure-Requests": "1", - "User-Agent": "Mozilla/5.0 (Linux; Android 11; Pixel 5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/94.0.4606.61 Mobile Safari/537.36", - "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8n", - "DNT": "1", - "Accept-Encoding": "gzip,deflate,br" + if (!step2) { + const state = urlBase64Encode(random(36)); + const authCodeVerifier = urlBase64Encode(random(32)); + const authCvHash = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(authCodeVerifier)); + const authCodeChallenge = urlBase64Encode(authCvHash); + const body = { + "state": state, + "redirect_uri": "npf71b963c1b7b6d119://auth", + "client_id": "71b963c1b7b6d119", + "scope": "openid user user.birthday user.mii user.screenName", + "response_type": "session_token_code", + "session_token_code_challenge": authCodeChallenge, + "session_token_code_challenge_method": "S256", + "theme": "login_form" + }; + const url = "https://accounts.nintendo.com/connect/1.0.0/authorize?" + new URLSearchParams(body); + const res = await fetch.get({ + url, + headers: { + "Host": "accounts.nintendo.com", + "Connection": "keep-alive", + "Cache-Control": "max-age=0", + "Upgrade-Insecure-Requests": "1", + "User-Agent": DEFAULT_APP_USER_AGENT, + "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8n", + "DNT": "1", + "Accept-Encoding": "gzip,deflate,br" + } + }); + return { + authCodeVerifier, + url: res.url + }; + } else { + const { login , authCodeVerifier } = step2; + const loginURL = new URL(login); + const params = new URLSearchParams(loginURL.hash.substring(1)); + const sessionTokenCode = params.get("session_token_code"); + if (!sessionTokenCode) { + throw new Error("No session token code provided"); } - }); - const login = (await promptLogin(res.url)).trim(); + const sessionToken = await getSessionToken({ + fetch, + sessionTokenCode, + authCodeVerifier + }); + if (!sessionToken) { + throw new Error("No session token found"); + } + return { + sessionToken + }; + } +} +async function loginManually(env) { + const { prompts: { promptLogin } } = env; + const step1 = await loginSteps(env); + const { url , authCodeVerifier } = step1; + const login = (await promptLogin(url)).trim(); if (!login) { throw new Error("No login URL provided"); } - const loginURL = new URL(login); - const params = new URLSearchParams(loginURL.hash.substring(1)); - const sessionTokenCode = params.get("session_token_code"); - if (!sessionTokenCode) { - throw new Error("No session token code provided"); - } - const sessionToken = await getSessionToken({ - fetch, - sessionTokenCode, - authCodeVerifier + const step2 = await loginSteps(env, { + authCodeVerifier, + login }); - if (!sessionToken) { - throw new Error("No session token found"); - } - return sessionToken; + return step2.sessionToken; } async function getGToken({ fApi , sessionToken , env }) { const fetch = env.newFetcher(); @@ -5460,7 +5471,7 @@ class Profile { } var Queries; (function(Queries) { - Queries["HomeQuery"] = "dba47124d5ec3090c97ba17db5d2f4b3"; + Queries["HomeQuery"] = "22e2fa8294168003c21b00c333c35384"; Queries["LatestBattleHistoriesQuery"] = "4f5f26e64bca394b45345a65a2f383bd"; Queries["RegularBattleHistoriesQuery"] = "d5b795d09e67ce153e622a184b7e7dfa"; Queries["BankaraBattleHistoriesQuery"] = "de4754588109b77dbcb90fbe44b612ee"; @@ -5468,7 +5479,7 @@ var Queries; Queries["PrivateBattleHistoriesQuery"] = "1d6ed57dc8b801863126ad4f351dfb9a"; Queries["VsHistoryDetailQuery"] = "291295ad311b99a6288fc95a5c4cb2d2"; Queries["CoopHistoryQuery"] = "6ed02537e4a65bbb5e7f4f23092f6154"; - Queries["CoopHistoryDetailQuery"] = "3cc5f826a6646b85f3ae45db51bd0707"; + Queries["CoopHistoryDetailQuery"] = "379f0d9b78b531be53044bcac031b34b"; Queries["myOutfitCommonDataFilteringConditionQuery"] = "d02ab22c9dccc440076055c8baa0fa7a"; Queries["myOutfitCommonDataEquipmentsQuery"] = "d29cd0c2b5e6bac90dd5b817914832f8"; Queries["HistoryRecordQuery"] = "32b6771f94083d8f04848109b7300af5"; @@ -5594,7 +5605,7 @@ class Splatnet3 { return false; } try { - await this.request(Queries.HomeQuery); + await this.request(Queries.ConfigureAnalyticsQuery); return true; } catch (_e) { return false; @@ -5843,8 +5854,8 @@ class StatInkAPI { } } _getAliasName(name) { - const STAT_INK_DOT = "·"; - const SPLATNET_DOT = "‧"; + const STAT_INK_DOT = "┬À"; + const SPLATNET_DOT = "ÔǺ"; if (name.includes(STAT_INK_DOT)) { return [ name, @@ -5894,12 +5905,6 @@ class StatInkExporter { return vsMode.mode === "FEST" && b64Number(vsMode.id) === 8; } async exportGame(game) { - if (game.type === "VsInfo" && this.isTriColor(game.detail)) { - return { - status: "skip", - reason: "Tri-color fest is not supported" - }; - } if (game.type === "VsInfo") { const body = await this.mapBattle(game); const { url } = await this.api.postBattle(body); @@ -5908,11 +5913,11 @@ class StatInkExporter { url }; } else { - const body1 = await this.mapCoop(game); - const { url: url1 } = await this.api.postCoop(body1); + const body = await this.mapCoop(game); + const { url } = await this.api.postCoop(body); return { status: "success", - url: url1 + url }; } } @@ -5955,7 +5960,7 @@ class StatInkExporter { } else if (modeId === 7) { return "splatfest_challenge"; } else if (modeId === 8) { - throw new Error("Tri-color battle is not supported"); + return "splatfest_open"; } } else if (vsMode === "X_MATCH") { return "xmatch"; @@ -6009,6 +6014,7 @@ class StatInkExporter { weapon: b64Number(player.weapon.id).toString(), inked: player.paint, gears: await this.mapGears(player), + crown: player.crown ? "yes" : "no", disconnected: player.result ? "no" : "yes" }; if (player.result) { @@ -6016,6 +6022,7 @@ class StatInkExporter { result.assist = player.result.assist; result.kill = result.kill_or_assist - result.assist; result.death = player.result.death; + result.signal = player.result.noroshiTry ?? undefined; result.special = player.result.special; } return result; @@ -6027,6 +6034,9 @@ class StatInkExporter { throw new Error("Self not found"); } const startedAt = Math.floor(new Date(playedTime).getTime() / 1000); + if (otherTeams.length === 0) { + throw new Error(`Other teams is empty`); + } const result = { uuid: await gameId(vsDetail.id), lobby: this.mapLobby(vsDetail), @@ -6038,7 +6048,7 @@ class StatInkExporter { rank_in_team: vsDetail.myTeam.players.indexOf(self) + 1, medals: vsDetail.awards.map((i)=>i.name), our_team_players: await Promise.all(myTeam.players.map(this.mapPlayer)), - their_team_players: await Promise.all(otherTeams.flatMap((i)=>i.players).map(this.mapPlayer)), + their_team_players: await Promise.all(otherTeams[0].players.map(this.mapPlayer)), agent: AGENT_NAME, agent_version: S3SI_VERSION, agent_variables: { @@ -6053,18 +6063,47 @@ class StatInkExporter { result.assist = self.result.assist; result.kill = result.kill_or_assist - result.assist; result.death = self.result.death; + result.signal = self.result.noroshiTry ?? undefined; result.special = self.result.special; } + result.our_team_color = this.mapColor(myTeam.color); + result.their_team_color = this.mapColor(otherTeams[0].color); + if (otherTeams.length === 2) { + result.third_team_color = this.mapColor(otherTeams[1].color); + } if (festMatch) { result.fest_dragon = SPLATNET3_STATINK_MAP.DRAGON[festMatch.dragonMatchType]; result.clout_change = festMatch.contribution; result.fest_power = festMatch.myFestPower ?? undefined; } - if (rule === "TURF_WAR") { + if (rule === "TURF_WAR" || rule === "TRI_COLOR") { result.our_team_percent = (myTeam?.result?.paintRatio ?? 0) * 100; result.their_team_percent = (otherTeams?.[0]?.result?.paintRatio ?? 0) * 100; result.our_team_inked = myTeam.players.reduce((acc, i)=>acc + i.paint, 0); result.their_team_inked = otherTeams?.[0].players.reduce((acc, i)=>acc + i.paint, 0); + if (myTeam.festTeamName) { + result.our_team_theme = myTeam.festTeamName; + } + if (myTeam.tricolorRole) { + result.our_team_role = myTeam.tricolorRole === "DEFENSE" ? "defender" : "attacker"; + } + if (otherTeams[0].festTeamName) { + result.their_team_theme = otherTeams[0].festTeamName; + } + if (otherTeams[0].tricolorRole) { + result.their_team_role = otherTeams[0].tricolorRole === "DEFENSE" ? "defender" : "attacker"; + } + if (otherTeams.length === 2) { + result.third_team_players = await Promise.all(otherTeams[1].players.map(this.mapPlayer)); + result.third_team_percent = (otherTeams[1]?.result?.paintRatio ?? 0) * 100; + result.third_team_inked = otherTeams[1].players.reduce((acc, i)=>acc + i.paint, 0); + if (otherTeams[1].festTeamName) { + result.third_team_theme = otherTeams[1].festTeamName; + } + if (otherTeams[1].tricolorRole) { + result.third_team_role = otherTeams[1].tricolorRole === "DEFENSE" ? "defender" : "attacker"; + } + } } if (knockout) { result.knockout = knockout === "NEITHER" ? "no" : "yes"; @@ -6100,9 +6139,6 @@ class StatInkExporter { result.rank_after_exp = rankState.rankPoint; if (!bankaraMatchChallenge?.isUdemaeUp && result.rank_exp_change === undefined) { result.rank_exp_change = result.rank_after_exp - result.rank_before_exp; - } else if (bankaraMatchChallenge?.isUdemaeUp && bankaraMatchChallenge.earnedUdemaePoint) { - result.rank_before_exp = result.rank_after_exp - bankaraMatchChallenge.earnedUdemaePoint; - result.rank_exp_change = undefined; } if (!result.rank_after) { [result.rank_after, result.rank_after_s_plus] = parseUdemae(rankState.rank); @@ -6110,6 +6146,16 @@ class StatInkExporter { } return result; } + mapColor(color) { + const float2hex = (i)=>Math.round(i * 255).toString(16).padStart(2, "0"); + const nums = [ + color.r, + color.g, + color.b, + color.a + ]; + return nums.map(float2hex).join(""); + } isRandom(image) { const RANDOM_FILENAME = "473fffb2442075078d8bb7125744905abdeae651b6a5b7453ae295582e45f7d1"; const url = image?.url; @@ -6158,7 +6204,7 @@ class StatInkExporter { name: player.name, number: player.nameId, splashtag_title: player.byname, - uniform: SPLATNET3_STATINK_MAP.COOP_UNIFORM_MAP[b64Number(player.uniform.id)], + uniform: b64Number(player.uniform.id).toString(), special: specialWeapon ? await this.mapSpecial(specialWeapon) : undefined, weapons: await Promise.all(weapons.map((w)=>this.mapCoopWeapon(w))), golden_eggs: goldenDeliverCount, @@ -6310,7 +6356,7 @@ class FileExporter { const { uid , timestamp } = parseHistoryDetailId(id); return `${uid}_${timestamp}Z.json`; } - async exportedGames({ uid , type }) { + async exportedGames({ uid , type , filter }) { const out = []; for await (const entry of Deno.readDir(this.exportPath)){ const filename = entry.name; @@ -6325,12 +6371,18 @@ class FileExporter { continue; } if (body.type === "VS" && type === "VsInfo") { + if (filter && !filter(body.data)) { + continue; + } out.push({ id: body.data.detail.id, filepath, timestamp }); } else if (body.type === "COOP" && type === "CoopInfo") { + if (filter && !filter(body.data)) { + continue; + } out.push({ id: body.data.detail.id, filepath, @@ -6405,6 +6457,29 @@ class FileExporter { } exportPath; } +const SEASONS = [ + { + id: "season202209", + name: "Drizzle Season 2022", + start: new Date("2022-09-01T00:00:00+00:00"), + end: new Date("2022-12-01T00:00:00+00:00") + }, + { + id: "season202212", + name: "Chill Season 2022", + start: new Date("2022-12-01T00:00:00+00:00"), + end: new Date("2023-03-01T00:00:00+00:00") + }, + { + id: "season202303", + name: "Fresh Season 2023", + start: new Date("2023-03-01T00:00:00+00:00"), + end: new Date("2023-06-01T00:00:00+00:00") + } +]; +const getSeason = (date)=>{ + return SEASONS.find((s)=>s.start <= date && date < s.end); +}; const splusParams = ()=>{ const out = []; for(let i = 0; i < 50; i++){ @@ -6520,8 +6595,27 @@ const RANK_PARAMS = [ ...splusParams() ]; function addRank(state, delta) { + if (!state) { + if (delta.isPromotion && delta.isRankUp) { + state = getRankStateByDelta(delta); + } else { + return; + } + } + if (state.gameId !== delta.before.gameId) { + throw new Error("Invalid state"); + } const { rank , rankPoint } = state; const { gameId , timestamp , rankAfter , isPromotion , isRankUp , isChallengeFirst } = delta; + if (state.timestamp) { + const oldSeason = getSeason(new Date(state.timestamp * 1000)); + if (oldSeason) { + const newSeason = getSeason(new Date(timestamp * 1000)); + if (newSeason?.id !== oldSeason.id) { + return; + } + } + } const rankIndex = RANK_PARAMS.findIndex((r)=>r.rank === rank); if (rankIndex === -1) { throw new Error(`Rank not found: ${rank}`); @@ -6529,34 +6623,46 @@ function addRank(state, delta) { const rankParam = RANK_PARAMS[rankIndex]; if (isChallengeFirst) { return { - gameId, - timestamp, - rank, - rankPoint: rankPoint - rankParam.charge + before: state, + after: { + gameId, + timestamp, + rank, + rankPoint: rankPoint - rankParam.charge + } }; } if (rankIndex === RANK_PARAMS.length - 1) { return { - timestamp, - gameId, - rank, - rankPoint: Math.min(rankPoint + delta.rankPoint, rankParam.pointRange[1]) + before: state, + after: { + timestamp, + gameId, + rank, + rankPoint: Math.min(rankPoint + delta.rankPoint, rankParam.pointRange[1]) + } }; } if (isPromotion && isRankUp) { const nextRankParam = RANK_PARAMS[rankIndex + 1]; return { - gameId, - timestamp, - rank: nextRankParam.rank, - rankPoint: nextRankParam.pointRange[0] + before: state, + after: { + gameId, + timestamp, + rank: nextRankParam.rank, + rankPoint: nextRankParam.pointRange[0] + } }; } return { - gameId, - timestamp, - rank: rankAfter ?? rank, - rankPoint: rankPoint + delta.rankPoint + before: state, + after: { + gameId, + timestamp, + rank: rankAfter ?? rank, + rankPoint: rankPoint + delta.rankPoint + } }; } const battleTime = (id)=>{ @@ -6564,32 +6670,56 @@ const battleTime = (id)=>{ const dateStr = timestamp.replace(/(\d{4})(\d{2})(\d{2})T(\d{2})(\d{2})(\d{2})/, "$1-$2-$3T$4:$5:$6Z"); return new Date(dateStr); }; -function generateDeltaList(state, flatten) { - const index = flatten.findIndex((i)=>i.gameId === state.gameId); - if (index === -1) { - return; +function beginPoint(state, flatten) { + if (state) { + const index = flatten.findIndex((i)=>i.gameId === state.gameId); + if (index !== -1) { + return [ + flatten[index], + flatten.slice(index) + ]; + } } - const unProcessed = flatten.slice(index); + if (flatten.length === 0) { + throw new Error("flatten must not be empty"); + } + return [ + flatten[0], + flatten + ]; +} +function getTimestamp(date) { + return Math.floor(date.getTime() / 1000); +} +function generateDeltaList(state, flatten) { + const [firstItem, unProcessed] = beginPoint(state, flatten); const deltaList = []; - let beforeGameId = state.gameId; + let before = { + gameId: firstItem.gameId, + timestamp: getTimestamp(firstItem.time) + }; for (const i of unProcessed.slice(1)){ if (!i.detail.bankaraMatch) { throw new TypeError("bankaraMatch must be defined"); } let delta = { - beforeGameId, + before, gameId: i.gameId, - timestamp: Math.floor(i.time.getTime() / 1000), + timestamp: getTimestamp(i.time), rankPoint: 0, isPromotion: false, isRankUp: false, isChallengeFirst: false }; - beforeGameId = i.gameId; + before = { + gameId: i.gameId, + timestamp: Math.floor(i.time.getTime() / 1000) + }; if (i.bankaraMatchChallenge) { if (i.index === 0 && i.bankaraMatchChallenge.state !== "INPROGRESS") { delta = { ...delta, + rank: i.detail.udemae, rankAfter: i.bankaraMatchChallenge.udemaeAfter ?? undefined, rankPoint: i.bankaraMatchChallenge.earnedUdemaePoint ?? 0, isPromotion: i.bankaraMatchChallenge.isPromo ?? false, @@ -6611,52 +6741,42 @@ function generateDeltaList(state, flatten) { } deltaList.push(delta); } - return deltaList; + return { + firstItem, + deltaList + }; } -function getRankState(i) { - const rank = i.detail.udemae; - if (!rank) { - throw new Error("rank must be defined"); +function getRankStateByDelta(i) { + const rank = i.rank; + const nextRank = i.rankAfter; + const earnedUdemaePoint = i.rankPoint; + if (!rank || !nextRank) { + throw new Error("rank and nextRank must be defined"); } const param = RANK_PARAMS.find((i)=>i.rank === rank); - if (!param) { - throw new Error(`Rank not found: ${rank}`); + const nextParam = RANK_PARAMS.find((i)=>i.rank === nextRank); + if (!param || !nextParam) { + throw new Error(`Rank or nextRank not found: ${rank} ${nextRank}`); } + const oldRankPoint = nextParam.pointRange[0] - earnedUdemaePoint; return { - gameId: i.gameId, - timestamp: Math.floor(i.time.getTime() / 1000), + gameId: i.before.gameId, + timestamp: i.before.timestamp, rank, - rankPoint: -1 + rankPoint: oldRankPoint }; } class RankTracker { deltaMap; + stateMap; constructor(state){ this.state = state; this.deltaMap = new Map(); + this.stateMap = new Map(); } async getRankStateById(id) { - if (!this.state) { - return; - } const gid = await gameId(id); - let cur = this.state; - let before = cur; - if (cur.gameId === gid) { - return; - } - while(cur.gameId !== gid){ - const delta = this.deltaMap.get(cur.gameId); - if (!delta) { - return; - } - before = cur; - cur = addRank(cur, delta); - } - return { - before, - after: cur - }; + return this.stateMap.get(gid); } setState(state) { this.state = state; @@ -6676,37 +6796,18 @@ class RankTracker { ...i, gameId })))); - const gameIdTime = new Map(flatten.map((i)=>[ - i.gameId, - i.time - ])); - let curState; - const oldestPromotion = flatten.find((i)=>i.bankaraMatchChallenge?.isPromo && i.bankaraMatchChallenge.isUdemaeUp); - const thisStateTime = gameIdTime.get(this.state?.gameId); - if (!thisStateTime && !oldestPromotion) { - return; - } else if (thisStateTime && !oldestPromotion) { - curState = this.state; - } else if (!thisStateTime && oldestPromotion) { - curState = getRankState(oldestPromotion); - } else if (thisStateTime && oldestPromotion) { - if (thisStateTime <= oldestPromotion.time) { - curState = this.state; - } else { - curState = getRankState(oldestPromotion); - } - } - if (!curState) { - return; - } - this.state = curState; - const deltaList = generateDeltaList(curState, flatten); - if (!deltaList) { + let curState = this.state; + const { firstItem , deltaList } = generateDeltaList(curState, flatten); + if (curState && firstItem.gameId !== curState.gameId) { return; } for (const delta of deltaList){ - this.deltaMap.set(delta.beforeGameId, delta); - curState = addRank(curState, delta); + this.deltaMap.set(delta.before.gameId, delta); + const result = addRank(curState, delta); + curState = result?.after; + if (result) { + this.stateMap.set(result.after.gameId, result); + } } return curState; } @@ -7072,26 +7173,26 @@ class App { } else { this.env.logger.log("Fetching coop battle list..."); const coopBattleList = await splatnet.getBattleList(BattleListType.Coop); - const { redraw: redraw1 , endBar: endBar1 } = this.exporterProgress("Export coop games"); - const fetcher1 = new GameFetcher({ + const { redraw , endBar } = this.exporterProgress("Export coop games"); + const fetcher = new GameFetcher({ cache: this.opts.cache ?? new FileCache(this.profile.state.cacheDir), state: this.profile.state, splatnet }); await Promise.all(exporters.map((e)=>showError(this.env, this.exportGameList({ type: "CoopInfo", - fetcher: fetcher1, + fetcher, exporter: e, gameList: coopBattleList, stepProgress: stats[e.name], onStep: ()=>{ - redraw1(e.name, progress(stats[e.name])); + redraw(e.name, progress(stats[e.name])); } })).catch((err)=>{ errors.push(err); this.env.logger.error(`\nFailed to export to ${e.name}:`, err); }))); - endBar1(); + endBar(); this.printStats(stats); if (errors.length > 0) { throw errors[0]; diff --git a/source/plugins/community/splatoon/token.ts b/source/plugins/community/splatoon/token.ts index 71641c91..dff26365 100644 --- a/source/plugins/community/splatoon/token.ts +++ b/source/plugins/community/splatoon/token.ts @@ -31,15 +31,16 @@ const args = [ '--exporter=none' ] try { - await Deno.spawn("deno", { + const command = new Deno.Command("deno", { args, stdin: "inherit", stdout: "inherit", stderr: "inherit", windowsRawArguments:true }) + await command.output() } -catch {} +catch (error) { console.log(error) } //Extract profile.json and print instructions try { diff --git a/source/plugins/steam/README.md b/source/plugins/steam/README.md new file mode 100644 index 00000000..8d720a2a --- /dev/null +++ b/source/plugins/steam/README.md @@ -0,0 +1,22 @@ + + + +## ➡️ Available options + + + + +## 🗝️ Obtaining a *Steam Web API* token + +Go to [steamcommunity.com/dev/apikey](https://steamcommunity.com/dev/apikey) to obtain a Steam Web API token: + +![Token](/.github/readme/imgs/plugin_steam_webtoken.png) + +To retrieve your Steam ID, access your user account on [store.steampowered.com/account](https://store.steampowered.com/account) and copy the identifier located behind the header: + +![User ID](/.github/readme/imgs/plugin_steam_userid.png) + +## ℹ️ Examples workflows + + + diff --git a/source/plugins/steam/examples.yml b/source/plugins/steam/examples.yml new file mode 100644 index 00000000..8a125ba7 --- /dev/null +++ b/source/plugins/steam/examples.yml @@ -0,0 +1,31 @@ +- name: Recently played games + uses: lowlighter/metrics@latest + with: + filename: metrics.plugin.steam.svg + token: NOT_NEEDED + base: "" + plugin_steam_token: ${{ secrets.STEAM_TOKEN }} + plugin_steam: yes + plugin_steam_user: 0 + plugin_steam_sections: recently-played + plugin_steam_achievements_limit: 0 + prod: + # ⚠️ Using mocked data for privacy reasons + with: + plugin_steam_token: MOCKED_TOKEN + use_mocked_data: yes + +- name: Profile and detailed game history + uses: lowlighter/metrics@latest + with: + filename: metrics.plugin.steam.full.svg + token: NOT_NEEDED + base: "" + plugin_steam_token: ${{ secrets.STEAM_TOKEN }} + plugin_steam: yes + plugin_steam_user: 0 + prod: + # ⚠️ Using mocked data for privacy reasons + with: + plugin_steam_token: MOCKED_TOKEN + use_mocked_data: yes \ No newline at end of file diff --git a/source/plugins/steam/index.mjs b/source/plugins/steam/index.mjs new file mode 100644 index 00000000..209eeef2 --- /dev/null +++ b/source/plugins/steam/index.mjs @@ -0,0 +1,104 @@ +//Setup +export default async function({login, q, imports, data, account}, {token, enabled = false, extras = false} = {}) { + //Plugin execution + try { + //Check if plugin is enabled and requirements are met + if ((!q.steam) || (!imports.metadata.plugins.steam.enabled(enabled, {extras}))) + return null + + //Load inputs + let {user, sections, "games.ignored": _games_ignored, "games.limit": _games_limit, "recent.games.limit": _recent_games_limit, "achievements.limit": _achievements_limit, "playtime.threshold": _playtime_threshold} = imports.metadata.plugins.steam.inputs({data, account, q}) + + const urls = { + games: { + owned: `https://api.steampowered.com/IPlayerService/GetOwnedGames/v0001/?key=${token}&steamid=${user}&format=json&include_appinfo=1`, + schema: `https://api.steampowered.com/ISteamUserStats/GetSchemaForGame/v0002/?key=${token}&format=json`, + details: "https://store.steampowered.com/api/appdetails?", + }, + player: { + summary: `https://api.steampowered.com/ISteamUser/GetPlayerSummaries/v0002/?key=${token}&steamids=${user}&format=json`, + level: `https://api.steampowered.com/IPlayerService/GetSteamLevel/v1/?key=${token}&steamid=${user}&format=json`, + achievement: `https://api.steampowered.com/ISteamUserStats/GetPlayerAchievements/v0001/?key=${token}&steamid=${user}&format=json&l=en`, + }, + } + const result = {sections, player: null, games: {count: 0, playtime: 0, achievements: 0}} + + //Fetch owned games + console.debug(`metrics/compute/${login}/plugins > steam > fetching owned games`) + let {data: {response: {game_count: count, games}}} = await imports.axios.get(urls.games.owned) + result.games.count = count + result.games.playtime = games.reduce((total, {playtime_forever: playtime}) => (total += playtime), 0) / 60 + + //Fetch game achievements and order games by section + for (const section of ["most-played", "recently-played"]) { + if (!sections.includes(section)) + continue + result.games[section] = await Promise.all( + games + .map(({appid: id, name, img_icon_url: icon, playtime_forever: playtime, rtime_last_played: played}) => ({id, name, icon: `http://media.steampowered.com/steamcommunity/public/images/apps/${id}/${icon}.jpg`, playtime: playtime / 60, played})) + .filter(({playtime}) => (playtime >= _playtime_threshold)) + .filter(({id}) => (!_games_ignored.includes(`${id}`))) + .sort((a, b) => ({"most-played": (b.playtime - a.playtime), "recently-played": (b.played - a.played)}[section])) + .slice(0, ({"most-played": _games_limit, "recently-played": _recent_games_limit}[section]) || Infinity) + .map(async game => { + const schema = {} + try { + console.debug(`metrics/compute/${login}/plugins > steam > fetching schema for "${game.name}" (${game.id})`) + const {data: {game: {availableGameStats: {achievements = []} = {}}}} = await imports.axios.get(`${urls.games.schema}&appid=${game.id}`) + Object.assign(schema, Object.fromEntries(achievements.map(({name, icon}) => [name, {icon}]))) + } + catch (error) { + console.debug(`metrics/compute/${login}/plugins > steam > failed to get schema for "${game.name}" (${game.id}) > ${error}`) + } + const about = {} + try { + console.debug(`metrics/compute/${login}/plugins > steam > fetching details for "${game.name}" (${game.id})`) + const {data: {[game.id]: {data}}} = await imports.axios.get(`${urls.games.details}&appids=${game.id}`) + about.description = data.short_description ?? "" + about.genres = data.genres?.map(({description}) => description) ?? [] + } + catch (error) { + console.debug(`metrics/compute/${login}/plugins > steam > failed to get details for "${game.name}" (${game.id}) > ${error}`) + } + + let achievements = [] + const rate = {total: Object.keys(schema).length, achieved: 0} + try { + console.debug(`metrics/compute/${login}/plugins > steam > fetching player achievements "${game.name}" (${game.id})`) + let {data: {playerstats: {achievements: list = []}}} = await imports.axios.get(`${urls.player.achievement}&appid=${game.id}`) + achievements = await Promise.all(list.map(async ({apiname: id, achieved, unlocktime: unlocked, name, description}) => ({icon: await imports.imgb64(schema[id]?.icon ?? null, {width: 32, height: 32}), achieved: !!achieved, unlocked, name, description, id}))) + achievements = achievements.sort((a, b) => (b.unlocked - a.unlocked)) + rate.achieved = achievements.filter(({achieved}) => achieved).length + achievements = achievements.slice(0, _achievements_limit) + } + catch (error) { + console.debug(`metrics/compute/${login}/plugins > steam > failed to get player achievements for "${game.name}" (${game.id}) > ${error}`) + } + return {...game, ...about, icon: await imports.imgb64(game.icon, {width: 64, height: 64}), achievements, rate} + }), + ) + } + + //Fetch player info + if (sections.includes("player")) { + console.debug(`metrics/compute/${login}/plugins > steam > fetching profile info`) + let {data: {response: {players: [info]}}} = await imports.axios.get(urls.player.summary) + console.debug(`metrics/compute/${login}/plugins > steam > fetching profile level`) + const {data: {response: {player_level: level}}} = await imports.axios.get(urls.player.level) + result.player = { + level, + avatar: await imports.imgb64(info.avatar, {width: 64, height: 64}), + created: info.timecreated, + name: info.personaname, + } + } + + //Results + console.log(JSON.stringify(result)) + return result + } + //Handle errors + catch (error) { + throw imports.format.error(error) + } +} diff --git a/source/plugins/steam/metadata.yml b/source/plugins/steam/metadata.yml new file mode 100644 index 00000000..d0b51951 --- /dev/null +++ b/source/plugins/steam/metadata.yml @@ -0,0 +1,93 @@ +name: 🕹️ Steam +category: social +description: | + This plugin can display your player profile and played games from your Steam account. +disclaimer: | + This plugin is not affiliated, associated, authorized, endorsed by, or in any way officially connected with [Steam](https://store.steampowered.com). + All product and company names are trademarks™ or registered® trademarks of their respective holders. +examples: + +Recently played games: https://github.com/lowlighter/metrics/blob/examples/metrics.plugin.steam.svg + Profile and detailed game history: https://github.com/lowlighter/metrics/blob/examples/metrics.plugin.steam.full.svg +supports: + - user + - organization +scopes: [] +inputs: + + plugin_steam: + description: | + Enable steam plugin + type: boolean + default: no + + plugin_steam_token: + description: | + Steam token + type: token + default: "" + extras: + - metrics.api.steam + + plugin_steam_sections: + description: | + Displayed sections + + - `player`: display profile + - `most-played`: display most played games + - `recently-played`: display recently played games + type: array + format: comma-separated + default: player, most-played, recently-played + options: + - player + - most-played + - recently-played + + plugin_steam_user: + description: | + Steam user id + + This can be found on your Steam user account details + type: string + preset: no + + plugin_steam_games_ignored: + description: | + Ignored games + + Use App id as they are referenced in Steam catalog + type: array + format: comma-separated + default: "" + example: 400, 620 + + plugin_steam_games_limit: + description: | + Display limit (Most played games) + type: number + min: 0 + zero: disable + default: 1 + + plugin_steam_recent_games_limit: + description: | + Display limit (Recently played games) + type: number + min: 0 + zero: disable + default: 1 + + plugin_steam_achievements_limit: + description: | + Display limit (Games achievements) + type: number + min: 0 + default: 2 + + plugin_steam_playtime_threshold: + description: | + Display threshold (Game playtime in hours) + type: number + min: 0 + default: 2 + diff --git a/source/templates/classic/partials/_.json b/source/templates/classic/partials/_.json index e8523ae5..a7886e43 100644 --- a/source/templates/classic/partials/_.json +++ b/source/templates/classic/partials/_.json @@ -42,5 +42,6 @@ "sponsorships", "poopmap", "fortune", - "splatoon" + "splatoon", + "steam" ] diff --git a/source/templates/classic/partials/steam.ejs b/source/templates/classic/partials/steam.ejs new file mode 100644 index 00000000..9d1345e3 --- /dev/null +++ b/source/templates/classic/partials/steam.ejs @@ -0,0 +1,108 @@ +<% if (plugins.steam) { %> +
+

+ + Steam +

+ <% if (plugins.steam.error) { %> +
+
+
+ + <%= plugins.steam.error.message %> +
+
+
+ <% } else { %> + <% if ((plugins.steam.sections.includes("player"))&&(plugins.steam.player)) { %> +
+
+
+ + <%= plugins.steam.player.name %> +
+
+ + <%= plugins.steam.games.count %> game<%= s(plugins.steam.games.count) %> +
+
+
+
+ + Steam level <%= plugins.steam.player.level %> +
+
+ + <%= f(parseInt(plugins.steam.games.playtime)) %> hour<%= s(plugins.steam.games.playtime) %> played +
+
+
+ <% } %> + <% for (const section of ["most-played", "recently-played"]) { if (plugins.steam.sections.includes(section)) { %> +
+

+ + <%= {"most-played":"Most played", "recently-played":"Recently played"}[section] %> +

+
+ <% for (const {name, icon, playtime, played, achievements, rate, genres, description} of plugins.steam.games[section]) { %> +
+ +
+
<%= name %>
+
+
+ + <%= genres.join(", ") %> +
+
+
+
+ + <%= f(parseInt(playtime)) %> hour<%= s(playtime) %> played +
+
+
+
+ + Last played on <%= f.date(played*1000, {date:true, timeZone:config.timezone?.name}) %> +
+
+
+
+
+ + <%= rate.achieved %> / <%= rate.total %> achievement<%= s(rate.total) %> unlocked +
+
+ <% { const achieved = achievements.filter(({achieved}) => achieved) %> + <% for (const {icon, name, description, unlocked} of achieved) { %> +
+ +
+
+
<%= name %>
+
<%= f.date(unlocked*1000, {date:true, timeZone:config.timezone?.name}) %>
+
+
<%= description %>
+
+
+ <% } %> + <% if ((achieved.length)&&(rate.achieved-achieved.length > 0)) { %> +
+
+
+<%= rate.achieved-achieved.length %> other<%= s(rate.achieved-achieved.length) %>...
+
+
+ <% } %> + <% } %> +
+
+
+ <% } %> +
+
+ <% } } %> + <% } %> +
+<% } %> diff --git a/source/templates/classic/style.css b/source/templates/classic/style.css index a9abb963..2215d1ed 100644 --- a/source/templates/classic/style.css +++ b/source/templates/classic/style.css @@ -1049,6 +1049,88 @@ border-radius: 7px; } +/* Steam */ + .steam .games { + margin-left: 28px; + } + .steam .media { + display: flex; + margin-bottom: 4px; + width: 450px; + } + .steam .media img { + margin: 0 10px; + border-radius: 7px; + } + .steam .media > img { + height: 32px; + width: 32px; + } + .steam .media .about { + flex-grow: 1; + } + .steam .media .name { + display: flex; + align-items: center; + justify-content: space-between; + font-size: 14px; + line-height: 14px; + color: #58a6ff; + } + .steam .media .infos { + font-size: 12px; + color: #666666; + display: flex; + align-items: center; + justify-content: space-between; + } + .steam .media .infos > div { + display: inline-flex; + align-items: center; + margin-right: 16px; + } + .steam .media .infos svg { + fill: currentColor; + height: 12px; + width: 12px; + margin: 0; + margin-right: 4px; + } + .steam .media .achievement img { + height: 22px; + width: 22px; + margin-right: 6px; + } + .steam .media .achievement .name { + display: flex; + justify-content: space-between; + } + .steam .media .achievement .name > div:first-child { + max-width: 280px; + } + .steam .media .achievement .description { + overflow: hidden; + text-overflow: ellipsis; + display: block; + width: 344px; + max-height: 38px; + font-size: 12px; + white-space: normal; + /* May not work in all browsers */ + display: -webkit-box; + -webkit-line-clamp: 2; + -webkit-box-orient: vertical; + } + .steam .media .achievement .unlocked { + font-size: 12px; + color: #666666; + font-style: italic; + flex-shrink: 0; + } + .steam .media .achievement .ellipsis { + margin-left: 18px; + } + /* Licenses */ .licenses { display: flex; diff --git a/tests/mocks/api/axios/get/steam.mjs b/tests/mocks/api/axios/get/steam.mjs new file mode 100644 index 00000000..3ebb23e6 --- /dev/null +++ b/tests/mocks/api/axios/get/steam.mjs @@ -0,0 +1,2263 @@ +/**Mocked data */ +export default function({faker, url, options, login = faker.internet.userName()}) { + // App details + if (/^https:..store.steampowered.com.api.appdetails*$/.test(url)) { + console.debug(`metrics/compute/mocks > mocking steam api result > ${url}`) + return ({ + status: 200, + data: { + "524220": { + "success": true, + "data": { + "type": "game", + "name": "NieR:Automata™", + "steam_appid": 524220, + "required_age": 0, + "is_free": false, + "dlc": [ + 580600, + ], + "detailed_description": + '

NieR:Automata™ Game of the YoRHa Edition<\/h1>


The NieR:Automata™ Game of the YoRHa Edition includes the game itself and comes packed with DLC and bonus content for the full experience of the award-winning post-apocalyptic action RPG, including: