From 612657b1755323a492ce551f0bfe08f6982e86f1 Mon Sep 17 00:00:00 2001 From: Soheab_ <33902984+Soheab@users.noreply.github.com> Date: Sun, 8 Jun 2025 00:25:12 +0200 Subject: [PATCH 01/19] Use new pins endpoints --- discord/abc.py | 26 ++++++++++++++++++++++---- discord/http.py | 19 +++++++++++++++---- discord/types/message.py | 10 ++++++++++ 3 files changed, 47 insertions(+), 8 deletions(-) diff --git a/discord/abc.py b/discord/abc.py index 692472f8f..a570510ee 100644 --- a/discord/abc.py +++ b/discord/abc.py @@ -1710,10 +1710,15 @@ class Messageable: data = await self._state.http.get_message(channel.id, id) return self._state.create_message(channel=channel, data=data) - async def pins(self) -> List[Message]: + async def pins(self, *, before: Optional[SnowflakeTime] = None, limit: Optional[int] = None) -> List[Message]: """|coro| - Retrieves all messages that are currently pinned in the channel. + Retrieves a maximum of 50 pinned messages from the destination. + + Requires the :attr:`~discord.Permissions.view_channel` permission. + + No pins will be returned if the user is missing the + :attr:`~discord.Permissions.read_message_history` permission. .. note:: @@ -1721,6 +1726,17 @@ class Messageable: objects returned by this method do not contain complete :attr:`.Message.reactions` data. + Parameters + ----------- + before: Optional[Union[:class:`~discord.abc.Snowflake`, :class:`datetime.datetime`]] + Retrieve pinned messages before this date or message. + If a datetime is provided, it is recommended to use a UTC aware datetime. + If the datetime is naive, it is assumed to be local time. + limit: Optional[int] + The maximum number of pinned messages to retrieve. Defaults to 50. + + This must be a number between 1 and 50. + Raises ------- ~discord.Forbidden @@ -1733,11 +1749,13 @@ class Messageable: List[:class:`~discord.Message`] The messages that are currently pinned. """ + if isinstance(before, datetime): + before = Object(id=utils.time_snowflake(before, high=False)) channel = await self._get_channel() state = self._state - data = await state.http.pins_from(channel.id) - return [state.create_message(channel=channel, data=m) for m in data] + data = await state.http.pins_from(channel.id, before=before.id if before else None, limit=limit) + return [state.create_message(channel=channel, data=m["message"]) for m in data["items"]] async def history( self, diff --git a/discord/http.py b/discord/http.py index 6617efa27..aa2488d8a 100644 --- a/discord/http.py +++ b/discord/http.py @@ -1036,7 +1036,7 @@ class HTTPClient: def pin_message(self, channel_id: Snowflake, message_id: Snowflake, reason: Optional[str] = None) -> Response[None]: r = Route( 'PUT', - '/channels/{channel_id}/pins/{message_id}', + '/channels/{channel_id}/messages/pins/{message_id}', channel_id=channel_id, message_id=message_id, ) @@ -1045,14 +1045,25 @@ class HTTPClient: def unpin_message(self, channel_id: Snowflake, message_id: Snowflake, reason: Optional[str] = None) -> Response[None]: r = Route( 'DELETE', - '/channels/{channel_id}/pins/{message_id}', + '/channels/{channel_id}/messages/pins/{message_id}', channel_id=channel_id, message_id=message_id, ) return self.request(r, reason=reason) - def pins_from(self, channel_id: Snowflake) -> Response[List[message.Message]]: - return self.request(Route('GET', '/channels/{channel_id}/pins', channel_id=channel_id)) + def pins_from( + self, + channel_id: Snowflake, + before: Optional[Snowflake] = None, + limit: Optional[int] = None, + ) -> Response[message.ChannelPins]: + params = {} + if before is not None: + params['before'] = before + if limit is not None: + params['limit'] = limit + + return self.request(Route('GET', '/channels/{channel_id}/messages/pins', channel_id=channel_id), params=params) # Member management diff --git a/discord/types/message.py b/discord/types/message.py index ae38db46f..02f877a4e 100644 --- a/discord/types/message.py +++ b/discord/types/message.py @@ -237,3 +237,13 @@ class AllowedMentions(TypedDict): roles: SnowflakeList users: SnowflakeList replied_user: bool + + +class MessagePin(TypedDict): + pinned_at: str + message: Message + + +class ChannelPins(TypedDict): + items: List[MessagePin] + has_more: bool From 7d7b2f5ed9f85e0b9b683ac6478b6b260a5e2cfb Mon Sep 17 00:00:00 2001 From: Soheab_ <33902984+Soheab@users.noreply.github.com> Date: Sat, 14 Jun 2025 15:44:59 +0200 Subject: [PATCH 02/19] Add Message.pinned_at and fix Message.pins --- discord/abc.py | 20 ++++++++++++-------- discord/http.py | 2 +- discord/message.py | 15 +++++++++++++++ 3 files changed, 28 insertions(+), 9 deletions(-) diff --git a/discord/abc.py b/discord/abc.py index a570510ee..28c858317 100644 --- a/discord/abc.py +++ b/discord/abc.py @@ -1710,7 +1710,7 @@ class Messageable: data = await self._state.http.get_message(channel.id, id) return self._state.create_message(channel=channel, data=data) - async def pins(self, *, before: Optional[SnowflakeTime] = None, limit: Optional[int] = None) -> List[Message]: + async def pins(self, *, before: Optional[datetime] = None, limit: Optional[int] = None) -> List[Message]: """|coro| Retrieves a maximum of 50 pinned messages from the destination. @@ -1728,8 +1728,8 @@ class Messageable: Parameters ----------- - before: Optional[Union[:class:`~discord.abc.Snowflake`, :class:`datetime.datetime`]] - Retrieve pinned messages before this date or message. + before: Optional[:class:`datetime.datetime`] + Retrieve pinned messages before this time. If a datetime is provided, it is recommended to use a UTC aware datetime. If the datetime is naive, it is assumed to be local time. limit: Optional[int] @@ -1749,13 +1749,17 @@ class Messageable: List[:class:`~discord.Message`] The messages that are currently pinned. """ - if isinstance(before, datetime): - before = Object(id=utils.time_snowflake(before, high=False)) + state = self._state + if before is not None: + if not isinstance(before, datetime): + raise TypeError(f'before must be a datetime object, not {before.__class__!r}') + if before.tzinfo is None: + raise TypeError( + 'before must be an aware datetime. Consider using discord.utils.utcnow() or datetime.datetime.now().astimezone() for local time.' + ) channel = await self._get_channel() - state = self._state - data = await state.http.pins_from(channel.id, before=before.id if before else None, limit=limit) - return [state.create_message(channel=channel, data=m["message"]) for m in data["items"]] + data = await state.http.pins_from(channel.id, before=before.isoformat() if before else None, limit=limit) async def history( self, diff --git a/discord/http.py b/discord/http.py index aa2488d8a..cd6008bc7 100644 --- a/discord/http.py +++ b/discord/http.py @@ -1054,7 +1054,7 @@ class HTTPClient: def pins_from( self, channel_id: Snowflake, - before: Optional[Snowflake] = None, + before: Optional[str] = None, limit: Optional[int] = None, ) -> Response[message.ChannelPins]: params = {} diff --git a/discord/message.py b/discord/message.py index 547e9c433..7487efb27 100644 --- a/discord/message.py +++ b/discord/message.py @@ -2171,6 +2171,7 @@ class Message(PartialMessage, Hashable): 'call', 'purchase_notification', 'message_snapshots', + '_pinned_at', ) if TYPE_CHECKING: @@ -2210,6 +2211,8 @@ class Message(PartialMessage, Hashable): self.application_id: Optional[int] = utils._get_as_snowflake(data, 'application_id') self.stickers: List[StickerItem] = [StickerItem(data=d, state=state) for d in data.get('sticker_items', [])] self.message_snapshots: List[MessageSnapshot] = MessageSnapshot._from_value(state, data.get('message_snapshots')) + # Set by Messageable.pins + self._pinned_at: Optional[datetime.datetime] = None self.poll: Optional[Poll] = None try: @@ -2629,6 +2632,18 @@ class Message(PartialMessage, Hashable): if self.guild is not None: # Fall back to guild threads in case one was created after the message return self._thread or self.guild.get_thread(self.id) + + @property + def pinned_at(self) -> Optional[datetime.datetime]: + """Optional[:class:`datetime.datetime`]: An aware UTC datetime object containing the time + when the message was pinned. + + .. note:: + This is only set for messages that are returned by :meth:`Messageable.pins`. + + .. versionadded:: 2.6 + """ + return self._pinned_at @property @deprecated('interaction_metadata') From 36b474c36756d40812d767cdef7f126db94f2083 Mon Sep 17 00:00:00 2001 From: Soheab_ <33902984+Soheab@users.noreply.github.com> Date: Sat, 14 Jun 2025 15:45:55 +0200 Subject: [PATCH 03/19] Fix Messageable.pins --- discord/abc.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/discord/abc.py b/discord/abc.py index 28c858317..f083e0db4 100644 --- a/discord/abc.py +++ b/discord/abc.py @@ -1760,6 +1760,12 @@ class Messageable: channel = await self._get_channel() data = await state.http.pins_from(channel.id, before=before.isoformat() if before else None, limit=limit) + ret: List[Message] = [] + for m in data["items"]: + message = state.create_message(channel=channel, data=m["message"]) + message._pinned_at = utils.parse_time(m.get("pinned_at")) + ret.append(message) + return ret async def history( self, From f501ab0c5b5f4b88e541bcebd1ceb355c8d5557b Mon Sep 17 00:00:00 2001 From: Soheab_ <33902984+Soheab@users.noreply.github.com> Date: Sat, 14 Jun 2025 15:48:08 +0200 Subject: [PATCH 04/19] Cleanup --- discord/abc.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/discord/abc.py b/discord/abc.py index f083e0db4..ab70ff01b 100644 --- a/discord/abc.py +++ b/discord/abc.py @@ -1749,7 +1749,6 @@ class Messageable: List[:class:`~discord.Message`] The messages that are currently pinned. """ - state = self._state if before is not None: if not isinstance(before, datetime): raise TypeError(f'before must be a datetime object, not {before.__class__!r}') @@ -1758,6 +1757,7 @@ class Messageable: 'before must be an aware datetime. Consider using discord.utils.utcnow() or datetime.datetime.now().astimezone() for local time.' ) + state = self._state channel = await self._get_channel() data = await state.http.pins_from(channel.id, before=before.isoformat() if before else None, limit=limit) ret: List[Message] = [] From a67fa1b53d9dbfd5ec59154097e3263e35d7bd29 Mon Sep 17 00:00:00 2001 From: Soheab_ <33902984+Soheab@users.noreply.github.com> Date: Sat, 14 Jun 2025 15:54:54 +0200 Subject: [PATCH 05/19] limit is actually 2-50 not 1-50 https://github.com/discord/discord-api-docs/pull/7585#discussion_r2146950055 --- discord/abc.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/discord/abc.py b/discord/abc.py index ab70ff01b..2871964e1 100644 --- a/discord/abc.py +++ b/discord/abc.py @@ -1735,7 +1735,7 @@ class Messageable: limit: Optional[int] The maximum number of pinned messages to retrieve. Defaults to 50. - This must be a number between 1 and 50. + This must be a number between 2 and 50. Raises ------- From a3b774b0e68bd05481efa6e159a032fe6d24bcde Mon Sep 17 00:00:00 2001 From: Soheab_ <33902984+Soheab@users.noreply.github.com> Date: Sat, 14 Jun 2025 16:00:48 +0200 Subject: [PATCH 06/19] forgot to run black on discord/message.py --- discord/message.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/discord/message.py b/discord/message.py index 7487efb27..8cb50fc9a 100644 --- a/discord/message.py +++ b/discord/message.py @@ -2632,10 +2632,10 @@ class Message(PartialMessage, Hashable): if self.guild is not None: # Fall back to guild threads in case one was created after the message return self._thread or self.guild.get_thread(self.id) - + @property def pinned_at(self) -> Optional[datetime.datetime]: - """Optional[:class:`datetime.datetime`]: An aware UTC datetime object containing the time + """Optional[:class:`datetime.datetime`]: An aware UTC datetime object containing the time when the message was pinned. .. note:: From 436e2b63aa5eee362e05b21eb3abe020de6492b3 Mon Sep 17 00:00:00 2001 From: Soheab_ <33902984+Soheab@users.noreply.github.com> Date: Sat, 14 Jun 2025 16:02:49 +0200 Subject: [PATCH 07/19] abc.Messageable --- discord/message.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/discord/message.py b/discord/message.py index 8cb50fc9a..6fbdecbfc 100644 --- a/discord/message.py +++ b/discord/message.py @@ -2639,7 +2639,7 @@ class Message(PartialMessage, Hashable): when the message was pinned. .. note:: - This is only set for messages that are returned by :meth:`Messageable.pins`. + This is only set for messages that are returned by :meth:`abc.Messageable.pins`. .. versionadded:: 2.6 """ From 1417d06702cdc9bbf896b87ba13c9cc51704ea79 Mon Sep 17 00:00:00 2001 From: Soheab_ <33902984+Soheab@users.noreply.github.com> Date: Fri, 20 Jun 2025 21:08:09 +0200 Subject: [PATCH 08/19] Add missing versionadded and exception --- discord/abc.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/discord/abc.py b/discord/abc.py index 2871964e1..39bb0d889 100644 --- a/discord/abc.py +++ b/discord/abc.py @@ -1732,17 +1732,23 @@ class Messageable: Retrieve pinned messages before this time. If a datetime is provided, it is recommended to use a UTC aware datetime. If the datetime is naive, it is assumed to be local time. + + .. versionadded:: 2.6 limit: Optional[int] The maximum number of pinned messages to retrieve. Defaults to 50. This must be a number between 2 and 50. + .. versionadded:: 2.6 + Raises ------- ~discord.Forbidden You do not have the permission to retrieve pinned messages. ~discord.HTTPException Retrieving the pinned messages failed. + TypeError + The ``before`` parameter was not an aware :class:`datetime.datetime` object. Returns -------- From e59658418c262568836ce053324d6fb19678357f Mon Sep 17 00:00:00 2001 From: Soheab_ <33902984+Soheab@users.noreply.github.com> Date: Sun, 22 Jun 2025 00:20:35 +0200 Subject: [PATCH 09/19] Make it paginate instead using the after param --- discord/abc.py | 110 ++++++++++++++++++++++++++++++++++++------------ discord/http.py | 5 ++- 2 files changed, 88 insertions(+), 27 deletions(-) diff --git a/discord/abc.py b/discord/abc.py index 39bb0d889..1486228c3 100644 --- a/discord/abc.py +++ b/discord/abc.py @@ -1710,15 +1710,20 @@ class Messageable: data = await self._state.http.get_message(channel.id, id) return self._state.create_message(channel=channel, data=data) - async def pins(self, *, before: Optional[datetime] = None, limit: Optional[int] = None) -> List[Message]: - """|coro| - - Retrieves a maximum of 50 pinned messages from the destination. + async def pins( + self, + *, + limit: Optional[int] = None, + before: SnowflakeTime = MISSING, + after: SnowflakeTime = MISSING, + ) -> AsyncIterator[Message]: + """Retrieves an :term:`asynchronous iterator` of the pinned messages in the channel. - Requires the :attr:`~discord.Permissions.view_channel` permission. + You must have :attr:`~discord.Permissions.view_channel` and + :attr:`~discord.Permissions.read_message_history` to get these. - No pins will be returned if the user is missing the - :attr:`~discord.Permissions.read_message_history` permission. + .. versionchanged:: 2.6 + Due to a change in Discord's API, this now returns a paginated iterator instead of a list. .. note:: @@ -1729,15 +1734,22 @@ class Messageable: Parameters ----------- before: Optional[:class:`datetime.datetime`] - Retrieve pinned messages before this time. + Retrieve pinned messages before this time or snowflake. If a datetime is provided, it is recommended to use a UTC aware datetime. If the datetime is naive, it is assumed to be local time. .. versionadded:: 2.6 - limit: Optional[int] - The maximum number of pinned messages to retrieve. Defaults to 50. + after: Optional[:class:`datetime.datetime`] + Retrieve pinned messages after this time or snowflake. + If a datetime is provided, it is recommended to use a UTC aware datetime. + If the datetime is naive, it is assumed to be local time. - This must be a number between 2 and 50. + .. versionadded:: 2.6 + limit: Optional[int] + The number of pinned messages to retrieve. If ``None``, it retrieves + every pinned message in the channel. Note, however, that this would + make it a slow operation. + Defaults to ``50``. .. versionadded:: 2.6 @@ -1755,23 +1767,69 @@ class Messageable: List[:class:`~discord.Message`] The messages that are currently pinned. """ - if before is not None: - if not isinstance(before, datetime): - raise TypeError(f'before must be a datetime object, not {before.__class__!r}') - if before.tzinfo is None: - raise TypeError( - 'before must be an aware datetime. Consider using discord.utils.utcnow() or datetime.datetime.now().astimezone() for local time.' - ) + if before is not MISSING and after is not MISSING: + raise TypeError('pins pagination does not support both before and after') - state = self._state + endpoint = self._state.http.pins_from channel = await self._get_channel() - data = await state.http.pins_from(channel.id, before=before.isoformat() if before else None, limit=limit) - ret: List[Message] = [] - for m in data["items"]: - message = state.create_message(channel=channel, data=m["message"]) - message._pinned_at = utils.parse_time(m.get("pinned_at")) - ret.append(message) - return ret + state = self._state + + async def _before_strategy(retrieve: int, before: Optional[SnowflakeTime], limit: Optional[int]): + before_time = ( + before.isoformat() + if isinstance(before, datetime) + else utils.snowflake_time(before.id).isoformat() + if before + else None + ) + data = await endpoint(channel.id, limit=retrieve, before=before_time) + + if data and data['items']: + if limit is not None: + limit -= len(data) + + before = utils.parse_time(data['items'][-1]['pinned_at']) + + return data, before, limit + + async def _after_strategy(retrieve: int, after: Optional[SnowflakeTime], limit: Optional[int]): + after_time = ( + after.isoformat() + if isinstance(after, datetime) + else utils.snowflake_time(after.id).isoformat() + if after + else None + ) + data = await endpoint(channel.id, limit=retrieve, after=after_time) + + if data and data['items']: + if limit is not None: + limit -= len(data) + + after = utils.parse_time(data['items'][-1]['pinned_at']) + + return data, after, limit + + if before: + strategy, time = _before_strategy, before + else: + strategy, time = _after_strategy, after + + while True: + retrieve = 50 if limit is None else min(limit, 50) + if retrieve < 1: + return + + data, time, limit = await strategy(retrieve, time, limit) + + # Terminate loop on next iteration; there's no data left after this + if len(data) < 50: + limit = 0 + + for m in data["items"]: + message = state.create_message(channel=channel, data=m['message']) + message._pinned_at = utils.parse_time(m['pinned_at']) + yield message async def history( self, diff --git a/discord/http.py b/discord/http.py index a907bbcd8..e6ac43113 100644 --- a/discord/http.py +++ b/discord/http.py @@ -1059,12 +1059,15 @@ class HTTPClient: def pins_from( self, channel_id: Snowflake, - before: Optional[str] = None, limit: Optional[int] = None, + before: Optional[str] = None, + after: Optional[str] = None, ) -> Response[message.ChannelPins]: params = {} if before is not None: params['before'] = before + if after is not None: + params['after'] = after if limit is not None: params['limit'] = limit From 7e38a2c0b114f766a6baafd4a872d6bb896f5ee0 Mon Sep 17 00:00:00 2001 From: Soheab_ <33902984+Soheab@users.noreply.github.com> Date: Sun, 22 Jun 2025 00:33:17 +0200 Subject: [PATCH 10/19] Update docstring --- discord/abc.py | 27 +++++++++++++++------------ 1 file changed, 15 insertions(+), 12 deletions(-) diff --git a/discord/abc.py b/discord/abc.py index 1486228c3..281c6c75d 100644 --- a/discord/abc.py +++ b/discord/abc.py @@ -1719,8 +1719,8 @@ class Messageable: ) -> AsyncIterator[Message]: """Retrieves an :term:`asynchronous iterator` of the pinned messages in the channel. - You must have :attr:`~discord.Permissions.view_channel` and - :attr:`~discord.Permissions.read_message_history` to get these. + You must have the following permissions to get these: + :attr:`~discord.Permissions.view_channel`, :attr:`~discord.Permissions.read_message_history`. .. versionchanged:: 2.6 Due to a change in Discord's API, this now returns a paginated iterator instead of a list. @@ -1733,24 +1733,24 @@ class Messageable: Parameters ----------- - before: Optional[:class:`datetime.datetime`] + limit: Optional[int] + The number of pinned messages to retrieve. If ``None``, it retrieves + every pinned message in the channel. Note, however, that this would + make it a slow operation. + Defaults to ``50``. + + .. versionadded:: 2.6 + before: Union[:class:`datetime.datetime`, :class:`.abc.Snowflake`] Retrieve pinned messages before this time or snowflake. If a datetime is provided, it is recommended to use a UTC aware datetime. If the datetime is naive, it is assumed to be local time. .. versionadded:: 2.6 - after: Optional[:class:`datetime.datetime`] + after: Union[:class:`datetime.datetime`, :class:`.abc.Snowflake`] Retrieve pinned messages after this time or snowflake. If a datetime is provided, it is recommended to use a UTC aware datetime. If the datetime is naive, it is assumed to be local time. - .. versionadded:: 2.6 - limit: Optional[int] - The number of pinned messages to retrieve. If ``None``, it retrieves - every pinned message in the channel. Note, however, that this would - make it a slow operation. - Defaults to ``50``. - .. versionadded:: 2.6 Raises @@ -1760,7 +1760,10 @@ class Messageable: ~discord.HTTPException Retrieving the pinned messages failed. TypeError - The ``before`` parameter was not an aware :class:`datetime.datetime` object. + + - The ``before`` parameter was not an aware :class:`datetime.datetime` object. + - The ``after`` parameter was not an aware :class:`datetime.datetime` object. + - The ``before`` and ``after`` parameters were both set. Returns -------- From deece62c1a144da770623cc48d237110f9a98402 Mon Sep 17 00:00:00 2001 From: Soheab_ <33902984+Soheab@users.noreply.github.com> Date: Sun, 22 Jun 2025 00:33:30 +0200 Subject: [PATCH 11/19] black discord\abc.py discord\http.py --- discord/abc.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/discord/abc.py b/discord/abc.py index 281c6c75d..07f02d093 100644 --- a/discord/abc.py +++ b/discord/abc.py @@ -1760,7 +1760,7 @@ class Messageable: ~discord.HTTPException Retrieving the pinned messages failed. TypeError - + - The ``before`` parameter was not an aware :class:`datetime.datetime` object. - The ``after`` parameter was not an aware :class:`datetime.datetime` object. - The ``before`` and ``after`` parameters were both set. From 6b5aefc31e5bdab58c17da9106c79314f48e7b6a Mon Sep 17 00:00:00 2001 From: Soheab_ <33902984+Soheab@users.noreply.github.com> Date: Sun, 22 Jun 2025 23:25:44 +0200 Subject: [PATCH 12/19] Remove after param but keep pagination --- discord/abc.py | 80 +++++++++++++------------------------------------- 1 file changed, 21 insertions(+), 59 deletions(-) diff --git a/discord/abc.py b/discord/abc.py index 07f02d093..ce12286f2 100644 --- a/discord/abc.py +++ b/discord/abc.py @@ -1715,7 +1715,6 @@ class Messageable: *, limit: Optional[int] = None, before: SnowflakeTime = MISSING, - after: SnowflakeTime = MISSING, ) -> AsyncIterator[Message]: """Retrieves an :term:`asynchronous iterator` of the pinned messages in the channel. @@ -1745,12 +1744,6 @@ class Messageable: If a datetime is provided, it is recommended to use a UTC aware datetime. If the datetime is naive, it is assumed to be local time. - .. versionadded:: 2.6 - after: Union[:class:`datetime.datetime`, :class:`.abc.Snowflake`] - Retrieve pinned messages after this time or snowflake. - If a datetime is provided, it is recommended to use a UTC aware datetime. - If the datetime is naive, it is assumed to be local time. - .. versionadded:: 2.6 Raises @@ -1762,74 +1755,43 @@ class Messageable: TypeError - The ``before`` parameter was not an aware :class:`datetime.datetime` object. - - The ``after`` parameter was not an aware :class:`datetime.datetime` object. - - The ``before`` and ``after`` parameters were both set. Returns -------- - List[:class:`~discord.Message`] - The messages that are currently pinned. + :class:`~discord.Message` + The pinned message with :attr:`.Message.pinned_at` set. """ - if before is not MISSING and after is not MISSING: - raise TypeError('pins pagination does not support both before and after') - - endpoint = self._state.http.pins_from channel = await self._get_channel() state = self._state + max_limit: int = 50 - async def _before_strategy(retrieve: int, before: Optional[SnowflakeTime], limit: Optional[int]): - before_time = ( - before.isoformat() - if isinstance(before, datetime) - else utils.snowflake_time(before.id).isoformat() - if before - else None - ) - data = await endpoint(channel.id, limit=retrieve, before=before_time) - - if data and data['items']: - if limit is not None: - limit -= len(data) - - before = utils.parse_time(data['items'][-1]['pinned_at']) - - return data, before, limit + time: Optional[SnowflakeTime] = before if before is not MISSING else None + while True: + limit = max_limit if limit is None else min(limit, max_limit) + if limit < 1: + return - async def _after_strategy(retrieve: int, after: Optional[SnowflakeTime], limit: Optional[int]): - after_time = ( - after.isoformat() - if isinstance(after, datetime) - else utils.snowflake_time(after.id).isoformat() - if after - else None + data = await self._state.http.pins_from( + channel_id=channel.id, + limit=limit, + before=(time if isinstance(time, datetime) else utils.snowflake_time(time.id)).isoformat() + if time is not None + else None, ) - data = await endpoint(channel.id, limit=retrieve, after=after_time) - if data and data['items']: + if data and data["items"]: + items = data["items"] if limit is not None: - limit -= len(data) - - after = utils.parse_time(data['items'][-1]['pinned_at']) - - return data, after, limit - - if before: - strategy, time = _before_strategy, before - else: - strategy, time = _after_strategy, after - - while True: - retrieve = 50 if limit is None else min(limit, 50) - if retrieve < 1: - return + limit -= len(items) - data, time, limit = await strategy(retrieve, time, limit) + time = utils.parse_time(items[-1]['pinned_at']) + items = data['items'] # Terminate loop on next iteration; there's no data left after this - if len(data) < 50: + if len(items) < max_limit or not data['has_more']: limit = 0 - for m in data["items"]: + for m in items: message = state.create_message(channel=channel, data=m['message']) message._pinned_at = utils.parse_time(m['pinned_at']) yield message From 38f014708df8403609d1d2664ed82a339c1a0e14 Mon Sep 17 00:00:00 2001 From: Soheab_ <33902984+Soheab@users.noreply.github.com> Date: Sun, 22 Jun 2025 23:27:12 +0200 Subject: [PATCH 13/19] Remove after param from http method too --- discord/http.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/discord/http.py b/discord/http.py index e6ac43113..662fe468c 100644 --- a/discord/http.py +++ b/discord/http.py @@ -1061,13 +1061,10 @@ class HTTPClient: channel_id: Snowflake, limit: Optional[int] = None, before: Optional[str] = None, - after: Optional[str] = None, ) -> Response[message.ChannelPins]: params = {} if before is not None: params['before'] = before - if after is not None: - params['after'] = after if limit is not None: params['limit'] = limit From f30d164f1204efa01a8d1db0f8e7d42a926a5e1e Mon Sep 17 00:00:00 2001 From: Soheab_ <33902984+Soheab@users.noreply.github.com> Date: Sun, 22 Jun 2025 23:31:24 +0200 Subject: [PATCH 14/19] Update abc.py --- discord/abc.py | 1 + 1 file changed, 1 insertion(+) diff --git a/discord/abc.py b/discord/abc.py index ce12286f2..916a1b3ad 100644 --- a/discord/abc.py +++ b/discord/abc.py @@ -1761,6 +1761,7 @@ class Messageable: :class:`~discord.Message` The pinned message with :attr:`.Message.pinned_at` set. """ + channel = await self._get_channel() state = self._state max_limit: int = 50 From 8993d48e1270118afc9aef6d02d534d7bc89a538 Mon Sep 17 00:00:00 2001 From: Soheab_ <33902984+Soheab@users.noreply.github.com> Date: Sun, 22 Jun 2025 23:40:52 +0200 Subject: [PATCH 15/19] Remove extraneous conversion between timestamp and datetime for every request --- discord/abc.py | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/discord/abc.py b/discord/abc.py index 916a1b3ad..a601f57ca 100644 --- a/discord/abc.py +++ b/discord/abc.py @@ -1766,7 +1766,11 @@ class Messageable: state = self._state max_limit: int = 50 - time: Optional[SnowflakeTime] = before if before is not MISSING else None + time: Optional[str] = ( + (before if isinstance(before, datetime) else utils.snowflake_time(before.id)).isoformat() + if before is not None + else None + ) while True: limit = max_limit if limit is None else min(limit, max_limit) if limit < 1: @@ -1775,9 +1779,7 @@ class Messageable: data = await self._state.http.pins_from( channel_id=channel.id, limit=limit, - before=(time if isinstance(time, datetime) else utils.snowflake_time(time.id)).isoformat() - if time is not None - else None, + before=time, ) if data and data["items"]: @@ -1785,7 +1787,7 @@ class Messageable: if limit is not None: limit -= len(items) - time = utils.parse_time(items[-1]['pinned_at']) + time = items[-1]['pinned_at'] items = data['items'] # Terminate loop on next iteration; there's no data left after this From 31a34ad7c5b6124db568e99c75ece3e4fced94c6 Mon Sep 17 00:00:00 2001 From: Soheab_ <33902984+Soheab@users.noreply.github.com> Date: Sun, 22 Jun 2025 23:53:08 +0200 Subject: [PATCH 16/19] Fix limit always being set to --- discord/abc.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/discord/abc.py b/discord/abc.py index a601f57ca..4e13973f5 100644 --- a/discord/abc.py +++ b/discord/abc.py @@ -1772,13 +1772,13 @@ class Messageable: else None ) while True: - limit = max_limit if limit is None else min(limit, max_limit) - if limit < 1: + retrieve = max_limit if limit is None else min(limit, max_limit) + if retrieve < 1: return data = await self._state.http.pins_from( channel_id=channel.id, - limit=limit, + limit=retrieve, before=time, ) From 7b040037b5d07adca31cfac061a8a871a3663742 Mon Sep 17 00:00:00 2001 From: Soheab_ <33902984+Soheab@users.noreply.github.com> Date: Mon, 23 Jun 2025 00:00:20 +0200 Subject: [PATCH 17/19] Add oldest_first param --- discord/abc.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/discord/abc.py b/discord/abc.py index 4e13973f5..24b4a2e14 100644 --- a/discord/abc.py +++ b/discord/abc.py @@ -1715,6 +1715,7 @@ class Messageable: *, limit: Optional[int] = None, before: SnowflakeTime = MISSING, + oldest_first: bool = False, ) -> AsyncIterator[Message]: """Retrieves an :term:`asynchronous iterator` of the pinned messages in the channel. @@ -1744,6 +1745,11 @@ class Messageable: If a datetime is provided, it is recommended to use a UTC aware datetime. If the datetime is naive, it is assumed to be local time. + .. versionadded:: 2.6 + oldest_first: :class:`bool` + If set to ``True``, return messages in oldest->newest order. + Defaults to ``False``. + .. versionadded:: 2.6 Raises @@ -1794,6 +1800,9 @@ class Messageable: if len(items) < max_limit or not data['has_more']: limit = 0 + if oldest_first: + reversed(items) + for m in items: message = state.create_message(channel=channel, data=m['message']) message._pinned_at = utils.parse_time(m['pinned_at']) From 4f67cabc7072fda2cb02223e799c796d892e793c Mon Sep 17 00:00:00 2001 From: Soheab_ <33902984+Soheab@users.noreply.github.com> Date: Mon, 23 Jun 2025 00:00:37 +0200 Subject: [PATCH 18/19] Remove unused exception from doc --- discord/abc.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/discord/abc.py b/discord/abc.py index 24b4a2e14..e93a881cd 100644 --- a/discord/abc.py +++ b/discord/abc.py @@ -1758,9 +1758,6 @@ class Messageable: You do not have the permission to retrieve pinned messages. ~discord.HTTPException Retrieving the pinned messages failed. - TypeError - - - The ``before`` parameter was not an aware :class:`datetime.datetime` object. Returns -------- From 98a0fe235412e6f7de18214321b95082b82e47af Mon Sep 17 00:00:00 2001 From: Soheab_ <33902984+Soheab@users.noreply.github.com> Date: Mon, 23 Jun 2025 00:10:18 +0200 Subject: [PATCH 19/19] Define items once --- discord/abc.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/discord/abc.py b/discord/abc.py index e93a881cd..7f2d83158 100644 --- a/discord/abc.py +++ b/discord/abc.py @@ -1785,14 +1785,13 @@ class Messageable: before=time, ) - if data and data["items"]: - items = data["items"] + items = data and data["items"] + if items: if limit is not None: limit -= len(items) time = items[-1]['pinned_at'] - items = data['items'] # Terminate loop on next iteration; there's no data left after this if len(items) < max_limit or not data['has_more']: limit = 0