-
Notifications
You must be signed in to change notification settings - Fork 2
/
Copy pathbot.py
246 lines (186 loc) · 7.72 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
import asyncio
import signal
import discord
import emoji
import sentry_sdk
from discord.ext import commands
from discord.ext.commands import BadArgument, CommandInvokeError
from helpers.config import config
from helpers.handler import Handler
intents = discord.Intents.all()
intents.message_content = True
bot = commands.Bot(
command_prefix=config.PREFIX,
description=config.DESCRIPTION,
intents=intents,
help_command=None,
case_sensitive=True,
)
if not config.DEBUG:
sentry_sdk.init(
dsn=config.SENTRY_KEY,
# Set traces_sample_rate to 1.0 to capture 100%
# of transactions for performance monitoring.
# We recommend adjusting this value in production.
traces_sample_rate=1.0,
)
# Event: Set bot presence and sync commands
@bot.event
async def on_ready() -> None:
global guild
guild = bot.get_guild(config.GUILD_ID)
print(f'Bot started. \nUsername: {bot.user.name}. \nID: {bot.user.id}', flush=True)
try:
await bot.change_presence(activity=config.activity(), status=config.status())
await (
bot.tree.sync()
) # Sync global commands (might take up to 1 hour to reflect globally)
except CommandInvokeError as e:
print(f'Error in command invocation: {e}', flush=True)
except BadArgument as e:
print(f'Error changing presence. Exception - {e}', flush=True)
except discord.HTTPException as e:
print(f'Failed to sync commands due to rate limiting: {e}', flush=True)
@bot.event
async def on_member_update(before_update, user: discord.Member):
"""
Function checks member and assigns role according to the username.
:param before_update:
:param user:
:return:
"""
# Return if the nickname hasn't changed
if before_update.nick == user.nick:
return
# Define role objects
vatsca_member = discord.utils.get(user.guild.roles, id=config.VATSCA_MEMBER_ROLE)
vatsim_member = discord.utils.get(user.guild.roles, id=config.VATSIM_MEMBER_ROLE)
# Create an instance of Handler
handler = Handler()
# Extract cid from nickname, exit early if not found
cid = await handler.get_cid(user)
try:
api_data = await handler.get_division_members()
should_have_vatsca = any(
int(entry['id']) == cid
and str(entry['subdivision']) == str(config.VATSIM_SUBDIVISION)
for entry in api_data
)
# Manage role assignments
tasks = []
if vatsim_member in user.roles:
# add VATSCA if required otherwise remove it
if should_have_vatsca and vatsca_member not in user.roles:
tasks.append(user.add_roles(vatsca_member))
elif not should_have_vatsca and vatsca_member in user.roles:
tasks.append(user.remove_roles(vatsca_member))
elif vatsca_member in user.roles:
tasks.append(
user.remove_roles(vatsca_member)
) # Remove VATSCA if the user doesnt have VATSIM role
if tasks:
await asyncio.gather(*tasks)
except discord.Forbidden as e:
print(f'Bot lacks permission for this action: {e}', flush=True)
except discord.HTTPException as e:
print(f'HTTP error: {e}', flush=True)
except Exception as e:
print(f'Unexpected error: {e}', flush=True)
async def send_dm(user, message):
"""Attempts to send a DM to the user and handles cases where DMs are closed."""
try:
await user.send(message)
except discord.Forbidden:
print(f'Could not send DM to {user.name}. They have DMs disabled.')
@bot.event
async def on_raw_reaction_add(payload):
if payload.guild_id is None or payload.user_id == bot.user.id:
return
guild = bot.get_guild(payload.guild_id)
user = guild.get_member(payload.user_id)
if not user: # User not found
return
emoji_name = emoji.demojize(
payload.emoji.name
) # Convert emoji to :emoji_name: format
message_id = str(payload.message_id) # Ensure consistency with config
if (
message_id in config.REACTION_MESSAGE_IDS
and emoji_name in config.REACTION_ROLES
):
role_id = int(config.REACTION_ROLES[emoji_name])
role = discord.utils.get(guild.roles, id=role_id)
if role and role not in user.roles:
await user.add_roles(role, reason=config.ROLE_REASONS['reaction_add'])
await send_dm(
user,
f'You have been given the `{role.name}` role because you reacted with {payload.emoji}',
)
@bot.event
async def on_raw_reaction_remove(payload):
if payload.guild_id is None or payload.user_id == bot.user.id:
return
guild = bot.get_guild(payload.guild_id)
user = guild.get_member(payload.user_id)
if not user: # User not found
return
emoji_name = emoji.demojize(
payload.emoji.name
) # Convert emoji to :emoji_name: format
message_id = str(payload.message_id) # Ensure consistency with config
if (
message_id in config.REACTION_MESSAGE_IDS
and emoji_name in config.REACTION_ROLES
):
role_id = int(config.REACTION_ROLES[emoji_name])
role = discord.utils.get(guild.roles, id=role_id)
if role and role in user.roles:
await user.remove_roles(role, reason=config.ROLE_REASONS['reaction_remove'])
await send_dm(
user,
f'You no longer have the `{role.name}` role because you removed your reaction.',
)
@bot.tree.error
async def on_app_command_error(
interaction: discord.Interaction, error: discord.app_commands.AppCommandError
):
"""Handles errors for application commands."""
if not interaction.response.is_done():
await interaction.response.defer(ephemeral=True)
error_map = {
discord.app_commands.MissingPermissions: 'You do not have the required permissions to use this command.',
discord.app_commands.BotMissingPermissions: 'The bot is missing the required permissions to execute this command.',
discord.app_commands.CommandNotFound: 'The command you are trying to use does not exist.',
discord.app_commands.CheckFailure: 'You do not meet the requirements to run this command.',
discord.app_commands.CommandOnCooldown: lambda e: f'Command is on cooldown. Try again in {e.retry_after:.2f} seconds.',
discord.app_commands.MissingRole: lambda e: f'You need the `{e.missing_role}` role to use this command.',
discord.app_commands.MissingAnyRole: lambda e: f'You need one of these roles: `{", ".join(e.missing_roles)}`.',
}
error_message = error_map.get(type(error), f'An unexpected error occurred: {error}')
if callable(error_message): # Handle dynamic error messages
error_message = error_message(error)
try:
await interaction.followup.send(error_message, ephemeral=True)
except discord.HTTPException as e:
print(f'Error sending error message: {e}')
print(f'Command Error: {error}')
@bot.event
async def on_error(event, *args, **kwargs):
print(f'Error in {event}: {args} {kwargs}', flush=True)
# Load all cogs at startup
@bot.event
async def on_connect():
await config.load_cogs(bot)
# Signal handling for graceful shutdown
def handle_exit_signal(signal_number, frame):
print('Received shutdown signal. Closing bot...', flush=True)
asyncio.create_task(bot.close())
# Register signals
signal.signal(signal.SIGINT, handle_exit_signal) # For CTRL + C
signal.signal(signal.SIGTERM, handle_exit_signal) # For termination signal
# Start the bot
if __name__ == '__main__':
try:
bot.run(config.BOT_TOKEN)
except Exception as e:
print(f'Error starting the bot. Exception - {e}', flush=True)