-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathbot.py
301 lines (262 loc) · 11.5 KB
/
bot.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
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
'''Written by Cael Shoop.'''
import os
import pytz
import asyncio
import logging
from datetime import datetime, timedelta
from dotenv import load_dotenv
from discord import (app_commands, Intents, Client, Message, Guild,
File, Interaction, TextChannel, SelectOption)
from discord.ui import Select, View
from discord.ext import tasks
from persistence import Persistence
from player import Player
from data import TrackerData
# .env
load_dotenv()
# Logger setup
logger = logging.getLogger("Event Scheduler")
logger.setLevel(logging.DEBUG)
formatter = logging.Formatter(fmt="[%(asctime)s] [%(levelname)s\t] %(message)s", datefmt="%Y-%m-%d %H:%M:%S")
file_handler = logging.FileHandler("scheduler.log")
file_handler.setLevel(logging.DEBUG)
file_handler.setFormatter(formatter)
console_handler = logging.StreamHandler()
console_handler.setLevel(logging.DEBUG)
console_handler.setFormatter(formatter)
logger.addHandler(file_handler)
logger.addHandler(console_handler)
# Persistence
persist = Persistence("info.json")
class Tracker:
def __init__(self,
guild: Guild,
textChannel: TextChannel,
usingRandomLetter: bool,
players: list,
prevData: TrackerData,
data: TrackerData):
self.guild = guild
self.textChannel = textChannel
self.usingRandomLetter = usingRandomLetter
if players is not None:
self.players = players
else:
self.players = []
self.prevData = prevData
self.data = data
def shift_data(self) -> None:
self.prevData = self.data
self.data.reset()
for player in self.players:
player.shift_data()
def to_dict(self) -> dict:
payload = {}
payload["guildId"] = self.guild.id
payload["textChannelId"] = self.textChannel.id
payload["usingRandomLetter"] = self.usingRandomLetter
payload["players"] = [player.to_dict() for player in self.players]
payload["prevData"] = self.prevData.to_dict()
payload["data"] = self.data.to_dict()
return payload
@classmethod
def from_interaction(cls, interaction: Interaction):
players = []
for member in interaction.channel.members:
player = Player.from_member(member)
players.append(player)
return cls(guild=interaction.guild,
textChannel=interaction.channel,
usingRandomLetter=False,
players=players,
prevData=None,
data=TrackerData()
)
@classmethod
def from_dict(cls, payload: dict):
try:
guild = client.get_guild(payload["guildId"])
except:
return None
try:
textChannel = client.get_channel(payload["textChannelId"])
except:
textChannel = None
return cls(
guild=guild,
textChannel=textChannel,
usingRandomLetter=payload["usingRandomLetter"],
players=[Player.from_dict(playerData) for playerData in payload["players"]],
prevData=TrackerData.from_dict(payload["prevData"]),
data=TrackerData.from_dict(payload["data"])
)
class TimezoneMenu(Select):
def __init__(self):
options = [
SelectOption(label="Europe/Berlin", description="EU Central Timezone"),
SelectOption(label="Canada/Atlantic", description="CA Atlantic Timezone"),
SelectOption(label="US/Eastern", description="US Eastern Timezone"),
SelectOption(label="US/Central", description="US Central Timezone"),
SelectOption(label="US/Mountain", description="US Mountain Timezone"),
SelectOption(label="US/Pacific", description="US Pacific Timezone"),
]
super().__init__(placeholder="Select a timezone...", options=options)
async def callback(self, interaction: Interaction):
content = "Failed to find you in the players list. Are you registered?"
tracker = client.get_tracker_for_channel(interaction.channel)
for player in tracker.players:
if player.name == interaction.user.name:
timezone = pytz.timezone(self.values[0])
player.resetTime = datetime.now().astimezone(tz=timezone).replace(hour=0, minute=0, second=0, microsecond=0) + timedelta(days=1)
logger.info(f"Reset time for {player.name} is now {player.resetTime.isoformat()}")
content = f"Successfully set timezone to {self.values[0]}!"
break
await interaction.response.send_message(content=content, ephemeral=True)
class TimezoneMenuView(View):
def __init__(self):
super().__init__()
self.add_item(TimezoneMenu())
class WordleTracker(Client):
FILENAME = "data.json"
def __init__(self, intents: Intents) -> None:
super().__init__(intents=intents)
self.tree = app_commands.CommandTree(self)
self.trackers = []
def load_data(self, data: dict) -> None:
if data is None:
logger.info("No json data found")
return
logger.info("Json data found")
for trackerData in data["trackers"]:
self.add_tracker(trackerData)
def get_tracker_for_channel(self, channel: TextChannel) -> Tracker:
for tracker in self.trackers:
if tracker.textChannel == channel:
return tracker
return None
def add_tracker(self, data: dict) -> None:
try:
tracker = Tracker.from_dict(data)
self.trackers.append(tracker)
logger.info("Added tracker")
except Exception as e:
logger.exception(f"Failed to load tracker: {e}")
def remove_tracker(self, tracker: Tracker) -> None:
try:
self.trackers.remove(tracker)
logger.info("Removed tracker")
except Exception as e:
logger.error(f"Failed to remove tracker: {e}")
def get_tracker_data(self) -> dict:
payload = {}
try:
payload["trackers"] = [tracker.to_dict() for tracker in self.trackers]
except Exception as e:
logger.exception(f"Failed to get tracker data: {e}")
finally:
return payload
discord_token = os.getenv("DISCORD_TOKEN")
client = WordleTracker(intents=Intents.all())
data = persist.read()
client.load_data(data)
client.get_previous_answers()
async def setup_hourly_call():
if midnight_call.is_running():
return
curTime = datetime.now()
nextHour = curTime.replace(minute=0, second=0, microsecond=0) + timedelta(hours=1)
offset = (nextHour - curTime).total_seconds()
await asyncio.sleep(offset)
await midnight_call.start()
@client.event
async def on_ready():
logger.info(f"{client.user} has connected to Discord!")
await setup_hourly_call()
@client.event
async def on_message(message: Message):
# Return if message isn't in a tracked channel
tracker = client.get_tracker_for_channel(message.channel)
if tracker is None:
return
# TODO parse player messages into scores and screenshots
@client.tree.command(name="register", description="Register for Wordle tracking.")
async def register_command(interaction: Interaction):
tracker = client.get_tracker_for_channel(interaction.channel)
if tracker is None:
content = f"WordleTracker is not bound to {interaction.channel.mention}."
await interaction.response.send_message(content=content, ephemeral=True)
return
for player in tracker.players:
if player.member.id == interaction.user.id:
if player.registered:
content = "You are already registered for Wordle tracking."
else:
player.registered = True
content = "You have been re-registered for Wordle tracking."
await interaction.response.send_message(content=content, ephemeral=True)
return
member = interaction.guild.get_member(interaction.user.id)
player = Player.from_member(member)
tracker.players.append(player)
content = "You have been registered for Wordle tracking."
await interaction.response.send_message(content=content, ephemeral=True)
@client.tree.command(name="deregister", description="Deregister from Wordle tracking. Use twice to delete saved data.")
async def deregister_command(interaction: Interaction):
tracker = client.get_tracker_for_channel(interaction.channel)
if tracker is None:
content = f"WordleTracker is not bound to {interaction.channel.mention}."
await interaction.response.send_message(content=content, ephemeral=True)
return
culled_players = []
content = "You are not registered for Wordle tracking."
for player in tracker.players:
if player.member.id == interaction.user.id:
if player.registered:
player.registered = False
content = "You have been deregistered from Wordle tracking."
culled_players.append(player)
else:
content = "Your Wordle data has been deleted."
else:
culled_players.append(player)
tracker.players = culled_players
await interaction.response.send_message(content=content, ephemeral=True)
@client.tree.command(name="timezone", description="Change your timezone for scoring and notification purposes.")
async def timezone_command(interaction: Interaction):
tracker = client.get_tracker_for_channel(interaction.channel)
if tracker is None:
content = f"WordleTracker is not bound to {interaction.channel.mention}."
await interaction.response.send_message(content=content, ephemeral=True)
return
content = 'Select a timezone:'
view = TimezoneMenuView()
await interaction.response.send_message(content=content, view=view, ephemeral=True)
@client.tree.command(name="randomletterstart", description="State a random letter to start the Wordle guessing with.")
@app_commands.describe(use_random_letters="Whether you want forced starting with a random letter.")
async def randomletterstart_command(interaction: Interaction, use_random_letters: bool = True):
tracker = client.get_tracker_for_channel(interaction.channel)
if tracker is None:
content = f"WordleTracker is not bound to {interaction.channel.mention}."
await interaction.response.send_message(content=content, ephemeral=True)
return
tracker.usingRandomLetter = use_random_letters
if tracker.usingRandomLetter:
tracker.data.get_new_letter()
content = f"WordleTracker will now provide random letters. The current letter is {tracker.data.letter}."
else:
content = "WordleTracker will no longer provide random letters."
await interaction.response.send_message(content=content)
@client.tree.command(name="textchannel", description="Set the text channel for Wordle Tracker.")
@app_commands.describe(use_random_letters="Whether you want forced starting with a random letter.")
async def textchannel_command(interaction: Interaction, use_random_letters: bool = False):
tracker = client.get_tracker_for_channel(interaction.channel)
if tracker is None:
tracker = Tracker.from_interaction(interaction)
tracker.usingRandomLetter = use_random_letters
persist.write(client.get_tracker_dict())
content = f"WordleTracker in this server will now operate in {interaction.channel.mention}."
await interaction.response.send_message(content=content, ephemeral=True)
@tasks.loop(hours=1)
async def midnight_call():
# TODO scoring for each timezone
pass