-
Notifications
You must be signed in to change notification settings - Fork 6
/
main.py
247 lines (192 loc) · 9.96 KB
/
main.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
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
import os
import time
import datetime
import json
import urllib.request
from dotenv import load_dotenv
from discord.ext import commands
load_dotenv()
TOKEN = os.getenv('DISCORD_TOKEN')
URL = os.getenv('AOC_URL')
COOKIE = os.getenv('AOC_COOKIE')
# Advent Of Code request that you don't poll their API more often than once every 15 minutes
POLL_MINS = 15
# Discord messages are limited to 2000 characters. This also includes space for 6 '`' characters for a code block
MAX_MESSAGE_LEN = 2000 - 6
PLAYER_STR_FORMAT = '{rank:2}) {name:{name_pad}} ({points:{points_pad}}) {stars:{stars_pad}}* ({star_time})\n'
CHANNEL_NAME = 'advent-of-code'
players_cache = ()
def get_players():
global players_cache
now = time.time()
debug_msg = 'Got Leaderboard From Cache'
# If the cache is more than POLL_MINS old, refresh the cache, else use the cache
if not players_cache or (now - players_cache[0]) > (60*POLL_MINS):
debug_msg = 'Got Leaderboard Fresh'
req = urllib.request.Request(URL)
req.add_header('Cookie', 'session=' + COOKIE)
page = urllib.request.urlopen(req).read()
data = json.loads(page)
# print(json.dumps(data, indent=4, sort_keys=True))
# Extract the data from the JSON, it's a mess
players = [(member['name'],
member['local_score'],
member['stars'],
int(member['last_star_ts']),
member['completion_day_level'],
member['id']) for member in data['members'].values()]
# Players that are anonymous have no name in the JSON, so give them a default name "Anon"
for i, player in enumerate(players):
if not player[0]:
anon_name = "anon #" + player[5]
players[i] = (anon_name, player[1], player[2], player[3], player[4], player[5])
# Sort the table primarily by score, secondly by stars and finally by timestamp
players.sort(key=lambda tup: tup[3])
players.sort(key=lambda tup: tup[2], reverse=True)
players.sort(key=lambda tup: tup[1], reverse=True)
players_cache = (now, players)
print(debug_msg)
return players_cache[1]
async def output_leaderboard(context, leaderboard_lst):
item_len = len(leaderboard_lst[0])
block_size = MAX_MESSAGE_LEN // item_len
tmp_leaderboard = leaderboard_lst
while (len(tmp_leaderboard) * item_len) > MAX_MESSAGE_LEN:
output_str = '```'
output_str += ''.join(tmp_leaderboard[:block_size])
output_str += '```'
await context.send(output_str)
tmp_leaderboard = tmp_leaderboard[block_size:]
output_str = '```'
output_str += ''.join(tmp_leaderboard)
output_str += '```'
await context.send(output_str)
# Create the bot and specify to only look for messages starting with '!'
bot = commands.Bot(command_prefix='!')
@bot.event
async def on_ready():
print(f'{bot.user.name} has connected to Discord and is in the following channels:')
for guild in bot.guilds:
print(' ', guild.name)
@bot.command(name='leaderboard', help='Responds with the current leaderboard')
async def leaderboard(context, num_players: int = 20):
# Only respond if used in a channel containing CHANNEL_NAME
if CHANNEL_NAME not in context.channel.name:
return
print('Leaderboard requested')
players = get_players()[:num_players]
# Get string lengths for the format string
max_name_len = len(max(players, key=lambda t: len(t[0]))[0])
max_points_len = len(str(max(players, key=lambda t: t[1])[1]))
max_stars_len = len(str(max(players, key=lambda t: t[2])[2]))
leaderboard = []
for i, player in enumerate(players):
leaderboard.append(PLAYER_STR_FORMAT.format(rank=i+1,
name=player[0], name_pad=max_name_len,
points=player[1], points_pad=max_points_len,
stars=player[2], stars_pad=max_stars_len,
star_time=time.strftime('%H:%M %d/%m', time.localtime(player[3]))))
await output_leaderboard(context, leaderboard)
@bot.command(name='rank', help='Responds with the current ranking of the supplied player')
async def rank(context, *name):
# Only respond if used in a channel containing CHANNEL_NAME
if CHANNEL_NAME not in context.channel.name:
return
# Join together all passed parameters with a space, this allows users to enter names with spaces
player_name = ' '.join(name)
print('Rank requested for: ', player_name)
players = get_players()
# Get the player with the matching name (case insensitive)
players = [(i, player) for i, player in enumerate(players) if player[0].upper() == player_name.upper()]
if players:
# Assume there was only one match
i, player = players[0]
result = '```'
result += PLAYER_STR_FORMAT.format(rank=i+1,
name=player[0], name_pad=len(player[0]),
points=player[1], points_pad=len(str(player[1])),
stars=player[2], stars_pad=len(str(player[2])),
star_time=time.strftime('%H:%M %d/%m', time.localtime(player[3])))
result += '```'
else:
result = 'Whoops, it looks like I can\'t find that player, are you sure they\'re playing?'
await context.send(result)
@bot.command(name='keen', help='Responds with today\'s keenest bean')
async def keen(context):
# Only respond if used in a channel containing CHANNEL_NAME
if CHANNEL_NAME not in context.channel.name:
return
print('Keenest bean requested')
all_players = get_players()
# Calculate the highest number of stars gained by anyone in the leaderboard
max_stars = max(all_players, key=lambda t: t[2])[2]
# Get list of players with max stars
players = [(i, player) for i, player in enumerate(all_players) if player[2] == max_stars]
# Find the first person who got the max stars
i, player = min(players, key=lambda t: t[1][3])
result = 'Today\'s keenest bean is:\n```'
result += PLAYER_STR_FORMAT.format(rank=i+1,
name=player[0], name_pad=len(player[0]),
points=player[1], points_pad=len(str(player[1])),
stars=player[2], stars_pad=len(str(player[2])),
star_time=time.strftime('%H:%M %d/%m', time.localtime(player[3])))
result += '```'
await context.send(result)
@bot.command(name='daily', help='Will give the daily leaderboard for specified day')
async def daily(context, day: str = None):
# The default day calculation cannot be in the function default value because the default
# value is evaluated when the program is started, not when the function is called
if day is None:
# The default day is whatever day's challenge has just come out
# So at 4.59AM UTC it will still show previous day's leaderboard
day = str((datetime.datetime.today() - datetime.timedelta(hours=5)).day)
# Only respond if used in a channel containing CHANNEL_NAME
if CHANNEL_NAME not in context.channel.name:
return
print("Daily leaderboard requested for day:", day)
players = get_players()
# Goes through all the players checking if they have data for that day and if they do adding to players_days
players_day = [player for player in players if day in player[4]]
# Players_day has all people who have finished one star for that day
first_star = []
second_star = []
# Adds all the players which has stars the into respective lists
for player_day in players_day:
if '1' in player_day[4][day]:
first_star.append((player_day[0], int(player_day[4][day]['1']['get_star_ts'])))
if '2' in player_day[4][day]:
second_star.append((player_day[0], int(player_day[4][day]['2']['get_star_ts'])))
# Sorts the two lists on timestamps
first_star.sort(key=lambda data: data[1])
second_star.sort(key=lambda data: data[1])
final_table = []
# Adds all the people from first list
for i, player in enumerate(first_star):
final_table.append((player[0], (len(players) - i), player[1], 1))
# Updates the list with all the people who got the second star and their score
for i, player in enumerate(second_star):
index = [i for i, item in enumerate(final_table) if item[0] == player[0]][0]
to_change = final_table[index]
final_table[index] = (to_change[0], (to_change[1] + (len(players) - i)), player[1], 2)
# Sorts the table primarily by score, and secondly by timestamp
final_table.sort(key=lambda data: data[2])
final_table.sort(reverse=True, key=lambda data: data[1])
# Outputs data
if not final_table:
result = "```No Scores for this day yet```"
await context.send(result)
else:
# Get string lengths for the format string
max_name_len = len(max(final_table, key=lambda t: len(t[0]))[0])
max_points_len = len(str(max(final_table, key=lambda t: t[1])[1]))
max_stars_len = len(str(max(final_table, key=lambda t: t[3])[3]))
leaderboard = []
for place, player in enumerate(final_table):
leaderboard.append(PLAYER_STR_FORMAT.format(rank=place+1,
name=player[0], name_pad=max_name_len,
points=player[1], points_pad=max_points_len,
stars=player[3], stars_pad=max_stars_len,
star_time=time.strftime('%H:%M %d/%m',
time.localtime(player[2]))))
await output_leaderboard(context, leaderboard)
bot.run(TOKEN)