#!/usr/bin/env python3 import argparse import json import os.path import requests import sys from datetime import datetime, timedelta from time import sleep from tools.datafiles import JSONFile from tools.irc import IrcBot, ServerMessage from tools.tools import human_readable_time_from_delta class AOCBot: def __init__(self, config_file: str = "config.json"): self.__irc_server = None self.__irc_port = None self.__irc_channel = None self.__irc_nick = None self.__irc_user = None self.__irc_real_name = None self.__aoc_group_id = None self.__aoc_session_file = None self.__aoc_session_id = None self.__aoc_user_agent = None self.__cache_file = None self.__cache_data = None self.__irc_bot = None self._load_config(config_file) def _load_config(self, config_file: str): config = JSONFile(config_file, False) try: self.__irc_server = config["IRC"]["server"] self.__irc_port = config["IRC"]["port"] self.__irc_channel = config["IRC"]["channel"] self.__irc_nick = config["IRC"]["nick"] self.__irc_user = config["IRC"]["user"] self.__irc_real_name = config["IRC"]["name"] self.__aoc_group_id = config["AOC"]["group_id"] self.__aoc_session_file = config["AOC"]["session_file"] self.__aoc_user_agent = config["AOC"]["user_agent"] self.__cache_file = config["AOC"]["cache_file"] except AttributeError as e: print("CONFIGURATION ERROR: %s" % e) def fetch_leaderboard(self, year: int = datetime.now().year) -> dict: return requests.get( "https://adventofcode.com/%d/leaderboard/private/view/%s.json" % (year, self.__aoc_group_id), headers={"User-Agent": self.__aoc_user_agent}, cookies={"session": self.__aoc_session_id}, ).json() def command_info(self, msg_from: str, message: str): self.__irc_bot.privmsg( msg_from, "I am %s => %s" % (self.__irc_bot.getUser().nickname, self.__irc_bot.getUser().username), ) self.__irc_bot.privmsg(msg_from, "I am currently in the following channels:") for c in self.__irc_bot.getChannelList(): self.__irc_bot.privmsg(msg_from, "%s => %s" % (c.name, c.topic)) def command_today(self, msg_from: str, message: str): if not message: day = str(datetime.now().day) else: day = message.split(" ")[0] try: if not (1 <= int(day) <= 25): self.__irc_bot.privmsg(self.__irc_channel, "Invalid day: " + day) return except ValueError: self.__irc_bot.privmsg(self.__irc_channel, "Invalid day: " + day) return day_start = datetime.today().replace(hour=6, minute=0, second=0, day=int(day)) today_list = [] for member, member_data in self.__cache_data.items(): if ( not member.startswith("__") and "days" in member_data and day in member_data["days"] ): today_list.append(member) self.__irc_bot.privmsg( self.__irc_channel, "Day %s's leaderboard (last updated: %s):" % (day, self.__cache_data["__last_update__"]), ) for i, member in enumerate( sorted( today_list, key=lambda x: self.__cache_data[x]["days"][day]["score"], reverse=True, ) ): if i > 3: sleep(1) # don't flood if "1" in self.__cache_data[member]["days"][day]: p1_time = "in " + human_readable_time_from_delta( datetime.fromisoformat(self.__cache_data[member]["days"][day]["1"]) - day_start ) else: p1_time = "*not yet solved*" if "2" in self.__cache_data[member]["days"][day]: p2_time = "in " + human_readable_time_from_delta( datetime.fromisoformat(self.__cache_data[member]["days"][day]["2"]) - day_start ) else: p2_time = "not yet solved" self.__irc_bot.privmsg( self.__irc_channel, "%d) %s (Scores: total: %d, today: %d) p1 %s, p2 %s" % ( i + 1, self.__cache_data[member]["name"], self.__cache_data[member]["score"], self.__cache_data[member]["days"][day]["score"], p1_time, p2_time, ), ) def command_quit(self, msg_from: str, message: str): if msg_from.startswith("stha!") or msg_from.startswith("Pennywise!"): self.__irc_bot.privmsg(msg_from, "Oh, ok ... bye :'(") self.__irc_bot.quit() sys.exit(0) def on_raw(self, msg_from: str, msg_type: str, msg_to: str, message: str): print( "[%s] <%s> (%s) -> <%s>: %s" % (datetime.now().strftime("%H:%M:%S"), msg_from, msg_type, msg_to, message) ) def calc_scores(self): member_count = len( [x for x in self.__cache_data.keys() if not x.startswith("__")] ) for day in map(str, range(1, 26)): p1_times = [] p2_times = [] for member, member_data in self.__cache_data.items(): if member.startswith("__") or day not in member_data["days"]: continue self.__cache_data[member]["days"][day]["score"] = 0 if "1" in member_data["days"][day]: p1_times.append(member_data["days"][day]["1"]) if "2" in member_data["days"][day]: p2_times.append(member_data["days"][day]["2"]) for member, member_data in self.__cache_data.items(): if member.startswith("__") or day not in member_data["days"]: continue if ( "1" in member_data["days"][day] and member_data["days"][day]["1"] in p1_times ): score = member_count - sorted(p1_times).index( member_data["days"][day]["1"] ) self.__cache_data[member]["days"][day]["score"] += score if ( "2" in member_data["days"][day] and member_data["days"][day]["2"] in p2_times ): score = member_count - sorted(p2_times).index( member_data["days"][day]["2"] ) self.__cache_data[member]["days"][day]["score"] += score def update_leaderboard(self): try: new_leaderboard = self.fetch_leaderboard() except Exception as e: print("Updating leaderboard failed: %s" % e) return # didn't work this time? Well, we'll just try again in 15min ... now = datetime.now() aoc_year = now.year if now.month == 12 else now.year - 1 new_stars = {} for member, member_data in new_leaderboard["members"].items(): if member not in self.__cache_data: self.__cache_data[member] = { "name": member_data["name"], "days": {}, } self.__cache_data[member]["global_score"] = int(member_data["global_score"]) self.__cache_data[member]["score"] = int(member_data["local_score"]) self.__cache_data[member]["stars"] = int(member_data["stars"]) for day in member_data["completion_day_level"]: day_start = datetime(aoc_year, 12, int(day), 6, 0, 0) if day not in self.__cache_data[member]["days"]: self.__cache_data[member]["days"][day] = {} for part in member_data["completion_day_level"][day]: if part not in self.__cache_data[member]["days"][day]: completion_time = datetime.fromtimestamp( member_data["completion_day_level"][day][part][ "get_star_ts" ] ) if member_data["name"] not in new_stars: new_stars[member_data["name"]] = {} finishing_time = human_readable_time_from_delta( completion_time - day_start ) new_stars[member_data["name"]][ "d" + day + "p" + part ] = finishing_time self.__cache_data[member]["days"][day][ part ] = completion_time.isoformat() if len(new_stars) > 0: self.__irc_bot.privmsg(self.__irc_channel, "New Stars found:") for member, parts in new_stars.items(): line = member + ": " line += ", ".join( "%s (%s)" % (part, new_stars[member][part]) for part in sorted(new_stars[member].keys()) ) self.__irc_bot.privmsg(self.__irc_channel, line) self.__cache_data["__last_update__"] = datetime.now().isoformat() self.__cache_data.save() self.calc_scores() def start(self): self.__cache_data = JSONFile(self.__cache_file, create=True) self.__aoc_session_id = ( open(self.__aoc_session_file, "r").readlines()[0].strip() ) self.__irc_bot = IrcBot( server=self.__irc_server, port=self.__irc_port, nick=self.__irc_nick, username=self.__irc_user, realname=self.__irc_real_name, ) self.__irc_bot.join(self.__irc_channel) self.__irc_bot.schedule( "update_leaderboard", timedelta(minutes=15), self.update_leaderboard ) self.__irc_bot.on(ServerMessage.RAW, self.on_raw) self.__irc_bot.register_channel_command( "!info", self.__irc_channel, self.command_info ) self.__irc_bot.register_channel_command( "!today", self.__irc_channel, self.command_today ) self.__irc_bot.register_channel_command( "!day", self.__irc_channel, self.command_today ) self.__irc_bot.register_privmsg_command("info", self.command_info) self.__irc_bot.register_privmsg_command("quit", self.command_quit) self.__irc_bot.run() if __name__ == "__main__": parser = argparse.ArgumentParser() parser.add_argument( "-c", "--config-file", help="config file to use (default: config.json)", default="config.json", required=False, ) args = parser.parse_args() config_file = args.config_file if not os.path.exists(config_file): print("Config File not found: %s" % config_file) sys.exit(1) aoc_bot = AOCBot(config_file) aoc_bot.start()