Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
612657b
Use new pins endpoints
Soheab Jun 7, 2025
7d7b2f5
Add Message.pinned_at and fix Message.pins
Soheab Jun 14, 2025
36b474c
Fix Messageable.pins
Soheab Jun 14, 2025
f501ab0
Cleanup
Soheab Jun 14, 2025
a67fa1b
limit is actually 2-50 not 1-50
Soheab Jun 14, 2025
a3b774b
forgot to run black on discord/message.py
Soheab Jun 14, 2025
436e2b6
abc.Messageable
Soheab Jun 14, 2025
cf08f4b
Merge remote-tracking branch 'upstream/master' into new-pins-endpoint
Soheab Jun 20, 2025
25cf99d
Merge remote-tracking branch 'upstream/master' into new-pins-endpoint
Soheab Jun 20, 2025
1417d06
Add missing versionadded and exception
Soheab Jun 20, 2025
e596584
Make it paginate instead using the after param
Soheab Jun 21, 2025
7e38a2c
Update docstring
Soheab Jun 21, 2025
deece62
black discord\abc.py discord\http.py
Soheab Jun 21, 2025
2126e88
Merge branch 'Rapptz:master' into new-pins-endpoint
Soheab Jun 22, 2025
6b5aefc
Remove after param but keep pagination
Soheab Jun 22, 2025
38f0147
Remove after param from http method too
Soheab Jun 22, 2025
f30d164
Update abc.py
Soheab Jun 22, 2025
8993d48
Remove extraneous conversion between timestamp and datetime for every…
Soheab Jun 22, 2025
31a34ad
Fix limit always being set to <max limit>
Soheab Jun 22, 2025
7b04003
Add oldest_first param
Soheab Jun 22, 2025
4f67cab
Remove unused exception from doc
Soheab Jun 22, 2025
98a0fe2
Define items once
Soheab Jun 22, 2025
4ced1eb
Merge remote-tracking branch 'upstream/master' into new-pins-endpoint
Soheab Aug 12, 2025
df5ca30
Backwards compat and fixes
Soheab Aug 13, 2025
76e74c4
Improve _PinsIterator
Soheab Aug 13, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
152 changes: 139 additions & 13 deletions discord/abc.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,8 +34,10 @@
AsyncIterator,
Callable,
Dict,
Generator,
Iterable,
List,
Literal,
Optional,
TYPE_CHECKING,
Protocol,
Expand All @@ -61,6 +63,7 @@
from .sticker import GuildSticker, StickerItem
from . import utils
from .flags import InviteFlags
import warnings

__all__ = (
'Snowflake',
Expand All @@ -74,7 +77,7 @@
T = TypeVar('T', bound=VoiceProtocol)

if TYPE_CHECKING:
from typing_extensions import Self

Check warning on line 80 in discord/abc.py

View workflow job for this annotation

GitHub Actions / check 3.x

Import "typing_extensions" could not be resolved from source (reportMissingModuleSource)

from .client import Client
from .user import ClientUser
Expand Down Expand Up @@ -114,6 +117,11 @@
MessageableChannel = Union[PartialMessageableChannel, GroupChannel]
SnowflakeTime = Union["Snowflake", datetime]

class PinnedMessage(Message):
pinned_at: datetime
pinned: Literal[True]


MISSING = utils.MISSING


Expand All @@ -125,6 +133,26 @@
_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 <channel>.pins()` is deprecated; use `async for message in <channel>.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:
Expand Down Expand Up @@ -1722,34 +1750,132 @@
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
You do not have the permission to retrieve pinned messages.
~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,
Expand Down
19 changes: 15 additions & 4 deletions discord/http.py
Original file line number Diff line number Diff line change
Expand Up @@ -1041,7 +1041,7 @@ def publish_message(self, channel_id: Snowflake, message_id: Snowflake) -> Respo
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,
)
Expand All @@ -1050,14 +1050,25 @@ def pin_message(self, channel_id: Snowflake, message_id: Snowflake, reason: Opti
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

Expand Down
15 changes: 15 additions & 0 deletions discord/message.py
Original file line number Diff line number Diff line change
Expand Up @@ -2174,6 +2174,7 @@ class Message(PartialMessage, Hashable):
'call',
'purchase_notification',
'message_snapshots',
'_pinned_at',
)

if TYPE_CHECKING:
Expand Down Expand Up @@ -2213,6 +2214,8 @@ def __init__(
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:
Expand Down Expand Up @@ -2633,6 +2636,18 @@ def thread(self) -> Optional[Thread]:
# 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]:
Expand Down
10 changes: 10 additions & 0 deletions discord/types/message.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Loading