Edit on GitHub

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))
class Events(discord.ext.commands.cog.Cog):
 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.

Events(bot_instance: tickets_plus.bot.TicketsPlusBot)
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.
async def ticket_creation( self: Any, confg: tickets_plus.database.layer.OnlineConfig, guilded: Tuple[discord.guild.Guild, tickets_plus.database.models.Guild], channel: discord.channel.TextChannel, user: None | discord.user.User = None) -> None:
 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.

async def message_discovery(self, message: discord.message.Message) -> None:
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
async def handle_anon( self, message: discord.message.Message, ticket: tickets_plus.database.models.Ticket, cnfg: tickets_plus.database.layer.OnlineConfig, guild: tickets_plus.database.models.Guild) -> None:
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.
async def update_autoclose( self, message: discord.message.Message, ticket: tickets_plus.database.models.Ticket, guild: tickets_plus.database.models.Guild, cnfg: tickets_plus.database.layer.OnlineConfig) -> None:
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.
@commands.Cog.listener(name='on_guild_channel_create')
async def on_channel_create(self, channel: discord.abc.GuildChannel) -> None:
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.
@commands.Cog.listener(name='on_guild_channel_delete')
async def on_channel_delete(self, channel: discord.abc.GuildChannel) -> None:
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.
@commands.Cog.listener(name='on_member_join')
async def on_member_join(self, member: discord.member.Member) -> None:
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.
@commands.Cog.listener(name='on_message')
async def on_message(self, message: discord.message.Message) -> None:
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.
async def setup(bot_instance: tickets_plus.bot.TicketsPlusBot) -> None:
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.