tickets_plus.cogs.events
This is the event handling extension for Tickets+.
We add any and all event listeners here. At the moment, we only have discord.py event listeners. But should we add any other event listeners, we can add them here.
Typical usage example:
from tickets_plus import bot bot_instance = bot.TicketsPlusBot(...) await bot_instance.load_extension("tickets_plus.cogs.events")
1"""This is the event handling extension for Tickets+. 2 3We add any and all event listeners here. 4At the moment, we only have discord.py event listeners. 5But should we add any other event listeners, we can add them here. 6 7Typical usage example: 8 ```py 9 from tickets_plus import bot 10 bot_instance = bot.TicketsPlusBot(...) 11 await bot_instance.load_extension("tickets_plus.cogs.events") 12 ``` 13""" 14# License: EPL-2.0 15# SPDX-License-Identifier: EPL-2.0 16# Copyright (c) 2021-present The Tickets+ Contributors 17# This Source Code may also be made available under the following 18# Secondary Licenses when the conditions for such availability set forth 19# in the Eclipse Public License, v. 2.0 are satisfied: GPL-3.0-only OR 20# If later approved by the Initial Contributor, GPL-3.0-or-later. 21from __future__ import annotations 22 23import asyncio 24import datetime 25import logging 26import re 27from typing import Any, Tuple 28 29import discord 30from discord import abc, utils 31from discord.ext import commands 32from sqlalchemy import orm 33 34from tickets_plus import bot 35from tickets_plus.database import layer, models 36from tickets_plus.ext import legacy 37 38 39class Events(commands.Cog, name="Events"): 40 """Event handling for Tickets+. 41 42 This cog handles all events for Tickets+. 43 This is used to handle events from Discord. 44 """ 45 46 def __init__(self, bot_instance: bot.TicketsPlusBot) -> None: 47 """Initialises the cog instance. 48 49 We set some attributes here, so we can use them later. 50 51 Args: 52 bot_instance: The bot instance. 53 """ 54 self._bt = bot_instance 55 logging.info("Loaded %s", self.__class__.__name__) 56 57 async def ticket_creation( 58 self: Any, 59 confg: layer.OnlineConfig, 60 guilded: Tuple[discord.Guild, models.Guild], 61 channel: discord.TextChannel, 62 user: None | discord.User = None, 63 ) -> None: 64 """Main ticket creation function. 65 66 Creates db and does some other stuff. 67 """ 68 gld, guild = guilded 69 ttypes = await confg.get_ticket_types(gld.id) 70 ticket_type = models.TicketType.default() 71 for ttype in ttypes: 72 if channel.name.startswith(ttype.prefix): 73 ticket_type = ttype 74 if ticket_type.ignore: 75 return 76 nts_thrd = None 77 if guild.legacy_threads: 78 nts_thrd = await legacy.thread_create(channel, guild, confg) 79 user_id = user.id if user else None 80 thr_id = nts_thrd.id if nts_thrd else None 81 await confg.get_ticket(channel.id, gld.id, user_id, thr_id) 82 if guild.helping_block: 83 overwrite = discord.PermissionOverwrite( 84 view_channel=False, 85 add_reactions=False, 86 send_messages=False, 87 read_messages=False, 88 read_message_history=False, 89 ) 90 rol = gld.get_role(guild.helping_block) 91 if rol is None: 92 guild.helping_block = None 93 else: 94 await channel.set_permissions( 95 rol, 96 overwrite=overwrite, 97 reason="Penalty Enforcement", 98 ) 99 if guild.community_roles and ticket_type.comaccs: 100 comm_roles = await confg.get_all_community_roles(gld.id) 101 overwrite = discord.PermissionOverwrite( 102 view_channel=True, 103 add_reactions=True, 104 send_messages=True, 105 read_messages=True, 106 read_message_history=True, 107 attach_files=True, 108 embed_links=True, 109 use_application_commands=True, 110 ) 111 for role in comm_roles: 112 rle = gld.get_role(role.role_id) 113 if rle is None: 114 continue 115 await channel.set_permissions( 116 rle, 117 overwrite=overwrite, 118 reason="Community Support Access", 119 ) 120 if guild.community_pings and ticket_type.comping: 121 comm_pings = await confg.get_all_community_pings(gld.id) 122 inv = await channel.send(" ".join([f"<@&{role.role_id}>" for role in comm_pings])) 123 await asyncio.sleep(0.25) 124 await inv.delete() 125 descr = (f"Ticket {channel.name}\n" 126 "Opened at " 127 f"<t:{int(channel.created_at.timestamp())}:f>") 128 if user: 129 descr += f"\nOpened by {user.mention}" 130 if guild.first_autoclose: 131 # skipcq: FLK-E501 # pylint: disable=line-too-long 132 descr += f"\nCloses <t:{int((channel.created_at + guild.first_autoclose).timestamp())}:R>" 133 # skipcq: FLK-E501 # pylint: disable=line-too-long 134 descr += "\nIf no one responds, the ticket will be closed automatically. Thank you for your patience!" 135 await channel.edit(topic=descr, reason="More information for the ticket.") 136 if guild.strip_buttons and ticket_type.strpbuttns: 137 await asyncio.sleep(5) 138 async for msg in channel.history(oldest_first=True, limit=2): 139 if await confg.check_ticket_bot(msg.author.id, gld.id) and msg.embeds: 140 await channel.send(embeds=msg.embeds) 141 await msg.delete() 142 await confg.commit() 143 144 async def message_discovery(self, message: discord.Message) -> None: 145 """Discovers the message linked to. 146 147 Fetches a message from its discord link and responds with the 148 message content. 149 150 Args: 151 message: The message to check for links 152 """ 153 alpha = re.search( 154 # skipcq: FLK-E501 # pylint: disable=line-too-long 155 r"https://(?:canary\.)?discord\.com/channels/(?P<srv>\d*)/(?P<cha>\d*)/(?P<msg>\d*)", 156 message.content, 157 ) 158 if alpha: 159 # We do not check any types in try as we are catching. 160 try: 161 gld = self._bt.get_guild(int(alpha.group("srv"))) 162 chan: abc.GuildChannel = ( 163 gld.get_channel_or_thread( # type: ignore 164 int(alpha.group("cha")))) 165 got_msg = await chan.fetch_message( # type: ignore 166 int(alpha.group("msg"))) 167 except ( 168 AttributeError, 169 discord.HTTPException, 170 ): 171 logging.warning("Message discovery failed.") 172 else: 173 time = got_msg.created_at.strftime("%d/%m/%Y %H:%M:%S") 174 if not got_msg.content and got_msg.embeds: 175 discovered_result = got_msg.embeds[0] 176 discovered_result.set_footer(text="[EMBED CAPTURED] Sent in" 177 f" {chan.name}" 178 f" at {time}") 179 else: 180 discovered_result = discord.Embed(description=got_msg.content, color=0x0D0EB4) 181 discovered_result.set_footer(text="Sent in " 182 f"{chan.name} at {time}" # type: ignore 183 ) 184 discovered_result.set_author( 185 name=got_msg.author.name, 186 icon_url=got_msg.author.display_avatar.url, 187 ) 188 discovered_result.set_image(url=got_msg.attachments[0].url if got_msg.attachments else None) 189 await message.reply(embed=discovered_result) 190 191 async def handle_anon(self, message: discord.Message, ticket: models.Ticket, cnfg: layer.OnlineConfig, 192 guild: models.Guild) -> None: 193 """Handles ticket anon messages. 194 195 Grabs a message and anonymizes it, then send it to the ticket. 196 197 Args: 198 message: The message to handle. 199 ticket: The ticket to send the message to. 200 cnfg: The database connection. 201 guild: The guild the ticket is in. 202 """ 203 if ticket.anonymous: 204 if ticket.user_id == message.author.id: 205 return 206 staff = False 207 staff_roles = await cnfg.get_all_staff_roles(guild.guild_id) 208 for role in staff_roles: 209 parsed_role = message.guild.get_role( # type: ignore 210 role.role_id) 211 if parsed_role in message.author.roles: # type: ignore 212 # Already checked for member 213 staff = True 214 break 215 if not staff: 216 return 217 await message.channel.send( 218 f"**{guild.staff_team_name}:** " 219 f"{utils.escape_mentions(message.content)}", 220 embeds=message.embeds, 221 ) 222 await message.delete() 223 224 async def update_autoclose(self, message: discord.Message, ticket: models.Ticket, guild: models.Guild, 225 cnfg: layer.OnlineConfig) -> None: 226 """Updates channel topic autoclose time. 227 228 Changes the channel topic to reflect the new autoclose time. 229 230 Args: 231 message: The message to check. 232 ticket: The ticket updated. 233 guild: The guild settings. 234 cnfg: The database connection. 235 """ 236 chan = message.channel 237 if guild.any_autoclose: 238 time_since_update = datetime.datetime.utcnow() - ticket.last_response 239 if time_since_update >= datetime.timedelta(minutes=5): 240 crrnt = chan.topic # type: ignore 241 if crrnt is None: 242 # pylint: disable=line-too-long 243 crrnt = ( 244 f"Ticket: {chan.name}\n" # type: ignore 245 # skipcq: FLK-E501 246 f"Closes: <t:{int((message.created_at + guild.any_autoclose).timestamp())}:R>") 247 else: 248 # pylint: disable=line-too-long 249 crrnt = re.sub( 250 r"<t:[0-9]*?:R>", 251 # skipcq: FLK-E501 252 f"<t:{int((message.created_at + guild.any_autoclose).timestamp())}:R>", 253 crrnt) 254 await chan.edit(topic=crrnt) # type: ignore 255 ticket.last_response = datetime.datetime.utcnow() 256 await cnfg.commit() 257 258 @commands.Cog.listener(name="on_guild_channel_create") 259 async def on_channel_create(self, channel: discord.abc.GuildChannel) -> None: 260 """Runs when a channel is created. 261 262 Handles the checking and facilitating of ticket creation. 263 This is the main event that handles the creation of tickets. 264 265 Args: 266 channel: The channel that was created. 267 """ 268 async with self._bt.get_connection() as confg: 269 if isinstance(channel, discord.channel.TextChannel): 270 gld = channel.guild 271 guild = await confg.get_guild( 272 gld.id, 273 ( 274 orm.selectinload(models.Guild.observers_roles), 275 orm.selectinload(models.Guild.community_roles), 276 orm.selectinload(models.Guild.community_pings), 277 ), 278 ) 279 if guild.integrated: 280 return 281 async for entry in gld.audit_logs(limit=3, action=discord.AuditLogAction.channel_create): 282 if not entry.user: 283 continue 284 if entry.target == channel and await confg.check_ticket_bot(entry.user.id, gld.id): 285 await self.ticket_creation(confg, (gld, guild), channel) 286 287 @commands.Cog.listener(name="on_guild_channel_delete") 288 async def on_channel_delete(self, channel: discord.abc.GuildChannel) -> None: 289 """Cleanups for when a ticket channel is deleted. 290 291 This is the main event that handles the deletion of tickets. 292 We remove stale tickets from the database. 293 294 Args: 295 channel: The channel that was deleted. 296 """ 297 if isinstance(channel, discord.channel.TextChannel): 298 async with self._bt.get_connection() as confg: 299 ticket = await confg.fetch_ticket(channel.id) 300 if ticket: 301 await confg.delete(ticket) 302 logging.info("Deleted ticket %s", channel.name) 303 await confg.commit() 304 305 @commands.Cog.listener(name="on_member_join") 306 async def on_member_join(self, member: discord.Member) -> None: 307 """Ensures penalty roles are sticky. 308 309 This just ensures that if a user has a penalty role, 310 it is reapplied when they join the server. 311 312 Args: 313 member: The member that joined. 314 """ 315 async with self._bt.get_connection() as cnfg: 316 actv_member = await cnfg.get_member(member.id, member.guild.id) 317 if actv_member.status: 318 if actv_member.status_till is not None: 319 # Split this up to avoid None comparison. 320 # pylint: disable=line-too-long 321 if actv_member.status_till <= datetime.datetime.utcnow(): # type: ignore 322 # Check if the penalty has expired. 323 actv_member.status = 0 324 actv_member.status_till = None 325 await cnfg.commit() 326 return 327 if actv_member.status == 1: 328 # Status 1 is a support block. 329 if actv_member.guild.support_block is None: 330 # If the role is unset, pardon the user. 331 actv_member.status = 0 332 actv_member.status_till = None 333 await cnfg.commit() 334 return 335 role = member.guild.get_role(actv_member.guild.support_block) 336 if role is not None: 337 await member.add_roles(role) 338 elif actv_member.status == 2: 339 # Status 2 is a helping block. 340 if actv_member.guild.helping_block is None: 341 # If the role is unset, pardon the user. 342 actv_member.status = 0 343 actv_member.status_till = None 344 await cnfg.commit() 345 return 346 role = member.guild.get_role(actv_member.guild.helping_block) 347 if role is not None: 348 await member.add_roles(role) 349 350 @commands.Cog.listener(name="on_message") 351 async def on_message(self, message: discord.Message) -> None: 352 """Handles all message-related features. 353 354 We use this to handle the message discovery feature, 355 searching for links to messages, resolving them, and 356 sending their contents as a reply. 357 Also handles anonymous mode for tickets. Resending 358 staff messages as the bot. 359 360 Args: 361 message: The message that was sent. 362 """ 363 async with self._bt.get_connection() as cnfg: 364 if message.author.bot or message.guild is None: 365 return 366 guild = await cnfg.get_guild(message.guild.id) 367 if guild.msg_discovery: 368 await self.message_discovery(message) 369 ticket = await cnfg.fetch_ticket(message.channel.id) 370 if ticket: 371 # Make sure the ticket exists 372 await self.handle_anon(message, ticket, cnfg, guild) 373 await self.update_autoclose(message, ticket, guild, cnfg) 374 375 376async def setup(bot_instance: bot.TicketsPlusBot) -> None: 377 """Sets up the Events handler. 378 379 This is called when the cog is loaded. 380 It adds the cog to the bot. 381 382 Args: 383 bot_instance: The bot that is loading the cog. 384 """ 385 await bot_instance.add_cog(Events(bot_instance))
40class Events(commands.Cog, name="Events"): 41 """Event handling for Tickets+. 42 43 This cog handles all events for Tickets+. 44 This is used to handle events from Discord. 45 """ 46 47 def __init__(self, bot_instance: bot.TicketsPlusBot) -> None: 48 """Initialises the cog instance. 49 50 We set some attributes here, so we can use them later. 51 52 Args: 53 bot_instance: The bot instance. 54 """ 55 self._bt = bot_instance 56 logging.info("Loaded %s", self.__class__.__name__) 57 58 async def ticket_creation( 59 self: Any, 60 confg: layer.OnlineConfig, 61 guilded: Tuple[discord.Guild, models.Guild], 62 channel: discord.TextChannel, 63 user: None | discord.User = None, 64 ) -> None: 65 """Main ticket creation function. 66 67 Creates db and does some other stuff. 68 """ 69 gld, guild = guilded 70 ttypes = await confg.get_ticket_types(gld.id) 71 ticket_type = models.TicketType.default() 72 for ttype in ttypes: 73 if channel.name.startswith(ttype.prefix): 74 ticket_type = ttype 75 if ticket_type.ignore: 76 return 77 nts_thrd = None 78 if guild.legacy_threads: 79 nts_thrd = await legacy.thread_create(channel, guild, confg) 80 user_id = user.id if user else None 81 thr_id = nts_thrd.id if nts_thrd else None 82 await confg.get_ticket(channel.id, gld.id, user_id, thr_id) 83 if guild.helping_block: 84 overwrite = discord.PermissionOverwrite( 85 view_channel=False, 86 add_reactions=False, 87 send_messages=False, 88 read_messages=False, 89 read_message_history=False, 90 ) 91 rol = gld.get_role(guild.helping_block) 92 if rol is None: 93 guild.helping_block = None 94 else: 95 await channel.set_permissions( 96 rol, 97 overwrite=overwrite, 98 reason="Penalty Enforcement", 99 ) 100 if guild.community_roles and ticket_type.comaccs: 101 comm_roles = await confg.get_all_community_roles(gld.id) 102 overwrite = discord.PermissionOverwrite( 103 view_channel=True, 104 add_reactions=True, 105 send_messages=True, 106 read_messages=True, 107 read_message_history=True, 108 attach_files=True, 109 embed_links=True, 110 use_application_commands=True, 111 ) 112 for role in comm_roles: 113 rle = gld.get_role(role.role_id) 114 if rle is None: 115 continue 116 await channel.set_permissions( 117 rle, 118 overwrite=overwrite, 119 reason="Community Support Access", 120 ) 121 if guild.community_pings and ticket_type.comping: 122 comm_pings = await confg.get_all_community_pings(gld.id) 123 inv = await channel.send(" ".join([f"<@&{role.role_id}>" for role in comm_pings])) 124 await asyncio.sleep(0.25) 125 await inv.delete() 126 descr = (f"Ticket {channel.name}\n" 127 "Opened at " 128 f"<t:{int(channel.created_at.timestamp())}:f>") 129 if user: 130 descr += f"\nOpened by {user.mention}" 131 if guild.first_autoclose: 132 # skipcq: FLK-E501 # pylint: disable=line-too-long 133 descr += f"\nCloses <t:{int((channel.created_at + guild.first_autoclose).timestamp())}:R>" 134 # skipcq: FLK-E501 # pylint: disable=line-too-long 135 descr += "\nIf no one responds, the ticket will be closed automatically. Thank you for your patience!" 136 await channel.edit(topic=descr, reason="More information for the ticket.") 137 if guild.strip_buttons and ticket_type.strpbuttns: 138 await asyncio.sleep(5) 139 async for msg in channel.history(oldest_first=True, limit=2): 140 if await confg.check_ticket_bot(msg.author.id, gld.id) and msg.embeds: 141 await channel.send(embeds=msg.embeds) 142 await msg.delete() 143 await confg.commit() 144 145 async def message_discovery(self, message: discord.Message) -> None: 146 """Discovers the message linked to. 147 148 Fetches a message from its discord link and responds with the 149 message content. 150 151 Args: 152 message: The message to check for links 153 """ 154 alpha = re.search( 155 # skipcq: FLK-E501 # pylint: disable=line-too-long 156 r"https://(?:canary\.)?discord\.com/channels/(?P<srv>\d*)/(?P<cha>\d*)/(?P<msg>\d*)", 157 message.content, 158 ) 159 if alpha: 160 # We do not check any types in try as we are catching. 161 try: 162 gld = self._bt.get_guild(int(alpha.group("srv"))) 163 chan: abc.GuildChannel = ( 164 gld.get_channel_or_thread( # type: ignore 165 int(alpha.group("cha")))) 166 got_msg = await chan.fetch_message( # type: ignore 167 int(alpha.group("msg"))) 168 except ( 169 AttributeError, 170 discord.HTTPException, 171 ): 172 logging.warning("Message discovery failed.") 173 else: 174 time = got_msg.created_at.strftime("%d/%m/%Y %H:%M:%S") 175 if not got_msg.content and got_msg.embeds: 176 discovered_result = got_msg.embeds[0] 177 discovered_result.set_footer(text="[EMBED CAPTURED] Sent in" 178 f" {chan.name}" 179 f" at {time}") 180 else: 181 discovered_result = discord.Embed(description=got_msg.content, color=0x0D0EB4) 182 discovered_result.set_footer(text="Sent in " 183 f"{chan.name} at {time}" # type: ignore 184 ) 185 discovered_result.set_author( 186 name=got_msg.author.name, 187 icon_url=got_msg.author.display_avatar.url, 188 ) 189 discovered_result.set_image(url=got_msg.attachments[0].url if got_msg.attachments else None) 190 await message.reply(embed=discovered_result) 191 192 async def handle_anon(self, message: discord.Message, ticket: models.Ticket, cnfg: layer.OnlineConfig, 193 guild: models.Guild) -> None: 194 """Handles ticket anon messages. 195 196 Grabs a message and anonymizes it, then send it to the ticket. 197 198 Args: 199 message: The message to handle. 200 ticket: The ticket to send the message to. 201 cnfg: The database connection. 202 guild: The guild the ticket is in. 203 """ 204 if ticket.anonymous: 205 if ticket.user_id == message.author.id: 206 return 207 staff = False 208 staff_roles = await cnfg.get_all_staff_roles(guild.guild_id) 209 for role in staff_roles: 210 parsed_role = message.guild.get_role( # type: ignore 211 role.role_id) 212 if parsed_role in message.author.roles: # type: ignore 213 # Already checked for member 214 staff = True 215 break 216 if not staff: 217 return 218 await message.channel.send( 219 f"**{guild.staff_team_name}:** " 220 f"{utils.escape_mentions(message.content)}", 221 embeds=message.embeds, 222 ) 223 await message.delete() 224 225 async def update_autoclose(self, message: discord.Message, ticket: models.Ticket, guild: models.Guild, 226 cnfg: layer.OnlineConfig) -> None: 227 """Updates channel topic autoclose time. 228 229 Changes the channel topic to reflect the new autoclose time. 230 231 Args: 232 message: The message to check. 233 ticket: The ticket updated. 234 guild: The guild settings. 235 cnfg: The database connection. 236 """ 237 chan = message.channel 238 if guild.any_autoclose: 239 time_since_update = datetime.datetime.utcnow() - ticket.last_response 240 if time_since_update >= datetime.timedelta(minutes=5): 241 crrnt = chan.topic # type: ignore 242 if crrnt is None: 243 # pylint: disable=line-too-long 244 crrnt = ( 245 f"Ticket: {chan.name}\n" # type: ignore 246 # skipcq: FLK-E501 247 f"Closes: <t:{int((message.created_at + guild.any_autoclose).timestamp())}:R>") 248 else: 249 # pylint: disable=line-too-long 250 crrnt = re.sub( 251 r"<t:[0-9]*?:R>", 252 # skipcq: FLK-E501 253 f"<t:{int((message.created_at + guild.any_autoclose).timestamp())}:R>", 254 crrnt) 255 await chan.edit(topic=crrnt) # type: ignore 256 ticket.last_response = datetime.datetime.utcnow() 257 await cnfg.commit() 258 259 @commands.Cog.listener(name="on_guild_channel_create") 260 async def on_channel_create(self, channel: discord.abc.GuildChannel) -> None: 261 """Runs when a channel is created. 262 263 Handles the checking and facilitating of ticket creation. 264 This is the main event that handles the creation of tickets. 265 266 Args: 267 channel: The channel that was created. 268 """ 269 async with self._bt.get_connection() as confg: 270 if isinstance(channel, discord.channel.TextChannel): 271 gld = channel.guild 272 guild = await confg.get_guild( 273 gld.id, 274 ( 275 orm.selectinload(models.Guild.observers_roles), 276 orm.selectinload(models.Guild.community_roles), 277 orm.selectinload(models.Guild.community_pings), 278 ), 279 ) 280 if guild.integrated: 281 return 282 async for entry in gld.audit_logs(limit=3, action=discord.AuditLogAction.channel_create): 283 if not entry.user: 284 continue 285 if entry.target == channel and await confg.check_ticket_bot(entry.user.id, gld.id): 286 await self.ticket_creation(confg, (gld, guild), channel) 287 288 @commands.Cog.listener(name="on_guild_channel_delete") 289 async def on_channel_delete(self, channel: discord.abc.GuildChannel) -> None: 290 """Cleanups for when a ticket channel is deleted. 291 292 This is the main event that handles the deletion of tickets. 293 We remove stale tickets from the database. 294 295 Args: 296 channel: The channel that was deleted. 297 """ 298 if isinstance(channel, discord.channel.TextChannel): 299 async with self._bt.get_connection() as confg: 300 ticket = await confg.fetch_ticket(channel.id) 301 if ticket: 302 await confg.delete(ticket) 303 logging.info("Deleted ticket %s", channel.name) 304 await confg.commit() 305 306 @commands.Cog.listener(name="on_member_join") 307 async def on_member_join(self, member: discord.Member) -> None: 308 """Ensures penalty roles are sticky. 309 310 This just ensures that if a user has a penalty role, 311 it is reapplied when they join the server. 312 313 Args: 314 member: The member that joined. 315 """ 316 async with self._bt.get_connection() as cnfg: 317 actv_member = await cnfg.get_member(member.id, member.guild.id) 318 if actv_member.status: 319 if actv_member.status_till is not None: 320 # Split this up to avoid None comparison. 321 # pylint: disable=line-too-long 322 if actv_member.status_till <= datetime.datetime.utcnow(): # type: ignore 323 # Check if the penalty has expired. 324 actv_member.status = 0 325 actv_member.status_till = None 326 await cnfg.commit() 327 return 328 if actv_member.status == 1: 329 # Status 1 is a support block. 330 if actv_member.guild.support_block is None: 331 # If the role is unset, pardon the user. 332 actv_member.status = 0 333 actv_member.status_till = None 334 await cnfg.commit() 335 return 336 role = member.guild.get_role(actv_member.guild.support_block) 337 if role is not None: 338 await member.add_roles(role) 339 elif actv_member.status == 2: 340 # Status 2 is a helping block. 341 if actv_member.guild.helping_block is None: 342 # If the role is unset, pardon the user. 343 actv_member.status = 0 344 actv_member.status_till = None 345 await cnfg.commit() 346 return 347 role = member.guild.get_role(actv_member.guild.helping_block) 348 if role is not None: 349 await member.add_roles(role) 350 351 @commands.Cog.listener(name="on_message") 352 async def on_message(self, message: discord.Message) -> None: 353 """Handles all message-related features. 354 355 We use this to handle the message discovery feature, 356 searching for links to messages, resolving them, and 357 sending their contents as a reply. 358 Also handles anonymous mode for tickets. Resending 359 staff messages as the bot. 360 361 Args: 362 message: The message that was sent. 363 """ 364 async with self._bt.get_connection() as cnfg: 365 if message.author.bot or message.guild is None: 366 return 367 guild = await cnfg.get_guild(message.guild.id) 368 if guild.msg_discovery: 369 await self.message_discovery(message) 370 ticket = await cnfg.fetch_ticket(message.channel.id) 371 if ticket: 372 # Make sure the ticket exists 373 await self.handle_anon(message, ticket, cnfg, guild) 374 await self.update_autoclose(message, ticket, guild, cnfg)
Event handling for Tickets+.
This cog handles all events for Tickets+. This is used to handle events from Discord.
47 def __init__(self, bot_instance: bot.TicketsPlusBot) -> None: 48 """Initialises the cog instance. 49 50 We set some attributes here, so we can use them later. 51 52 Args: 53 bot_instance: The bot instance. 54 """ 55 self._bt = bot_instance 56 logging.info("Loaded %s", self.__class__.__name__)
Initialises the cog instance.
We set some attributes here, so we can use them later.
Arguments:
- bot_instance: The bot instance.
58 async def ticket_creation( 59 self: Any, 60 confg: layer.OnlineConfig, 61 guilded: Tuple[discord.Guild, models.Guild], 62 channel: discord.TextChannel, 63 user: None | discord.User = None, 64 ) -> None: 65 """Main ticket creation function. 66 67 Creates db and does some other stuff. 68 """ 69 gld, guild = guilded 70 ttypes = await confg.get_ticket_types(gld.id) 71 ticket_type = models.TicketType.default() 72 for ttype in ttypes: 73 if channel.name.startswith(ttype.prefix): 74 ticket_type = ttype 75 if ticket_type.ignore: 76 return 77 nts_thrd = None 78 if guild.legacy_threads: 79 nts_thrd = await legacy.thread_create(channel, guild, confg) 80 user_id = user.id if user else None 81 thr_id = nts_thrd.id if nts_thrd else None 82 await confg.get_ticket(channel.id, gld.id, user_id, thr_id) 83 if guild.helping_block: 84 overwrite = discord.PermissionOverwrite( 85 view_channel=False, 86 add_reactions=False, 87 send_messages=False, 88 read_messages=False, 89 read_message_history=False, 90 ) 91 rol = gld.get_role(guild.helping_block) 92 if rol is None: 93 guild.helping_block = None 94 else: 95 await channel.set_permissions( 96 rol, 97 overwrite=overwrite, 98 reason="Penalty Enforcement", 99 ) 100 if guild.community_roles and ticket_type.comaccs: 101 comm_roles = await confg.get_all_community_roles(gld.id) 102 overwrite = discord.PermissionOverwrite( 103 view_channel=True, 104 add_reactions=True, 105 send_messages=True, 106 read_messages=True, 107 read_message_history=True, 108 attach_files=True, 109 embed_links=True, 110 use_application_commands=True, 111 ) 112 for role in comm_roles: 113 rle = gld.get_role(role.role_id) 114 if rle is None: 115 continue 116 await channel.set_permissions( 117 rle, 118 overwrite=overwrite, 119 reason="Community Support Access", 120 ) 121 if guild.community_pings and ticket_type.comping: 122 comm_pings = await confg.get_all_community_pings(gld.id) 123 inv = await channel.send(" ".join([f"<@&{role.role_id}>" for role in comm_pings])) 124 await asyncio.sleep(0.25) 125 await inv.delete() 126 descr = (f"Ticket {channel.name}\n" 127 "Opened at " 128 f"<t:{int(channel.created_at.timestamp())}:f>") 129 if user: 130 descr += f"\nOpened by {user.mention}" 131 if guild.first_autoclose: 132 # skipcq: FLK-E501 # pylint: disable=line-too-long 133 descr += f"\nCloses <t:{int((channel.created_at + guild.first_autoclose).timestamp())}:R>" 134 # skipcq: FLK-E501 # pylint: disable=line-too-long 135 descr += "\nIf no one responds, the ticket will be closed automatically. Thank you for your patience!" 136 await channel.edit(topic=descr, reason="More information for the ticket.") 137 if guild.strip_buttons and ticket_type.strpbuttns: 138 await asyncio.sleep(5) 139 async for msg in channel.history(oldest_first=True, limit=2): 140 if await confg.check_ticket_bot(msg.author.id, gld.id) and msg.embeds: 141 await channel.send(embeds=msg.embeds) 142 await msg.delete() 143 await confg.commit()
Main ticket creation function.
Creates db and does some other stuff.
145 async def message_discovery(self, message: discord.Message) -> None: 146 """Discovers the message linked to. 147 148 Fetches a message from its discord link and responds with the 149 message content. 150 151 Args: 152 message: The message to check for links 153 """ 154 alpha = re.search( 155 # skipcq: FLK-E501 # pylint: disable=line-too-long 156 r"https://(?:canary\.)?discord\.com/channels/(?P<srv>\d*)/(?P<cha>\d*)/(?P<msg>\d*)", 157 message.content, 158 ) 159 if alpha: 160 # We do not check any types in try as we are catching. 161 try: 162 gld = self._bt.get_guild(int(alpha.group("srv"))) 163 chan: abc.GuildChannel = ( 164 gld.get_channel_or_thread( # type: ignore 165 int(alpha.group("cha")))) 166 got_msg = await chan.fetch_message( # type: ignore 167 int(alpha.group("msg"))) 168 except ( 169 AttributeError, 170 discord.HTTPException, 171 ): 172 logging.warning("Message discovery failed.") 173 else: 174 time = got_msg.created_at.strftime("%d/%m/%Y %H:%M:%S") 175 if not got_msg.content and got_msg.embeds: 176 discovered_result = got_msg.embeds[0] 177 discovered_result.set_footer(text="[EMBED CAPTURED] Sent in" 178 f" {chan.name}" 179 f" at {time}") 180 else: 181 discovered_result = discord.Embed(description=got_msg.content, color=0x0D0EB4) 182 discovered_result.set_footer(text="Sent in " 183 f"{chan.name} at {time}" # type: ignore 184 ) 185 discovered_result.set_author( 186 name=got_msg.author.name, 187 icon_url=got_msg.author.display_avatar.url, 188 ) 189 discovered_result.set_image(url=got_msg.attachments[0].url if got_msg.attachments else None) 190 await message.reply(embed=discovered_result)
Discovers the message linked to.
Fetches a message from its discord link and responds with the message content.
Arguments:
- message: The message to check for links
192 async def handle_anon(self, message: discord.Message, ticket: models.Ticket, cnfg: layer.OnlineConfig, 193 guild: models.Guild) -> None: 194 """Handles ticket anon messages. 195 196 Grabs a message and anonymizes it, then send it to the ticket. 197 198 Args: 199 message: The message to handle. 200 ticket: The ticket to send the message to. 201 cnfg: The database connection. 202 guild: The guild the ticket is in. 203 """ 204 if ticket.anonymous: 205 if ticket.user_id == message.author.id: 206 return 207 staff = False 208 staff_roles = await cnfg.get_all_staff_roles(guild.guild_id) 209 for role in staff_roles: 210 parsed_role = message.guild.get_role( # type: ignore 211 role.role_id) 212 if parsed_role in message.author.roles: # type: ignore 213 # Already checked for member 214 staff = True 215 break 216 if not staff: 217 return 218 await message.channel.send( 219 f"**{guild.staff_team_name}:** " 220 f"{utils.escape_mentions(message.content)}", 221 embeds=message.embeds, 222 ) 223 await message.delete()
Handles ticket anon messages.
Grabs a message and anonymizes it, then send it to the ticket.
Arguments:
- message: The message to handle.
- ticket: The ticket to send the message to.
- cnfg: The database connection.
- guild: The guild the ticket is in.
225 async def update_autoclose(self, message: discord.Message, ticket: models.Ticket, guild: models.Guild, 226 cnfg: layer.OnlineConfig) -> None: 227 """Updates channel topic autoclose time. 228 229 Changes the channel topic to reflect the new autoclose time. 230 231 Args: 232 message: The message to check. 233 ticket: The ticket updated. 234 guild: The guild settings. 235 cnfg: The database connection. 236 """ 237 chan = message.channel 238 if guild.any_autoclose: 239 time_since_update = datetime.datetime.utcnow() - ticket.last_response 240 if time_since_update >= datetime.timedelta(minutes=5): 241 crrnt = chan.topic # type: ignore 242 if crrnt is None: 243 # pylint: disable=line-too-long 244 crrnt = ( 245 f"Ticket: {chan.name}\n" # type: ignore 246 # skipcq: FLK-E501 247 f"Closes: <t:{int((message.created_at + guild.any_autoclose).timestamp())}:R>") 248 else: 249 # pylint: disable=line-too-long 250 crrnt = re.sub( 251 r"<t:[0-9]*?:R>", 252 # skipcq: FLK-E501 253 f"<t:{int((message.created_at + guild.any_autoclose).timestamp())}:R>", 254 crrnt) 255 await chan.edit(topic=crrnt) # type: ignore 256 ticket.last_response = datetime.datetime.utcnow() 257 await cnfg.commit()
Updates channel topic autoclose time.
Changes the channel topic to reflect the new autoclose time.
Arguments:
- message: The message to check.
- ticket: The ticket updated.
- guild: The guild settings.
- cnfg: The database connection.
259 @commands.Cog.listener(name="on_guild_channel_create") 260 async def on_channel_create(self, channel: discord.abc.GuildChannel) -> None: 261 """Runs when a channel is created. 262 263 Handles the checking and facilitating of ticket creation. 264 This is the main event that handles the creation of tickets. 265 266 Args: 267 channel: The channel that was created. 268 """ 269 async with self._bt.get_connection() as confg: 270 if isinstance(channel, discord.channel.TextChannel): 271 gld = channel.guild 272 guild = await confg.get_guild( 273 gld.id, 274 ( 275 orm.selectinload(models.Guild.observers_roles), 276 orm.selectinload(models.Guild.community_roles), 277 orm.selectinload(models.Guild.community_pings), 278 ), 279 ) 280 if guild.integrated: 281 return 282 async for entry in gld.audit_logs(limit=3, action=discord.AuditLogAction.channel_create): 283 if not entry.user: 284 continue 285 if entry.target == channel and await confg.check_ticket_bot(entry.user.id, gld.id): 286 await self.ticket_creation(confg, (gld, guild), channel)
Runs when a channel is created.
Handles the checking and facilitating of ticket creation. This is the main event that handles the creation of tickets.
Arguments:
- channel: The channel that was created.
288 @commands.Cog.listener(name="on_guild_channel_delete") 289 async def on_channel_delete(self, channel: discord.abc.GuildChannel) -> None: 290 """Cleanups for when a ticket channel is deleted. 291 292 This is the main event that handles the deletion of tickets. 293 We remove stale tickets from the database. 294 295 Args: 296 channel: The channel that was deleted. 297 """ 298 if isinstance(channel, discord.channel.TextChannel): 299 async with self._bt.get_connection() as confg: 300 ticket = await confg.fetch_ticket(channel.id) 301 if ticket: 302 await confg.delete(ticket) 303 logging.info("Deleted ticket %s", channel.name) 304 await confg.commit()
Cleanups for when a ticket channel is deleted.
This is the main event that handles the deletion of tickets. We remove stale tickets from the database.
Arguments:
- channel: The channel that was deleted.
306 @commands.Cog.listener(name="on_member_join") 307 async def on_member_join(self, member: discord.Member) -> None: 308 """Ensures penalty roles are sticky. 309 310 This just ensures that if a user has a penalty role, 311 it is reapplied when they join the server. 312 313 Args: 314 member: The member that joined. 315 """ 316 async with self._bt.get_connection() as cnfg: 317 actv_member = await cnfg.get_member(member.id, member.guild.id) 318 if actv_member.status: 319 if actv_member.status_till is not None: 320 # Split this up to avoid None comparison. 321 # pylint: disable=line-too-long 322 if actv_member.status_till <= datetime.datetime.utcnow(): # type: ignore 323 # Check if the penalty has expired. 324 actv_member.status = 0 325 actv_member.status_till = None 326 await cnfg.commit() 327 return 328 if actv_member.status == 1: 329 # Status 1 is a support block. 330 if actv_member.guild.support_block is None: 331 # If the role is unset, pardon the user. 332 actv_member.status = 0 333 actv_member.status_till = None 334 await cnfg.commit() 335 return 336 role = member.guild.get_role(actv_member.guild.support_block) 337 if role is not None: 338 await member.add_roles(role) 339 elif actv_member.status == 2: 340 # Status 2 is a helping block. 341 if actv_member.guild.helping_block is None: 342 # If the role is unset, pardon the user. 343 actv_member.status = 0 344 actv_member.status_till = None 345 await cnfg.commit() 346 return 347 role = member.guild.get_role(actv_member.guild.helping_block) 348 if role is not None: 349 await member.add_roles(role)
Ensures penalty roles are sticky.
This just ensures that if a user has a penalty role, it is reapplied when they join the server.
Arguments:
- member: The member that joined.
351 @commands.Cog.listener(name="on_message") 352 async def on_message(self, message: discord.Message) -> None: 353 """Handles all message-related features. 354 355 We use this to handle the message discovery feature, 356 searching for links to messages, resolving them, and 357 sending their contents as a reply. 358 Also handles anonymous mode for tickets. Resending 359 staff messages as the bot. 360 361 Args: 362 message: The message that was sent. 363 """ 364 async with self._bt.get_connection() as cnfg: 365 if message.author.bot or message.guild is None: 366 return 367 guild = await cnfg.get_guild(message.guild.id) 368 if guild.msg_discovery: 369 await self.message_discovery(message) 370 ticket = await cnfg.fetch_ticket(message.channel.id) 371 if ticket: 372 # Make sure the ticket exists 373 await self.handle_anon(message, ticket, cnfg, guild) 374 await self.update_autoclose(message, ticket, guild, cnfg)
Handles all message-related features.
We use this to handle the message discovery feature, searching for links to messages, resolving them, and sending their contents as a reply. Also handles anonymous mode for tickets. Resending staff messages as the bot.
Arguments:
- message: The message that was sent.
377async def setup(bot_instance: bot.TicketsPlusBot) -> None: 378 """Sets up the Events handler. 379 380 This is called when the cog is loaded. 381 It adds the cog to the bot. 382 383 Args: 384 bot_instance: The bot that is loading the cog. 385 """ 386 await bot_instance.add_cog(Events(bot_instance))
Sets up the Events handler.
This is called when the cog is loaded. It adds the cog to the bot.
Arguments:
- bot_instance: The bot that is loading the cog.