forked from ioistired/AoC-Bot
-
Notifications
You must be signed in to change notification settings - Fork 1
/
aoc.py
144 lines (117 loc) · 4.46 KB
/
aoc.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
# Copyright © 2018–2019 Io Mintz <[email protected]>
#
# AoC Bot is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published
# by the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# AoC Bot is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with AoC Bot. If not, see <https://www.gnu.org/licenses/>.
import asyncio
import collections
import datetime as dt
import io
import json
import logging
import operator
import os
import sys
import time
import typing
from pathlib import Path
from yarl import URL
RATE_LIMIT = 15 * 60
logger = logging.getLogger(__name__)
def score_leaderboard(leaderboard: dict) -> typing.Dict[int, typing.List[dict]]:
scores = collections.defaultdict(list)
for member in leaderboard['members'].values():
partial = partial_member(member)
scores[member['stars']].append(member)
return sorted_dict(scores, reverse=True)
def owner(leaderboard: dict) -> dict:
return partial_member(leaderboard['members'][leaderboard['owner_id']])
def partial_member(member: dict) -> dict:
return {k: member[k] for k in ('id', 'name')}
def sorted_dict(d: dict, *, key=None, reverse=False) -> dict:
return {k: d[k] for k in sorted(d, key=key, reverse=reverse)}
def format_leaderboard(leaderboard):
out = io.StringIO()
scores = score_leaderboard(leaderboard)
year = leaderboard['event']
out.write('[tlmn00bs ')
#out.write(owner(leaderboard)['name'])
#out.write("'s ")
out.write(str(year))
out.write(' leaderboard](')
out.write('https://adventofcode.com/')
out.write(year)
out.write('/leaderboard/private/view/')
out.write(leaderboard['owner_id'])
out.write('?order=stars)\n')
for score, members in scores.items():
sorted_members = sorted(members, key=lambda member: int(member['id']))
out.write('**')
out.write(str(score))
out.write('** ⭐ ')
out.write(', '.join(map(operator.itemgetter('name'), sorted_members)))
out.write('\n')
return out.getvalue()
async def leaderboard(client, event=None):
"""Fetch the latest leaderboard from the web if and only if it has not been fetched recently.
Otherwise retrieve from disk.
"""
now = time.time()
event = event or most_recent_event()
try:
last_modified = os.stat(f'leaderboards/{event}.json').st_mtime
except FileNotFoundError:
return await refresh_saved_leaderboard(client, event)
if now - last_modified > RATE_LIMIT:
return await refresh_saved_leaderboard(client, event)
# we've fetched it recently
return load_leaderboard(event)
async def refresh_saved_leaderboard(client, event=None):
"""save the latest leaderboard to disk"""
leaderboard = await fetch_leaderboard(client, event)
save_leaderboard(leaderboard)
return leaderboard
def save_leaderboard(leaderboard):
with open(Path('leaderboards') / (leaderboard['event'] + '.json'), 'w') as f:
json.dump(leaderboard, f, indent=4, ensure_ascii=False)
f.write('\n')
def load_leaderboard(event):
with open(Path('leaderboards') / (event + '.json')) as f:
return json.load(f)
def validate_headers(resp):
if resp.status == 302:
url = URL(resp.headers['Location'])
if url.parts[-2:] == ('leaderboard', 'private'):
raise RuntimeError('You are not a member of the configured leaderboard.')
if url.parts[-1] == 'leaderboard':
raise RuntimeError('An improper session cookie has been configured.')
elif resp.status != 200:
resp.raise_for_status()
async def login(client):
async with client.http.head(leaderboard_url(client), allow_redirects=False) as resp:
validate_headers(resp)
async def fetch_leaderboard(client, event=None):
logger.debug('Fetching {event or "most recent"} leaderboard over HTTP')
async with client.http.get(
leaderboard_url(client, event),
allow_redirects=False, # redirects are used as error codes
) as resp:
validate_headers(resp)
return await resp.json()
def most_recent_event():
now = dt.datetime.utcnow() - dt.timedelta(hours=5) # ehh, who cares about DST
return str(now.year if now.month == 12 else now.year - 1)
def leaderboard_url(client, event=None):
event = event or most_recent_event()
return f'https://adventofcode.com/{event}/leaderboard/private/view/{client.config["aoc_leaderboard_id"]}.json'
if __name__ == '__main__':
main()