We are currently solving the following issue within our repository. Here is the issue text:
--- BEGIN ISSUE ---
Publish messages to the #reddit channel
Since the #reddit channel has moved to the new News category we should convert it to an announcement channel and use the `.publish()` method on messages sent there so users can subscribe to daily reddit updates.

[Message.publish() in the discord.py docs](https://discordpy.readthedocs.io/en/latest/api.html#discord.Message.publish)

--- 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/reddit.py`
Content:
```
1 import asyncio
2 import logging
3 import random
4 import textwrap
5 from collections import namedtuple
6 from datetime import datetime, timedelta
7 from typing import List
8 
9 from aiohttp import BasicAuth, ClientError
10 from discord import Colour, Embed, TextChannel
11 from discord.ext.commands import Cog, Context, group
12 from discord.ext.tasks import loop
13 
14 from bot.bot import Bot
15 from bot.constants import Channels, ERROR_REPLIES, Emojis, Reddit as RedditConfig, STAFF_ROLES, Webhooks
16 from bot.converters import Subreddit
17 from bot.decorators import with_role
18 from bot.pagination import LinePaginator
19 
20 log = logging.getLogger(__name__)
21 
22 AccessToken = namedtuple("AccessToken", ["token", "expires_at"])
23 
24 
25 class Reddit(Cog):
26     """Track subreddit posts and show detailed statistics about them."""
27 
28     HEADERS = {"User-Agent": "python3:python-discord/bot:1.0.0 (by /u/PythonDiscord)"}
29     URL = "https://www.reddit.com"
30     OAUTH_URL = "https://oauth.reddit.com"
31     MAX_RETRIES = 3
32 
33     def __init__(self, bot: Bot):
34         self.bot = bot
35 
36         self.webhook = None
37         self.access_token = None
38         self.client_auth = BasicAuth(RedditConfig.client_id, RedditConfig.secret)
39 
40         bot.loop.create_task(self.init_reddit_ready())
41         self.auto_poster_loop.start()
42 
43     def cog_unload(self) -> None:
44         """Stop the loop task and revoke the access token when the cog is unloaded."""
45         self.auto_poster_loop.cancel()
46         if self.access_token and self.access_token.expires_at > datetime.utcnow():
47             asyncio.create_task(self.revoke_access_token())
48 
49     async def init_reddit_ready(self) -> None:
50         """Sets the reddit webhook when the cog is loaded."""
51         await self.bot.wait_until_guild_available()
52         if not self.webhook:
53             self.webhook = await self.bot.fetch_webhook(Webhooks.reddit)
54 
55     @property
56     def channel(self) -> TextChannel:
57         """Get the #reddit channel object from the bot's cache."""
58         return self.bot.get_channel(Channels.reddit)
59 
60     async def get_access_token(self) -> None:
61         """
62         Get a Reddit API OAuth2 access token and assign it to self.access_token.
63 
64         A token is valid for 1 hour. There will be MAX_RETRIES to get a token, after which the cog
65         will be unloaded and a ClientError raised if retrieval was still unsuccessful.
66         """
67         for i in range(1, self.MAX_RETRIES + 1):
68             response = await self.bot.http_session.post(
69                 url=f"{self.URL}/api/v1/access_token",
70                 headers=self.HEADERS,
71                 auth=self.client_auth,
72                 data={
73                     "grant_type": "client_credentials",
74                     "duration": "temporary"
75                 }
76             )
77 
78             if response.status == 200 and response.content_type == "application/json":
79                 content = await response.json()
80                 expiration = int(content["expires_in"]) - 60  # Subtract 1 minute for leeway.
81                 self.access_token = AccessToken(
82                     token=content["access_token"],
83                     expires_at=datetime.utcnow() + timedelta(seconds=expiration)
84                 )
85 
86                 log.debug(f"New token acquired; expires on UTC {self.access_token.expires_at}")
87                 return
88             else:
89                 log.debug(
90                     f"Failed to get an access token: "
91                     f"status {response.status} & content type {response.content_type}; "
92                     f"retrying ({i}/{self.MAX_RETRIES})"
93                 )
94 
95             await asyncio.sleep(3)
96 
97         self.bot.remove_cog(self.qualified_name)
98         raise ClientError("Authentication with the Reddit API failed. Unloading the cog.")
99 
100     async def revoke_access_token(self) -> None:
101         """
102         Revoke the OAuth2 access token for the Reddit API.
103 
104         For security reasons, it's good practice to revoke the token when it's no longer being used.
105         """
106         response = await self.bot.http_session.post(
107             url=f"{self.URL}/api/v1/revoke_token",
108             headers=self.HEADERS,
109             auth=self.client_auth,
110             data={
111                 "token": self.access_token.token,
112                 "token_type_hint": "access_token"
113             }
114         )
115 
116         if response.status == 204 and response.content_type == "application/json":
117             self.access_token = None
118         else:
119             log.warning(f"Unable to revoke access token: status {response.status}.")
120 
121     async def fetch_posts(self, route: str, *, amount: int = 25, params: dict = None) -> List[dict]:
122         """A helper method to fetch a certain amount of Reddit posts at a given route."""
123         # Reddit's JSON responses only provide 25 posts at most.
124         if not 25 >= amount > 0:
125             raise ValueError("Invalid amount of subreddit posts requested.")
126 
127         # Renew the token if necessary.
128         if not self.access_token or self.access_token.expires_at < datetime.utcnow():
129             await self.get_access_token()
130 
131         url = f"{self.OAUTH_URL}/{route}"
132         for _ in range(self.MAX_RETRIES):
133             response = await self.bot.http_session.get(
134                 url=url,
135                 headers={**self.HEADERS, "Authorization": f"bearer {self.access_token.token}"},
136                 params=params
137             )
138             if response.status == 200 and response.content_type == 'application/json':
139                 # Got appropriate response - process and return.
140                 content = await response.json()
141                 posts = content["data"]["children"]
142                 return posts[:amount]
143 
144             await asyncio.sleep(3)
145 
146         log.debug(f"Invalid response from: {url} - status code {response.status}, mimetype {response.content_type}")
147         return list()  # Failed to get appropriate response within allowed number of retries.
148 
149     async def get_top_posts(self, subreddit: Subreddit, time: str = "all", amount: int = 5) -> Embed:
150         """
151         Get the top amount of posts for a given subreddit within a specified timeframe.
152 
153         A time of "all" will get posts from all time, "day" will get top daily posts and "week" will get the top
154         weekly posts.
155 
156         The amount should be between 0 and 25 as Reddit's JSON requests only provide 25 posts at most.
157         """
158         embed = Embed(description="")
159 
160         posts = await self.fetch_posts(
161             route=f"{subreddit}/top",
162             amount=amount,
163             params={"t": time}
164         )
165 
166         if not posts:
167             embed.title = random.choice(ERROR_REPLIES)
168             embed.colour = Colour.red()
169             embed.description = (
170                 "Sorry! We couldn't find any posts from that subreddit. "
171                 "If this problem persists, please let us know."
172             )
173 
174             return embed
175 
176         for post in posts:
177             data = post["data"]
178 
179             text = data["selftext"]
180             if text:
181                 text = textwrap.shorten(text, width=128, placeholder="...")
182                 text += "\n"  # Add newline to separate embed info
183 
184             ups = data["ups"]
185             comments = data["num_comments"]
186             author = data["author"]
187 
188             title = textwrap.shorten(data["title"], width=64, placeholder="...")
189             link = self.URL + data["permalink"]
190 
191             embed.description += (
192                 f"**[{title}]({link})**\n"
193                 f"{text}"
194                 f"{Emojis.upvotes} {ups} {Emojis.comments} {comments} {Emojis.user} {author}\n\n"
195             )
196 
197         embed.colour = Colour.blurple()
198         return embed
199 
200     @loop()
201     async def auto_poster_loop(self) -> None:
202         """Post the top 5 posts daily, and the top 5 posts weekly."""
203         # once we upgrade to d.py 1.3 this can be removed and the loop can use the `time=datetime.time.min` parameter
204         now = datetime.utcnow()
205         tomorrow = now + timedelta(days=1)
206         midnight_tomorrow = tomorrow.replace(hour=0, minute=0, second=0)
207         seconds_until = (midnight_tomorrow - now).total_seconds()
208 
209         await asyncio.sleep(seconds_until)
210 
211         await self.bot.wait_until_guild_available()
212         if not self.webhook:
213             await self.bot.fetch_webhook(Webhooks.reddit)
214 
215         if datetime.utcnow().weekday() == 0:
216             await self.top_weekly_posts()
217             # if it's a monday send the top weekly posts
218 
219         for subreddit in RedditConfig.subreddits:
220             top_posts = await self.get_top_posts(subreddit=subreddit, time="day")
221             await self.webhook.send(username=f"{subreddit} Top Daily Posts", embed=top_posts)
222 
223     async def top_weekly_posts(self) -> None:
224         """Post a summary of the top posts."""
225         for subreddit in RedditConfig.subreddits:
226             # Send and pin the new weekly posts.
227             top_posts = await self.get_top_posts(subreddit=subreddit, time="week")
228 
229             message = await self.webhook.send(wait=True, username=f"{subreddit} Top Weekly Posts", embed=top_posts)
230 
231             if subreddit.lower() == "r/python":
232                 if not self.channel:
233                     log.warning("Failed to get #reddit channel to remove pins in the weekly loop.")
234                     return
235 
236                 # Remove the oldest pins so that only 12 remain at most.
237                 pins = await self.channel.pins()
238 
239                 while len(pins) >= 12:
240                     await pins[-1].unpin()
241                     del pins[-1]
242 
243                 await message.pin()
244 
245     @group(name="reddit", invoke_without_command=True)
246     async def reddit_group(self, ctx: Context) -> None:
247         """View the top posts from various subreddits."""
248         await ctx.send_help(ctx.command)
249 
250     @reddit_group.command(name="top")
251     async def top_command(self, ctx: Context, subreddit: Subreddit = "r/Python") -> None:
252         """Send the top posts of all time from a given subreddit."""
253         async with ctx.typing():
254             embed = await self.get_top_posts(subreddit=subreddit, time="all")
255 
256         await ctx.send(content=f"Here are the top {subreddit} posts of all time!", embed=embed)
257 
258     @reddit_group.command(name="daily")
259     async def daily_command(self, ctx: Context, subreddit: Subreddit = "r/Python") -> None:
260         """Send the top posts of today from a given subreddit."""
261         async with ctx.typing():
262             embed = await self.get_top_posts(subreddit=subreddit, time="day")
263 
264         await ctx.send(content=f"Here are today's top {subreddit} posts!", embed=embed)
265 
266     @reddit_group.command(name="weekly")
267     async def weekly_command(self, ctx: Context, subreddit: Subreddit = "r/Python") -> None:
268         """Send the top posts of this week from a given subreddit."""
269         async with ctx.typing():
270             embed = await self.get_top_posts(subreddit=subreddit, time="week")
271 
272         await ctx.send(content=f"Here are this week's top {subreddit} posts!", embed=embed)
273 
274     @with_role(*STAFF_ROLES)
275     @reddit_group.command(name="subreddits", aliases=("subs",))
276     async def subreddits_command(self, ctx: Context) -> None:
277         """Send a paginated embed of all the subreddits we're relaying."""
278         embed = Embed()
279         embed.title = "Relayed subreddits."
280         embed.colour = Colour.blurple()
281 
282         await LinePaginator.paginate(
283             RedditConfig.subreddits,
284             ctx, embed,
285             footer_text="Use the reddit commands along with these to view their posts.",
286             empty=False,
287             max_lines=15
288         )
289 
290 
291 def setup(bot: Bot) -> None:
292     """Load the Reddit cog."""
293     if not RedditConfig.secret or not RedditConfig.client_id:
294         log.error("Credentials not provided, cog not loaded.")
295         return
296     bot.add_cog(Reddit(bot))
297
```
--- 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/reddit.py b/bot/cogs/reddit.py
--- a/bot/cogs/reddit.py
+++ b/bot/cogs/reddit.py
@@ -218,7 +218,10 @@
 
         for subreddit in RedditConfig.subreddits:
             top_posts = await self.get_top_posts(subreddit=subreddit, time="day")
-            await self.webhook.send(username=f"{subreddit} Top Daily Posts", embed=top_posts)
+            message = await self.webhook.send(username=f"{subreddit} Top Daily Posts", embed=top_posts)
+
+            if message.channel.is_news():
+                await message.publish()
 
     async def top_weekly_posts(self) -> None:
         """Post a summary of the top posts."""
@@ -242,6 +245,9 @@
 
                 await message.pin()
 
+                if message.channel.is_news():
+                    await message.publish()
+
     @group(name="reddit", invoke_without_command=True)
     async def reddit_group(self, ctx: Context) -> None:
         """View the top posts from various subreddits."""

{"golden_diff": "diff --git a/bot/cogs/reddit.py b/bot/cogs/reddit.py\n--- a/bot/cogs/reddit.py\n+++ b/bot/cogs/reddit.py\n@@ -218,7 +218,10 @@\n \n         for subreddit in RedditConfig.subreddits:\n             top_posts = await self.get_top_posts(subreddit=subreddit, time=\"day\")\n-            await self.webhook.send(username=f\"{subreddit} Top Daily Posts\", embed=top_posts)\n+            message = await self.webhook.send(username=f\"{subreddit} Top Daily Posts\", embed=top_posts)\n+\n+            if message.channel.is_news():\n+                await message.publish()\n \n     async def top_weekly_posts(self) -> None:\n         \"\"\"Post a summary of the top posts.\"\"\"\n@@ -242,6 +245,9 @@\n \n                 await message.pin()\n \n+                if message.channel.is_news():\n+                    await message.publish()\n+\n     @group(name=\"reddit\", invoke_without_command=True)\n     async def reddit_group(self, ctx: Context) -> None:\n         \"\"\"View the top posts from various subreddits.\"\"\"\n", "issue": "Publish messages to the #reddit channel\nSince the #reddit channel has moved to the new News category we should convert it to an announcement channel and use the `.publish()` method on messages sent there so users can subscribe to daily reddit updates.\r\n\r\n[Message.publish() in the discord.py docs](https://discordpy.readthedocs.io/en/latest/api.html#discord.Message.publish)\n", "before_files": [{"content": "import asyncio\nimport logging\nimport random\nimport textwrap\nfrom collections import namedtuple\nfrom datetime import datetime, timedelta\nfrom typing import List\n\nfrom aiohttp import BasicAuth, ClientError\nfrom discord import Colour, Embed, TextChannel\nfrom discord.ext.commands import Cog, Context, group\nfrom discord.ext.tasks import loop\n\nfrom bot.bot import Bot\nfrom bot.constants import Channels, ERROR_REPLIES, Emojis, Reddit as RedditConfig, STAFF_ROLES, Webhooks\nfrom bot.converters import Subreddit\nfrom bot.decorators import with_role\nfrom bot.pagination import LinePaginator\n\nlog = logging.getLogger(__name__)\n\nAccessToken = namedtuple(\"AccessToken\", [\"token\", \"expires_at\"])\n\n\nclass Reddit(Cog):\n    \"\"\"Track subreddit posts and show detailed statistics about them.\"\"\"\n\n    HEADERS = {\"User-Agent\": \"python3:python-discord/bot:1.0.0 (by /u/PythonDiscord)\"}\n    URL = \"https://www.reddit.com\"\n    OAUTH_URL = \"https://oauth.reddit.com\"\n    MAX_RETRIES = 3\n\n    def __init__(self, bot: Bot):\n        self.bot = bot\n\n        self.webhook = None\n        self.access_token = None\n        self.client_auth = BasicAuth(RedditConfig.client_id, RedditConfig.secret)\n\n        bot.loop.create_task(self.init_reddit_ready())\n        self.auto_poster_loop.start()\n\n    def cog_unload(self) -> None:\n        \"\"\"Stop the loop task and revoke the access token when the cog is unloaded.\"\"\"\n        self.auto_poster_loop.cancel()\n        if self.access_token and self.access_token.expires_at > datetime.utcnow():\n            asyncio.create_task(self.revoke_access_token())\n\n    async def init_reddit_ready(self) -> None:\n        \"\"\"Sets the reddit webhook when the cog is loaded.\"\"\"\n        await self.bot.wait_until_guild_available()\n        if not self.webhook:\n            self.webhook = await self.bot.fetch_webhook(Webhooks.reddit)\n\n    @property\n    def channel(self) -> TextChannel:\n        \"\"\"Get the #reddit channel object from the bot's cache.\"\"\"\n        return self.bot.get_channel(Channels.reddit)\n\n    async def get_access_token(self) -> None:\n        \"\"\"\n        Get a Reddit API OAuth2 access token and assign it to self.access_token.\n\n        A token is valid for 1 hour. There will be MAX_RETRIES to get a token, after which the cog\n        will be unloaded and a ClientError raised if retrieval was still unsuccessful.\n        \"\"\"\n        for i in range(1, self.MAX_RETRIES + 1):\n            response = await self.bot.http_session.post(\n                url=f\"{self.URL}/api/v1/access_token\",\n                headers=self.HEADERS,\n                auth=self.client_auth,\n                data={\n                    \"grant_type\": \"client_credentials\",\n                    \"duration\": \"temporary\"\n                }\n            )\n\n            if response.status == 200 and response.content_type == \"application/json\":\n                content = await response.json()\n                expiration = int(content[\"expires_in\"]) - 60  # Subtract 1 minute for leeway.\n                self.access_token = AccessToken(\n                    token=content[\"access_token\"],\n                    expires_at=datetime.utcnow() + timedelta(seconds=expiration)\n                )\n\n                log.debug(f\"New token acquired; expires on UTC {self.access_token.expires_at}\")\n                return\n            else:\n                log.debug(\n                    f\"Failed to get an access token: \"\n                    f\"status {response.status} & content type {response.content_type}; \"\n                    f\"retrying ({i}/{self.MAX_RETRIES})\"\n                )\n\n            await asyncio.sleep(3)\n\n        self.bot.remove_cog(self.qualified_name)\n        raise ClientError(\"Authentication with the Reddit API failed. Unloading the cog.\")\n\n    async def revoke_access_token(self) -> None:\n        \"\"\"\n        Revoke the OAuth2 access token for the Reddit API.\n\n        For security reasons, it's good practice to revoke the token when it's no longer being used.\n        \"\"\"\n        response = await self.bot.http_session.post(\n            url=f\"{self.URL}/api/v1/revoke_token\",\n            headers=self.HEADERS,\n            auth=self.client_auth,\n            data={\n                \"token\": self.access_token.token,\n                \"token_type_hint\": \"access_token\"\n            }\n        )\n\n        if response.status == 204 and response.content_type == \"application/json\":\n            self.access_token = None\n        else:\n            log.warning(f\"Unable to revoke access token: status {response.status}.\")\n\n    async def fetch_posts(self, route: str, *, amount: int = 25, params: dict = None) -> List[dict]:\n        \"\"\"A helper method to fetch a certain amount of Reddit posts at a given route.\"\"\"\n        # Reddit's JSON responses only provide 25 posts at most.\n        if not 25 >= amount > 0:\n            raise ValueError(\"Invalid amount of subreddit posts requested.\")\n\n        # Renew the token if necessary.\n        if not self.access_token or self.access_token.expires_at < datetime.utcnow():\n            await self.get_access_token()\n\n        url = f\"{self.OAUTH_URL}/{route}\"\n        for _ in range(self.MAX_RETRIES):\n            response = await self.bot.http_session.get(\n                url=url,\n                headers={**self.HEADERS, \"Authorization\": f\"bearer {self.access_token.token}\"},\n                params=params\n            )\n            if response.status == 200 and response.content_type == 'application/json':\n                # Got appropriate response - process and return.\n                content = await response.json()\n                posts = content[\"data\"][\"children\"]\n                return posts[:amount]\n\n            await asyncio.sleep(3)\n\n        log.debug(f\"Invalid response from: {url} - status code {response.status}, mimetype {response.content_type}\")\n        return list()  # Failed to get appropriate response within allowed number of retries.\n\n    async def get_top_posts(self, subreddit: Subreddit, time: str = \"all\", amount: int = 5) -> Embed:\n        \"\"\"\n        Get the top amount of posts for a given subreddit within a specified timeframe.\n\n        A time of \"all\" will get posts from all time, \"day\" will get top daily posts and \"week\" will get the top\n        weekly posts.\n\n        The amount should be between 0 and 25 as Reddit's JSON requests only provide 25 posts at most.\n        \"\"\"\n        embed = Embed(description=\"\")\n\n        posts = await self.fetch_posts(\n            route=f\"{subreddit}/top\",\n            amount=amount,\n            params={\"t\": time}\n        )\n\n        if not posts:\n            embed.title = random.choice(ERROR_REPLIES)\n            embed.colour = Colour.red()\n            embed.description = (\n                \"Sorry! We couldn't find any posts from that subreddit. \"\n                \"If this problem persists, please let us know.\"\n            )\n\n            return embed\n\n        for post in posts:\n            data = post[\"data\"]\n\n            text = data[\"selftext\"]\n            if text:\n                text = textwrap.shorten(text, width=128, placeholder=\"...\")\n                text += \"\\n\"  # Add newline to separate embed info\n\n            ups = data[\"ups\"]\n            comments = data[\"num_comments\"]\n            author = data[\"author\"]\n\n            title = textwrap.shorten(data[\"title\"], width=64, placeholder=\"...\")\n            link = self.URL + data[\"permalink\"]\n\n            embed.description += (\n                f\"**[{title}]({link})**\\n\"\n                f\"{text}\"\n                f\"{Emojis.upvotes} {ups} {Emojis.comments} {comments} {Emojis.user} {author}\\n\\n\"\n            )\n\n        embed.colour = Colour.blurple()\n        return embed\n\n    @loop()\n    async def auto_poster_loop(self) -> None:\n        \"\"\"Post the top 5 posts daily, and the top 5 posts weekly.\"\"\"\n        # once we upgrade to d.py 1.3 this can be removed and the loop can use the `time=datetime.time.min` parameter\n        now = datetime.utcnow()\n        tomorrow = now + timedelta(days=1)\n        midnight_tomorrow = tomorrow.replace(hour=0, minute=0, second=0)\n        seconds_until = (midnight_tomorrow - now).total_seconds()\n\n        await asyncio.sleep(seconds_until)\n\n        await self.bot.wait_until_guild_available()\n        if not self.webhook:\n            await self.bot.fetch_webhook(Webhooks.reddit)\n\n        if datetime.utcnow().weekday() == 0:\n            await self.top_weekly_posts()\n            # if it's a monday send the top weekly posts\n\n        for subreddit in RedditConfig.subreddits:\n            top_posts = await self.get_top_posts(subreddit=subreddit, time=\"day\")\n            await self.webhook.send(username=f\"{subreddit} Top Daily Posts\", embed=top_posts)\n\n    async def top_weekly_posts(self) -> None:\n        \"\"\"Post a summary of the top posts.\"\"\"\n        for subreddit in RedditConfig.subreddits:\n            # Send and pin the new weekly posts.\n            top_posts = await self.get_top_posts(subreddit=subreddit, time=\"week\")\n\n            message = await self.webhook.send(wait=True, username=f\"{subreddit} Top Weekly Posts\", embed=top_posts)\n\n            if subreddit.lower() == \"r/python\":\n                if not self.channel:\n                    log.warning(\"Failed to get #reddit channel to remove pins in the weekly loop.\")\n                    return\n\n                # Remove the oldest pins so that only 12 remain at most.\n                pins = await self.channel.pins()\n\n                while len(pins) >= 12:\n                    await pins[-1].unpin()\n                    del pins[-1]\n\n                await message.pin()\n\n    @group(name=\"reddit\", invoke_without_command=True)\n    async def reddit_group(self, ctx: Context) -> None:\n        \"\"\"View the top posts from various subreddits.\"\"\"\n        await ctx.send_help(ctx.command)\n\n    @reddit_group.command(name=\"top\")\n    async def top_command(self, ctx: Context, subreddit: Subreddit = \"r/Python\") -> None:\n        \"\"\"Send the top posts of all time from a given subreddit.\"\"\"\n        async with ctx.typing():\n            embed = await self.get_top_posts(subreddit=subreddit, time=\"all\")\n\n        await ctx.send(content=f\"Here are the top {subreddit} posts of all time!\", embed=embed)\n\n    @reddit_group.command(name=\"daily\")\n    async def daily_command(self, ctx: Context, subreddit: Subreddit = \"r/Python\") -> None:\n        \"\"\"Send the top posts of today from a given subreddit.\"\"\"\n        async with ctx.typing():\n            embed = await self.get_top_posts(subreddit=subreddit, time=\"day\")\n\n        await ctx.send(content=f\"Here are today's top {subreddit} posts!\", embed=embed)\n\n    @reddit_group.command(name=\"weekly\")\n    async def weekly_command(self, ctx: Context, subreddit: Subreddit = \"r/Python\") -> None:\n        \"\"\"Send the top posts of this week from a given subreddit.\"\"\"\n        async with ctx.typing():\n            embed = await self.get_top_posts(subreddit=subreddit, time=\"week\")\n\n        await ctx.send(content=f\"Here are this week's top {subreddit} posts!\", embed=embed)\n\n    @with_role(*STAFF_ROLES)\n    @reddit_group.command(name=\"subreddits\", aliases=(\"subs\",))\n    async def subreddits_command(self, ctx: Context) -> None:\n        \"\"\"Send a paginated embed of all the subreddits we're relaying.\"\"\"\n        embed = Embed()\n        embed.title = \"Relayed subreddits.\"\n        embed.colour = Colour.blurple()\n\n        await LinePaginator.paginate(\n            RedditConfig.subreddits,\n            ctx, embed,\n            footer_text=\"Use the reddit commands along with these to view their posts.\",\n            empty=False,\n            max_lines=15\n        )\n\n\ndef setup(bot: Bot) -> None:\n    \"\"\"Load the Reddit cog.\"\"\"\n    if not RedditConfig.secret or not RedditConfig.client_id:\n        log.error(\"Credentials not provided, cog not loaded.\")\n        return\n    bot.add_cog(Reddit(bot))\n", "path": "bot/cogs/reddit.py"}], "after_files": [{"content": "import asyncio\nimport logging\nimport random\nimport textwrap\nfrom collections import namedtuple\nfrom datetime import datetime, timedelta\nfrom typing import List\n\nfrom aiohttp import BasicAuth, ClientError\nfrom discord import Colour, Embed, TextChannel\nfrom discord.ext.commands import Cog, Context, group\nfrom discord.ext.tasks import loop\n\nfrom bot.bot import Bot\nfrom bot.constants import Channels, ERROR_REPLIES, Emojis, Reddit as RedditConfig, STAFF_ROLES, Webhooks\nfrom bot.converters import Subreddit\nfrom bot.decorators import with_role\nfrom bot.pagination import LinePaginator\n\nlog = logging.getLogger(__name__)\n\nAccessToken = namedtuple(\"AccessToken\", [\"token\", \"expires_at\"])\n\n\nclass Reddit(Cog):\n    \"\"\"Track subreddit posts and show detailed statistics about them.\"\"\"\n\n    HEADERS = {\"User-Agent\": \"python3:python-discord/bot:1.0.0 (by /u/PythonDiscord)\"}\n    URL = \"https://www.reddit.com\"\n    OAUTH_URL = \"https://oauth.reddit.com\"\n    MAX_RETRIES = 3\n\n    def __init__(self, bot: Bot):\n        self.bot = bot\n\n        self.webhook = None\n        self.access_token = None\n        self.client_auth = BasicAuth(RedditConfig.client_id, RedditConfig.secret)\n\n        bot.loop.create_task(self.init_reddit_ready())\n        self.auto_poster_loop.start()\n\n    def cog_unload(self) -> None:\n        \"\"\"Stop the loop task and revoke the access token when the cog is unloaded.\"\"\"\n        self.auto_poster_loop.cancel()\n        if self.access_token and self.access_token.expires_at > datetime.utcnow():\n            asyncio.create_task(self.revoke_access_token())\n\n    async def init_reddit_ready(self) -> None:\n        \"\"\"Sets the reddit webhook when the cog is loaded.\"\"\"\n        await self.bot.wait_until_guild_available()\n        if not self.webhook:\n            self.webhook = await self.bot.fetch_webhook(Webhooks.reddit)\n\n    @property\n    def channel(self) -> TextChannel:\n        \"\"\"Get the #reddit channel object from the bot's cache.\"\"\"\n        return self.bot.get_channel(Channels.reddit)\n\n    async def get_access_token(self) -> None:\n        \"\"\"\n        Get a Reddit API OAuth2 access token and assign it to self.access_token.\n\n        A token is valid for 1 hour. There will be MAX_RETRIES to get a token, after which the cog\n        will be unloaded and a ClientError raised if retrieval was still unsuccessful.\n        \"\"\"\n        for i in range(1, self.MAX_RETRIES + 1):\n            response = await self.bot.http_session.post(\n                url=f\"{self.URL}/api/v1/access_token\",\n                headers=self.HEADERS,\n                auth=self.client_auth,\n                data={\n                    \"grant_type\": \"client_credentials\",\n                    \"duration\": \"temporary\"\n                }\n            )\n\n            if response.status == 200 and response.content_type == \"application/json\":\n                content = await response.json()\n                expiration = int(content[\"expires_in\"]) - 60  # Subtract 1 minute for leeway.\n                self.access_token = AccessToken(\n                    token=content[\"access_token\"],\n                    expires_at=datetime.utcnow() + timedelta(seconds=expiration)\n                )\n\n                log.debug(f\"New token acquired; expires on UTC {self.access_token.expires_at}\")\n                return\n            else:\n                log.debug(\n                    f\"Failed to get an access token: \"\n                    f\"status {response.status} & content type {response.content_type}; \"\n                    f\"retrying ({i}/{self.MAX_RETRIES})\"\n                )\n\n            await asyncio.sleep(3)\n\n        self.bot.remove_cog(self.qualified_name)\n        raise ClientError(\"Authentication with the Reddit API failed. Unloading the cog.\")\n\n    async def revoke_access_token(self) -> None:\n        \"\"\"\n        Revoke the OAuth2 access token for the Reddit API.\n\n        For security reasons, it's good practice to revoke the token when it's no longer being used.\n        \"\"\"\n        response = await self.bot.http_session.post(\n            url=f\"{self.URL}/api/v1/revoke_token\",\n            headers=self.HEADERS,\n            auth=self.client_auth,\n            data={\n                \"token\": self.access_token.token,\n                \"token_type_hint\": \"access_token\"\n            }\n        )\n\n        if response.status == 204 and response.content_type == \"application/json\":\n            self.access_token = None\n        else:\n            log.warning(f\"Unable to revoke access token: status {response.status}.\")\n\n    async def fetch_posts(self, route: str, *, amount: int = 25, params: dict = None) -> List[dict]:\n        \"\"\"A helper method to fetch a certain amount of Reddit posts at a given route.\"\"\"\n        # Reddit's JSON responses only provide 25 posts at most.\n        if not 25 >= amount > 0:\n            raise ValueError(\"Invalid amount of subreddit posts requested.\")\n\n        # Renew the token if necessary.\n        if not self.access_token or self.access_token.expires_at < datetime.utcnow():\n            await self.get_access_token()\n\n        url = f\"{self.OAUTH_URL}/{route}\"\n        for _ in range(self.MAX_RETRIES):\n            response = await self.bot.http_session.get(\n                url=url,\n                headers={**self.HEADERS, \"Authorization\": f\"bearer {self.access_token.token}\"},\n                params=params\n            )\n            if response.status == 200 and response.content_type == 'application/json':\n                # Got appropriate response - process and return.\n                content = await response.json()\n                posts = content[\"data\"][\"children\"]\n                return posts[:amount]\n\n            await asyncio.sleep(3)\n\n        log.debug(f\"Invalid response from: {url} - status code {response.status}, mimetype {response.content_type}\")\n        return list()  # Failed to get appropriate response within allowed number of retries.\n\n    async def get_top_posts(self, subreddit: Subreddit, time: str = \"all\", amount: int = 5) -> Embed:\n        \"\"\"\n        Get the top amount of posts for a given subreddit within a specified timeframe.\n\n        A time of \"all\" will get posts from all time, \"day\" will get top daily posts and \"week\" will get the top\n        weekly posts.\n\n        The amount should be between 0 and 25 as Reddit's JSON requests only provide 25 posts at most.\n        \"\"\"\n        embed = Embed(description=\"\")\n\n        posts = await self.fetch_posts(\n            route=f\"{subreddit}/top\",\n            amount=amount,\n            params={\"t\": time}\n        )\n\n        if not posts:\n            embed.title = random.choice(ERROR_REPLIES)\n            embed.colour = Colour.red()\n            embed.description = (\n                \"Sorry! We couldn't find any posts from that subreddit. \"\n                \"If this problem persists, please let us know.\"\n            )\n\n            return embed\n\n        for post in posts:\n            data = post[\"data\"]\n\n            text = data[\"selftext\"]\n            if text:\n                text = textwrap.shorten(text, width=128, placeholder=\"...\")\n                text += \"\\n\"  # Add newline to separate embed info\n\n            ups = data[\"ups\"]\n            comments = data[\"num_comments\"]\n            author = data[\"author\"]\n\n            title = textwrap.shorten(data[\"title\"], width=64, placeholder=\"...\")\n            link = self.URL + data[\"permalink\"]\n\n            embed.description += (\n                f\"**[{title}]({link})**\\n\"\n                f\"{text}\"\n                f\"{Emojis.upvotes} {ups} {Emojis.comments} {comments} {Emojis.user} {author}\\n\\n\"\n            )\n\n        embed.colour = Colour.blurple()\n        return embed\n\n    @loop()\n    async def auto_poster_loop(self) -> None:\n        \"\"\"Post the top 5 posts daily, and the top 5 posts weekly.\"\"\"\n        # once we upgrade to d.py 1.3 this can be removed and the loop can use the `time=datetime.time.min` parameter\n        now = datetime.utcnow()\n        tomorrow = now + timedelta(days=1)\n        midnight_tomorrow = tomorrow.replace(hour=0, minute=0, second=0)\n        seconds_until = (midnight_tomorrow - now).total_seconds()\n\n        await asyncio.sleep(seconds_until)\n\n        await self.bot.wait_until_guild_available()\n        if not self.webhook:\n            await self.bot.fetch_webhook(Webhooks.reddit)\n\n        if datetime.utcnow().weekday() == 0:\n            await self.top_weekly_posts()\n            # if it's a monday send the top weekly posts\n\n        for subreddit in RedditConfig.subreddits:\n            top_posts = await self.get_top_posts(subreddit=subreddit, time=\"day\")\n            message = await self.webhook.send(username=f\"{subreddit} Top Daily Posts\", embed=top_posts)\n\n            if message.channel.is_news():\n                await message.publish()\n\n    async def top_weekly_posts(self) -> None:\n        \"\"\"Post a summary of the top posts.\"\"\"\n        for subreddit in RedditConfig.subreddits:\n            # Send and pin the new weekly posts.\n            top_posts = await self.get_top_posts(subreddit=subreddit, time=\"week\")\n\n            message = await self.webhook.send(wait=True, username=f\"{subreddit} Top Weekly Posts\", embed=top_posts)\n\n            if subreddit.lower() == \"r/python\":\n                if not self.channel:\n                    log.warning(\"Failed to get #reddit channel to remove pins in the weekly loop.\")\n                    return\n\n                # Remove the oldest pins so that only 12 remain at most.\n                pins = await self.channel.pins()\n\n                while len(pins) >= 12:\n                    await pins[-1].unpin()\n                    del pins[-1]\n\n                await message.pin()\n\n                if message.channel.is_news():\n                    await message.publish()\n\n    @group(name=\"reddit\", invoke_without_command=True)\n    async def reddit_group(self, ctx: Context) -> None:\n        \"\"\"View the top posts from various subreddits.\"\"\"\n        await ctx.send_help(ctx.command)\n\n    @reddit_group.command(name=\"top\")\n    async def top_command(self, ctx: Context, subreddit: Subreddit = \"r/Python\") -> None:\n        \"\"\"Send the top posts of all time from a given subreddit.\"\"\"\n        async with ctx.typing():\n            embed = await self.get_top_posts(subreddit=subreddit, time=\"all\")\n\n        await ctx.send(content=f\"Here are the top {subreddit} posts of all time!\", embed=embed)\n\n    @reddit_group.command(name=\"daily\")\n    async def daily_command(self, ctx: Context, subreddit: Subreddit = \"r/Python\") -> None:\n        \"\"\"Send the top posts of today from a given subreddit.\"\"\"\n        async with ctx.typing():\n            embed = await self.get_top_posts(subreddit=subreddit, time=\"day\")\n\n        await ctx.send(content=f\"Here are today's top {subreddit} posts!\", embed=embed)\n\n    @reddit_group.command(name=\"weekly\")\n    async def weekly_command(self, ctx: Context, subreddit: Subreddit = \"r/Python\") -> None:\n        \"\"\"Send the top posts of this week from a given subreddit.\"\"\"\n        async with ctx.typing():\n            embed = await self.get_top_posts(subreddit=subreddit, time=\"week\")\n\n        await ctx.send(content=f\"Here are this week's top {subreddit} posts!\", embed=embed)\n\n    @with_role(*STAFF_ROLES)\n    @reddit_group.command(name=\"subreddits\", aliases=(\"subs\",))\n    async def subreddits_command(self, ctx: Context) -> None:\n        \"\"\"Send a paginated embed of all the subreddits we're relaying.\"\"\"\n        embed = Embed()\n        embed.title = \"Relayed subreddits.\"\n        embed.colour = Colour.blurple()\n\n        await LinePaginator.paginate(\n            RedditConfig.subreddits,\n            ctx, embed,\n            footer_text=\"Use the reddit commands along with these to view their posts.\",\n            empty=False,\n            max_lines=15\n        )\n\n\ndef setup(bot: Bot) -> None:\n    \"\"\"Load the Reddit cog.\"\"\"\n    if not RedditConfig.secret or not RedditConfig.client_id:\n        log.error(\"Credentials not provided, cog not loaded.\")\n        return\n    bot.add_cog(Reddit(bot))\n", "path": "bot/cogs/reddit.py"}]}