diff --git a/.forgejo/workflows/build.yml b/.forgejo/workflows/build.yml index 601ca4b..6d85c91 100644 --- a/.forgejo/workflows/build.yml +++ b/.forgejo/workflows/build.yml @@ -7,11 +7,11 @@ on: jobs: docker: - runs-on: debian-bookworm + runs-on: ${{ vars.RUNNER_LABEL }} steps: - - name: checkout + - name: Checkout uses: actions/checkout@v6 - + - name: install docker??? run: | apt update && apt install wget cmake -y && \ diff --git a/README.md b/README.md index 1996705..475a43c 100644 --- a/README.md +++ b/README.md @@ -12,12 +12,14 @@ # SplatNet -[![Commits](https://img.shields.io/badge/dynamic/json?url=https%3A%2F%2Fgit.crafterpika.cc%2Fapi%2Fv1%2Frepos%2Fkittentm%2Fsplatnet-backend%2Fcommits&query=%24.length&label=commits&style=for-the-badge&color=blue)](https://git.crafterpika.cc/kittentm/splatnet-backend/commits/branch/main) +![Last Commit](https://img.shields.io/gitea/last-commit/kittentm/splatnet-backend?gitea_url=https://git.crafterpika.cc&style=for-the-badge&color=blue) [![Build Status](https://git.crafterpika.cc/kittentm/splatnet-backend/badges/workflows/build.yml/badge.svg?style=for-the-badge)](https://git.crafterpika.cc/kittentm/splatnet-backend/actions) -[![Languages](https://img.shields.io/badge/dynamic/json?url=https%3A%2F%2Fgit.crafterpika.cc%2Fapi%2Fv1%2Frepos%2Fkittentm%2Fsplatnet-backend%2Flanguages&query=%24.*~&label=language&style=for-the-badge&color=yellow)](https://git.crafterpika.cc/kittentm/splatnet-backend) +[![Languages](https://img.shields.io/badge/dynamic/json?url=https%3A%2F%2Fgit.crafterpika.cc%2Fapi%2Fv1%2Frepos%2Fkittentm%2Fsplatnet-backend%2Flanguages&query=%24.*~&label=language&style=for-the-badge&color=blue)](https://git.crafterpika.cc/kittentm/splatnet-backend) [![License: GPL v3](https://img.shields.io/badge/License-GPLv3-blue.svg?style=for-the-badge&logo=gnu&logoColor=white)](https://git.crafterpika.cc/kittentm/splatnet-backend/src/branch/main/LICENSE) +![L1 Service Status](https://img.shields.io/website?url=https%3A%2F%2Fsplatnet.spbr.net%2F&up_message=online&down_message=offline&style=for-the-badge&up_color=blue&down_color=red&label=Production+Instance) +![T1 Service Status](https://img.shields.io/website?url=https%3A%2F%2Fsplatoon.cubexmine.dev%2F&up_message=online&down_message=offline&style=for-the-badge&up_color=blue&down_color=red&label=Testing+Instance) -This is the backend for the [SplatNet](https://git.crafterpika.cc/kittentm/splatnet) Revival frontend. It is a combination of Python & JS. +This is the backend for the [SplatNet](https://git.crafterpika.cc/kittentm/splatnet) Revival frontend. diff --git a/code_of_conduct.md b/code_of_conduct.md new file mode 100644 index 0000000..b9b2b84 --- /dev/null +++ b/code_of_conduct.md @@ -0,0 +1,40 @@ +# Contributor Covenant Code of Conduct + +## Our Pledge + +In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation. + +## Our Standards + +Examples of behavior that contributes to creating a positive environment include: + +* Using welcoming and inclusive language +* Being respectful of differing viewpoints and experiences +* Gracefully accepting constructive criticism +* Focusing on what is best for the community +* Showing empathy towards other community members + +Examples of unacceptable behavior by participants include: + +* The use of sexualized language or imagery and unwelcome sexual attention or advances +* Trolling, insulting/derogatory comments, and personal or political attacks +* Public or private harassment +* Publishing others' private information, such as a physical or electronic address, without explicit permission +* Other conduct which could reasonably be considered inappropriate in a professional setting + +## Our Responsibilities + +Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior. + +Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful. + +## Scope + +This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers. + +## Attribution + +This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at [https://contributor-covenant.org/version/1/4][version] + +[homepage]: https://contributor-covenant.org +[version]: https://contributor-covenant.org/version/1/4/ diff --git a/config.py b/config.py index 7d81b09..66597cf 100644 --- a/config.py +++ b/config.py @@ -18,6 +18,7 @@ class Settings(BaseSettings): twitter_client_id: str twitter_client_secret: str twitter_redirect_uri: str + account_client_secret: str model_config = SettingsConfigDict( env_file=[ diff --git a/judd/package-lock.json b/judd/package-lock.json index f8f456a..4091e11 100644 --- a/judd/package-lock.json +++ b/judd/package-lock.json @@ -336,6 +336,41 @@ "url": "https://opencollective.com/express" } }, + "node_modules/agent-base": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", + "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", + "license": "MIT", + "dependencies": { + "debug": "4" + }, + "engines": { + "node": ">= 6.0.0" + } + }, + "node_modules/agent-base/node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/agent-base/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, "node_modules/append-field": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/append-field/-/append-field-1.0.0.tgz", @@ -349,13 +384,14 @@ "license": "MIT" }, "node_modules/axios": { - "version": "1.15.2", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.15.2.tgz", - "integrity": "sha512-wLrXxPtcrPTsNlJmKjkPnNPK2Ihe0hn0wGSaTEiHRPxwjvJwT3hKmXF4dpqxmPO9SoNb2FsYXj/xEo0gHN+D5A==", + "version": "1.18.0", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.18.0.tgz", + "integrity": "sha512-E32NzpYKp++W7XRe52rHiXV2ehxmh3wbdgO7MHeFM+vqxLBYHzt0ElkiImtOBxtOmyp0yoC8C6uESVV84Y2/hw==", "license": "MIT", "dependencies": { - "follow-redirects": "^1.15.11", + "follow-redirects": "^1.16.0", "form-data": "^4.0.5", + "https-proxy-agent": "^5.0.1", "proxy-from-env": "^2.1.0" } }, @@ -631,9 +667,9 @@ ] }, "node_modules/discord.js": { - "version": "14.26.3", - "resolved": "https://registry.npmjs.org/discord.js/-/discord.js-14.26.3.tgz", - "integrity": "sha512-XEKtYn28YFsiJ5l4fLRyikdbo6RD5oFyqfVHQlvXz2104JhH/E8slN28dbky05w3DCrJcNVWvhVvcJCTSl/KIg==", + "version": "14.26.4", + "resolved": "https://registry.npmjs.org/discord.js/-/discord.js-14.26.4.tgz", + "integrity": "sha512-4oBp8tc6Kf8IDBwAHhbsMaAqx1b5fob9SNasZT7V6yyyUydoO5i5fGuX7TmvRtR+q/WgKRnRViRoAWnG7fNyvA==", "license": "Apache-2.0", "dependencies": { "@discordjs/builders": "^1.14.1", @@ -797,12 +833,12 @@ } }, "node_modules/express-rate-limit": { - "version": "8.3.2", - "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-8.3.2.tgz", - "integrity": "sha512-77VmFeJkO0/rvimEDuUC5H30oqUC4EyOhyGccfqoLebB0oiEYfM7nwPrsDsBL1gsTpwfzX8SFy2MT3TDyRq+bg==", + "version": "8.5.2", + "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-8.5.2.tgz", + "integrity": "sha512-5Kb34ipNX694DH48vN9irak1Qx30nb0PLYHXfJgw4YEjiC3ZEmZJhwOp+VfiCYwFzvFTdB9QkArYS5kXa2cx2A==", "license": "MIT", "dependencies": { - "ip-address": "10.1.0" + "ip-address": "^10.2.0" }, "engines": { "node": ">= 16" @@ -1112,6 +1148,42 @@ "url": "https://opencollective.com/express" } }, + "node_modules/https-proxy-agent": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", + "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", + "license": "MIT", + "dependencies": { + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/https-proxy-agent/node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/https-proxy-agent/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, "node_modules/iconv-lite": { "version": "0.7.2", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.2.tgz", @@ -1144,9 +1216,9 @@ "license": "ISC" }, "node_modules/ip-address": { - "version": "10.1.0", - "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.1.0.tgz", - "integrity": "sha512-XXADHxXmvT9+CRxhXg56LJovE+bmWnEWB78LB83VZTprKTmaC5QfruXocxzTZ2Kl0DNwKuBdlIhjL8LeY8Sf8Q==", + "version": "10.2.0", + "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.2.0.tgz", + "integrity": "sha512-/+S6j4E9AHvW9SWMSEY9Xfy66O5PWvVEJ08O0y5JGyEKQpojb0K0GKpz/v5HJ/G0vi3D2sjGK78119oXZeE0qA==", "license": "MIT", "engines": { "node": ">= 12" @@ -1168,9 +1240,9 @@ "license": "MIT" }, "node_modules/joi": { - "version": "18.1.2", - "resolved": "https://registry.npmjs.org/joi/-/joi-18.1.2.tgz", - "integrity": "sha512-rF5MAmps5esSlhCA+N1b6IYHDw9j/btzGaqfgie522jS02Ju/HXBxamlXVlKEHAxoMKQL77HWI8jlqWsFuekZA==", + "version": "18.2.3", + "resolved": "https://registry.npmjs.org/joi/-/joi-18.2.3.tgz", + "integrity": "sha512-N5A3KTWQpPWT4ExxxPlUx7WmykGXRzhNidWhV41d6Abu9YfI2NyWCJuxdPnslJCPWtbRpSVOWSnSS6GakLM/Rg==", "license": "BSD-3-Clause", "dependencies": { "@hapi/address": "^5.1.1", @@ -1276,31 +1348,23 @@ } }, "node_modules/morgan": { - "version": "1.10.1", - "resolved": "https://registry.npmjs.org/morgan/-/morgan-1.10.1.tgz", - "integrity": "sha512-223dMRJtI/l25dJKWpgij2cMtywuG/WiUKXdvwfbhGKBhy1puASqXwFzmWZ7+K73vUPoR7SS2Qz2cI/g9MKw0A==", + "version": "1.11.0", + "resolved": "https://registry.npmjs.org/morgan/-/morgan-1.11.0.tgz", + "integrity": "sha512-zSkVu3t18r39pw4ixfBKvfZi3y2UOqr7d4WYwcj3m8nXpEQK4rPO6GLzs/CExoRgmX3y9EjmmcXqv6jq0SK46g==", "license": "MIT", "dependencies": { "basic-auth": "~2.0.1", "debug": "2.6.9", "depd": "~2.0.0", - "on-finished": "~2.3.0", + "on-finished": "~2.4.1", "on-headers": "~1.1.0" }, "engines": { "node": ">= 0.8.0" - } - }, - "node_modules/morgan/node_modules/on-finished": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz", - "integrity": "sha512-ikqdkGAAyf/X/gPhXGvfgAytDZtDbr+bkNUJ0N9h5MI/dmdgCs3l6hoHrcUv41sRKew3jIwrp4qQDXiK99Utww==", - "license": "MIT", - "dependencies": { - "ee-first": "1.1.1" }, - "engines": { - "node": ">= 0.8" + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" } }, "node_modules/ms": { @@ -1310,9 +1374,9 @@ "license": "MIT" }, "node_modules/multer": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/multer/-/multer-2.1.1.tgz", - "integrity": "sha512-mo+QTzKlx8R7E5ylSXxWzGoXoZbOsRMpyitcht8By2KHvMbf3tjwosZ/Mu/XYU6UuJ3VZnODIrak5ZrPiPyB6A==", + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/multer/-/multer-2.2.0.tgz", + "integrity": "sha512-6rdyFg2kLrMh9Jee7/BMPuV9lEAd7lLW2YUpF9/YxR7njyoUwwQ0ZPh3TaIY50Sw6vlyD2HW3wGOkTS4P79xrQ==", "license": "MIT", "dependencies": { "append-field": "^1.0.0", @@ -1399,15 +1463,14 @@ } }, "node_modules/pg": { - "version": "8.20.0", - "resolved": "https://registry.npmjs.org/pg/-/pg-8.20.0.tgz", - "integrity": "sha512-ldhMxz2r8fl/6QkXnBD3CR9/xg694oT6DZQ2s6c/RI28OjtSOpxnPrUCGOBJ46RCUxcWdx3p6kw/xnDHjKvaRA==", + "version": "8.21.0", + "resolved": "https://registry.npmjs.org/pg/-/pg-8.21.0.tgz", + "integrity": "sha512-AUP1EYJuHraQGsVoCQVIcM7TEJVGtDzxWtGFZd8rds9d+CCXlU5Js1rYgfLNvxy9iJrpHjGrRjoi/3BT9fRyiA==", "license": "MIT", - "peer": true, "dependencies": { - "pg-connection-string": "^2.12.0", - "pg-pool": "^3.13.0", - "pg-protocol": "^1.13.0", + "pg-connection-string": "^2.13.0", + "pg-pool": "^3.14.0", + "pg-protocol": "^1.14.0", "pg-types": "2.2.0", "pgpass": "1.0.5" }, @@ -1415,7 +1478,7 @@ "node": ">= 16.0.0" }, "optionalDependencies": { - "pg-cloudflare": "^1.3.0" + "pg-cloudflare": "^1.4.0" }, "peerDependencies": { "pg-native": ">=3.0.1" @@ -1427,16 +1490,16 @@ } }, "node_modules/pg-cloudflare": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/pg-cloudflare/-/pg-cloudflare-1.3.0.tgz", - "integrity": "sha512-6lswVVSztmHiRtD6I8hw4qP/nDm1EJbKMRhf3HCYaqud7frGysPv7FYJ5noZQdhQtN2xJnimfMtvQq21pdbzyQ==", + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/pg-cloudflare/-/pg-cloudflare-1.4.0.tgz", + "integrity": "sha512-Vo7z/6rrQYxpNRylp4Tlob2elzbh+N/MOQbxFVWCxS7oEx6jF53GTJFxK2WWpKuBRkmiin4Mt+xofFDjx09R0A==", "license": "MIT", "optional": true }, "node_modules/pg-connection-string": { - "version": "2.12.0", - "resolved": "https://registry.npmjs.org/pg-connection-string/-/pg-connection-string-2.12.0.tgz", - "integrity": "sha512-U7qg+bpswf3Cs5xLzRqbXbQl85ng0mfSV/J0nnA31MCLgvEaAo7CIhmeyrmJpOr7o+zm0rXK+hNnT5l9RHkCkQ==", + "version": "2.13.0", + "resolved": "https://registry.npmjs.org/pg-connection-string/-/pg-connection-string-2.13.0.tgz", + "integrity": "sha512-EMnU9E2fSULdsbErBbMaXJvFeD9B4+nPcM3f+4lsiCR0BHLPrLVjv3DbyM2hgQQviKJaTWIRRTjKjWlHg3p2ig==", "license": "MIT" }, "node_modules/pg-hstore": { @@ -1461,18 +1524,18 @@ } }, "node_modules/pg-pool": { - "version": "3.13.0", - "resolved": "https://registry.npmjs.org/pg-pool/-/pg-pool-3.13.0.tgz", - "integrity": "sha512-gB+R+Xud1gLFuRD/QgOIgGOBE2KCQPaPwkzBBGC9oG69pHTkhQeIuejVIk3/cnDyX39av2AxomQiyPT13WKHQA==", + "version": "3.14.0", + "resolved": "https://registry.npmjs.org/pg-pool/-/pg-pool-3.14.0.tgz", + "integrity": "sha512-gKtPkFdQPU3DksooVLi9LsjZxrsBUZIpa+7aVx+LV5pNh0KzP4Zleud2po+ConrxbuXGBJ6Hfer6hdgpIBpBaw==", "license": "MIT", "peerDependencies": { "pg": ">=8.0" } }, "node_modules/pg-protocol": { - "version": "1.13.0", - "resolved": "https://registry.npmjs.org/pg-protocol/-/pg-protocol-1.13.0.tgz", - "integrity": "sha512-zzdvXfS6v89r6v7OcFCHfHlyG/wvry1ALxZo4LqgUoy7W9xhBDMaqOuMiF3qEV45VqsN6rdlcehHrfDtlCPc8w==", + "version": "1.14.0", + "resolved": "https://registry.npmjs.org/pg-protocol/-/pg-protocol-1.14.0.tgz", + "integrity": "sha512-n5taZ1kO3s9ngDTVxsEznOqCyToTgz0FLuPq0B33COy5pPpuWJpY3/2oRBVETuOgzdqRXfWpM9HIhp2LBBT1BA==", "license": "MIT" }, "node_modules/pg-types": { diff --git a/judd/routes/post.js b/judd/routes/post.js index fef1834..85d531e 100644 --- a/judd/routes/post.js +++ b/judd/routes/post.js @@ -39,7 +39,8 @@ router.post('/post', postLimiter, async (request, response, next) => { isPidValid = false; } } catch (err) { - isPidValid = false; + console.error(`Caught error when validating ${pid}:`, err.message || err); + isPidValid = true; } if (!isPidValid) { diff --git a/main.py b/main.py index b707506..3496c9e 100644 --- a/main.py +++ b/main.py @@ -14,11 +14,13 @@ from routes.equipment import equipment_history from routes.equipment import equipment from services.boss_retrieval import process_boss_file from routes import Ranking +from routes import ranking_grabstats from routes import twitter_link from contextlib import asynccontextmanager -from datetime import datetime, timedelta +from datetime import datetime, timedelta, timezone from sqlalchemy import delete from config import BASE_DIR +import yaml import asyncio import subprocess import signal @@ -31,6 +33,7 @@ JUDD_SERVER_JS = os.path.join(JUDD_CWD, "server.js") JUDD_CMD = ["node", JUDD_SERVER_JS] judd_process = None +skip_upcoming_reset = False async def boss_worker_loop(): print("background worker started") @@ -43,30 +46,79 @@ async def boss_worker_loop(): await asyncio.sleep(3600) async def weekly_rank_reset_loop(): + global skip_upcoming_reset print("weekly_rank_reset worker started") - while True: - now = datetime.now() - days_ahead = 6 - now.weekday() + + FES_BOSS_PATH = os.path.join(BASE_DIR, "fes_boss.yaml") + + def get_next_sunday_reset_time_utc(): + now_utc = datetime.now(timezone.utc) + days_ahead = 6 - now_utc.weekday() if days_ahead <= 0: days_ahead += 7 + next_sunday_utc = now_utc + timedelta(days=days_ahead) + return next_sunday_utc.replace(hour=0, minute=0, second=0, microsecond=0) + + async def check_splatfest_schedule_loop(): + global skip_upcoming_reset + while True: + try: + target_reset_utc = get_next_sunday_reset_time_utc() + print(f"checking if a splatfest will be active at: {target_reset_utc.isoformat()}") + + if os.path.exists(FES_BOSS_PATH): + with open(FES_BOSS_PATH, "r", encoding="utf-8") as f: + fes_data = yaml.safe_load(f) + + if fes_data and "Time" in fes_data: + start_time = datetime.fromisoformat(fes_data["Time"]["Start"]) + end_time = datetime.fromisoformat(fes_data["Time"]["End"]) + + start_time_utc = start_time.astimezone(timezone.utc) + end_time_utc = end_time.astimezone(timezone.utc) + + print(f"detected splatfest schedule: {start_time_utc.isoformat()} to {end_time_utc.isoformat()}") + + if start_time_utc <= target_reset_utc <= end_time_utc: + skip_upcoming_reset = True + print("Splatfest detected in reset window") + else: + skip_upcoming_reset = False + print("No splatfest active in reset window, yay!!!") + else: + print("could not find yaml, defaulted to no fest") + skip_upcoming_reset = False + + except Exception as e: + print(f"failed to check fest schedule: {e}") - next_sunday = now + timedelta(days=days_ahead) - next_run = next_sunday.replace(hour=0, minute=0, second=0, microsecond=0) + await asyncio.sleep(7200) + + asyncio.create_task(check_splatfest_schedule_loop()) + + while True: + now_utc = datetime.now(timezone.utc) + next_run_utc = get_next_sunday_reset_time_utc() + wait_seconds = (next_run_utc - now_utc).total_seconds() - wait_seconds = (next_run - now).total_seconds() - print(f"Next ranking reset scheduled for: {next_run} (in {wait_seconds:.0f} seconds)") + print(f"Next ranking reset scheduled for: {next_run_utc.isoformat()} (in {wait_seconds:.0f} seconds)") try: await asyncio.sleep(wait_seconds) - print("Executing weekly ranking reset...") - with SessionLocal() as db: - db.execute(delete(PlayerRank)) - db.commit() - print("Ranking table cleared successfully.") - await FastAPICache.clear() - print("Cache cleared") + print("ranking reset triggerred!") + if skip_upcoming_reset: + print("Reset skipped due to active splatfest") + else: + print("Executing weekly ranking reset...") + with SessionLocal() as db: + db.execute(delete(PlayerRank)) + db.commit() + print("Ranking table cleared successfully.") + await FastAPICache.clear() + print("Cache cleared") + await asyncio.sleep(60) except asyncio.CancelledError: @@ -136,6 +188,7 @@ app.include_router(equipment_history.router, prefix="/api/v1") app.include_router(equipment.router, prefix="/api/v1") app.include_router(Ranking.router, prefix="/api/v1") app.include_router(twitter_link.router, prefix="/api/v1") +app.include_router(ranking_grabstats.router, prefix="/api/v1") def start(): uvicorn.run("main:app", host="0.0.0.0", port=settings.port, reload=False) diff --git a/pyproject.toml b/pyproject.toml index eb49aff..0a55d4d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -7,45 +7,45 @@ requires-python = ">=3.11" dependencies = [ "annotated-doc==0.0.4", "annotated-types==0.7.0", - "anyio==4.13.0", + "anyio==4.14.0", "argon2-cffi==25.1.0", "argon2-cffi-bindings==25.1.0", - "certifi==2026.4.22", + "certifi==2026.6.17", "cffi==2.0.0", "charset-normalizer==3.4.7", - "click==8.3.3", + "click==8.4.1", "colorama==0.4.6", - "cryptography==46.0.7", - "fastapi==0.136.1", + "cryptography==49.0.0", + "fastapi==0.137.1", "fastapi-cache2==0.2.2", - "greenlet==3.4.0", + "greenlet==3.5.2", "h11==0.16.0", "httpcore==1.0.9", "httpx==0.28.1", - "idna==3.13", + "idna==3.18", "oauthlib==3.3.1", "oead==1.2.9.post4", "pendulum==3.2.0", "psycopg2-binary==2.9.12", "pycparser==3.0", - "pydantic==2.13.3", - "pydantic-core==2.46.3", - "pydantic-settings==2.14.0", + "pydantic==2.13.4", + "pydantic-core==2.46.4", + "pydantic-settings==2.14.1", "python-dateutil==2.9.0.post0", "python-dotenv==1.2.2", - "python-multipart==0.0.27", + "python-multipart==0.0.32", "pyyaml==6.0.3", - "requests==2.33.1", + "requests==2.34.2", "requests-oauthlib==2.0.0", "six==1.17.0", - "sqlalchemy==2.0.49", - "starlette==1.0.0", + "sqlalchemy==2.0.51", + "starlette==1.3.1", "tweepy==4.16.0", "typing-extensions==4.15.0", "typing-inspection==0.4.2", "tzdata==2026.2", - "urllib3==2.6.3", - "uvicorn==0.46.0", + "urllib3==2.7.0", + "uvicorn==0.49.0", "jinja2==3.1.6", ] diff --git a/renovate.json b/renovate.json index 3f918de..401475e 100644 --- a/renovate.json +++ b/renovate.json @@ -4,7 +4,7 @@ "packageRules": [ { "matchUpdateTypes": ["minor", "patch"], - "automerge": true + "automerge": false }, { "matchUpdateTypes": ["major"], diff --git a/routes/equipment/equipment.py b/routes/equipment/equipment.py index 41ef3f7..72c397e 100644 --- a/routes/equipment/equipment.py +++ b/routes/equipment/equipment.py @@ -33,7 +33,7 @@ async def get_equipment_composite(request: Request, db: DBSession = Depends(get_ try: decrypted_pass = cipher.decrypt(user.spfn_pass_enc.encode()).decode() token_data = auth.get_token(user.username, decrypted_pass) - profile = json.loads(auth.get_profile(token_data["token"])) + profile = auth.get_profile(token_data["access_token"]) pid_val = int(profile.get("pid")) mii_name = profile.get("name", user.username) diff --git a/routes/equipment/equipment_history.py b/routes/equipment/equipment_history.py index a74309e..54060ff 100644 --- a/routes/equipment/equipment_history.py +++ b/routes/equipment/equipment_history.py @@ -27,7 +27,7 @@ async def get_history(request: Request, db: DBSession = Depends(get_db)): try: decrypted_pass = cipher.decrypt(user.spfn_pass_enc.encode()).decode() token_data = auth.get_token(user.username, decrypted_pass) - profile = json.loads(auth.get_profile(token_data["token"])) + profile = auth.get_profile(token_data["access_token"]) pid_val = int(profile.get("pid")) print(f"Fetching History for PID: {pid_val}") diff --git a/routes/me.py b/routes/me.py index 4d20b11..72a2456 100644 --- a/routes/me.py +++ b/routes/me.py @@ -33,10 +33,10 @@ async def get_my_profile(request: Request, db: DBSession = Depends(get_db)): decrypted_pass = cipher.decrypt(user.spfn_pass_enc.encode()).decode() token_data = auth.get_token(user.username, decrypted_pass) - if not token_data or "token" not in token_data: + if not token_data or "access_token" not in token_data: return RedirectResponse(url="/sign_in/") - profile_data = auth.get_profile(token_data["token"]) + profile_data = auth.get_profile(token_data["access_token"]) if not profile_data: raise HTTPException(status_code=404, detail="Profile not found") diff --git a/routes/ranking_grabstats.py b/routes/ranking_grabstats.py new file mode 100644 index 0000000..b46f47e --- /dev/null +++ b/routes/ranking_grabstats.py @@ -0,0 +1,72 @@ +from fastapi import APIRouter, Depends, HTTPException +from sqlalchemy.orm import Session as DBSession +from sqlalchemy import desc +from database import SessionLocal, PlayerRank, EquipmentLast +from fastapi_cache.decorator import cache + +router = APIRouter() + +def get_db(): + db = SessionLocal() + try: + yield db + finally: + db.close() + +@router.get("/leaderboard/getplayerstats/{pid}") +@cache(expire=60) +async def get_player_stats(pid: int, mode: int | None = None, db: DBSession = Depends(get_db)): + try: + query = ( + db.query(PlayerRank, EquipmentLast) + .outerjoin(EquipmentLast, PlayerRank.PId == EquipmentLast.PId) + .filter(PlayerRank.PId == pid) + ) + + if mode is not None: + query = query.filter(PlayerRank.GameMode == mode) + + result = query.first() + + if not result: + raise HTTPException(status_code=404, detail="Player not found") + + player, gear = result + + player_data = { + "PId": player.PId, + "MiiName": player.MiiName, + "Rank": player.Rank, + "GameMode": player.GameMode, + "WinSum": player.WinSum, + "LoseSum": player.LoseSum, + "RankingScore": round(player.RankingScore, 2), + "weapon": gear.weapon if gear else None, + "headgear": gear.Gear_Head if gear else None, + "clothes": gear.Gear_Clothes if gear else None, + "shoes": gear.Gear_Shoes if gear else None, + } + + if player.GameMode == 2: + top_100_query = ( + db.query(PlayerRank.PId) + .order_by(desc(PlayerRank.FesPower)) + .limit(100) + .all() + ) + top_100_tuples = set(top_100_query) + + is_top_100 = (player.PId,) in top_100_tuples + player_data["FesPower"] = player.FesPower + player_data["is_top_100_fes"] = is_top_100 + else: + player_data["FesPower"] = None + player_data["is_top_100_fes"] = False + + return player_data + + except HTTPException as http_ex: + raise http_ex + except Exception as e: + print(f"Error fetching stats for player {pid}: {e}") + return {"error": str(e)} \ No newline at end of file diff --git a/routes/sso.py b/routes/sso.py index e33d9cd..7bbbfcc 100644 --- a/routes/sso.py +++ b/routes/sso.py @@ -37,7 +37,7 @@ async def login( try: print("fetching spfn token") data = auth.get_token(username, password) - if not data or "token" not in data: + if not data or "access_token" not in data: print("auth failed") return RedirectResponse(f"{auth_path}{sep}error=auth&username={username}", 303) diff --git a/routes/twitter_link.py b/routes/twitter_link.py index 9addb7b..65fcd00 100644 --- a/routes/twitter_link.py +++ b/routes/twitter_link.py @@ -93,7 +93,7 @@ async def confirm_twitter(state: str, code: str, db: DBSession = Depends(get_db) decrypted_pass = cipher.decrypt(user.spfn_pass_enc.encode()).decode() profile_auth = auth.get_token(user.username, decrypted_pass) - profile_response = auth.get_profile(profile_auth["token"]) + profile_response = auth.get_profile(profile_auth["access_token"]) if isinstance(profile_response, str): try: @@ -149,7 +149,7 @@ async def get_twitter_status(request: Request, db: DBSession = Depends(get_db)): try: decrypted_pass = cipher.decrypt(user.spfn_pass_enc.encode()).decode() profile_auth = auth.get_token(user.username, decrypted_pass) - profile_res = auth.get_profile(profile_auth["token"]) + profile_res = auth.get_profile(profile_auth["access_token"]) if isinstance(profile_res, str): profile_data = json.loads(profile_res) @@ -183,7 +183,7 @@ async def unlink_twitter(request: Request, db: DBSession = Depends(get_db)): user = db.query(User).filter(User.username == db_session.username).first() decrypted_pass = cipher.decrypt(user.spfn_pass_enc.encode()).decode() profile_auth = auth.get_token(user.username, decrypted_pass) - profile_data = auth.get_profile(profile_auth["token"]) + profile_data = auth.get_profile(profile_auth["access_token"]) if isinstance(profile_data, str): profile_data = json.loads(profile_data) diff --git a/security.md b/security.md new file mode 100644 index 0000000..9634fba --- /dev/null +++ b/security.md @@ -0,0 +1,3 @@ +# Security Policy + +Report security vulnerabilities through the security tab on this repo. Only vulnerabilities on the latest commit of any branch will be considered. diff --git a/services/auth.py b/services/auth.py index d25218d..1064edc 100644 --- a/services/auth.py +++ b/services/auth.py @@ -1,35 +1,41 @@ import requests -import base64 +from config import settings from argon2 import PasswordHasher API_URL = "https://account.spfn.net/api/v2" +CLIENT_ID = "splatnet" +CLIENT_SECRET = settings.account_client_secret ph = PasswordHasher() def hash_password(password: str): return ph.hash(password) -def verify_password(hashed: str, password: str): - try: - return ph.verify(hashed, password) - except Exception: - return False - def get_token(username, password): - creds = f"{username} {password}" - encoded = base64.b64encode(creds.encode()).decode() + url = f"{API_URL}/oauth2/generate_token" - headers = { - "Authorization": f"Basic {encoded}", - "User-Agent": "saturday & allison should lwk kiss..." + payload = { + "grant_type": "password", + "username": username, + "password": password, + "client_id": CLIENT_ID, + "client_secret": CLIENT_SECRET } - response = requests.get(f"{API_URL}/oauth2/generate_token", headers=headers, timeout=10) + headers = { + "User-Agent": "chiyo & eri should lwk kiss..." + } + + response = requests.post(url, data=payload, headers=headers, timeout=10) return response.json() if response.ok else None def get_profile(token): + url = f"{API_URL}/users/@me/profile" + headers = { "Authorization": f"Bearer {token}", - "User-Agent": "every update im going to change this user agent because im bored!!" + "User-Agent": "lets make the most of the night like were gonna die young!!" } - response = requests.get(f"{API_URL}/users/@me/profile", headers=headers, timeout=10) - return response.text if response.ok else None \ No newline at end of file + + response = requests.get(url, headers=headers, timeout=10) + + return response.json() if response.ok else None \ No newline at end of file diff --git a/support.md b/support.md new file mode 100644 index 0000000..7cfc89e --- /dev/null +++ b/support.md @@ -0,0 +1,3 @@ +# Support + +For support, join [our discord](https://discord.gg/splatfestival).