diff --git a/Makefile b/Makefile index 5c9441e747d13f01ad7290d05797d5284343a1f7..4f16d4d49a538c211d248bb3f30ff05cb71b0452 100644 --- a/Makefile +++ b/Makefile @@ -10,6 +10,10 @@ devserver: init-db routes routes: flask --app aocp routes +.PHONY: test-verifier +test-verifier: + flask --app aocp --debug test-verifier + .PHONY: init-db init-db: flask --app aocp init-db @@ -19,3 +23,8 @@ init-db: reset-db: $(RM) instance/aocp.sqlite flask --app aocp init-db + + +.PHONY: test-aoc-py +test-aoc-py: + flask --app aocp --debug test-aoc-py \ No newline at end of file diff --git a/aocp/__init__.py b/aocp/__init__.py index 9ce84a4b16b8f080f579c2eb69f11b2b2666949c..195e0ddf3c5c5f9d5dd2f5d5ea8471af8ef0426b 100644 --- a/aocp/__init__.py +++ b/aocp/__init__.py @@ -1,5 +1,7 @@ from . import db from . import auth +from . import verifier +from . import aoc import os import json @@ -92,4 +94,10 @@ def create_app(test_config=None): auth.init_app(app) app.register_blueprint(auth.bp) + # Initialize aoc API + aoc.init_app(app) + + # Initialize verifier + verifier.init_app(app) + return app diff --git a/aocp/aoc.py b/aocp/aoc.py new file mode 100755 index 0000000000000000000000000000000000000000..6689b52af24d5e1c10fd39f6baacaf77e438a4f9 --- /dev/null +++ b/aocp/aoc.py @@ -0,0 +1,105 @@ +from typing import Literal +import dateparser +from datetime import date, datetime +from enum import Enum +from flask import current_app +import requests +from flask import app +import re +import click +import requests + +current_year = current_app.config["YEAR"] + +def get_timeout(resp:str)->int: + """ + extracts a timeout from the response text. + returns the timeout in sec or 0 if parsing wasnt successful. + Example: get_timeout("please wait 20m") would return 1200. + """ + current_time = datetime.now() + timeout = dateparser.parse(" ".join(re.findall(r'\d+s|\d+m|\d+h',resp))) + if timeout: + return (current_time - timeout).seconds + else: + return 0 + + +class Status(Enum): + """ + possible status after sending a request to aoc website. + gets returned by submit_answer + """ + PASS = 0 + FAIL = 1 + RATE_LIMIT = 2 + COMPLETED = 3 + NOT_LOGGED_IN = 4 + UNKNOWN = 5 + + + +def submit_answer(year:int, day:int, level:int, answer:str)->tuple[Status,int|None]: + """ + submits an answer to the aoc website for a given year and day. + returns a Status according to the result and, if there is a timeout, the timeout in seconds. + """ + payload = {'level': level, 'answer': answer} + r = requests.post( + f'https://adventofcode.com/{year}/day/{day}/answer', + data=payload, + headers={ + "User-Agent": 'https://aoc.fg.informatik.uni-goettingen.de/ by fachgruppe@informatik.uni-goettingen.de', + }, + cookies={'session': current_app.config["SESSION_COOKIE"]} + ) + response = r.text + if "That's the right answer" in response: + #click.echo("CORRECT ANSWER") + + return Status.PASS, None + elif "That's not the right answer" in response: + #click.echo("WROOOOONG") + + return Status.FAIL, None + elif 'You gave an answer too recently' in response: + timeout = get_timeout(response) + #click.echo("RATE LIMIT: "+str(timeout)) + + return Status.RATE_LIMIT, timeout + + elif 'Did you already complete it?' in response: + #click.echo("ALREADY COMPLETED") + + return Status.COMPLETED, None + + elif '[Log In]' in response: + #click.echo("NOT LOGGED IN") + + return Status.NOT_LOGGED_IN, None + + else: + #click.echo("HMMM- SUS") + return Status.UNKNOWN, response + +def get_puzzle(year:int, day:int)->bytes: + """ + returns the puzzle of a day as bytes. + """ + puzzle = requests.get("https://adventofcode.com/"+str(year)+"/day/"+str(day)+"/input",allow_redirects=True, headers={ + "User-Agent": 'https://aoc.fg.informatik.uni-goettingen.de/ by fachgruppe@informatik.uni-goettingen.de', + },cookies={'session': current_app.config["SESSION_COOKIE"]}).content + return puzzle + + +@click.command('test-aoc-py') +def test_aoc_py_command(): + """testing commands""" + current_app.logger.info("testing get_puzzle") + current_app.logger.debug(get_puzzle(20)) + +def init_app(app): + """ + Gets called by __init__.py on initialization of the flask App. + """ + app.cli.add_command(test_aoc_py_command) diff --git a/aocp/score.py b/aocp/score.py new file mode 100644 index 0000000000000000000000000000000000000000..63b55b1db5d33e2803c893b482735008f25bf84b --- /dev/null +++ b/aocp/score.py @@ -0,0 +1,48 @@ +from datetime import datetime +from zoneinfo import ZoneInfo + +# timedelta is hour after conversion! +# return 0 points if not current day + +def calculate_points(day:int, timedelta:int, is_first:bool)->int: + """ + Calculates the points based on day, time and if the solver was first. + + + Parameters: + day -- Day of the solution. + timedelta -- Hour of the solution. + is_first -- Boolean (True if person was first to solve problem). + + Returns: + score -- the score + """ + # get "base points" depending on day + base_points = 1+ day // 5 # base points increase by 1 every 5 days. + # bonus time points: (all ranges inclusive) + # 0-8: 2 points + # 8-16: 1 point + # 16-24: 0 points + bonus_time_points = (24-timedelta) // 8 + if is_first: # bonus point if person is first to solve on given day. + bonus_first = 1 + else: + bonus_first = 0 + score = base_points + bonus_time_points + bonus_first + return score + +def is_first_solve(sub_id:tuple[int,int,int],day:int): + # incomplete function + first = get_first(day) + return sub_id == first + +def get_datetime_info(timestamp:datetime): + conv_time = timestamp.astimezone(ZoneInfo("EST")) # convert to EST time + day = conv_time.day + hour = conv_time.hour + return day,hour + +def get_first(day): + # missing function + pass + # query all "CORRECT"s of <DAY> and return the SUBMISSION ID first one diff --git a/aocp/verifier.py b/aocp/verifier.py new file mode 100644 index 0000000000000000000000000000000000000000..ba4fcf3de8370a351e1a0797fb5e957013a8e071 --- /dev/null +++ b/aocp/verifier.py @@ -0,0 +1,69 @@ +import dateparser +from datetime import date +from flask import current_app +from flask import app +import click +from . import db as database +import time +from . import aoc +from typing import cast + + +# p consists of: id, timestamp, answer, status, +def verify(): + """ + fetches all currently pending submissions from the database and checks + if theyre correct. Checks if a solution already exists in the solution db + and uses it; makes a request to the aoc website otherwise. + if the submitted answer is correct the status of the submission in the db + gets updated accordingly. + if a ratelimit is returned this program will sleep for the remaining time. + """ + db = database.get_db() # dataframe + pending = fetch_all_pending() # returns a dataframe + for index, p in pending.iterrows(): + (uid,day,val) = cast(tuple,index) + year = current_app.config["YEAR"] + solution = db.fetch_day_solution(day) # is equal to todays solution, if there is one. Is None otherwise. + if solution: + if solution == val: + db.update_submission_status(userid=uid,day=day,value=val,status="CORRECT") + else: + db.update_submission_status(userid=uid,day=day,value=val,status="INCORRECT") + else: + status, timeout = aoc.submit_answer(year, day, 1, val) + if status == aoc.Status.PASS: + db.update_submission_status(userid=uid,day=day,value=val,status="CORRECT") + db.set_day_solution(day,val) + elif status == aoc.Status.FAIL: + db.update_submission_status(userid=uid,day=day,value=val,status="INCORRECT") + elif status == aoc.Status.RATE_LIMIT: + if timeout: + time.sleep(timeout) + elif status == aoc.Status.NOT_LOGGED_IN: + current_app.logger.error("no/invalid aoc session cookie.") + elif status == aoc.Status.COMPLETED: + current_app.logger.warning("request made although puzzle already solved. not good.") + elif status == aoc.Status.UNKNOWN: + current_app.logger.warning("unknown status returned.") + + +def fetch_all_pending(): + return database.get_db().fetch_submissions(status="PENDING").sample(frac=1) + # gets all database entries with the status "pending" as a dataframe + +@click.command('test-verifier') +def test_verifier_command(): + """Execute the verifier once for testing.""" + click.echo('Testing verifier...') + #submit_answer(2022,1,1,"30") + current_app.logger.debug(database.get_db().set_day_puzzle(day=3,puzzle=b'203nsk20')) + current_app.logger.debug(database.get_db().set_day_puzzle(day=7,puzzle=b'2138')) + current_app.logger.debug(database.get_db().set_day_puzzle(day=20,puzzle=b'abcabca')) + current_app.logger.debug(database.get_db().set_day_puzzle(day=1,puzzle=b'aksjndaks')) + current_app.logger.debug(database.get_db().set_day_solution(3,solution="123")) + current_app.logger.debug(database.get_db().set_day_solution(7,solution="13")) + current_app.logger.debug(database.get_db().set_day_solution(20,solution="abcabca")) + current_app.logger.debug(verify()) +def init_app(app): + app.cli.add_command(test_verifier_command) diff --git a/requirements.txt b/requirements.txt index de3815dd61c4eec6123f8444c314cf6238c7cd9e..74149a9ea74687ee0c39463eeb2680bf9ae30b09 100644 --- a/requirements.txt +++ b/requirements.txt @@ -7,6 +7,7 @@ Jinja2>=3.1.2 MarkupSafe>=2.1.3 packaging>=23.2 Werkzeug>=3.0.0 +dateparser>=1.1.8 requests>=2.31.0 authlib>=1.2.1 pandas>=2.1.2