mirror of
https://github.com/FAUSheppy/tmnf-replay-server.git
synced 2025-12-06 15:11:36 +01:00
Compare commits
19 Commits
notificati
...
e54f70eaac
| Author | SHA1 | Date | |
|---|---|---|---|
| e54f70eaac | |||
| a40bf057ee | |||
| 807860351d | |||
| d89ccf42ff | |||
| 7a3ce3ef32 | |||
| ae92f51201 | |||
| 6f80c7bd32 | |||
| 7479499f78 | |||
| 4fa83afe16 | |||
| 42f292fb7a | |||
| 710d519344 | |||
| 8690c3b7bc | |||
| 8258f57a09 | |||
| 430dd53f77 | |||
| b4203095a9 | |||
| c28049b94c | |||
| fd61cb12a4 | |||
| 6488eebd1b | |||
| 59af2e0ec6 |
2
.github/workflows/main.yaml
vendored
2
.github/workflows/main.yaml
vendored
@@ -4,6 +4,8 @@ on:
|
|||||||
push:
|
push:
|
||||||
branches:
|
branches:
|
||||||
- "master"
|
- "master"
|
||||||
|
schedule:
|
||||||
|
- cron: "0 2 * * 0"
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
docker:
|
docker:
|
||||||
|
|||||||
31
notifications.py
Normal file
31
notifications.py
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
import sys
|
||||||
|
import requests
|
||||||
|
|
||||||
|
def send_notification(app, target_user, mapname, old_replay, new_replay):
|
||||||
|
'''Build notification and handoff to dispatcher'''
|
||||||
|
|
||||||
|
url = app.config["DISPATCH_SERVER"]
|
||||||
|
|
||||||
|
if not url:
|
||||||
|
return
|
||||||
|
|
||||||
|
# send to event dispatcher #
|
||||||
|
message = "TM: Record broken on {}\n\n".format(mapname)
|
||||||
|
message += "Old time: {}\n".format(old_replay.get_human_readable_time())
|
||||||
|
message += "New time: {}\n".format(new_replay.get_human_readable_time())
|
||||||
|
message += "\nby {}".format(new_replay.clean_login())
|
||||||
|
|
||||||
|
payload = {
|
||||||
|
"users": [target_user],
|
||||||
|
"msg" : message,
|
||||||
|
"method" : "any"
|
||||||
|
}
|
||||||
|
|
||||||
|
url_and_token = "/smart-send?dispatch-access-token={}".format(app.config["DISPATCH_TOKEN"])
|
||||||
|
r = requests.post(app.config["DISPATCH_SERVER"] + url_and_token, json=payload)
|
||||||
|
|
||||||
|
if not r.ok:
|
||||||
|
msg = "Error handing off notification to dispatch ({} {})".format(r.status_code, r.content)
|
||||||
|
print(msg, file=sys.stderr)
|
||||||
|
else:
|
||||||
|
print("Handed off notification for {} to dispatch".format(target_user), file=sys.stderr)
|
||||||
1
req.txt
1
req.txt
@@ -3,3 +3,4 @@ flask-sqlalchemy
|
|||||||
pygbx
|
pygbx
|
||||||
werkzeug
|
werkzeug
|
||||||
xmltodict
|
xmltodict
|
||||||
|
requests
|
||||||
|
|||||||
153
server.py
153
server.py
@@ -10,6 +10,7 @@ import datetime
|
|||||||
|
|
||||||
from pygbx import Gbx, GbxType
|
from pygbx import Gbx, GbxType
|
||||||
import tm2020parser
|
import tm2020parser
|
||||||
|
import notifications
|
||||||
|
|
||||||
import sqlalchemy
|
import sqlalchemy
|
||||||
from sqlalchemy import Column, Integer, String, Boolean, or_, and_, asc, desc
|
from sqlalchemy import Column, Integer, String, Boolean, or_, and_, asc, desc
|
||||||
@@ -21,6 +22,27 @@ app.config["SQLALCHEMY_TRACK_MODIFICATIONS"] = False
|
|||||||
app.config["SQLALCHEMY_DATABASE_URI"] = os.environ.get("SQLITE_LOCATION") or "sqlite:///sqlite.db"
|
app.config["SQLALCHEMY_DATABASE_URI"] = os.environ.get("SQLITE_LOCATION") or "sqlite:///sqlite.db"
|
||||||
db = SQLAlchemy(app)
|
db = SQLAlchemy(app)
|
||||||
|
|
||||||
|
SEASON_ORDERING = ["Winter", "Spring", "Summer", "Fall"]
|
||||||
|
def filter_for_current_season(maps):
|
||||||
|
|
||||||
|
maps = list(maps)
|
||||||
|
|
||||||
|
year = str(datetime.datetime.now().year)
|
||||||
|
|
||||||
|
season = 0
|
||||||
|
for m in maps:
|
||||||
|
|
||||||
|
if year not in m.mapname:
|
||||||
|
continue
|
||||||
|
else:
|
||||||
|
for i, season_name in enumerate(SEASON_ORDERING):
|
||||||
|
if season_name in m.mapname and i > season:
|
||||||
|
season = i
|
||||||
|
|
||||||
|
filter_func = lambda m: SEASON_ORDERING[season] in m.mapname and year in m.mapname
|
||||||
|
maps_season = list(filter(filter_func, maps))
|
||||||
|
return maps_season
|
||||||
|
|
||||||
class Map(db.Model):
|
class Map(db.Model):
|
||||||
|
|
||||||
__tablename__ = "maps"
|
__tablename__ = "maps"
|
||||||
@@ -77,6 +99,79 @@ class Map(db.Model):
|
|||||||
delta = datetime.datetime.now() - parsed
|
delta = datetime.datetime.now() - parsed
|
||||||
return delta.days
|
return delta.days
|
||||||
|
|
||||||
|
class UserSettings(db.Model):
|
||||||
|
|
||||||
|
__tablename__ = "user_settings"
|
||||||
|
|
||||||
|
user = Column(String, primary_key=True)
|
||||||
|
|
||||||
|
show_tm_2020 = Column(Boolean)
|
||||||
|
show_tmnf = Column(Boolean)
|
||||||
|
show_tm_2020_current = Column(Boolean)
|
||||||
|
|
||||||
|
notifications_all = Column(Boolean)
|
||||||
|
notifications_self = Column(Boolean)
|
||||||
|
|
||||||
|
@app.route("/update-user-settings", methods=["GET", "POST"])
|
||||||
|
def update_user_settings():
|
||||||
|
|
||||||
|
user = flask.request.headers.get("X-Forwarded-Preferred-Username")
|
||||||
|
user_helper = user or "anonymous"
|
||||||
|
|
||||||
|
settings = db.session.query(UserSettings).filter(UserSettings.user==user_helper).first()
|
||||||
|
|
||||||
|
# handle new settings #/
|
||||||
|
if not settings:
|
||||||
|
settings = UserSettings(user=user_helper, show_tm_2020=False, show_tmnf=False,
|
||||||
|
show_tm_2020_current=True, notifications_self=True,
|
||||||
|
notifications_all=False)
|
||||||
|
db.session.add(settings)
|
||||||
|
db.session.commit()
|
||||||
|
settings = db.session.query(UserSettings).filter(UserSettings.user==user_helper).first()
|
||||||
|
|
||||||
|
|
||||||
|
if flask.request.method == "GET":
|
||||||
|
|
||||||
|
key = flask.request.args.get("key")
|
||||||
|
|
||||||
|
# some sanity checks #
|
||||||
|
if not key:
|
||||||
|
return ("key missing in args", 422)
|
||||||
|
if key == "user" or key.startswith("_") or "sql" in key:
|
||||||
|
return ("key {} is not allowed".format(key), 422)
|
||||||
|
|
||||||
|
# return attribute #
|
||||||
|
return (str(getattr(settings, key)), 200)
|
||||||
|
|
||||||
|
elif flask.request.method == "POST":
|
||||||
|
json_dict = flask.request.json
|
||||||
|
key_value_list = json_dict.get("payload")
|
||||||
|
|
||||||
|
if not key_value_list:
|
||||||
|
return ("'payload' field empty", 422)
|
||||||
|
|
||||||
|
for el in key_value_list:
|
||||||
|
|
||||||
|
key = el.get("key")
|
||||||
|
value = el.get("value")
|
||||||
|
|
||||||
|
if key == "user" or key.startswith("_") or "sql" in key:
|
||||||
|
return ("key {} is not allowed".format(key), 422)
|
||||||
|
|
||||||
|
if key is None or value is None:
|
||||||
|
return ("element in payload list does not contain key and value", 422)
|
||||||
|
|
||||||
|
try:
|
||||||
|
getattr(settings, key)
|
||||||
|
setattr(settings, key, bool(value))
|
||||||
|
db.session.merge(settings)
|
||||||
|
db.session.commit()
|
||||||
|
return ("", 204)
|
||||||
|
except AttributeError:
|
||||||
|
return ("key {} not part of user settings".format(key), 422)
|
||||||
|
else:
|
||||||
|
raise AssertionError("Unsupported Method: {}".format(flask.request.method))
|
||||||
|
|
||||||
class ParsedReplay(db.Model):
|
class ParsedReplay(db.Model):
|
||||||
|
|
||||||
__tablename__ = "replays"
|
__tablename__ = "replays"
|
||||||
@@ -109,7 +204,10 @@ class ParsedReplay(db.Model):
|
|||||||
if t.seconds < 60*60:
|
if t.seconds < 60*60:
|
||||||
t_string = t_string[2:]
|
t_string = t_string[2:]
|
||||||
if t.microseconds != 0:
|
if t.microseconds != 0:
|
||||||
return t_string[:-4]
|
if self.game == "tmnf":
|
||||||
|
return t_string[:-4]
|
||||||
|
else:
|
||||||
|
return t_string[:-3]
|
||||||
return t_string + ".00"
|
return t_string + ".00"
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
@@ -311,7 +409,7 @@ def ranks():
|
|||||||
|
|
||||||
|
|
||||||
@app.route("/map-info")
|
@app.route("/map-info")
|
||||||
def list():
|
def map_info():
|
||||||
player = flask.request.headers.get("X-Forwarded-Preferred-Username")
|
player = flask.request.headers.get("X-Forwarded-Preferred-Username")
|
||||||
header_col = ["Player", "Time", "Date", "Replay"]
|
header_col = ["Player", "Time", "Date", "Replay"]
|
||||||
map_uid = flask.request.args.get("map_uid")
|
map_uid = flask.request.args.get("map_uid")
|
||||||
@@ -323,17 +421,24 @@ def mapnames():
|
|||||||
'''Index Location'''
|
'''Index Location'''
|
||||||
|
|
||||||
# TODO list by user
|
# TODO list by user
|
||||||
player = flask.request.headers.get("X-Forwarded-Preferred-Username")
|
player = flask.request.headers.get("X-Forwarded-Preferred-Username") or "anonymous"
|
||||||
maps_query = db.session.query(Map).order_by(asc(Map.mapname))
|
maps_query = db.session.query(Map).order_by(asc(Map.mapname))
|
||||||
|
|
||||||
# limit leaderboard to game #
|
# limit leaderboard to game #
|
||||||
game = flask.request.args.get("game")
|
settings = db.session.query(UserSettings).filter(UserSettings.user==player).first()
|
||||||
if game == "tm2020":
|
if settings:
|
||||||
maps_query = maps_query.filter(Map.game=="tm2020")
|
if not settings.show_tm_2020 and not settings.show_tmnf:
|
||||||
elif game=="tmnf":
|
maps_query = maps_query.filter(Map.game=="tm2020")
|
||||||
maps_query = maps_query.filter(Map.game!="tm2020")
|
latest_season = tm2020parser.get_latest_season_from_maps(maps_query.all())
|
||||||
else:
|
# handle no replays #
|
||||||
pass
|
if latest_season:
|
||||||
|
maps_query = maps_query.filter(Map.map_uid.like("{}%".format(latest_season)))
|
||||||
|
elif settings.show_tm_2020 and not settings.show_tmnf:
|
||||||
|
maps_query = maps_query.filter(Map.game=="tm2020")
|
||||||
|
elif not settings.show_tm_2020 and settings.show_tmnf:
|
||||||
|
maps_query = maps_query.filter(Map.game=="tmnf")
|
||||||
|
else:
|
||||||
|
pass
|
||||||
|
|
||||||
maps = maps_query.all()
|
maps = maps_query.all()
|
||||||
|
|
||||||
@@ -341,6 +446,9 @@ def mapnames():
|
|||||||
allowed = ("A", "B", "C", "D", "E", "Fall", "Winter", "Spring", "Summer")
|
allowed = ("A", "B", "C", "D", "E", "Fall", "Winter", "Spring", "Summer")
|
||||||
maps_filtered = filter(lambda m: m.mapname.startswith(allowed), maps)
|
maps_filtered = filter(lambda m: m.mapname.startswith(allowed), maps)
|
||||||
|
|
||||||
|
if settings.show_tm_2020_current:
|
||||||
|
maps_filtered = filter_for_current_season(maps_filtered)
|
||||||
|
|
||||||
return flask.render_template("index.html", maps=maps_filtered, player=player)
|
return flask.render_template("index.html", maps=maps_filtered, player=player)
|
||||||
|
|
||||||
@app.route("/open-info")
|
@app.route("/open-info")
|
||||||
@@ -390,6 +498,7 @@ def upload():
|
|||||||
replay = replay_from_path(fullpath, uploader=uploader)
|
replay = replay_from_path(fullpath, uploader=uploader)
|
||||||
db.session.add(replay)
|
db.session.add(replay)
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
|
check_replay_trigger(replay)
|
||||||
except ValueError as e:
|
except ValueError as e:
|
||||||
results += [(fname, str(e))]
|
results += [(fname, str(e))]
|
||||||
continue
|
continue
|
||||||
@@ -405,9 +514,33 @@ def upload():
|
|||||||
else:
|
else:
|
||||||
return flask.render_template("upload.html")
|
return flask.render_template("upload.html")
|
||||||
|
|
||||||
|
def check_replay_trigger(replay):
|
||||||
|
|
||||||
|
map_obj = db.session.query(Map).filter(Map.map_uid == replay.map_uid).first()
|
||||||
|
best = map_obj.get_best_replay()
|
||||||
|
second = map_obj.get_second_best_replay()
|
||||||
|
|
||||||
|
if not second:
|
||||||
|
return
|
||||||
|
|
||||||
|
if replay.filehash != best.filehash:
|
||||||
|
return
|
||||||
|
|
||||||
|
if second.uploader == replay.uploader:
|
||||||
|
return
|
||||||
|
|
||||||
|
settings = db.session.query(UserSettings).filter(UserSettings.user == second.uploader).first()
|
||||||
|
if settings and settings.notifications_self:
|
||||||
|
notifications.send_notification(app, settings.user, map_obj.map_uid, second, replay)
|
||||||
|
|
||||||
def create_app():
|
def create_app():
|
||||||
|
|
||||||
db.create_all()
|
db.create_all()
|
||||||
|
|
||||||
|
app.config["DISPATCH_SERVER"] = os.environ.get("DISPATCH_SERVER")
|
||||||
|
if app.config["DISPATCH_SERVER"]:
|
||||||
|
app.config["DISPATCH_TOKEN"] = os.environ["DISPATCH_TOKEN"]
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
|
|
||||||
parser = argparse.ArgumentParser(description='TM Replay Server',
|
parser = argparse.ArgumentParser(description='TM Replay Server',
|
||||||
|
|||||||
@@ -27,6 +27,15 @@ table{
|
|||||||
border-color: transparent;
|
border-color: transparent;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.user-settings{
|
||||||
|
display: inline-flex;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-check{
|
||||||
|
margin-right: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
.m-auto{
|
.m-auto{
|
||||||
margin: auto;
|
margin: auto;
|
||||||
}
|
}
|
||||||
|
|||||||
47
static/user_settings.js
Normal file
47
static/user_settings.js
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
/* event listener for all sliders */
|
||||||
|
const sliders = Array.from(document.getElementsByClassName("form-check-input"))
|
||||||
|
|
||||||
|
/* safety switch -> never run set before get */
|
||||||
|
var sliders_set = false
|
||||||
|
|
||||||
|
/* defer */
|
||||||
|
sliders_load_all()
|
||||||
|
|
||||||
|
/* get initial values & set listeners */
|
||||||
|
function sliders_load_all(){
|
||||||
|
Promise.all(sliders.map(s => {
|
||||||
|
fetch("/update-user-settings?key=" + s.id, { credentials: "include" }).then(response => {
|
||||||
|
response.text().then(data => {
|
||||||
|
if(data == "True"){
|
||||||
|
s.checked = true
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
s.addEventListener("change", submit)
|
||||||
|
})).then(
|
||||||
|
sliders_set = true
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/* submit settings */
|
||||||
|
function submit(e){
|
||||||
|
|
||||||
|
console.log("submit")
|
||||||
|
if(!sliders_set){
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const s = e.target
|
||||||
|
const json_data = JSON.stringify({ payload : [ { key : s.id, value : s.checked } ] })
|
||||||
|
fetch("/update-user-settings", {
|
||||||
|
method: "POST",
|
||||||
|
credentials: "include",
|
||||||
|
headers: {'Content-Type': 'application/json'},
|
||||||
|
body: json_data
|
||||||
|
}
|
||||||
|
).then(response => {
|
||||||
|
if(s.id.startsWith("show")){
|
||||||
|
window.location.reload()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -3,15 +3,28 @@
|
|||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
{% include "upload-button.html" %}
|
{% include "upload-button.html" %}
|
||||||
<button class="ml-4 mt-4 mb-4 btn btn-info" onclick="window.location.href='/?game=tmnf'">
|
|
||||||
TMNF
|
<div class="user-settings">
|
||||||
</button>
|
<div class="form-check form-switch">
|
||||||
<button class="mt-4 mb-4 btn btn-info" onclick="window.location.href='/?game=tm2020'">
|
<input class="form-check-input" type="checkbox" role="switch" id="show_tm_2020_current">
|
||||||
TM2020
|
<label class="form-check-label" for="show_tm_2020_current">Only current TM2020</label>
|
||||||
</button>
|
</div>
|
||||||
<button class="mt-4 mb-4 btn btn-info" onclick="window.location.href='/'">
|
<div class="form-check form-switch">
|
||||||
All
|
<input class="form-check-input" type="checkbox" role="switch" id="show_tm_2020">
|
||||||
</button>
|
<label class="form-check-label" for="show_tm_2020">Show All TM2020</label>
|
||||||
|
</div>
|
||||||
|
<div class="form-check form-switch">
|
||||||
|
<input class="form-check-input" type="checkbox" role="switch" id="show_tmnf">
|
||||||
|
<label class="form-check-label" for="show_tmnf">Show All TMNF</label>
|
||||||
|
</div>
|
||||||
|
<div class="form-check form-switch">
|
||||||
|
<input class="form-check-input" type="checkbox" role="switch" id="notifications_self">
|
||||||
|
<label class="form-check-label" for="notifications_self">Send notifcations</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script src="/static/user_settings.js" defer></script>
|
||||||
|
|
||||||
<table class="m-auto">
|
<table class="m-auto">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
|
|||||||
@@ -5,6 +5,41 @@ import hashlib
|
|||||||
import pygbx
|
import pygbx
|
||||||
import xmltodict
|
import xmltodict
|
||||||
|
|
||||||
|
def get_latest_season_from_maps(maps):
|
||||||
|
'''Determine the latest season in DB'''
|
||||||
|
|
||||||
|
order = ["Winter", "Spring", "Summer", "Fall"]
|
||||||
|
|
||||||
|
max_year = 0
|
||||||
|
max_season = "Winter"
|
||||||
|
|
||||||
|
if not maps:
|
||||||
|
return
|
||||||
|
|
||||||
|
for m in maps:
|
||||||
|
splitted = m.map_uid.split(" ")
|
||||||
|
|
||||||
|
# not a campain map #
|
||||||
|
if len(splitted) < 3:
|
||||||
|
return
|
||||||
|
|
||||||
|
season = splitted[0]
|
||||||
|
year = splitted[1]
|
||||||
|
|
||||||
|
# also not a campaing map #
|
||||||
|
if not season in order:
|
||||||
|
return
|
||||||
|
try:
|
||||||
|
year = int(year)
|
||||||
|
except ValueError:
|
||||||
|
return
|
||||||
|
|
||||||
|
if year > max_year or year == max_year and order.index(season) >= order.index(max_season):
|
||||||
|
max_year = year
|
||||||
|
max_season = season
|
||||||
|
|
||||||
|
return "{} {}".format(max_season, max_year)
|
||||||
|
|
||||||
class GhostWrapper():
|
class GhostWrapper():
|
||||||
|
|
||||||
def __init__(self, fullpath, uploader):
|
def __init__(self, fullpath, uploader):
|
||||||
|
|||||||
Reference in New Issue
Block a user