From 705eb2c2a5ab504f754626ea78d290280386e6b9 Mon Sep 17 00:00:00 2001 From: Soheab <33902984+Soheab@users.noreply.github.com> Date: Thu, 14 Aug 2025 02:47:14 +0200 Subject: [PATCH] Update to support new pin endpoints --- discord/abc.py | 152 +++++++++++++++++++++++++++++++++++---- discord/http.py | 19 +++-- discord/message.py | 15 ++++ discord/types/message.py | 10 +++ 4 files changed, 179 insertions(+), 17 deletions(-) diff --git a/discord/abc.py b/discord/abc.py index a56451da6..1da7a721c 100644 --- a/discord/abc.py +++ b/discord/abc.py @@ -34,8 +34,10 @@ from typing import ( AsyncIterator, Callable, Dict, + Generator, Iterable, List, + Literal, Optional, TYPE_CHECKING, Protocol, @@ -61,6 +63,7 @@ from .voice_client import VoiceClient, VoiceProtocol from .sticker import GuildSticker, StickerItem from . import utils from .flags import InviteFlags +import warnings __all__ = ( 'Snowflake', @@ -114,6 +117,11 @@ if TYPE_CHECKING: MessageableChannel = Union[PartialMessageableChannel, GroupChannel] SnowflakeTime = Union["Snowflake", datetime] + class PinnedMessage(Message): + pinned_at: datetime + pinned: Literal[True] + + MISSING = utils.MISSING @@ -125,6 +133,26 @@ class _Undefined: _undefined: Any = _Undefined() +class _PinsIterator: + def __init__(self, iterator: AsyncIterator[PinnedMessage]) -> None: + self.__iterator: AsyncIterator[PinnedMessage] = iterator + + def __await__(self) -> Generator[Any, None, List[PinnedMessage]]: + warnings.warn( + "`await .pins()` is deprecated; use `async for message in .pins()` instead.", + DeprecationWarning, + stacklevel=2, + ) + + async def gather() -> List[PinnedMessage]: + return [msg async for msg in self.__iterator] + + return gather().__await__() + + def __aiter__(self) -> AsyncIterator[PinnedMessage]: + return self.__iterator + + async def _single_delete_strategy(messages: Iterable[Message], *, reason: Optional[str] = None): for m in messages: try: @@ -1754,17 +1782,119 @@ 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]: - """|coro| + async def __pins( + self, + *, + limit: Optional[int] = 50, + before: Optional[SnowflakeTime] = None, + oldest_first: bool = False, + ) -> AsyncIterator[PinnedMessage]: + channel = await self._get_channel() + state = self._state + max_limit: int = 50 + + time: Optional[str] = ( + (before if isinstance(before, datetime) else utils.snowflake_time(before.id)).isoformat() + if before is not None + else None + ) + + while True: + retrieve = max_limit if limit is None else min(limit, max_limit) + if retrieve < 1: + break + + data = await self._state.http.pins_from( + channel_id=channel.id, + limit=retrieve, + before=time, + ) + + items = data and data['items'] + if items: + if limit is not None: + limit -= len(items) + + time = items[-1]['pinned_at'] + + # Terminate loop on next iteration; there's no data left after this + if len(items) < max_limit or not data['has_more']: + limit = 0 + + if oldest_first: + items = reversed(items) + + count = 0 + for count, m in enumerate(items, start=1): + message: Message = state.create_message(channel=channel, data=m['message']) + message._pinned_at = utils.parse_time(m['pinned_at']) + yield message # pyright: ignore[reportReturnType] + + if count < max_limit: + break + + def pins( + self, + *, + limit: Optional[int] = 50, + before: Optional[SnowflakeTime] = None, + oldest_first: bool = False, + ) -> _PinsIterator: + """Retrieves an :term:`asynchronous iterator` of the pinned messages in the channel. - Retrieves all messages that are currently pinned in the channel. + You must have :attr:`~discord.Permissions.view_channel` and + :attr:`~discord.Permissions.read_message_history` in order to use this. + + .. versionchanged:: 2.6 + + Due to a change in Discord's API, this now returns a paginated iterator instead of a list. + + For backwards compatibility, you can still retrieve a list of pinned messages by + using ``await`` on the returned object. This is however deprecated. .. note:: Due to a limitation with the Discord API, the :class:`.Message` - objects returned by this method do not contain complete + object returned by this method does not contain complete :attr:`.Message.reactions` data. + Examples + --------- + + Usage :: + + counter = 0 + async for message in channel.pins(limit=250): + counter += 1 + + Flattening into a list: :: + + messages = [message async for message in channel.pins(limit=50)] + # messages is now a list of Message... + + All parameters are optional. + + Parameters + ----------- + 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: Optional[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 + oldest_first: :class:`bool` + If set to ``True``, return messages in oldest pin->newest pin order. + Defaults to ``False``. + + .. versionadded:: 2.6 + Raises ------- ~discord.Forbidden @@ -1772,16 +1902,12 @@ class Messageable: ~discord.HTTPException Retrieving the pinned messages failed. - Returns - -------- - List[:class:`~discord.Message`] - The messages that are currently pinned. + Yields + ------- + :class:`~discord.Message` + The pinned message with :attr:`.Message.pinned_at` set. """ - - 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] + return _PinsIterator(self.__pins(limit=limit, before=before, oldest_first=oldest_first)) async def history( self, diff --git a/discord/http.py b/discord/http.py index 800e4cab1..9d1b85a50 100644 --- a/discord/http.py +++ b/discord/http.py @@ -1047,7 +1047,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, ) @@ -1056,14 +1056,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, + limit: Optional[int] = None, + before: Optional[str] = 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/message.py b/discord/message.py index 039ac1ba7..d0af26124 100644 --- a/discord/message.py +++ b/discord/message.py @@ -2185,6 +2185,7 @@ class Message(PartialMessage, Hashable): 'call', 'purchase_notification', 'message_snapshots', + '_pinned_at', ) if TYPE_CHECKING: @@ -2224,6 +2225,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: @@ -2644,6 +2647,18 @@ class Message(PartialMessage, Hashable): # 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:`abc.Messageable.pins`. + + .. versionadded:: 2.6 + """ + return self._pinned_at + @property @deprecated('interaction_metadata') def interaction(self) -> Optional[MessageInteraction]: diff --git a/discord/types/message.py b/discord/types/message.py index 6c260d44d..dfb251f28 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