re-code the userscript
Some checks failed
Deploy App / docker (ubuntu-latest) (push) Failing after 7s

This commit is contained in:
Lee 2024-04-25 23:39:26 +01:00
parent a8158c73c8
commit c8990f21cd
17 changed files with 1928 additions and 122 deletions

31
.gitea/workflows/ci.yml Normal file

@ -0,0 +1,31 @@
name: Deploy App
on:
push:
branches: ["master"]
paths-ignore:
- .gitignore
- README.md
- LICENSE
jobs:
docker:
strategy:
matrix:
arch: ["ubuntu-latest"]
runs-on: ${{ matrix.arch }}
# Steps to run
steps:
# Checkout the repo
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0
# Deploy to Dokku
- name: Push to dokku
uses: dokku/github-action@master
with:
git_remote_url: "ssh://dokku@10.0.50.175:22/ssu-scripts"
ssh_private_key: ${{ secrets.SSH_PRIVATE_KEY }}

210
.gitignore vendored Normal file

@ -0,0 +1,210 @@
### JetBrains template
# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider
# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839
# User-specific stuff
.idea/**/workspace.xml
.idea/**/tasks.xml
.idea/**/usage.statistics.xml
.idea/**/dictionaries
.idea/**/shelf
# AWS User-specific
.idea/**/aws.xml
# Generated files
.idea/**/contentModel.xml
# Sensitive or high-churn files
.idea/**/dataSources/
.idea/**/dataSources.ids
.idea/**/dataSources.local.xml
.idea/**/sqlDataSources.xml
.idea/**/dynamic.xml
.idea/**/uiDesigner.xml
.idea/**/dbnavigator.xml
# Gradle
.idea/**/gradle.xml
.idea/**/libraries
# Gradle and Maven with auto-import
# When using Gradle or Maven with auto-import, you should exclude module files,
# since they will be recreated, and may cause churn. Uncomment if using
# auto-import.
# .idea/artifacts
# .idea/compiler.xml
# .idea/jarRepositories.xml
# .idea/modules.xml
# .idea/*.iml
# .idea/modules
# *.iml
# *.ipr
# CMake
cmake-build-*/
# Mongo Explorer plugin
.idea/**/mongoSettings.xml
# File-based project format
*.iws
# IntelliJ
out/
# mpeltonen/sbt-idea plugin
.idea_modules/
# JIRA plugin
atlassian-ide-plugin.xml
# Cursive Clojure plugin
.idea/replstate.xml
# SonarLint plugin
.idea/sonarlint/
# Crashlytics plugin (for Android Studio and IntelliJ)
com_crashlytics_export_strings.xml
crashlytics.properties
crashlytics-build.properties
fabric.properties
# Editor-based Rest Client
.idea/httpRequests
# Android studio 3.1+ serialized cache file
.idea/caches/build_file_checksums.ser
### Node template
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
lerna-debug.log*
.pnpm-debug.log*
# Diagnostic reports (https://nodejs.org/api/report.html)
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
# Runtime data
pids
*.pid
*.seed
*.pid.lock
# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov
# Coverage directory used by tools like istanbul
coverage
*.lcov
# nyc test coverage
.nyc_output
# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
.grunt
# Bower dependency directory (https://bower.io/)
bower_components
# node-waf configuration
.lock-wscript
# Compiled binary addons (https://nodejs.org/api/addons.html)
build/Release
# Dependency directories
node_modules/
jspm_packages/
# Snowpack dependency directory (https://snowpack.dev/)
web_modules/
# TypeScript cache
*.tsbuildinfo
# Optional npm cache directory
.npm
# Optional eslint cache
.eslintcache
# Optional stylelint cache
.stylelintcache
# Microbundle cache
.rpt2_cache/
.rts2_cache_cjs/
.rts2_cache_es/
.rts2_cache_umd/
# Optional REPL history
.node_repl_history
# Output of 'npm pack'
*.tgz
# Yarn Integrity file
.yarn-integrity
# dotenv environment variable files
.env
.env.development.local
.env.test.local
.env.production.local
.env.local
# parcel-bundler cache (https://parceljs.org/)
.cache
.parcel-cache
# Next.js build output
.next
out
# Nuxt.js build / generate output
.nuxt
dist
# Gatsby files
.cache/
# Comment in the public line in if your project uses Gatsby and not Next.js
# https://nextjs.org/blog/next-9-1#public-directory-support
# public
# vuepress build output
.vuepress/dist
# vuepress v2.x temp and cache directory
.temp
# Docusaurus cache and generated files
.docusaurus
# Serverless directories
.serverless/
# FuseBox cache
.fusebox/
# DynamoDB Local files
.dynamodb/
# TernJS port file
.tern-port
# Stores VSCode versions used for testing VSCode extensions
.vscode-test
# yarn v2
.yarn/cache
.yarn/unplugged
.yarn/build-state.yml
.yarn/install-state.gz
.pnp.*

5
.idea/.gitignore generated vendored Normal file

@ -0,0 +1,5 @@
# Default ignored files
/shelf/
/workspace.xml
# Editor-based HTTP Client requests
/httpRequests/

35
Dockerfile Normal file

@ -0,0 +1,35 @@
FROM node:21-alpine AS base
WORKDIR /app
COPY package.json yarn.lock* package-lock.json* pnpm-lock.yaml* ./
RUN \
if [ -f yarn.lock ]; then yarn --frozen-lockfile; \
elif [ -f package-lock.json ]; then npm ci; \
elif [ -f pnpm-lock.yaml ]; then corepack enable pnpm && pnpm i --frozen-lockfile; \
else echo "Lockfile not found." && exit 1; \
fi
# Rebuild the source code only when needed
FROM base AS builder
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY . .
# Set the environment to production
ENV NODE_ENV=production
# Build the scripts
RUN \
if [ -f yarn.lock ]; then yarn run build; \
elif [ -f package-lock.json ]; then npm run build; \
elif [ -f pnpm-lock.yaml ]; then corepack enable pnpm && pnpm run build; \
else echo "Lockfile not found." && exit 1; \
fi
FROM nginx:alpine AS runner
COPY --from=builder /app/dist/ /usr/share/nginx/html
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]

23
package.json Normal file

@ -0,0 +1,23 @@
{
"name": "scoresaberutils-script",
"version": "1.0.0",
"description": "The TamperMonkey script for ScoreSaber Utils",
"scripts": {
"build": "webpack --config webpack.config.js",
"dev": "webpack --config webpack.config.js --watch"
},
"private": true,
"author": "",
"license": "ISC",
"devDependencies": {
"nodemon-webpack-plugin": "^4.8.2",
"ts-loader": "^9.5.1",
"typescript": "^5.4.5",
"webpack": "^5.91.0",
"webpack-cli": "^5.1.4",
"webpack-userscript": "^3.2.2"
},
"dependencies": {
"lodash": "^4.17.21"
}
}

1350
pnpm-lock.yaml generated Normal file

File diff suppressed because it is too large Load Diff

@ -1,122 +0,0 @@
// ==UserScript==
// @name ScoreSaber Utils
// @namespace https://ssu.fascinated.cc
// @version 1.0.7
// @description Useful additions to ScoreSaber!
// @author Fascinated
// @match https://scoresaber.com/*
// @icon https://www.google.com/s2/favicons?sz=64&domain=scoresaber.com
// @license MIT
// @updateURL https://git.fascinated.cc/Fascinated/ScoreSaberUtils-Script/raw/branch/master/scoresaber-utils.user.js
// @downloadURL https://git.fascinated.cc/Fascinated/ScoreSaberUtils-Script/raw/branch/master/scoresaber-utils.user.js
// @run-at document-end
// ==/UserScript==
/**
* Fetches data from an API endpoint.
*
* @param {string} url The URL of the API endpoint
* @returns {Promise<any>} The JSON response from the API
*/
async function fetchData(url) {
const response = await fetch(url);
if (!response.ok) {
throw new Error(`Failed to fetch data: ${response.status} ${response.statusText}`);
}
return await response.json();
}
/**
* Inserts a stat into the specified container.
*
* @param {string} containerSelector The selector for the container to insert the stat into
* @param {string} stat The stat name
* @param {string} value The stat value
* @param {string} hoverText The hover text
*/
function addStat(containerSelector, stat, value, hoverText) {
const container = document.querySelector(containerSelector);
if (!container) return;
const svelteClass = container.classList.item(1);
const statElement = document.createElement("div");
statElement.className = `stat-item ${svelteClass}`;
statElement.innerHTML = `
<span class="stat-title ${svelteClass}">${stat}</span>
<span class="stat-spacer ${svelteClass}"></span>
<span class="stat-content ${svelteClass} has-hover" title="${hoverText}">${value}</span>
`;
container.appendChild(statElement);
}
/**
* Delays execution for the specified duration.
*
* @param {number} ms The duration to delay in milliseconds
* @returns {Promise<void>} A promise that resolves after the delay
*/
function sleep(ms) {
return new Promise((resolve) => setTimeout(resolve, ms));
}
/**
* Loads ScoreSaber Utils data on player pages.
*/
async function loadPlayerData(path) {
// Wait for the stats container to load
while (!document.querySelector(".stats-container")) {
await sleep(250);
}
const playerId = path.split("/")[2];
// Get the title element
await sleep(250);
const titleElement = document.querySelector(".title.is-5.player.has-text-centered-mobile");
if (!titleElement) {
console.error("Failed to find title element");
return;
}
const svelteClass = titleElement.classList.item(1);
// Add a loading indicator
const loadingElement = document.createElement("span");
loadingElement.className = `title-header pp ${svelteClass}`;
loadingElement.textContent = "Loading ScoreSaber Utils Data...";
titleElement.appendChild(loadingElement);
try {
const playerData = await fetchData(`https://ssu.fascinated.cc/account/${playerId}`);
addStat(
".stats-container",
"+1 PP",
`${playerData.rawPerGlobalPerformancePoints.toFixed(2)}pp`,
"The amount of pp to increase the global pp by 1pp"
);
} catch (error) {
console.error("Failed to load player data:", error);
}
// Remove the loading indicator
loadingElement.remove();
}
// Watch for URL changes
let previousUrl = "";
const observer = new MutationObserver(() => {
const currentUrl = location.pathname; // Get the current URL without parameters
if (currentUrl == previousUrl) {
return;
}
previousUrl = currentUrl;
console.log("Switching Page:", currentUrl);
// Load player data on player pages
if (currentUrl.startsWith("/u/")) {
loadPlayerData(currentUrl);
}
});
const config = { subtree: true, childList: true };
observer.observe(document, config);

74
src/common/page-utils.ts Normal file

@ -0,0 +1,74 @@
/**
* Get a callback for when the page changes
*
* @param callback The callback to call when the page changes
*/
export function pageChangeCallback(callback: (path: string) => void) {
let previousUrl = "";
const observer = new MutationObserver(() => {
const currentUrl = location.pathname; // Get the current URL without parameters
if (currentUrl == previousUrl) {
return;
}
previousUrl = currentUrl;
callback(currentUrl);
});
const config = { subtree: true, childList: true };
observer.observe(document, config);
}
/**
* Gets the Svelte class of an element
*
* @param baseClass The base class of the element
*/
export function getSvelteClass(baseClass: string): string | null {
const element = document.querySelector(baseClass);
if (!element) {
return null;
}
// Get the Svelte class
for (let string of element.className.split(" ")) {
if (string.startsWith("svelte-")) {
return string;
}
}
return null;
}
/**
* Gets an element from the page and waits
* for it to load or be available
*
* @param selector the selector of the element
* @param checkInterval the interval to check for the element
*/
export function getElement(selector: string, checkInterval: number = 250): Promise<HTMLElement> {
return new Promise((resolve, reject) => {
const element = document.querySelector(selector);
if (element) {
resolve(element as HTMLElement);
return;
}
let checkCount = 0;
const checkElement = () => {
const element = document.querySelector(selector);
if (element) {
clearInterval(interval);
resolve(element as HTMLElement);
return;
}
checkCount++;
if (checkCount * checkInterval >= 2500) { // Give up after 2.5 seconds
clearInterval(interval);
reject(new Error("Element not found within timeout"));
return;
}
};
const interval = setInterval(checkElement, checkInterval);
});
}

26
src/common/player.ts Normal file

@ -0,0 +1,26 @@
import {API_URL} from "../consts";
/**
* Gets a player from the ScoreSaber Utils API
*
* @param id The player's ID
*/
export async function getPlayer(id: string) {
const response = await fetch(`${API_URL}/account/${id}`);
// There was an error fetching the player
if (!response.ok) {
throw new Error("Failed to fetch player");
}
// Return the player's data
return response.json();
}
/**
* Gets the player id from the current URL.
*/
export function getPlayerIdFromUrl(): string {
const url = new URL(location.href);
return url.pathname.split("/")[2];
}

9
src/common/utils.ts Normal file

@ -0,0 +1,9 @@
/**
* Delays execution for the specified duration.
*
* @param ms The duration to delay in milliseconds
* @returns A promise that resolves after the delay
*/
export function sleep(ms: number): Promise<void> {
return new Promise((resolve) => setTimeout(resolve, ms));
}

4
src/consts.ts Normal file

@ -0,0 +1,4 @@
/**
* The URL of the ScoreSaber Utils API
*/
export const API_URL = "https://ssu.fascinated.cc";

3
src/index.ts Normal file

@ -0,0 +1,3 @@
import PageHandler from "./pages/page-handler";
new PageHandler();

@ -0,0 +1,57 @@
import Page from "../page";
import {getElement, getSvelteClass} from "../../common/page-utils";
import {getPlayer, getPlayerIdFromUrl} from "../../common/player";
export default class PlayerPage extends Page {
constructor() {
super("/u/");
}
public async onLoad() {
try {
// Wait for the title element to load, so we know the page is fully loaded
const titleElement = await getElement(".title.is-5.player.has-text-centered-mobile", 250);
const id = getPlayerIdFromUrl();
const player = await getPlayer(id);
console.log(player);
await this.addStats(player);
} catch (error) {
console.error("Failed to load player page", error);
}
}
/**
* Add the custom stats to the player's page
*
* @param player the player to add stats for
*/
private async addStats(player: any) {
await this.addStat(
"+1 PP",
`${player.rawPerGlobalPerformancePoints.toFixed(2)}pp`,
"Raw performance points to gain +1 global PP"
);
}
/**
* Add a stat to the player's stats
*
* @param stat the title of the stat
* @param value the value of the stat
* @param hover the hover text of the stat
*/
private async addStat(stat: string, value: string, hover?: string) {
const statsContainer = await getElement(".stats-container");
const statElement = document.createElement("div");
const svelteClass = getSvelteClass(".stats-container");
statElement.className = `stat-item ${svelteClass}`;
statElement.innerHTML = `
<span class="stat-title ${svelteClass}">${stat}</span>
<span class="stat-spacer ${svelteClass}"></span>
<span class="stat-content ${hover && "has-hover"} ${svelteClass}" ${hover && `title="${hover}"`}>${value}</span>`;
statsContainer.appendChild(statElement);
}
}

34
src/pages/page-handler.ts Normal file

@ -0,0 +1,34 @@
import Page from "./page";
import PlayerPage from "./impl/player-page";
import {pageChangeCallback} from "../common/page-utils";
export default class PageHandler {
private pages: Page[] = [];
constructor() {
// Register the pages to handle
this.registerPage(new PlayerPage());
// Handle page changes
pageChangeCallback((path) => {
console.log(`Page changed to: ${path}`);
for (let page of this.pages) {
if (path.startsWith(page.route)) {
console.log(`Handling page: ${page.route}`);
page.onLoad();
}
}
});
}
/**
* Registers a page to be handled
*
* @param page The page to register
*/
private registerPage(page: Page) {
console.log(`Registered page: ${page.route}`)
this.pages.push(page);
}
}

24
src/pages/page.ts Normal file

@ -0,0 +1,24 @@
export default class Page {
/**
* The route of the page
* eg: /ranking
*/
private readonly _route: string;
constructor(route: string) {
this._route = route;
}
/*
* This gets called when the page is loaded
*/
public onLoad() {}
/**
* The route of the page
*/
get route(): string {
return this._route;
}
}

11
tsconfig.json Normal file

@ -0,0 +1,11 @@
{
"compilerOptions": {
"outDir": "./dist/",
"noImplicitAny": true,
"module": "es6",
"target": "es5",
"jsx": "react",
"allowJs": true,
"moduleResolution": "node"
}
}

32
webpack.config.js Normal file

@ -0,0 +1,32 @@
const path = require('path');
const { UserscriptPlugin } = require('webpack-userscript');
module.exports = {
entry: './src/index.ts',
mode: process.env.NODE_ENV || 'development',
module: {
rules: [
{
test: /\.tsx?$/,
use: 'ts-loader',
exclude: /node_modules/,
},
],
},
resolve: {
extensions: ['.tsx', '.ts', '.js'],
},
plugins: [
new UserscriptPlugin({
headers: {
name: "ScoreSaber Utils",
"run-at": "document-end",
match: "https://scoresaber.com/*"
}
})
],
output: {
filename: 'bundle.js',
path: path.resolve(__dirname, 'dist'),
},
};