aocbot/aoc_bot.py

274 lines
11 KiB
Python

#!/usr/bin/env python3
import argparse
import os.path
import time
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"]
if "sasl_password" in config["IRC"]:
self.__sasl_password = config["IRC"]["sasl_password"]
else:
self.__sasl_password = None
except AttributeError as e:
print("CONFIGURATION ERROR: %s" % e)
sys.exit(1)
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):
now = datetime.now()
aoc_year = now.year if now.month == 12 else now.year - 1
try:
new_leaderboard = self.fetch_leaderboard(aoc_year)
except Exception as e:
print("Updating leaderboard failed: %s" % e)
return # didn't work this time? Well, we'll just try again in 15min ...
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):
print("[AoCBot] Loading Cache File")
self.__cache_data = JSONFile(self.__cache_file, create=True)
print("[AoCBot] Loading Session ID")
try:
self.__aoc_session_id = open(self.__aoc_session_file, "r").readlines()[0].strip()
except FileNotFoundError:
print("[AoCBot] Session ID not found: %s" % self.__aoc_session_file)
sys.exit(1)
print("[AoCBot] Starting IrcBot")
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,
sasl_password=self.__sasl_password,
)
print("[AoCBot] Joining Channel %s" % self.__irc_channel)
self.__irc_bot.join(self.__irc_channel)
print("[AoCBot] Scheduling Leaderboard Update for Group ID %s" % self.__aoc_group_id)
self.__irc_bot.schedule("update_leaderboard", timedelta(minutes=15), self.update_leaderboard)
print("[AoCBot] Registering Commands")
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)
print("[AoCBot] Starting Main Loop")
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()