We are currently solving the following issue within our repository. Here is the issue text:
--- BEGIN ISSUE ---
Verify command shouldn't log tracebacks for DM failure.
Sentry Issue: [BOT-11](https://sentry.io/organizations/python-discord/issues/1526042507/?referrer=github_integration)

It currently uses `log.exception()` for all exceptions. `Forbidden` should be a special case and use a lower log level. A better approach may be to only handle `Forbidden` and re-raise all other exceptions (to defer it to the default command error handler). A `finally` block can be used to still delete the message afterwards.

```py
        try:
            await ctx.author.send(WELCOME_MESSAGE)
        except Exception:
            # Catch the exception, in case they have DMs off or something
            log.exception(f"Unable to send welcome message to user {ctx.author}.")
```

```
Forbidden: 403 FORBIDDEN (error code: 50007): Cannot send messages to this user
  File "bot/cogs/verification.py", line 129, in accept_command
    await ctx.author.send(WELCOME_MESSAGE)
  File "discord/abc.py", line 856, in send
    data = await state.http.send_message(channel.id, content, tts=tts, embed=embed, nonce=nonce)
  File "discord/http.py", line 216, in request
    raise Forbidden(r, data)

Unable to send welcome message to user <REDACTED>.
```

--- END ISSUE ---

Below are some code segments, each from a relevant file. One or more of these files may contain bugs.
--- BEGIN FILES ---
Path: `bot/cogs/verification.py`
Content:
```
1 import logging
2 from datetime import datetime
3 
4 from discord import Colour, Message, NotFound, Object
5 from discord.ext import tasks
6 from discord.ext.commands import Cog, Context, command
7 
8 from bot.bot import Bot
9 from bot.cogs.moderation import ModLog
10 from bot.constants import (
11     Bot as BotConfig,
12     Channels, Colours, Event,
13     Filter, Icons, MODERATION_ROLES, Roles
14 )
15 from bot.decorators import InChannelCheckFailure, in_channel, without_role
16 from bot.utils.checks import without_role_check
17 
18 log = logging.getLogger(__name__)
19 
20 WELCOME_MESSAGE = f"""
21 Hello! Welcome to the server, and thanks for verifying yourself!
22 
23 For your records, these are the documents you accepted:
24 
25 `1)` Our rules, here: <https://pythondiscord.com/pages/rules>
26 `2)` Our privacy policy, here: <https://pythondiscord.com/pages/privacy> - you can find information on how to have \
27 your information removed here as well.
28 
29 Feel free to review them at any point!
30 
31 Additionally, if you'd like to receive notifications for the announcements we post in <#{Channels.announcements}> \
32 from time to time, you can send `!subscribe` to <#{Channels.bot}> at any time to assign yourself the \
33 **Announcements** role. We'll mention this role every time we make an announcement.
34 
35 If you'd like to unsubscribe from the announcement notifications, simply send `!unsubscribe` to <#{Channels.bot}>.
36 """
37 
38 PERIODIC_PING = (
39     f"@everyone To verify that you have read our rules, please type `{BotConfig.prefix}accept`."
40     f" If you encounter any problems during the verification process, ping the <@&{Roles.admin}> role in this channel."
41 )
42 BOT_MESSAGE_DELETE_DELAY = 10
43 
44 
45 class Verification(Cog):
46     """User verification and role self-management."""
47 
48     def __init__(self, bot: Bot):
49         self.bot = bot
50         self.periodic_ping.start()
51 
52     @property
53     def mod_log(self) -> ModLog:
54         """Get currently loaded ModLog cog instance."""
55         return self.bot.get_cog("ModLog")
56 
57     @Cog.listener()
58     async def on_message(self, message: Message) -> None:
59         """Check new message event for messages to the checkpoint channel & process."""
60         if message.channel.id != Channels.verification:
61             return  # Only listen for #checkpoint messages
62 
63         if message.author.bot:
64             # They're a bot, delete their message after the delay.
65             # But not the periodic ping; we like that one.
66             if message.content != PERIODIC_PING:
67                 await message.delete(delay=BOT_MESSAGE_DELETE_DELAY)
68             return
69 
70         # if a user mentions a role or guild member
71         # alert the mods in mod-alerts channel
72         if message.mentions or message.role_mentions:
73             log.debug(
74                 f"{message.author} mentioned one or more users "
75                 f"and/or roles in {message.channel.name}"
76             )
77 
78             embed_text = (
79                 f"{message.author.mention} sent a message in "
80                 f"{message.channel.mention} that contained user and/or role mentions."
81                 f"\n\n**Original message:**\n>>> {message.content}"
82             )
83 
84             # Send pretty mod log embed to mod-alerts
85             await self.mod_log.send_log_message(
86                 icon_url=Icons.filtering,
87                 colour=Colour(Colours.soft_red),
88                 title=f"User/Role mentioned in {message.channel.name}",
89                 text=embed_text,
90                 thumbnail=message.author.avatar_url_as(static_format="png"),
91                 channel_id=Channels.mod_alerts,
92                 ping_everyone=Filter.ping_everyone,
93             )
94 
95         ctx = await self.bot.get_context(message)  # type: Context
96 
97         if ctx.command is not None and ctx.command.name == "accept":
98             return  # They used the accept command
99 
100         for role in ctx.author.roles:
101             if role.id == Roles.verified:
102                 log.warning(f"{ctx.author} posted '{ctx.message.content}' "
103                             "in the verification channel, but is already verified.")
104                 return  # They're already verified
105 
106         log.debug(f"{ctx.author} posted '{ctx.message.content}' in the verification "
107                   "channel. We are providing instructions how to verify.")
108         await ctx.send(
109             f"{ctx.author.mention} Please type `!accept` to verify that you accept our rules, "
110             f"and gain access to the rest of the server.",
111             delete_after=20
112         )
113 
114         log.trace(f"Deleting the message posted by {ctx.author}")
115 
116         try:
117             await ctx.message.delete()
118         except NotFound:
119             log.trace("No message found, it must have been deleted by another bot.")
120 
121     @command(name='accept', aliases=('verify', 'verified', 'accepted'), hidden=True)
122     @without_role(Roles.verified)
123     @in_channel(Channels.verification)
124     async def accept_command(self, ctx: Context, *_) -> None:  # We don't actually care about the args
125         """Accept our rules and gain access to the rest of the server."""
126         log.debug(f"{ctx.author} called !accept. Assigning the 'Developer' role.")
127         await ctx.author.add_roles(Object(Roles.verified), reason="Accepted the rules")
128         try:
129             await ctx.author.send(WELCOME_MESSAGE)
130         except Exception:
131             # Catch the exception, in case they have DMs off or something
132             log.exception(f"Unable to send welcome message to user {ctx.author}.")
133 
134         log.trace(f"Deleting the message posted by {ctx.author}.")
135 
136         try:
137             self.mod_log.ignore(Event.message_delete, ctx.message.id)
138             await ctx.message.delete()
139         except NotFound:
140             log.trace("No message found, it must have been deleted by another bot.")
141 
142     @command(name='subscribe')
143     @in_channel(Channels.bot)
144     async def subscribe_command(self, ctx: Context, *_) -> None:  # We don't actually care about the args
145         """Subscribe to announcement notifications by assigning yourself the role."""
146         has_role = False
147 
148         for role in ctx.author.roles:
149             if role.id == Roles.announcements:
150                 has_role = True
151                 break
152 
153         if has_role:
154             await ctx.send(f"{ctx.author.mention} You're already subscribed!")
155             return
156 
157         log.debug(f"{ctx.author} called !subscribe. Assigning the 'Announcements' role.")
158         await ctx.author.add_roles(Object(Roles.announcements), reason="Subscribed to announcements")
159 
160         log.trace(f"Deleting the message posted by {ctx.author}.")
161 
162         await ctx.send(
163             f"{ctx.author.mention} Subscribed to <#{Channels.announcements}> notifications.",
164         )
165 
166     @command(name='unsubscribe')
167     @in_channel(Channels.bot)
168     async def unsubscribe_command(self, ctx: Context, *_) -> None:  # We don't actually care about the args
169         """Unsubscribe from announcement notifications by removing the role from yourself."""
170         has_role = False
171 
172         for role in ctx.author.roles:
173             if role.id == Roles.announcements:
174                 has_role = True
175                 break
176 
177         if not has_role:
178             await ctx.send(f"{ctx.author.mention} You're already unsubscribed!")
179             return
180 
181         log.debug(f"{ctx.author} called !unsubscribe. Removing the 'Announcements' role.")
182         await ctx.author.remove_roles(Object(Roles.announcements), reason="Unsubscribed from announcements")
183 
184         log.trace(f"Deleting the message posted by {ctx.author}.")
185 
186         await ctx.send(
187             f"{ctx.author.mention} Unsubscribed from <#{Channels.announcements}> notifications."
188         )
189 
190     # This cannot be static (must have a __func__ attribute).
191     async def cog_command_error(self, ctx: Context, error: Exception) -> None:
192         """Check for & ignore any InChannelCheckFailure."""
193         if isinstance(error, InChannelCheckFailure):
194             error.handled = True
195 
196     @staticmethod
197     def bot_check(ctx: Context) -> bool:
198         """Block any command within the verification channel that is not !accept."""
199         if ctx.channel.id == Channels.verification and without_role_check(ctx, *MODERATION_ROLES):
200             return ctx.command.name == "accept"
201         else:
202             return True
203 
204     @tasks.loop(hours=12)
205     async def periodic_ping(self) -> None:
206         """Every week, mention @everyone to remind them to verify."""
207         messages = self.bot.get_channel(Channels.verification).history(limit=10)
208         need_to_post = True  # True if a new message needs to be sent.
209 
210         async for message in messages:
211             if message.author == self.bot.user and message.content == PERIODIC_PING:
212                 delta = datetime.utcnow() - message.created_at  # Time since last message.
213                 if delta.days >= 7:  # Message is older than a week.
214                     await message.delete()
215                 else:
216                     need_to_post = False
217 
218                 break
219 
220         if need_to_post:
221             await self.bot.get_channel(Channels.verification).send(PERIODIC_PING)
222 
223     @periodic_ping.before_loop
224     async def before_ping(self) -> None:
225         """Only start the loop when the bot is ready."""
226         await self.bot.wait_until_ready()
227 
228     def cog_unload(self) -> None:
229         """Cancel the periodic ping task when the cog is unloaded."""
230         self.periodic_ping.cancel()
231 
232 
233 def setup(bot: Bot) -> None:
234     """Load the Verification cog."""
235     bot.add_cog(Verification(bot))
236
```
--- END FILES ---

Please first localize the bug based on the issue statement, and then generate a patch according to the `git diff` format fenced by three backticks.

Here is an example:

```diff
diff --git a/examples/server_async.py b/examples/server_async.py
--- a/examples/server_async.py
+++ b/examples/server_async.py
@@ -313,4 +313,4 @@
 
 
 if __name__ == "__main__":
-    asyncio.run(run_async_server("."), debug=True)
+    asyncio.run(run_async_server(), debug=True)
diff --git a/examples/server_sync.py b/examples/server_sync.py
--- a/examples/server_sync.py
+++ b/examples/server_sync.py
@@ -313,5 +313,5 @@
 
 
 if __name__ == "__main__":
-    server = run_sync_server(".")
+    server = run_sync_server()
     server.shutdown()

```

diff --git a/bot/cogs/verification.py b/bot/cogs/verification.py
--- a/bot/cogs/verification.py
+++ b/bot/cogs/verification.py
@@ -1,7 +1,8 @@
 import logging
+from contextlib import suppress
 from datetime import datetime
 
-from discord import Colour, Message, NotFound, Object
+from discord import Colour, Forbidden, Message, NotFound, Object
 from discord.ext import tasks
 from discord.ext.commands import Cog, Context, command
 
@@ -127,17 +128,13 @@
         await ctx.author.add_roles(Object(Roles.verified), reason="Accepted the rules")
         try:
             await ctx.author.send(WELCOME_MESSAGE)
-        except Exception:
-            # Catch the exception, in case they have DMs off or something
-            log.exception(f"Unable to send welcome message to user {ctx.author}.")
-
-        log.trace(f"Deleting the message posted by {ctx.author}.")
-
-        try:
-            self.mod_log.ignore(Event.message_delete, ctx.message.id)
-            await ctx.message.delete()
-        except NotFound:
-            log.trace("No message found, it must have been deleted by another bot.")
+        except Forbidden:
+            log.info(f"Sending welcome message failed for {ctx.author}.")
+        finally:
+            log.trace(f"Deleting accept message by {ctx.author}.")
+            with suppress(NotFound):
+                self.mod_log.ignore(Event.message_delete, ctx.message.id)
+                await ctx.message.delete()
 
     @command(name='subscribe')
     @in_channel(Channels.bot)

{"golden_diff": "diff --git a/bot/cogs/verification.py b/bot/cogs/verification.py\n--- a/bot/cogs/verification.py\n+++ b/bot/cogs/verification.py\n@@ -1,7 +1,8 @@\n import logging\n+from contextlib import suppress\n from datetime import datetime\n \n-from discord import Colour, Message, NotFound, Object\n+from discord import Colour, Forbidden, Message, NotFound, Object\n from discord.ext import tasks\n from discord.ext.commands import Cog, Context, command\n \n@@ -127,17 +128,13 @@\n         await ctx.author.add_roles(Object(Roles.verified), reason=\"Accepted the rules\")\n         try:\n             await ctx.author.send(WELCOME_MESSAGE)\n-        except Exception:\n-            # Catch the exception, in case they have DMs off or something\n-            log.exception(f\"Unable to send welcome message to user {ctx.author}.\")\n-\n-        log.trace(f\"Deleting the message posted by {ctx.author}.\")\n-\n-        try:\n-            self.mod_log.ignore(Event.message_delete, ctx.message.id)\n-            await ctx.message.delete()\n-        except NotFound:\n-            log.trace(\"No message found, it must have been deleted by another bot.\")\n+        except Forbidden:\n+            log.info(f\"Sending welcome message failed for {ctx.author}.\")\n+        finally:\n+            log.trace(f\"Deleting accept message by {ctx.author}.\")\n+            with suppress(NotFound):\n+                self.mod_log.ignore(Event.message_delete, ctx.message.id)\n+                await ctx.message.delete()\n \n     @command(name='subscribe')\n     @in_channel(Channels.bot)\n", "issue": "Verify command shouldn't log tracebacks for DM failure.\nSentry Issue: [BOT-11](https://sentry.io/organizations/python-discord/issues/1526042507/?referrer=github_integration)\r\n\r\nIt currently uses `log.exception()` for all exceptions. `Forbidden` should be a special case and use a lower log level. A better approach may be to only handle `Forbidden` and re-raise all other exceptions (to defer it to the default command error handler). A `finally` block can be used to still delete the message afterwards.\r\n\r\n```py\r\n        try:\r\n            await ctx.author.send(WELCOME_MESSAGE)\r\n        except Exception:\r\n            # Catch the exception, in case they have DMs off or something\r\n            log.exception(f\"Unable to send welcome message to user {ctx.author}.\")\r\n```\r\n\r\n```\r\nForbidden: 403 FORBIDDEN (error code: 50007): Cannot send messages to this user\r\n  File \"bot/cogs/verification.py\", line 129, in accept_command\r\n    await ctx.author.send(WELCOME_MESSAGE)\r\n  File \"discord/abc.py\", line 856, in send\r\n    data = await state.http.send_message(channel.id, content, tts=tts, embed=embed, nonce=nonce)\r\n  File \"discord/http.py\", line 216, in request\r\n    raise Forbidden(r, data)\r\n\r\nUnable to send welcome message to user <REDACTED>.\r\n```\n", "before_files": [{"content": "import logging\nfrom datetime import datetime\n\nfrom discord import Colour, Message, NotFound, Object\nfrom discord.ext import tasks\nfrom discord.ext.commands import Cog, Context, command\n\nfrom bot.bot import Bot\nfrom bot.cogs.moderation import ModLog\nfrom bot.constants import (\n    Bot as BotConfig,\n    Channels, Colours, Event,\n    Filter, Icons, MODERATION_ROLES, Roles\n)\nfrom bot.decorators import InChannelCheckFailure, in_channel, without_role\nfrom bot.utils.checks import without_role_check\n\nlog = logging.getLogger(__name__)\n\nWELCOME_MESSAGE = f\"\"\"\nHello! Welcome to the server, and thanks for verifying yourself!\n\nFor your records, these are the documents you accepted:\n\n`1)` Our rules, here: <https://pythondiscord.com/pages/rules>\n`2)` Our privacy policy, here: <https://pythondiscord.com/pages/privacy> - you can find information on how to have \\\nyour information removed here as well.\n\nFeel free to review them at any point!\n\nAdditionally, if you'd like to receive notifications for the announcements we post in <#{Channels.announcements}> \\\nfrom time to time, you can send `!subscribe` to <#{Channels.bot}> at any time to assign yourself the \\\n**Announcements** role. We'll mention this role every time we make an announcement.\n\nIf you'd like to unsubscribe from the announcement notifications, simply send `!unsubscribe` to <#{Channels.bot}>.\n\"\"\"\n\nPERIODIC_PING = (\n    f\"@everyone To verify that you have read our rules, please type `{BotConfig.prefix}accept`.\"\n    f\" If you encounter any problems during the verification process, ping the <@&{Roles.admin}> role in this channel.\"\n)\nBOT_MESSAGE_DELETE_DELAY = 10\n\n\nclass Verification(Cog):\n    \"\"\"User verification and role self-management.\"\"\"\n\n    def __init__(self, bot: Bot):\n        self.bot = bot\n        self.periodic_ping.start()\n\n    @property\n    def mod_log(self) -> ModLog:\n        \"\"\"Get currently loaded ModLog cog instance.\"\"\"\n        return self.bot.get_cog(\"ModLog\")\n\n    @Cog.listener()\n    async def on_message(self, message: Message) -> None:\n        \"\"\"Check new message event for messages to the checkpoint channel & process.\"\"\"\n        if message.channel.id != Channels.verification:\n            return  # Only listen for #checkpoint messages\n\n        if message.author.bot:\n            # They're a bot, delete their message after the delay.\n            # But not the periodic ping; we like that one.\n            if message.content != PERIODIC_PING:\n                await message.delete(delay=BOT_MESSAGE_DELETE_DELAY)\n            return\n\n        # if a user mentions a role or guild member\n        # alert the mods in mod-alerts channel\n        if message.mentions or message.role_mentions:\n            log.debug(\n                f\"{message.author} mentioned one or more users \"\n                f\"and/or roles in {message.channel.name}\"\n            )\n\n            embed_text = (\n                f\"{message.author.mention} sent a message in \"\n                f\"{message.channel.mention} that contained user and/or role mentions.\"\n                f\"\\n\\n**Original message:**\\n>>> {message.content}\"\n            )\n\n            # Send pretty mod log embed to mod-alerts\n            await self.mod_log.send_log_message(\n                icon_url=Icons.filtering,\n                colour=Colour(Colours.soft_red),\n                title=f\"User/Role mentioned in {message.channel.name}\",\n                text=embed_text,\n                thumbnail=message.author.avatar_url_as(static_format=\"png\"),\n                channel_id=Channels.mod_alerts,\n                ping_everyone=Filter.ping_everyone,\n            )\n\n        ctx = await self.bot.get_context(message)  # type: Context\n\n        if ctx.command is not None and ctx.command.name == \"accept\":\n            return  # They used the accept command\n\n        for role in ctx.author.roles:\n            if role.id == Roles.verified:\n                log.warning(f\"{ctx.author} posted '{ctx.message.content}' \"\n                            \"in the verification channel, but is already verified.\")\n                return  # They're already verified\n\n        log.debug(f\"{ctx.author} posted '{ctx.message.content}' in the verification \"\n                  \"channel. We are providing instructions how to verify.\")\n        await ctx.send(\n            f\"{ctx.author.mention} Please type `!accept` to verify that you accept our rules, \"\n            f\"and gain access to the rest of the server.\",\n            delete_after=20\n        )\n\n        log.trace(f\"Deleting the message posted by {ctx.author}\")\n\n        try:\n            await ctx.message.delete()\n        except NotFound:\n            log.trace(\"No message found, it must have been deleted by another bot.\")\n\n    @command(name='accept', aliases=('verify', 'verified', 'accepted'), hidden=True)\n    @without_role(Roles.verified)\n    @in_channel(Channels.verification)\n    async def accept_command(self, ctx: Context, *_) -> None:  # We don't actually care about the args\n        \"\"\"Accept our rules and gain access to the rest of the server.\"\"\"\n        log.debug(f\"{ctx.author} called !accept. Assigning the 'Developer' role.\")\n        await ctx.author.add_roles(Object(Roles.verified), reason=\"Accepted the rules\")\n        try:\n            await ctx.author.send(WELCOME_MESSAGE)\n        except Exception:\n            # Catch the exception, in case they have DMs off or something\n            log.exception(f\"Unable to send welcome message to user {ctx.author}.\")\n\n        log.trace(f\"Deleting the message posted by {ctx.author}.\")\n\n        try:\n            self.mod_log.ignore(Event.message_delete, ctx.message.id)\n            await ctx.message.delete()\n        except NotFound:\n            log.trace(\"No message found, it must have been deleted by another bot.\")\n\n    @command(name='subscribe')\n    @in_channel(Channels.bot)\n    async def subscribe_command(self, ctx: Context, *_) -> None:  # We don't actually care about the args\n        \"\"\"Subscribe to announcement notifications by assigning yourself the role.\"\"\"\n        has_role = False\n\n        for role in ctx.author.roles:\n            if role.id == Roles.announcements:\n                has_role = True\n                break\n\n        if has_role:\n            await ctx.send(f\"{ctx.author.mention} You're already subscribed!\")\n            return\n\n        log.debug(f\"{ctx.author} called !subscribe. Assigning the 'Announcements' role.\")\n        await ctx.author.add_roles(Object(Roles.announcements), reason=\"Subscribed to announcements\")\n\n        log.trace(f\"Deleting the message posted by {ctx.author}.\")\n\n        await ctx.send(\n            f\"{ctx.author.mention} Subscribed to <#{Channels.announcements}> notifications.\",\n        )\n\n    @command(name='unsubscribe')\n    @in_channel(Channels.bot)\n    async def unsubscribe_command(self, ctx: Context, *_) -> None:  # We don't actually care about the args\n        \"\"\"Unsubscribe from announcement notifications by removing the role from yourself.\"\"\"\n        has_role = False\n\n        for role in ctx.author.roles:\n            if role.id == Roles.announcements:\n                has_role = True\n                break\n\n        if not has_role:\n            await ctx.send(f\"{ctx.author.mention} You're already unsubscribed!\")\n            return\n\n        log.debug(f\"{ctx.author} called !unsubscribe. Removing the 'Announcements' role.\")\n        await ctx.author.remove_roles(Object(Roles.announcements), reason=\"Unsubscribed from announcements\")\n\n        log.trace(f\"Deleting the message posted by {ctx.author}.\")\n\n        await ctx.send(\n            f\"{ctx.author.mention} Unsubscribed from <#{Channels.announcements}> notifications.\"\n        )\n\n    # This cannot be static (must have a __func__ attribute).\n    async def cog_command_error(self, ctx: Context, error: Exception) -> None:\n        \"\"\"Check for & ignore any InChannelCheckFailure.\"\"\"\n        if isinstance(error, InChannelCheckFailure):\n            error.handled = True\n\n    @staticmethod\n    def bot_check(ctx: Context) -> bool:\n        \"\"\"Block any command within the verification channel that is not !accept.\"\"\"\n        if ctx.channel.id == Channels.verification and without_role_check(ctx, *MODERATION_ROLES):\n            return ctx.command.name == \"accept\"\n        else:\n            return True\n\n    @tasks.loop(hours=12)\n    async def periodic_ping(self) -> None:\n        \"\"\"Every week, mention @everyone to remind them to verify.\"\"\"\n        messages = self.bot.get_channel(Channels.verification).history(limit=10)\n        need_to_post = True  # True if a new message needs to be sent.\n\n        async for message in messages:\n            if message.author == self.bot.user and message.content == PERIODIC_PING:\n                delta = datetime.utcnow() - message.created_at  # Time since last message.\n                if delta.days >= 7:  # Message is older than a week.\n                    await message.delete()\n                else:\n                    need_to_post = False\n\n                break\n\n        if need_to_post:\n            await self.bot.get_channel(Channels.verification).send(PERIODIC_PING)\n\n    @periodic_ping.before_loop\n    async def before_ping(self) -> None:\n        \"\"\"Only start the loop when the bot is ready.\"\"\"\n        await self.bot.wait_until_ready()\n\n    def cog_unload(self) -> None:\n        \"\"\"Cancel the periodic ping task when the cog is unloaded.\"\"\"\n        self.periodic_ping.cancel()\n\n\ndef setup(bot: Bot) -> None:\n    \"\"\"Load the Verification cog.\"\"\"\n    bot.add_cog(Verification(bot))\n", "path": "bot/cogs/verification.py"}], "after_files": [{"content": "import logging\nfrom contextlib import suppress\nfrom datetime import datetime\n\nfrom discord import Colour, Forbidden, Message, NotFound, Object\nfrom discord.ext import tasks\nfrom discord.ext.commands import Cog, Context, command\n\nfrom bot.bot import Bot\nfrom bot.cogs.moderation import ModLog\nfrom bot.constants import (\n    Bot as BotConfig,\n    Channels, Colours, Event,\n    Filter, Icons, MODERATION_ROLES, Roles\n)\nfrom bot.decorators import InChannelCheckFailure, in_channel, without_role\nfrom bot.utils.checks import without_role_check\n\nlog = logging.getLogger(__name__)\n\nWELCOME_MESSAGE = f\"\"\"\nHello! Welcome to the server, and thanks for verifying yourself!\n\nFor your records, these are the documents you accepted:\n\n`1)` Our rules, here: <https://pythondiscord.com/pages/rules>\n`2)` Our privacy policy, here: <https://pythondiscord.com/pages/privacy> - you can find information on how to have \\\nyour information removed here as well.\n\nFeel free to review them at any point!\n\nAdditionally, if you'd like to receive notifications for the announcements we post in <#{Channels.announcements}> \\\nfrom time to time, you can send `!subscribe` to <#{Channels.bot}> at any time to assign yourself the \\\n**Announcements** role. We'll mention this role every time we make an announcement.\n\nIf you'd like to unsubscribe from the announcement notifications, simply send `!unsubscribe` to <#{Channels.bot}>.\n\"\"\"\n\nPERIODIC_PING = (\n    f\"@everyone To verify that you have read our rules, please type `{BotConfig.prefix}accept`.\"\n    f\" If you encounter any problems during the verification process, ping the <@&{Roles.admin}> role in this channel.\"\n)\nBOT_MESSAGE_DELETE_DELAY = 10\n\n\nclass Verification(Cog):\n    \"\"\"User verification and role self-management.\"\"\"\n\n    def __init__(self, bot: Bot):\n        self.bot = bot\n        self.periodic_ping.start()\n\n    @property\n    def mod_log(self) -> ModLog:\n        \"\"\"Get currently loaded ModLog cog instance.\"\"\"\n        return self.bot.get_cog(\"ModLog\")\n\n    @Cog.listener()\n    async def on_message(self, message: Message) -> None:\n        \"\"\"Check new message event for messages to the checkpoint channel & process.\"\"\"\n        if message.channel.id != Channels.verification:\n            return  # Only listen for #checkpoint messages\n\n        if message.author.bot:\n            # They're a bot, delete their message after the delay.\n            # But not the periodic ping; we like that one.\n            if message.content != PERIODIC_PING:\n                await message.delete(delay=BOT_MESSAGE_DELETE_DELAY)\n            return\n\n        # if a user mentions a role or guild member\n        # alert the mods in mod-alerts channel\n        if message.mentions or message.role_mentions:\n            log.debug(\n                f\"{message.author} mentioned one or more users \"\n                f\"and/or roles in {message.channel.name}\"\n            )\n\n            embed_text = (\n                f\"{message.author.mention} sent a message in \"\n                f\"{message.channel.mention} that contained user and/or role mentions.\"\n                f\"\\n\\n**Original message:**\\n>>> {message.content}\"\n            )\n\n            # Send pretty mod log embed to mod-alerts\n            await self.mod_log.send_log_message(\n                icon_url=Icons.filtering,\n                colour=Colour(Colours.soft_red),\n                title=f\"User/Role mentioned in {message.channel.name}\",\n                text=embed_text,\n                thumbnail=message.author.avatar_url_as(static_format=\"png\"),\n                channel_id=Channels.mod_alerts,\n                ping_everyone=Filter.ping_everyone,\n            )\n\n        ctx = await self.bot.get_context(message)  # type: Context\n\n        if ctx.command is not None and ctx.command.name == \"accept\":\n            return  # They used the accept command\n\n        for role in ctx.author.roles:\n            if role.id == Roles.verified:\n                log.warning(f\"{ctx.author} posted '{ctx.message.content}' \"\n                            \"in the verification channel, but is already verified.\")\n                return  # They're already verified\n\n        log.debug(f\"{ctx.author} posted '{ctx.message.content}' in the verification \"\n                  \"channel. We are providing instructions how to verify.\")\n        await ctx.send(\n            f\"{ctx.author.mention} Please type `!accept` to verify that you accept our rules, \"\n            f\"and gain access to the rest of the server.\",\n            delete_after=20\n        )\n\n        log.trace(f\"Deleting the message posted by {ctx.author}\")\n\n        try:\n            await ctx.message.delete()\n        except NotFound:\n            log.trace(\"No message found, it must have been deleted by another bot.\")\n\n    @command(name='accept', aliases=('verify', 'verified', 'accepted'), hidden=True)\n    @without_role(Roles.verified)\n    @in_channel(Channels.verification)\n    async def accept_command(self, ctx: Context, *_) -> None:  # We don't actually care about the args\n        \"\"\"Accept our rules and gain access to the rest of the server.\"\"\"\n        log.debug(f\"{ctx.author} called !accept. Assigning the 'Developer' role.\")\n        await ctx.author.add_roles(Object(Roles.verified), reason=\"Accepted the rules\")\n        try:\n            await ctx.author.send(WELCOME_MESSAGE)\n        except Forbidden:\n            log.info(f\"Sending welcome message failed for {ctx.author}.\")\n        finally:\n            log.trace(f\"Deleting accept message by {ctx.author}.\")\n            with suppress(NotFound):\n                self.mod_log.ignore(Event.message_delete, ctx.message.id)\n                await ctx.message.delete()\n\n    @command(name='subscribe')\n    @in_channel(Channels.bot)\n    async def subscribe_command(self, ctx: Context, *_) -> None:  # We don't actually care about the args\n        \"\"\"Subscribe to announcement notifications by assigning yourself the role.\"\"\"\n        has_role = False\n\n        for role in ctx.author.roles:\n            if role.id == Roles.announcements:\n                has_role = True\n                break\n\n        if has_role:\n            await ctx.send(f\"{ctx.author.mention} You're already subscribed!\")\n            return\n\n        log.debug(f\"{ctx.author} called !subscribe. Assigning the 'Announcements' role.\")\n        await ctx.author.add_roles(Object(Roles.announcements), reason=\"Subscribed to announcements\")\n\n        log.trace(f\"Deleting the message posted by {ctx.author}.\")\n\n        await ctx.send(\n            f\"{ctx.author.mention} Subscribed to <#{Channels.announcements}> notifications.\",\n        )\n\n    @command(name='unsubscribe')\n    @in_channel(Channels.bot)\n    async def unsubscribe_command(self, ctx: Context, *_) -> None:  # We don't actually care about the args\n        \"\"\"Unsubscribe from announcement notifications by removing the role from yourself.\"\"\"\n        has_role = False\n\n        for role in ctx.author.roles:\n            if role.id == Roles.announcements:\n                has_role = True\n                break\n\n        if not has_role:\n            await ctx.send(f\"{ctx.author.mention} You're already unsubscribed!\")\n            return\n\n        log.debug(f\"{ctx.author} called !unsubscribe. Removing the 'Announcements' role.\")\n        await ctx.author.remove_roles(Object(Roles.announcements), reason=\"Unsubscribed from announcements\")\n\n        log.trace(f\"Deleting the message posted by {ctx.author}.\")\n\n        await ctx.send(\n            f\"{ctx.author.mention} Unsubscribed from <#{Channels.announcements}> notifications.\"\n        )\n\n    # This cannot be static (must have a __func__ attribute).\n    async def cog_command_error(self, ctx: Context, error: Exception) -> None:\n        \"\"\"Check for & ignore any InChannelCheckFailure.\"\"\"\n        if isinstance(error, InChannelCheckFailure):\n            error.handled = True\n\n    @staticmethod\n    def bot_check(ctx: Context) -> bool:\n        \"\"\"Block any command within the verification channel that is not !accept.\"\"\"\n        if ctx.channel.id == Channels.verification and without_role_check(ctx, *MODERATION_ROLES):\n            return ctx.command.name == \"accept\"\n        else:\n            return True\n\n    @tasks.loop(hours=12)\n    async def periodic_ping(self) -> None:\n        \"\"\"Every week, mention @everyone to remind them to verify.\"\"\"\n        messages = self.bot.get_channel(Channels.verification).history(limit=10)\n        need_to_post = True  # True if a new message needs to be sent.\n\n        async for message in messages:\n            if message.author == self.bot.user and message.content == PERIODIC_PING:\n                delta = datetime.utcnow() - message.created_at  # Time since last message.\n                if delta.days >= 7:  # Message is older than a week.\n                    await message.delete()\n                else:\n                    need_to_post = False\n\n                break\n\n        if need_to_post:\n            await self.bot.get_channel(Channels.verification).send(PERIODIC_PING)\n\n    @periodic_ping.before_loop\n    async def before_ping(self) -> None:\n        \"\"\"Only start the loop when the bot is ready.\"\"\"\n        await self.bot.wait_until_ready()\n\n    def cog_unload(self) -> None:\n        \"\"\"Cancel the periodic ping task when the cog is unloaded.\"\"\"\n        self.periodic_ping.cancel()\n\n\ndef setup(bot: Bot) -> None:\n    \"\"\"Load the Verification cog.\"\"\"\n    bot.add_cog(Verification(bot))\n", "path": "bot/cogs/verification.py"}]}