From 6b0d584c21fd45e0d5b6c61e9268cef62c281543 Mon Sep 17 00:00:00 2001 From: Thom Breugelmans <> Date: Mon, 25 Nov 2024 22:34:39 +0100 Subject: [PATCH 1/2] rewrite such that AoCH leaderboard is production ready --- app.py => AoCH/__init__.py | 49 +++- Dockerfile | 15 ++ README.md | 8 +- get_json.js | 46 ---- pyproject.toml | 15 ++ requirements.txt | 27 +- static/scripts/main.js | 199 ++++++++++++++ static/stylesheets/style.css | 208 +++++++++++++++ style.css | 0 templates/challenge.html | 69 +++++ templates/index.html | 444 ++----------------------------- templates/leaderboard.html | 73 +++++ templates/leaderboard_today.html | 73 +++++ 13 files changed, 731 insertions(+), 495 deletions(-) rename app.py => AoCH/__init__.py (86%) create mode 100644 Dockerfile delete mode 100644 get_json.js create mode 100644 pyproject.toml create mode 100644 static/scripts/main.js create mode 100644 static/stylesheets/style.css delete mode 100644 style.css create mode 100644 templates/challenge.html create mode 100644 templates/leaderboard.html create mode 100644 templates/leaderboard_today.html diff --git a/app.py b/AoCH/__init__.py similarity index 86% rename from app.py rename to AoCH/__init__.py index adfe5b1..2bedbc4 100644 --- a/app.py +++ b/AoCH/__init__.py @@ -3,14 +3,15 @@ from datetime import datetime, timedelta import requests -from dotenv import load_dotenv -from flask import Flask, render_template, render_template_string +from flask import Blueprint, Flask, render_template, render_template_string from flask_cors import CORS -load_dotenv() +import logging -app = Flask(__name__) -CORS(app) + +logger = logging.getLogger('aoch') +logger.setLevel(logging.INFO) +bp = Blueprint('aoch', __name__) data = {} # the two below variables are used to keep track of when was last pulled, so we do not overload the Advent of Code servers @@ -35,6 +36,20 @@ } +def create_app(): + cur_folder = os.path.dirname(os.path.realpath(__file__)) + app = Flask(__name__, + static_url_path='', + static_folder=os.path.join(cur_folder, '../static'), + template_folder=os.path.join(cur_folder, '../templates')) + CORS(app) + + app.register_blueprint(bp) + app.add_url_rule('/', endpoint='index') + + return app + + def current_time() -> tuple[int, int, int]: """ Returns the current day, month, and year in a tuple of strings. @@ -70,10 +85,11 @@ def get_data(today: tuple[int, int, int]): if (now - last_pull) < timedelta(minutes=15): return data - app.logger.info("Pulling new data from Advent of Code!") + logger.info("Pulling new data from Advent of Code!") url = f"https://adventofcode.com/{this_year}/leaderboard/private/view/{leaderboard_id}.json" response = requests.get(url, headers=headers, timeout=5) + logger.info(f'Getting data for {this_year}, response: {response.status_code}') data = response.json() last_pull = now return data @@ -100,6 +116,7 @@ def get_day_assignment(today: tuple[int, int, int]): url = f"https://adventofcode.com/{this_year}/day/{this_day}" response = requests.get(url, headers=headers, timeout=5) + logger.info(f'Getting challenge for {this_day}-{this_month}-{this_year}, response: {response.status_code}') assignment["puzzle"] = response.text assignment["last_pulled"] = datetime(year=this_year, month=this_month, day=this_day) return response.text @@ -217,7 +234,7 @@ def return_global_data(members): return global_data -@app.route("/data") +@bp.route("/data") def return_data(): """ Endpoint for the frontend. @@ -235,7 +252,20 @@ def return_data(): return data -@app.route("/") +@bp.route("/challenge") +def return_challenge(): + return render_template("challenge.html") + +@bp.route("/leaderboard") +def return_leaderboard(): + return render_template("leaderboard.html") + +@bp.route("/leaderboard/today") +def return_leaderboard_today(): + return render_template("leaderboard_today.html") + + +@bp.route("/") def return_index(): """ Endpoint for the frontend. @@ -244,4 +274,5 @@ def return_index(): if __name__ == "__main__": - app.run(port=5000, debug=True) + app = create_app() + app.run() diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..3bef492 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,15 @@ +FROM python:3-alpine as build +RUN pip install --upgrade pip + +RUN mkdir /app +COPY requirements.txt /app +COPY pyproject.toml /app +COPY AoCH/ /app/AoCH/ +COPY static/ /app/static/ +COPY templates/ /app/templates/ + +WORKDIR /app +RUN python -m pip install -r requirements.txt + +EXPOSE 8080:8080 +CMD ["waitress-serve", "--call", "AoCH:create_app"] diff --git a/README.md b/README.md index 904d958..501e1f7 100644 --- a/README.md +++ b/README.md @@ -8,8 +8,8 @@ This project shows the current statistics and leaderboard of CH members for the This script follows the guidelines stated by [Advent of Code](https://www.reddit.com/r/adventofcode/wiki/faqs/automation), namely: - it includes the emails of the maintainers and a link to the current repo in the User-Agent header for all outbound requests; -- it throttles the requests made to the website by only requesting (private) leaderboard updates every 15 minutes (`L55: get_data()`); -- the puzzle for each day is requested only once and 'cached' runtime only, so restarting the server removes the 'cache' (`L79: get_day_assignment()`). +- it throttles the requests made to the website by only requesting (private) leaderboard updates every 15 minutes (`L68: get_data()`); +- the puzzle for each day is requested only once and 'cached' runtime only, so restarting the server removes the 'cache' (`L93: get_day_assignment()`). Do not misuse this leaderboard we created and if you decide to fork this repository, please update the User-Agent to your own email and repository. @@ -55,8 +55,8 @@ pip install -r requirements.txt ### 4. Adding environmental variables -Create a `.env` file in the project folder. Add the following two variables: `session` and `leaderboard_id`. `session` is the cookie stored by AoC if you authenticate in the browser (valid for a month) and `leaderboard_id` can be found in the url of the leaderboard you are trying to add. +Configuration of the leaderboard is stored in environment variables. Add the following two variables: `session` and `leaderboard_id`. `session` is the cookie stored by AoC if you authenticate in the browser (valid for a month) and `leaderboard_id` can be found in the url of the leaderboard you are trying to add. ### 5. Start the server -Start the server by running the `app.py` file. If the server has started, you can go to `localhost:5000` in your browser. +Start the server by running the application with `waitress`, e.g. `session= leaderboard_id= waitress-serve --call AoCH:create_app` diff --git a/get_json.js b/get_json.js deleted file mode 100644 index efca6ee..0000000 --- a/get_json.js +++ /dev/null @@ -1,46 +0,0 @@ -// Function that gets the JSON data from the server -// and returns it as a JSON object - -// function get_json() { -// var json = null; -// $.ajax({ -// url: "https://adventofcode.com/2023/leaderboard/private/view/954860.json", -// dataType: "json", -// crossDomain: true, -// setCookies: -// "session=53616c7465645f5f1c43c14b203359c399a1c7373e63dd4884a54869a787103f3b76a8df0dbcfcab49920c81cd573200b657ea0016db75fb023f35c8ed37264e", -// xhrFields: { -// withCredentials: true, -// }, -// success: function (data) { -// json = data; -// }, -// }); -// return json; -// } - -function get_json() { - let headers = new Headers(); - headers.append("Access-Control-Allow-Origin", "http://localhost:3000"); - headers.append("Access-Control-Allow-Credentials", "true"); - headers.append("Access-Control-Allow-Methods", "GET"); - headers.append("Access-Control-Allow-Headers", "Content-Type"); - headers.append( - "Cookie", - "session=53616c7465645f5f1c43c14b203359c399a1c7373e63dd4884a54869a787103f3b76a8df0dbcfcab49920c81cd573200b657ea0016db75fb023f35c8ed37264e" - ); - fetch("https://adventofcode.com/2023/leaderboard/private/view/954860.json", { - method: "GET", - headers: headers, - mode: "no-cors", - }) - .then((response) => { - console.log(response); - return response; - }) - .then((response) => response.json()) - .then((data) => { - console.log(data); - return data; - }); -} diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..5bd3564 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,15 @@ +[project] +name = "AoCH" +version = "2024.0.0" +description = "Advent of Code leaderboard for in CH" +dependencies = [ + "flask", + "requests", +] + +[build-system] +requires = ["flit_core<4"] +build-backend = "flit_core.buildapi" + +[tool.flit.sdist] +include = ["static/", "templates/"] diff --git a/requirements.txt b/requirements.txt index d4d525a..8caf7fb 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,15 +1,14 @@ -blinker==1.7.0 -certifi==2023.11.17 -charset-normalizer==3.3.2 +blinker==1.9.0 +certifi==2024.8.30 +charset-normalizer==3.4.0 click==8.1.7 -colorama==0.4.6 -Flask==3.0.0 -Flask-Cors==4.0.0 -idna==3.6 -itsdangerous==2.1.2 -Jinja2==3.1.2 -MarkupSafe==2.1.3 -python-dotenv==1.0.0 -requests==2.31.0 -urllib3==2.1.0 -Werkzeug==3.0.1 +Flask==3.1.0 +Flask-Cors==5.0.0 +idna==3.10 +itsdangerous==2.2.0 +Jinja2==3.1.4 +MarkupSafe==3.0.2 +requests==2.32.3 +urllib3==2.2.3 +waitress==3.0.2 +Werkzeug==3.1.3 diff --git a/static/scripts/main.js b/static/scripts/main.js new file mode 100644 index 0000000..c5f60de --- /dev/null +++ b/static/scripts/main.js @@ -0,0 +1,199 @@ +/// insert child into parent +/// `parent` - the HTML element to insert the child into +/// `rank` - integer indicating the rank of the person +/// `child` - the element to insert (gotten from json request to AoC) +/// `moving` - bool indicating if the children should be moving +/// `today` - boolean for if it is individual for todays leaderboard +function insertIndividual(parent, rank, child, moving, today) { + if (typeof child !== "object") return; + + let child_el = document.createElement("div"); + child_el.classList.add("individual"); + let rank_el = document.createElement("span"); + rank_el.textContent = rank.toString(); + let score = document.createElement("span"); + score.textContent = child.score.toString(); + let name = document.createElement("span"); + name.textContent = + child.name == null || child.name == undefined ? "unknown" : child.name; + + child_el.appendChild(rank_el); + child_el.appendChild(score); + child_el.appendChild(name); + if (today) { + // element gets added to today leaderboard, thus contains timestamps when it has been handed in + let star1 = document.createElement("span"); + let star2 = document.createElement("span"); + + star1.textContent = child.star1 ? child.star1 : "-"; + star2.textContent = child.star2 ? child.star2 : "-"; + + child_el.appendChild(star1); + child_el.appendChild(star2); + } else { + // the element gets added to the total leaderboard + let stars = document.createElement("span"); + let text = ""; + // child.stars is a list of 0, 1, or 2 where the number is the amount of stars gotten said day + child.stars.forEach((star, index) => { + // add all stars with their color + now = new Date(); + let day = now.getDate(); + let month = now.getMonth() + 1; + if (month != 12) { + day = 25; + } + if (index == day - 1) { + text += ' max_in_list) + insertIndividual(el, 1, children[0], true, today); + for (let i = children.length - 1; i >= 0; i--) { + let child = children[i]; + insertIndividual( + el, + i + 1, + child, + children.length > max_in_list, + today + ); + } +} + +function cycleChildren() { + let self = this; + if (self.nextElementSibling) { + self.innerHTML = self.nextElementSibling.innerHTML; + } else { + self.innerHTML = self.parentElement.firstElementChild.innerHTML; + } +} + +let step = 0; + +function move_left(el) { + el.classList.add("left"); + el.classList.remove("middle", "right"); + if (el.children[1].children.length > 0) { + for ( + let i = 0; + i < el.children[1].lastElementChild.children.length; + i++ + ) { + el.children[1].lastElementChild.children[i].classList.remove("move"); + } + } +} + +function move_middle(el) { + el.classList.add("middle"); + el.classList.remove("right", "left"); + if ( + el.children[1].children.length > 0 && + ((el.children[1].offsetHeight / screen.height) * 100) / 2.2 < + el.children[1].lastElementChild.children.length + ) { + for ( + let i = 0; + i < el.children[1].lastElementChild.children.length; + i++ + ) { + if (!el.classList.contains("nomove")) + el.children[1].lastElementChild.children[i].classList.add("move"); + } + } +} + +function move_right(el) { + el.classList.add("right"); + el.classList.remove("middle"); + if (el.children[1].children.length > 0) { + for ( + let i = 0; + i < el.children[1].lastElementChild.children.length; + i++ + ) { + el.children[1].lastElementChild.children[i].classList.remove("move"); + } + } +} + +function swap(el) { + let from = "left"; + let to = "right"; + if (el.classList.contains(to)) { + from = "right"; + to = "left"; + } + el.classList.remove("transition"); + setTimeout(() => { + el.classList.remove(from); + el.classList.add(to); + setTimeout(() => { + el.classList.add("transition"); + }, 100); + }, 100); +} + +function move(el, offset) { + let cur_step = (step + offset) % 6; + if (cur_step == 0) { + move_left(el); + } else if (cur_step == 1) { + swap(el); + } else if (cur_step == 2) { + move_middle(el); + } else if (cur_step == 3) { + move_right(el); + } else if (cur_step == 4) { + swap(el); + } else if (cur_step == 5) { + move_middle(el); + } +} + +function transitions() { + let l = document.querySelectorAll(".container > div")[2]; + let m = document.querySelectorAll(".container > div")[0]; + let r = document.querySelectorAll(".container > div")[1]; + + move(l, 4); + move(m, 0); + move(r, 2); + step = (step + 1) % 6; +} diff --git a/static/stylesheets/style.css b/static/stylesheets/style.css new file mode 100644 index 0000000..a451261 --- /dev/null +++ b/static/stylesheets/style.css @@ -0,0 +1,208 @@ +@keyframes elementMove { +0% { + transform: translateY(0px); +} + +94% { + transform: translateY(0px); +} + +100% { + transform: translateY(-105%); +} +} + +body { +margin: 0; +overflow-y: hidden; +overflow-x: hidden; +background-color: black; +color: #f8ffdd; +font-family: "Roboto", sans-serif; +font-size: 1.5vh; +} + +.container { +width: 100vw; +height: 100vh; +position: absolute; +top: 0; +left: 0; +background-image: url("/images/background.jpg"); +background-size: cover; +} + +h1 { +position: absolute; +width: 60%; +height: 4.7vh; +left: 20%; +top: 0.5vh; +margin: 0; +padding-top: 0.3vh; +padding-bottom: 0; +background-color: rgba(50, 30, 30, 0.8); +text-align: center; +font-size: 4.5vh; +} +h2 { +position: absolute; +width: 50%; +height: 3vh; +left: 25%; +top: 0; +margin: 0; +background-color: rgba(50, 30, 30, 0.8); +text-align: center; +font-size: 2.5vh; +} + +.leaderboard_today, +.leaderboard_total, +.task_today { +position: absolute; +height: 94vh; +width: 95vw; +top: 5.5vh; +} + +.background-image { +position: absolute; +height: 100%; +width: 100%; +object-fit: cover; +} + +.wrapper { +position: relative; +top: 3.3vh; +left: 0; +width: 95vw; +height: calc(100% - 5.8vh); +clip-path: inset(0px 0px 0px 0px); +} + +.wrapper > .content { +clip-path: inset(0px 0px 0px 0px); +} + +.wrapper > .header { +height: 2vh; +margin-bottom: 0.7vh; +} + +.wrapper > div { +width: 100%; +} + +.individual { +position: relative; +width: 100%; +height: 2vh; +margin-bottom: 0.1vh; +transform: translate3d(0px, 0px, 0px); +font-size: 1.5vh; +} + +.individual > * { +background-color: rgba(50, 30, 30, 0.8); +margin-left: 0.5vw; +display: inline-block; +width: 25%; +text-align: left; +overflow-x: hidden; +white-space: nowrap; +padding-left: 2px; +} + +.individual > *:first-child { +margin-left: 0; +padding-left: 0; +text-align: center; +width: 7%; +} + +.individual > *:nth-child(2) { +padding-left: 0; +text-align: center; +width: 10%; +} + +.individual > *:nth-child(3) { +width: 30%; +} + +.leaderboard_total > .wrapper > div > .individual > *:last-child, +.leaderboard_today > .wrapper > .header > .individual > *:last-child { +width: 51%; +} + +.task_today > .wrapper > .content { +background-color: rgba(0, 0, 0, 0.418); +padding: 10px; +padding-top: 2.5vh; +font-size: 1vh; +} +.task_today > .wrapper > .content > h2 { +font-size: 1.7vh; +background: none; +height: 2.2vh; +} + +.task_today > .wrapper > .content > a { +color: cornflowerblue; +} + +.header > .individual { +height: 100%; +font-size: 1.7vh; +} + +.transition { +transition: left 300ms linear; +} + +.middle { +left: 2.5vw; +} + +.left { +left: -100vw; +} + +.right { +left: 100vw; +} + +.move { +animation-name: elementMove; +animation-timing-function: linear; +animation-duration: 3s; +animation-iteration-count: infinite; +animation-direction: normal; +animation-delay: 0.5s; +} + +.star { +font-size: 1.3vh; +color: #333333; +--star-color: #111111; +} + +.star.gold { +color: #ffff66; +--star-color: #cece48; +} + +.star.silver { +color: #9999cc; +--star-color: #77779d; +} + +.star.today { +--glow-width: 5px; +text-shadow: 0 0 var(--glow-width) #fff, + 0 0 calc(var(--glow-width) * 1.5) var(--star-color), + 0 0 calc(var(--glow-width) * 2) var(--star-color), + 0 0 calc(var(--glow-width) * 2.5) var(--star-color); +} \ No newline at end of file diff --git a/style.css b/style.css deleted file mode 100644 index e69de29..0000000 diff --git a/templates/challenge.html b/templates/challenge.html new file mode 100644 index 0000000..e5e6197 --- /dev/null +++ b/templates/challenge.html @@ -0,0 +1,69 @@ + + + + + AoCH Leaderboard + + + + + +
+ +

Advent of Code

+
+

Todays Leaderboard

+
+
+
+ rankscorenamesubmitted parts +
+
+
+
+
+
+

December Leaderboard

+
+
+
+ rankscorenamestars +
+
+
+
+
+
+

Todays Puzzle

+ +
+
+
+
+
+ + + + diff --git a/templates/index.html b/templates/index.html index 6660cd3..0e92e6e 100644 --- a/templates/index.html +++ b/templates/index.html @@ -1,223 +1,21 @@ + AoCH Leaderboard - +
- +

Advent of Code

Todays Leaderboard

@@ -256,224 +54,26 @@

Todays Puzzle

+ + + diff --git a/templates/leaderboard_today.html b/templates/leaderboard_today.html new file mode 100644 index 0000000..1f6b5df --- /dev/null +++ b/templates/leaderboard_today.html @@ -0,0 +1,73 @@ + + + + + AoCH Leaderboard + + + + + +
+ +

Advent of Code

+
+

Todays Leaderboard

+
+
+
+ rankscorenamesubmitted parts +
+
+
+
+
+
+

December Leaderboard

+
+
+
+ rankscorenamestars +
+
+
+
+
+
+

Todays Puzzle

+ +
+
+
+
+
+ + + + From 14e1d1b4ddc4d58ddd7aa9441b14681a38a5ea74 Mon Sep 17 00:00:00 2001 From: Thom Breugelmans <> Date: Mon, 25 Nov 2024 22:36:49 +0100 Subject: [PATCH 2/2] Update run instructions docker --- README.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/README.md b/README.md index 501e1f7..5aedeec 100644 --- a/README.md +++ b/README.md @@ -60,3 +60,7 @@ Configuration of the leaderboard is stored in environment variables. Add the fol ### 5. Start the server Start the server by running the application with `waitress`, e.g. `session= leaderboard_id= waitress-serve --call AoCH:create_app` + +## Docker + +The project can also be run using the provided `Dockerfile` for this, simply build the docker image, `docker build -t aoch .` and run it: `docker run aoch`.