-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathminiBot.py
More file actions
672 lines (581 loc) · 25 KB
/
miniBot.py
File metadata and controls
672 lines (581 loc) · 25 KB
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
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
import discord
from datetime import datetime, timedelta, date
import re
import random
import asyncio
import emoji
test = False
real_guild_id = 1177377997993549824
test_guild_id = 1213896278614745158
if test:
guild_id = test_guild_id
token_file_name = 'files/test_token'
else:
guild_id = real_guild_id
token_file_name = 'files/token'
"""
result class
date: result date
time: time, in seconds that it took to complete the puzzle
"""
class Result:
def __init__(self, date: datetime.date, time: int):
self.date = date
self.time = time
def __str__(self):
return f'{self.date}:{self.time}'
"""
preference types:
no_congrats
no_rekkening
no_leaderboard
goofy_ratio
"""
class Preference:
def __init__(self, type: str, value: int):
self.type = type
self.value = value
def __str__(self):
return f'{self.type}:{self.value}'
"""
Command class.
type: 'user' | 'admin'
content: just the command (no 'minibot' or 'mb')
user: the discord User executing the command.
message: discord Message in which the command was sent
"""
class Command:
def __init__(self, type: str, content: str, user: discord.User, message: discord.Message):
self.type = type
self.content = content
self.user = user
self.message = message
"""
user class for MiniBot
name: username
id: discord API User object
results: list of type Result
preferences: list of type Preference
times_placed: list of number of times placed 1st, 2nd, etc. index 0 will be empty so index 1 is 1st, etc.
"""
class MbUser:
def __init__(self, id: discord.User):
self.name = id.name
self.id = id
self.results = []
self.preferences = []
self.times_placed = []
async def add(self, res: Result, chn: discord.TextChannel):
self.results.append(res)
self.results = [r for r in self.results if r.date >= date.today()]# remove any results that are from earlier than today
if res.date > date.today():# if it's tomorrow's puzzle
await chn.send(f'Great job on tomorrow\'s puzzle, {self.id.display_name}!')
await self.congratulate(chn)
def get_preference(self, pref_type: str) -> Preference:
for p in self.preferences:
if p.type == pref_type:
return p
return None
def clear_preference(self, pref_type: str):
self.preferences = [pref for pref in self.preferences if pref.type != pref_type]
def set_preference(self, preference: Preference):
if preference.type.startswith('yes'):
self.clear_preference('no' + preference.type[3:])
return
if preference.type == 'goofy_ratio':
if preference.value > 10:
preference.value = 10
if preference.value < 0:
preference.value = 0
if preference.value == 0:
self.clear_preference('goofy_ratio')
return
# if they already have the preference, remove it
self.clear_preference(preference.type)
self.preferences.append(preference)
def get_preferences_string(self):
string = self.name
for preference in self.preferences:
string += ' ' + str(preference)
return string
async def congratulate(self, chnl: discord.TextChannel):
if self.get_preference('no_congrats'):
return
file_name = 'files/congratsMessages'
# if the player has specified a ratio for how often they want to see goofy congratulation messages
if self.get_preference('goofy_ratio'):
# if they roll more than their ratio, they get a normal message.
# if they roll less than or equal to their ratio, they get a goofy one. IDK, this made sense when I wrote it
if random.randrange(1,11) <= self.get_preference('goofy_ratio').value:
file_name = 'files/goofyCongratsMessages'
file = open(file_name, 'r')
congrats_messages = file.readlines()
file.close()
# send a random congratulation message from the appropriate list to the appropriate channel
message = congrats_messages[random.randrange(0,len(congrats_messages))]
message = message.replace('uname', self.id.display_name)
await chnl.send(message)
def place(self, pl):
# if the list isn't long enough to accept the place given, extend it the proper length
if pl >= len(self.times_placed):
self.times_placed += [0] * (pl - len(self.times_placed) +1)
self.times_placed[pl] += 1
def get_todays_result(self) -> Result:
for r in self.results:
if r.date == date.today():
return r
return None
def get_results_string(self):
string = self.name
for r in self.results:
string += ' ' + str(r)
return string
"""
placing class for leaderboard
user: MbUser
result: Result
place: place given in leaderboard
"""
class Placing:
def __init__(self, user: MbUser, result: Result):
self.user = user
self.result = result
self.place = -1
"""
poorly written class for permissions.
permissions is a list of strings
each string consists of a permission name, followed by the users that have that permission
"""
class Permissions:
def __init__(self,fname: str):
self.permissions = []
file = open(fname, 'r')
self.permissions = file.readlines()
file.close()
# accepts a string, str, which is the permission we're asking for
# returns a list of strings, which are the usernames of users with that permission, or None
def get(self, str):
for i in self.permissions:
if i.startswith(str):
return i[len(str):].split()
return None
def has_permission(self, name, permission: str) -> bool:
return name in self.get(permission)
"""
bot class.
users: list of type MbUser
client: discord Client object
guild_id: id of guild: int
"""
class MiniBot:
def __init__(self, client: discord.Client, guild_id: int):
self.users = [] # list of type MbUser
self.per = Permissions('files/permissions')
self.client = client
self.guild_id = guild_id
def is_user(self, userID: discord.User):
for u in self.users:
if u.id == userID:
return True
return False
def get_mb_user(self, userID: discord.User) -> MbUser:
for u in self.users:
if u.id == userID:
return u
return None
# add a Result to the user that submitted it
async def add(self, res: Result, mes: discord.Message):
# if the submitter isn't already in the system
userID = mes.author
if not self.is_user(userID):
self.users.append(MbUser(userID))
# add the results to the user's results
await self.get_mb_user(userID).add(res, mes.channel)
self.write_results()
# return Result object, if the message is a mini Result
def check_result(self, message_content: str) -> Result:
# regular expressions to match the patterns
url_pattern = r'https://www.nytimes.com/badges/games/mini.html\?d=(\d{4}-\d{2}-\d{2})&t=(\d+)'
app_pattern = r'I solved the (\d{1,2}/\d{1,2}/\d{4}) New York Times Mini Crossword in (\d{1,2}):(\d{2})'
# search for the patterns
url_match = re.search(url_pattern, message_content)
app_match = re.search(app_pattern, message_content)
if url_match:
date_str = url_match.group(1)
date_format = '%Y-%m-%d'
time = int(url_match.group(2))
elif app_match:
date_str = app_match.group(1)
date_format = '%m/%d/%Y'
minutes = int(app_match.group(2))
seconds = int(app_match.group(3))
time = minutes * 60 + seconds # convert minutes and seconds to seconds
else:
return None
# convert the date string to a datetime.date object
date = datetime.strptime(date_str, date_format).date()
# create a Result object and return it
return Result(date,time)
def read_preferences(self):
guild = self.client.get_guild(self.guild_id)
if not guild:
print('not guild')
return 1
file = open('files/preferences', 'r')
file_contents = file.readlines()
file.close()
for line in file_contents:
# if the line is a comment, do nothing
if line.startswith('#'):
continue
words = line.split()
# if it's an empty line, do nothing
if len(words) == 0:
continue
name = words[0]
preferences = words[1:]
# get the discord user from their name
duser = discord.utils.get(guild.members, name=name)
# if we can't find the discord user
if not duser:
print('no user found for ' + name)
continue
# add them to the system if they're not already in it
my_user = self.get_mb_user(duser)
if not my_user:
my_user = MbUser(duser)
self.users.append(my_user)
for preference in preferences:
pref_tokens = preference.split(':')
my_user.set_preference(Preference(pref_tokens[0], int(pref_tokens[1])))
def read_results(self):
guild = self.client.get_guild(self.guild_id)
if not guild:
print('not guild')
return 1
file = open('files/results', 'r')
file_contents = file.readlines()
file.close()
for line in file_contents:
# if the line is a comment, do nothing
if line.startswith('#'):
continue
words = line.split()
# if it's an empty line, do nothing
if len(words) == 0:
continue
name = words[0]
results = words[1:]
# get the discord user from their name
duser = discord.utils.get(guild.members, name=name)
# if we can't find the discord user
if not duser:
print('no user found for ' + name)
continue
# add them to the system if they're not already in it
my_user = self.get_mb_user(duser)
if not my_user:
my_user = MbUser(duser)
self.users.append(my_user)
for result in results:
res_tokens = result.split(':')
date_format = '%Y-%m-%d'
date = datetime.strptime(res_tokens[0], date_format).date()
my_user.results.append(Result(date, int(res_tokens[1])))
def write_preferences(self):
file = open('files/preferences', 'r')
file_contents = file.readlines()
file.close()
# get rid of everything except the comments section
file_contents = [line for line in file_contents if line.startswith('#')]
for user in self.users:
string = user.get_preferences_string() + '\n'
# if they actually have preferences
if len(string.split()) > 1:
file_contents.append(string)
file = open('files/preferences', 'w')
file.writelines(file_contents)
file.close()
def write_results(self):
file = open('files/results', 'r')
file_contents = file.readlines()
file.close()
# get rid of everything except the comments section
file_contents = [line for line in file_contents if line.startswith('#')]
for user in self.users:
string = user.get_results_string() + '\n'
# if they actually have results stored, add it to the string to write to the file
if len(string.split()) > 1:
file_contents.append(string)
file = open('files/results', 'w')
file.writelines(file_contents)
file.close()
def read_in_info(self):
self.read_preferences()
self.read_results()
# at this point, command.content doesn't contain 'minibot' or anything like that
# this function is so long and so bad, holy shit.
async def run_command(self, command: Command, channel: discord.TextChannel):
setting_preference = False
args = command.content.split()
if len(args) == 0:
return
command_content_original = command.content
command.content = command.content.lower()
command_zero = args[0]
# ------------user commands-----------------------
user_commands = ['no_congrats', 'yes_congrats', 'no_rekkening', 'yes_rekkening', 'no_leaderboard', 'yes_leaderboard', 'goofy_ratio', 'help', 'h', 'my_preferences', 'mp', 'my_times', 'mt']
if command.type == 'user':
user = self.get_mb_user(command.user)
if not user:
self.users.append(MbUser(command.user))
if command_zero not in user_commands:
# just assume that nobody was trying to use a command, and is just talking about/to MiniBot
return
if command_zero == 'my_times' or command_zero == 'mt':
await channel.send(user.get_results_string())
return
if command_zero == 'goofy_ratio' and len(args) > 1:
setting_preference = True
self.get_mb_user(command.user).set_preference(Preference('goofy_ratio', int(args[1])))
elif command_zero == 'help' or command_zero == 'h':
file = open('files/helpMessage', 'r')
if not file:
return
help_message = file.read()
file.close()
await channel.send(help_message)
return
elif command_zero == 'my_preferences' or command_zero == 'mp':
preferences_string = self.get_mb_user(command.user).get_preferences_string()
await channel.send(preferences_string)
else: # it's 'no_congrats', 'yes_congrats', 'no_rekkening', 'yes_rekkening', 'no_leaderboard', 'yes_leaderboard'
setting_preference = True
self.get_mb_user(command.user).set_preference(Preference(args[0], 1))
responses = ['Okay', 'Awesome', 'Sweet', 'Cool', 'Gotcha']
response = responses[random.randrange(0,len(responses))]
if setting_preference:
await channel.send(f'{response}, {command.user.display_name}, preference set.')
self.write_preferences()
return
# ---------------admin commands--------------------------
else:# it's an admin command
admin_commands = ['say', 'leaderboard', 'lb', 'help', 'h', 'react', 'reply', 'read_info', 'ri', 'add_time', 'at', 'su']
if not self.per.has_permission(command.user.name, 'admin'):
await channel.send('Permission denied.')
return
if command_zero == 'add_time' or command_zero == 'at':
if len(args) < 4:
await command.message.reply('usage: add_time person day time')
return
duser = discord.utils.get(command.message.guild.members, name=args[1])
if not duser:
await command.message.reply('usage: add_time person day time')
return
my_user = self.get_mb_user(duser)
if not my_user:
self.users.append(MbUser(duser))
if args[2].lower() == 'today':
day = date.today()
elif args[2].lower() == 'tomorrow':
day = date.today() + timedelta(1)
else:
await command.message.reply('usage: add_time person day time')
return
time = -1
try:
if ":" in args[3]:
minutes = args[3].split(':')[0]
seconds = args[3].split(':')[1]
time = (int(minutes) * 60) + int(seconds)
else:
time = int(args[3])
except:
await command.message.reply('usage: add_time person day time')
return
await my_user.add(Result(day, time), command.message.channel)
self.write_results()
if command_zero == 'su':
guild = self.client.get_guild(self.guild_id)
channel = discord.utils.get(command.message.guild.channels, name=args[2])
if not channel:
await command.message.reply('usage: su username channel command')
return
duser = discord.utils.get(guild.members, name=args[1])
if not duser:
await command.message.reply('usage: su username channel command')
return
await self.run_command(Command('user', ' '.join(args[3:]), duser, command.message), channel)
if command.message.reference: # if it's a reply, that means I'm running the command as the user
referenced_message = await command.message.channel.fetch_message(command.message.reference.message_id)
await self.run_command(Command('user', command.content, referenced_message.author, command.message), command.message.channel)
return
if command_zero not in admin_commands:
await command.message.reply('Command not recognized')
return
if command_zero == 'say':
if not args[1]:
return
channel = discord.utils.get(command.message.guild.channels, name=args[1])
if channel:
say_channel_length = len(args[0]) + len(args[1]) + 2
message = command_content_original[say_channel_length:] # message is everything after say <channel> TODO replace this with ' '.join()
await channel.send(message)
else:
await command.message.reply('channel not found')
if command_zero == 'leaderboard' or command_zero == 'lb':
await self.daily_leaderboard()
if command_zero == 'help' or command_zero == 'h':
file = open('files/helpMessageAdmin', 'r')
if not file:
return
help_message = file.read()
file.close()
await command.message.reply(help_message)
return
if command_zero == 'react':
channel = discord.utils.get(command.message.guild.channels, name=args[1])
if not channel:
await command.message.reply('not channel')
return
if (not args[1]) or (not args[2]) or (not args[3]):# if 'react' isn't followed by 2 strings, do nothing
await command.message.reply('not enough args. react channel messageID emoji')
return
if not emoji.is_emoji(args[3]):
await command.message.reply('not emoji')
return
try:
message_to_react = await channel.fetch_message(int(args[2]))
await message_to_react.add_reaction(args[3])
except discord.errors.NotFound:
await command.message.reply('discord.errors.NotFound')
if command_zero == 'reply':
channel = discord.utils.get(command.message.guild.channels, name=args[1])
if not channel:
await command.message.reply('not channel')
return
if (not args[1]) or (not args[2]) or (not args[3]):# if 'react' isn't followed by 2 strings, do nothing
await command.message.reply('not enough args. reply channel messageID newMessage')
return
try:
message_to_reply = await channel.fetch_message(int(args[2]))
beginning_length = len(args[0]) + len(args[1]) + len(args[2]) + 3
await message_to_reply.reply(command.message.content[beginning_length:])
except discord.errors.NotFound:
await command.message.reply('discord.errors.NotFound')
if command_zero == 'read_info' or command_zero == 'ri':
self.read_in_info()
admin_responses = ['You got it, boss', 'Sure thing, boss']
response = admin_responses[random.randrange(0,len(admin_responses))]
await command.message.reply(response)
async def check_command(self, message: discord.Message):
possible_command_tokens = ['minibot', 'mb']
possible_admin_command_tokens = ['minibot_admin', 'mb_admin']
command_token = None
command = False
admin_command = False
for pct in possible_command_tokens:
if message.content.lower().startswith(pct):
command_token = pct
command = True
# If it's in my secret admin channel
if message.channel.name == 'secret-minibot-commands':
command_token = ''
command = True
admin_command = True
# If it's an admin command
for pact in possible_admin_command_tokens:
if message.content.lower().startswith(pact):
command_token = pact
command = True
admin_command = True
# if it's a command
if command:
command_type = 'admin' if admin_command else 'user'
content = message.content[len(command_token):]
author = message.author
await self.run_command(Command(command_type, content, author, message), message.channel)
async def feed(self, message: discord.Message):
r = self.check_result(message.content)
# if it's a Result
if r:
await self.add(r, message)
else:
await self.check_command(message)
# returns a string representation of time - seconds:int -> 'm:ss'
def format_time(time: int) -> str:
if time < 60:
return str(time)
else:
minutes = time // 60
seconds = time % 60
minutes_string = str(minutes)
zero = '0' if seconds < 10 else ''
seconds_string = zero + str(seconds)
return minutes_string + ':' + seconds_string
async def daily_leaderboard(self):
guild = self.client.get_guild(self.guild_id)
if not guild:
print('guild not found')
return
channel = discord.utils.get(guild.channels, name='puzzles')
if not channel:
print('channel not found')
return
message = 'DAILY LEADERBOARD:\n'
placings = []
unsorted_placings = []
# get everyone in an unsorted list of placings
for u in self.users:
r = u.get_todays_result()
if r and not u.get_preference('no_leaderboard'):
unsorted_placings.append(Placing(u,r))
# sort the list
if (len(unsorted_placings) > 0):
placings = sorted(unsorted_placings, key = lambda x: x.result.time)
placings.insert(0,'zero')
# give everyone a place number
i = 1
while (i < len(placings)):
message += f'{i}. {placings[i].user.id.display_name} {MiniBot.format_time(placings[i].result.time)}\n'
placings[i].user.place(i)
i += 1
await channel.send(message)
intents = discord.Intents.default()
intents.messages = True
intents.message_content = True
intents.members = True
client = discord.Client(intents=intents)
# global object
bot = MiniBot(client, guild_id)
# Event handler for when the bot is ready
@client.event
async def on_ready():
print(f'Logged in as {client.user.name}')
await asyncio.sleep(10)
bot.read_in_info()
await daily_scheduler()
# Event handler for messages
@client.event
async def on_message(message):
# Don't respond to your own message
if message.author == client.user:
return
# send the message to the bot
await bot.feed(message)
# Schedule the task to run every day at 8 PM
async def daily_scheduler():
while not client.is_closed():
# Get current date and time
current_time = datetime.now()
# Check if it's 8pm
if current_time.strftime('%H:%M') == '20:00':
await bot.daily_leaderboard()
# Wait for 1 minute before checking again
await asyncio.sleep(60)
# bot's token
token_file = open(token_file_name, 'r')
token = token_file.read()
token_file.close()
# Run the bot
client.run(token)