-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathmain.py
415 lines (324 loc) · 15.7 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
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
'''
Main for Workflow Bot
Created on Monday 1st January 2024
@author: Harry New
'''
import discord
import logging
from datetime import datetime
import pathlib
import logging.config
import logging.handlers
import json
import asyncio
import traceback
import commands
import workflow
from json_storage import save_to_json,convert_from_json
# - - - - - - - - - - - - - - - - - - - - - - -
def init_logging():
global logger
logger = logging.getLogger()
# Getting logger.
logger.setLevel(logging.INFO)
# Creating formatter.
formatter = logging.Formatter(fmt="%(asctime)s:%(levelname)s: %(message)s",datefmt="%Y-%m-%d %H:%M:%S")
# Creating stream handler.
stdout = logging.StreamHandler()
stdout.setLevel(logging.INFO)
stdout.setFormatter(formatter)
logger.addHandler(stdout)
# Creating file handler.
file_handler = logging.handlers.RotatingFileHandler(filename="logs/workflow_bot.log",mode="a",backupCount=3,maxBytes=10000000)
file_handler.setLevel(logging.INFO)
file_handler.setFormatter(formatter)
logger.addHandler(file_handler)
# Adding logger to commands.
commands.init_commands(logger)
return logger
def init_client(token):
logger.info("- - - - - - - - - - - - - - - - - - - - - -")
# Initialising and running client.
client = discord.Client(intents=intents)
tree = discord.app_commands.CommandTree(client)
# Initialising events.
init_events(client,tree)
logger.info("Initialising events.")
# Initialising commands.
init_commands(client,tree)
logger.info("Initialising commands.")
logger.info("Running client.")
logger.info("- - - - - - - - - - - - - - - - - - - - - -")
client.run(TOKEN,reconnect=True,log_handler=None)
return client
def init_events(client,tree):
global temp_client
temp_client = client
# On connect event.
@client.event
async def on_connect():
# Initialising saved data.
logger.info("Initialising saved data.")
await init_saved(client)
# Initialising active message looping.
logger.info("Initialising message looping.")
await init_message_looping(client)
# On resumed event.
@client.event
async def on_resumed():
# Resuming saved data.
logger.info("Resuming saved data.")
await init_saved(client)
# Resuming active message looping.
logger.info("Resuming message looping.")
await init_message_looping(client)
# On guild join event.
@client.event
async def on_guild_join(guild):
logger.info("- - - - - - - - - - - - - - - - - - - - - -")
logger.info(f"Joined discord server ({guild.name}).")
for role in await guild.fetch_roles():
if role.name == "Workflow Manager":
admin_role = role
break
else:
# Creating "Workflow Manager" role.
admin_role = await guild.create_role(name="Workflow Manager", colour=discord.Colour.teal())
logger.info("Workflow Manager role has been created.")
# Creating workflow for guild.
new_workflow = workflow.Workflow()
logger.info("Creating new workflow for server.")
# Adding workflow to dictionary.
workflows[str(guild.id)] = new_workflow
logger.info("Adding workflow to workflows dictionary.")
# On role update event.
@client.event
async def on_member_update(before, after):
logging.info("Member's role updated event.")
# Getting workflow.
guild = after.guild
workflow = workflows[str(guild.id)]
# Getting list of manager role ids.
manager_ids = workflow.get_manager_role_ids()
# Getting new role.
for role in after.roles:
if role not in before.roles:
new_role = role
# Check if manager role.
if new_role.id in manager_ids:
logging.info(f"Manager role updated, {new_role.name}")
team = workflow.get_team_from_manager_id(new_role.id)
# Getting team role.
team_role = guild.get_role(team.role_id)
# Adding normal team role.
await after.add_roles(team_role)
await team_role.edit(reason="Trigger guild role event.")
logging.info(f"Adding member to standard team role, {team_role.name}")
logger.info("- - - - - - - - - - - - - - - - - - - - - -")
@client.event
async def on_guild_role_delete(role):
logging.info("Role deleted event.")
# Getting workflow.
guild = role.guild
workflow = workflows[str(guild.id)]
# Getting list of manager role ids.
manager_ids = workflow.get_manager_role_ids()
team_role_ids = workflow.get_role_ids()
# Check if manager role.
if role.id in manager_ids:
logging.info(f"Manager role deleted, {role.name}")
# Getting team and deleting.
team = workflow.get_team_from_manager_id(role.id)
logging.info(f"Deleting team, {team.name}")
workflow.del_team(workflow.teams.index(team)+1)
# Removing team member role.
logging.info(f"Deleting team member role.")
role = guild.get_role(team.role_id)
await role.edit(reason="Trigger guild role event.")
await role.delete()
elif role.id in team_role_ids:
logging.info(f"Team member role deleted, {role.name}")
# Getting team and deleting.
team = workflow.get_team_from_role_id(role.id)
logging.info(f"Deleting team, {team.name}")
workflow.del_team(workflow.teams.index(team)+1)
# Removing team manager role.
logging.info(f"Deleting team member role.")
role = guild.get_role(team.manager_role_id)
await role.edit(reason="Trigger guild role event.")
await role.delete()
logger.info("- - - - - - - - - - - - - - - - - - - - - -")
else:
logger.info("- - - - - - - - - - - - - - - - - - - - - -")
pass
# On guild remove event.
@client.event
async def on_guild_remove(guild):
logger.info(f"Removed discord server ({guild.name}).")
# Removing guild from workflow.
workflows.pop(str(guild.id))
logger.info("Removing guild from workflows dictionary.")
logger.info("- - - - - - - - - - - - - - - - - - - - - -")
# On disconnect event.
@client.event
async def on_disconnect():
# Saving data to json.
save_to_json(workflows)
logger.info("- - - - - - - - - - - - - - - - - - - - - -")
@client.event
async def on_ready():
logger.info("Starting command tree syncing.")
await tree.sync()
logger.info("Finished command tree syncing.")
logger.info("- - - - - - - - - - - - - - - - - - - - - -")
def init_commands(client,tree):
def is_developer():
def predicate(interaction: discord.Interaction) -> bool:
return interaction.user.id == 449461280437436416
return discord.app_commands.check(predicate)
def is_team_manager():
def predicate(interaction: discord.Interaction) -> bool:
return commands.misc.check_team_manager(interaction.user,interaction.guild,workflows[str(interaction.guild.id)])
return discord.app_commands.check(predicate)
@tree.command(name="help",description="Provides a list of commands that can be used with the Workflow Bot.")
@discord.app_commands.checks.cooldown(1,300)
async def help_command(interaction):
logger.info("Requesting help command.")
await commands.help_command(interaction,workflows[str(interaction.guild.id)])
@help_command.error
async def on_help_error(interaction:discord.Interaction, error:discord.app_commands.AppCommandError):
if isinstance(error, discord.app_commands.CommandOnCooldown):
await interaction.response.send_message("Please wait before sending the `/help` command again.",ephemeral=True)
@tree.command(name="tutorial",description="Provides a tutorial to explain how the bot works to users.")
@discord.app_commands.checks.cooldown(1,300)
async def tutorial_command(interaction):
logger.info("Requesting tutorial command.")
await commands.tutorial_command(interaction,workflows[str(interaction.guild.id)])
@tutorial_command.error
async def on_tutorial_error(interaction:discord.Interaction, error:discord.app_commands.AppCommandError):
if isinstance(error, discord.app_commands.CommandOnCooldown):
await interaction.response.send_message("Please wait before sending the `/tutorial` command again.",ephemeral=True)
@tree.command(name="set_active_channel",description="Sets the current channel to the active channel and displays all existing projects in the workflow.")
@discord.app_commands.checks.has_role("Workflow Manager")
async def set_active_channel_command(interaction):
logger.info("Requesting set active channel command.")
await commands.set_active_channel_command(interaction,workflows[str(interaction.guild.id)],client)
@set_active_channel_command.error
async def on_set_active_channel_error(interaction: discord.Interaction, error: discord.app_commands.AppCommandError):
if isinstance(error, discord.app_commands.MissingRole):
await interaction.response.send_message("You do not have the necessary role to use this command.",ephemeral=True)
@tree.command(name="teams",description="Displays all existing teams on the server and allows adding, editing and deleting teams.")
@discord.app_commands.checks.has_role("Workflow Manager")
async def teams_command(interaction):
logger.info("Requesting teams command.")
await commands.display_teams(interaction,workflows[str(interaction.guild.id)],client)
@teams_command.error
async def on_teams_error(interaction: discord.Interaction, error: discord.app_commands.AppCommandError):
if isinstance(error, discord.app_commands.CheckFailure):
await interaction.response.send_message("You do not have the necessary role to use this command.",ephemeral=True)
@tree.command(name="manage_projects",description="Allows projects to be selected and managed by managers.")
@discord.app_commands.checks.has_role("Workflow Manager")
async def manage_projects_command(interaction):
logger.info("Requesting manage projects by Workflow Manager command.")
await commands.manage_projects(interaction,client,workflows[str(interaction.guild.id)])
@manage_projects_command.error
async def on_manage_projects_error(interaction: discord.Interaction, error: discord.app_commands.AppCommandError):
if isinstance(error, discord.app_commands.MissingRole) and commands.misc.check_team_manager(interaction.user,interaction.guild,workflows[str(interaction.guild.id)]):
logger.info("Requesting manage projects by Team Manager command.")
await commands.manage_projects(interaction,client,workflows[str(interaction.guild.id)])
elif isinstance(error, discord.app_commands.MissingRole):
await interaction.response.send_message("You do not have the necessary role to use this command.",ephemeral=True)
@tree.command(name="manage_tasks",description="Allows tasks to be selected and managed.")
async def manage_tasks_command(interaction):
logger.info("Requesting manage tasks command.")
await commands.manage_tasks(interaction,client,workflows[str(interaction.guild.id)])
@tree.command(name="days_of_code",description="Allows users to track progress of 100 Days of Code project.")
@discord.app_commands.checks.has_role("Workflow Manager")
async def days_of_code_command(interaction):
try:
logger.info("Requesting days of code command by Workflow Manager.")
if not workflows[str(interaction.guild.id)].check_days_of_code():
await commands.send_new_project_message(interaction,workflows[str(interaction.guild.id)],client)
else:
await commands.send_standard_message(interaction,workflows[str(interaction.guild.id)],client)
except Exception as e:
print(traceback.format_exc())
@days_of_code_command.error
async def on_days_of_code_error(interaction:discord.Interaction,error:discord.app_commands.AppCommandError):
if isinstance(error, discord.app_commands.CheckFailure):
if workflows[str(interaction.guild.id)].check_days_of_code():
await commands.send_standard_message(interaction,workflows[str(interaction.guild.id)],client)
else:
await interaction.response.send_message(embed=discord.Embed(description="No 100 Days of Code project available, please ask your Workflow Manager to create the project."))
@tree.command(name="disconnect",description="Disconnects the bot and saves data to JSON.")
@is_developer()
async def disconnect(interaction):
logger.info("Requesting disconnect command.")
await commands.disconnect_command(client)
@disconnect.error
async def on_disconnect_error(interaction: discord.Interaction, error: discord.app_commands.AppCommandError):
if isinstance(error, discord.app_commands.CheckFailure):
await interaction.response.send_message("You do not have the necessary permissions to use this command.",ephemeral=True)
@tree.command(name="reset_server",description="Resets Workflow information stored about the server.")
@is_developer()
async def reset_server(interaction):
try:
logger.info("Requesting reset server command.")
# Deleting original Workflow.
del workflows[str(interaction.guild.id)]
# Creating new Workflow.
workflows[str(interaction.guild.id)] = workflow.Workflow()
except KeyError as e:
logger.error(f"KeyError: {e}")
logger.error(f"Guild {e} not included in Workflows.")
# Creating workflow object.
logger.info("Creating new Workflow object for server.")
workflows[str(interaction.guild.id)] = workflow.Workflow()
# Sending message.
await interaction.response.send_message("WorkflowBot has been reset for this server.",ephemeral=True)
@reset_server.error
async def on_reset_server_error(interaction: discord.Interaction, error: discord.app_commands.AppCommandError):
if isinstance(error, discord.app_commands.CheckFailure):
await interaction.response.send_message("You do not have the necessary permissions to use this command.",ephemeral=True)
async def init_saved(client):
# Loading workflows dictionary.
global workflows
load_check = False
try:
# Loading json.
json_file = open('server_workflows.json',)
workflows_import = json.load(json_file)
load_check = True
except:
# Creating new dictionary.
workflows = {}
if load_check:
workflows = await convert_from_json(workflows_import, client)
async def init_message_looping(client):
task_list = []
for guild_id in workflows.keys():
logger.info(f"Restarting message looping, {guild_id}")
if workflows[guild_id].active_message:
logger.info(f"Active message retrieved.")
restart_looping_task = asyncio.create_task(commands.restart_looping(client,workflows[guild_id],await client.fetch_guild(guild_id)))
task_list.append(restart_looping_task)
# Restarting progress looping.
logger.info("Restarting progress looping.")
progress_task = asyncio.create_task(commands.restart_days_of_code_looping(workflows,client))
task_list.append(progress_task)
logger.info("- - - - - - - - - - - - - - - - - - - - - -")
if len(task_list) != 0:
await asyncio.wait(task_list)
# - - - - - - - - - - - - - - - - - - - - - - -
if __name__ == "__main__":
global client
# Initialising logging.
init_logging()
# Initialising intents.
TOKEN = 'MTE5MTQ0NzQ5OTM4NzQzNzEwOA.GgSomy.bK3obSpCL8KdShCpJss8zyw3DOFcb5saIL785g'
intents = discord.Intents.default()
intents.message_content = True
intents.members = True
intents.guilds = True
# Initialising client.
client = init_client(TOKEN)