Compare commits

..
Author SHA1 Message Date
Renovate Bot
0368d8dedc fix(deps): update dependency pydantic-core to v2.46.4 2026-05-06 13:46:40 +00:00
18 changed files with 116 additions and 360 deletions

View file

@ -7,9 +7,9 @@ on:
jobs:
docker:
runs-on: ${{ vars.RUNNER_LABEL }}
runs-on: debian-bookworm
steps:
- name: Checkout
- name: checkout
uses: actions/checkout@v6
- name: install docker???

View file

@ -12,14 +12,12 @@
# SplatNet
![Last Commit](https://img.shields.io/gitea/last-commit/kittentm/splatnet-backend?gitea_url=https://git.crafterpika.cc&style=for-the-badge&color=blue)
[![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)
[![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=blue)](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=yellow)](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.
This is the backend for the [SplatNet](https://git.crafterpika.cc/kittentm/splatnet) Revival frontend. It is a combination of Python & JS.
</div>

View file

@ -1,40 +0,0 @@
# 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/

View file

@ -18,7 +18,6 @@ class Settings(BaseSettings):
twitter_client_id: str
twitter_client_secret: str
twitter_redirect_uri: str
account_client_secret: str
model_config = SettingsConfigDict(
env_file=[

173
judd/package-lock.json generated
View file

@ -336,41 +336,6 @@
"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",
@ -384,14 +349,13 @@
"license": "MIT"
},
"node_modules/axios": {
"version": "1.18.0",
"resolved": "https://registry.npmjs.org/axios/-/axios-1.18.0.tgz",
"integrity": "sha512-E32NzpYKp++W7XRe52rHiXV2ehxmh3wbdgO7MHeFM+vqxLBYHzt0ElkiImtOBxtOmyp0yoC8C6uESVV84Y2/hw==",
"version": "1.15.2",
"resolved": "https://registry.npmjs.org/axios/-/axios-1.15.2.tgz",
"integrity": "sha512-wLrXxPtcrPTsNlJmKjkPnNPK2Ihe0hn0wGSaTEiHRPxwjvJwT3hKmXF4dpqxmPO9SoNb2FsYXj/xEo0gHN+D5A==",
"license": "MIT",
"dependencies": {
"follow-redirects": "^1.16.0",
"follow-redirects": "^1.15.11",
"form-data": "^4.0.5",
"https-proxy-agent": "^5.0.1",
"proxy-from-env": "^2.1.0"
}
},
@ -667,9 +631,9 @@
]
},
"node_modules/discord.js": {
"version": "14.26.4",
"resolved": "https://registry.npmjs.org/discord.js/-/discord.js-14.26.4.tgz",
"integrity": "sha512-4oBp8tc6Kf8IDBwAHhbsMaAqx1b5fob9SNasZT7V6yyyUydoO5i5fGuX7TmvRtR+q/WgKRnRViRoAWnG7fNyvA==",
"version": "14.26.3",
"resolved": "https://registry.npmjs.org/discord.js/-/discord.js-14.26.3.tgz",
"integrity": "sha512-XEKtYn28YFsiJ5l4fLRyikdbo6RD5oFyqfVHQlvXz2104JhH/E8slN28dbky05w3DCrJcNVWvhVvcJCTSl/KIg==",
"license": "Apache-2.0",
"dependencies": {
"@discordjs/builders": "^1.14.1",
@ -833,12 +797,12 @@
}
},
"node_modules/express-rate-limit": {
"version": "8.5.2",
"resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-8.5.2.tgz",
"integrity": "sha512-5Kb34ipNX694DH48vN9irak1Qx30nb0PLYHXfJgw4YEjiC3ZEmZJhwOp+VfiCYwFzvFTdB9QkArYS5kXa2cx2A==",
"version": "8.3.2",
"resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-8.3.2.tgz",
"integrity": "sha512-77VmFeJkO0/rvimEDuUC5H30oqUC4EyOhyGccfqoLebB0oiEYfM7nwPrsDsBL1gsTpwfzX8SFy2MT3TDyRq+bg==",
"license": "MIT",
"dependencies": {
"ip-address": "^10.2.0"
"ip-address": "10.1.0"
},
"engines": {
"node": ">= 16"
@ -1148,42 +1112,6 @@
"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",
@ -1216,9 +1144,9 @@
"license": "ISC"
},
"node_modules/ip-address": {
"version": "10.2.0",
"resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.2.0.tgz",
"integrity": "sha512-/+S6j4E9AHvW9SWMSEY9Xfy66O5PWvVEJ08O0y5JGyEKQpojb0K0GKpz/v5HJ/G0vi3D2sjGK78119oXZeE0qA==",
"version": "10.1.0",
"resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.1.0.tgz",
"integrity": "sha512-XXADHxXmvT9+CRxhXg56LJovE+bmWnEWB78LB83VZTprKTmaC5QfruXocxzTZ2Kl0DNwKuBdlIhjL8LeY8Sf8Q==",
"license": "MIT",
"engines": {
"node": ">= 12"
@ -1240,9 +1168,9 @@
"license": "MIT"
},
"node_modules/joi": {
"version": "18.2.3",
"resolved": "https://registry.npmjs.org/joi/-/joi-18.2.3.tgz",
"integrity": "sha512-N5A3KTWQpPWT4ExxxPlUx7WmykGXRzhNidWhV41d6Abu9YfI2NyWCJuxdPnslJCPWtbRpSVOWSnSS6GakLM/Rg==",
"version": "18.1.2",
"resolved": "https://registry.npmjs.org/joi/-/joi-18.1.2.tgz",
"integrity": "sha512-rF5MAmps5esSlhCA+N1b6IYHDw9j/btzGaqfgie522jS02Ju/HXBxamlXVlKEHAxoMKQL77HWI8jlqWsFuekZA==",
"license": "BSD-3-Clause",
"dependencies": {
"@hapi/address": "^5.1.1",
@ -1348,23 +1276,31 @@
}
},
"node_modules/morgan": {
"version": "1.11.0",
"resolved": "https://registry.npmjs.org/morgan/-/morgan-1.11.0.tgz",
"integrity": "sha512-zSkVu3t18r39pw4ixfBKvfZi3y2UOqr7d4WYwcj3m8nXpEQK4rPO6GLzs/CExoRgmX3y9EjmmcXqv6jq0SK46g==",
"version": "1.10.1",
"resolved": "https://registry.npmjs.org/morgan/-/morgan-1.10.1.tgz",
"integrity": "sha512-223dMRJtI/l25dJKWpgij2cMtywuG/WiUKXdvwfbhGKBhy1puASqXwFzmWZ7+K73vUPoR7SS2Qz2cI/g9MKw0A==",
"license": "MIT",
"dependencies": {
"basic-auth": "~2.0.1",
"debug": "2.6.9",
"depd": "~2.0.0",
"on-finished": "~2.4.1",
"on-finished": "~2.3.0",
"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"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/express"
"engines": {
"node": ">= 0.8"
}
},
"node_modules/ms": {
@ -1374,9 +1310,9 @@
"license": "MIT"
},
"node_modules/multer": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/multer/-/multer-2.2.0.tgz",
"integrity": "sha512-6rdyFg2kLrMh9Jee7/BMPuV9lEAd7lLW2YUpF9/YxR7njyoUwwQ0ZPh3TaIY50Sw6vlyD2HW3wGOkTS4P79xrQ==",
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/multer/-/multer-2.1.1.tgz",
"integrity": "sha512-mo+QTzKlx8R7E5ylSXxWzGoXoZbOsRMpyitcht8By2KHvMbf3tjwosZ/Mu/XYU6UuJ3VZnODIrak5ZrPiPyB6A==",
"license": "MIT",
"dependencies": {
"append-field": "^1.0.0",
@ -1463,14 +1399,15 @@
}
},
"node_modules/pg": {
"version": "8.21.0",
"resolved": "https://registry.npmjs.org/pg/-/pg-8.21.0.tgz",
"integrity": "sha512-AUP1EYJuHraQGsVoCQVIcM7TEJVGtDzxWtGFZd8rds9d+CCXlU5Js1rYgfLNvxy9iJrpHjGrRjoi/3BT9fRyiA==",
"version": "8.20.0",
"resolved": "https://registry.npmjs.org/pg/-/pg-8.20.0.tgz",
"integrity": "sha512-ldhMxz2r8fl/6QkXnBD3CR9/xg694oT6DZQ2s6c/RI28OjtSOpxnPrUCGOBJ46RCUxcWdx3p6kw/xnDHjKvaRA==",
"license": "MIT",
"peer": true,
"dependencies": {
"pg-connection-string": "^2.13.0",
"pg-pool": "^3.14.0",
"pg-protocol": "^1.14.0",
"pg-connection-string": "^2.12.0",
"pg-pool": "^3.13.0",
"pg-protocol": "^1.13.0",
"pg-types": "2.2.0",
"pgpass": "1.0.5"
},
@ -1478,7 +1415,7 @@
"node": ">= 16.0.0"
},
"optionalDependencies": {
"pg-cloudflare": "^1.4.0"
"pg-cloudflare": "^1.3.0"
},
"peerDependencies": {
"pg-native": ">=3.0.1"
@ -1490,16 +1427,16 @@
}
},
"node_modules/pg-cloudflare": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/pg-cloudflare/-/pg-cloudflare-1.4.0.tgz",
"integrity": "sha512-Vo7z/6rrQYxpNRylp4Tlob2elzbh+N/MOQbxFVWCxS7oEx6jF53GTJFxK2WWpKuBRkmiin4Mt+xofFDjx09R0A==",
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/pg-cloudflare/-/pg-cloudflare-1.3.0.tgz",
"integrity": "sha512-6lswVVSztmHiRtD6I8hw4qP/nDm1EJbKMRhf3HCYaqud7frGysPv7FYJ5noZQdhQtN2xJnimfMtvQq21pdbzyQ==",
"license": "MIT",
"optional": true
},
"node_modules/pg-connection-string": {
"version": "2.13.0",
"resolved": "https://registry.npmjs.org/pg-connection-string/-/pg-connection-string-2.13.0.tgz",
"integrity": "sha512-EMnU9E2fSULdsbErBbMaXJvFeD9B4+nPcM3f+4lsiCR0BHLPrLVjv3DbyM2hgQQviKJaTWIRRTjKjWlHg3p2ig==",
"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==",
"license": "MIT"
},
"node_modules/pg-hstore": {
@ -1524,18 +1461,18 @@
}
},
"node_modules/pg-pool": {
"version": "3.14.0",
"resolved": "https://registry.npmjs.org/pg-pool/-/pg-pool-3.14.0.tgz",
"integrity": "sha512-gKtPkFdQPU3DksooVLi9LsjZxrsBUZIpa+7aVx+LV5pNh0KzP4Zleud2po+ConrxbuXGBJ6Hfer6hdgpIBpBaw==",
"version": "3.13.0",
"resolved": "https://registry.npmjs.org/pg-pool/-/pg-pool-3.13.0.tgz",
"integrity": "sha512-gB+R+Xud1gLFuRD/QgOIgGOBE2KCQPaPwkzBBGC9oG69pHTkhQeIuejVIk3/cnDyX39av2AxomQiyPT13WKHQA==",
"license": "MIT",
"peerDependencies": {
"pg": ">=8.0"
}
},
"node_modules/pg-protocol": {
"version": "1.14.0",
"resolved": "https://registry.npmjs.org/pg-protocol/-/pg-protocol-1.14.0.tgz",
"integrity": "sha512-n5taZ1kO3s9ngDTVxsEznOqCyToTgz0FLuPq0B33COy5pPpuWJpY3/2oRBVETuOgzdqRXfWpM9HIhp2LBBT1BA==",
"version": "1.13.0",
"resolved": "https://registry.npmjs.org/pg-protocol/-/pg-protocol-1.13.0.tgz",
"integrity": "sha512-zzdvXfS6v89r6v7OcFCHfHlyG/wvry1ALxZo4LqgUoy7W9xhBDMaqOuMiF3qEV45VqsN6rdlcehHrfDtlCPc8w==",
"license": "MIT"
},
"node_modules/pg-types": {

View file

@ -39,8 +39,7 @@ router.post('/post', postLimiter, async (request, response, next) => {
isPidValid = false;
}
} catch (err) {
console.error(`Caught error when validating ${pid}:`, err.message || err);
isPidValid = true;
isPidValid = false;
}
if (!isPidValid) {

83
main.py
View file

@ -14,13 +14,11 @@ 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, timezone
from datetime import datetime, timedelta
from sqlalchemy import delete
from config import BASE_DIR
import yaml
import asyncio
import subprocess
import signal
@ -33,7 +31,6 @@ 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")
@ -46,78 +43,29 @@ 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")
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()
while True:
now = datetime.now()
days_ahead = 6 - now.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()}")
next_sunday = now + timedelta(days=days_ahead)
next_run = next_sunday.replace(hour=0, minute=0, second=0, microsecond=0)
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}")
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()
print(f"Next ranking reset scheduled for: {next_run_utc.isoformat()} (in {wait_seconds:.0f} seconds)")
wait_seconds = (next_run - now).total_seconds()
print(f"Next ranking reset scheduled for: {next_run} (in {wait_seconds:.0f} seconds)")
try:
await asyncio.sleep(wait_seconds)
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")
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)
@ -188,7 +136,6 @@ 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)

View file

@ -7,45 +7,45 @@ requires-python = ">=3.11"
dependencies = [
"annotated-doc==0.0.4",
"annotated-types==0.7.0",
"anyio==4.14.0",
"anyio==4.13.0",
"argon2-cffi==25.1.0",
"argon2-cffi-bindings==25.1.0",
"certifi==2026.6.17",
"certifi==2026.4.22",
"cffi==2.0.0",
"charset-normalizer==3.4.7",
"click==8.4.1",
"click==8.3.3",
"colorama==0.4.6",
"cryptography==49.0.0",
"fastapi==0.137.1",
"cryptography==46.0.7",
"fastapi==0.136.1",
"fastapi-cache2==0.2.2",
"greenlet==3.5.2",
"greenlet==3.4.0",
"h11==0.16.0",
"httpcore==1.0.9",
"httpx==0.28.1",
"idna==3.18",
"idna==3.13",
"oauthlib==3.3.1",
"oead==1.2.9.post4",
"pendulum==3.2.0",
"psycopg2-binary==2.9.12",
"pycparser==3.0",
"pydantic==2.13.4",
"pydantic==2.13.3",
"pydantic-core==2.46.4",
"pydantic-settings==2.14.1",
"pydantic-settings==2.14.0",
"python-dateutil==2.9.0.post0",
"python-dotenv==1.2.2",
"python-multipart==0.0.32",
"python-multipart==0.0.27",
"pyyaml==6.0.3",
"requests==2.34.2",
"requests==2.33.1",
"requests-oauthlib==2.0.0",
"six==1.17.0",
"sqlalchemy==2.0.51",
"starlette==1.3.1",
"sqlalchemy==2.0.49",
"starlette==1.0.0",
"tweepy==4.16.0",
"typing-extensions==4.15.0",
"typing-inspection==0.4.2",
"tzdata==2026.2",
"urllib3==2.7.0",
"uvicorn==0.49.0",
"urllib3==2.6.3",
"uvicorn==0.46.0",
"jinja2==3.1.6",
]

View file

@ -4,7 +4,7 @@
"packageRules": [
{
"matchUpdateTypes": ["minor", "patch"],
"automerge": false
"automerge": true
},
{
"matchUpdateTypes": ["major"],

View file

@ -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 = auth.get_profile(token_data["access_token"])
profile = json.loads(auth.get_profile(token_data["token"]))
pid_val = int(profile.get("pid"))
mii_name = profile.get("name", user.username)

View file

@ -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 = auth.get_profile(token_data["access_token"])
profile = json.loads(auth.get_profile(token_data["token"]))
pid_val = int(profile.get("pid"))
print(f"Fetching History for PID: {pid_val}")

View file

@ -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 "access_token" not in token_data:
if not token_data or "token" not in token_data:
return RedirectResponse(url="/sign_in/")
profile_data = auth.get_profile(token_data["access_token"])
profile_data = auth.get_profile(token_data["token"])
if not profile_data:
raise HTTPException(status_code=404, detail="Profile not found")

View file

@ -1,72 +0,0 @@
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)}

View file

@ -37,7 +37,7 @@ async def login(
try:
print("fetching spfn token")
data = auth.get_token(username, password)
if not data or "access_token" not in data:
if not data or "token" not in data:
print("auth failed")
return RedirectResponse(f"{auth_path}{sep}error=auth&username={username}", 303)

View file

@ -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["access_token"])
profile_response = auth.get_profile(profile_auth["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["access_token"])
profile_res = auth.get_profile(profile_auth["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["access_token"])
profile_data = auth.get_profile(profile_auth["token"])
if isinstance(profile_data, str):
profile_data = json.loads(profile_data)

View file

@ -1,3 +0,0 @@
# Security Policy
Report security vulnerabilities through the security tab on this repo. Only vulnerabilities on the latest commit of any branch will be considered.

View file

@ -1,41 +1,35 @@
import requests
from config import settings
import base64
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 get_token(username, password):
url = f"{API_URL}/oauth2/generate_token"
def verify_password(hashed: str, password: str):
try:
return ph.verify(hashed, password)
except Exception:
return False
payload = {
"grant_type": "password",
"username": username,
"password": password,
"client_id": CLIENT_ID,
"client_secret": CLIENT_SECRET
}
def get_token(username, password):
creds = f"{username} {password}"
encoded = base64.b64encode(creds.encode()).decode()
headers = {
"User-Agent": "chiyo & eri should lwk kiss..."
"Authorization": f"Basic {encoded}",
"User-Agent": "saturday & allison should lwk kiss..."
}
response = requests.post(url, data=payload, headers=headers, timeout=10)
response = requests.get(f"{API_URL}/oauth2/generate_token", 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": "lets make the most of the night like were gonna die young!!"
"User-Agent": "every update im going to change this user agent because im bored!!"
}
response = requests.get(url, headers=headers, timeout=10)
return response.json() if response.ok else None
response = requests.get(f"{API_URL}/users/@me/profile", headers=headers, timeout=10)
return response.text if response.ok else None

View file

@ -1,3 +0,0 @@
# Support
For support, join [our discord](https://discord.gg/splatfestival).