Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
9e104db
implement the new pin endpoints
Snipy7374 Jul 18, 2025
e162b3f
pass messageable to the async iterator instead of the channel object
Snipy7374 Jul 19, 2025
ea2c0a3
fix before behaviour
Snipy7374 Jul 19, 2025
55ffca7
fix type check
Snipy7374 Jul 19, 2025
a9da445
add changelog entries
Snipy7374 Jul 19, 2025
843a07e
update changelog entry
Snipy7374 Jul 19, 2025
20b5c06
add doc note for passing Snowflake
Snipy7374 Jul 19, 2025
5dffc57
remove debugging print
Snipy7374 Jul 19, 2025
99644ad
avoid type ignores where possible
Snipy7374 Jul 19, 2025
a40b09c
Merge branch 'master' into ft/pins
Snipy7374 Jul 19, 2025
2ee3750
Update changelog/1305.feature.rst
Snipy7374 Aug 22, 2025
379f269
fix docs
Snipy7374 Aug 22, 2025
6c253d5
convert python3.10+ union to Optional
Snipy7374 Aug 22, 2025
95a2e99
remove channel_id member
Snipy7374 Aug 22, 2025
9d5918e
move channel initialization for pins iterator
Snipy7374 Aug 26, 2025
1a02aca
Update disnake/abc.py
Snipy7374 Aug 28, 2025
f6881aa
Update disnake/abc.py
Snipy7374 Aug 28, 2025
04fdabb
Update disnake/message.py
Snipy7374 Aug 28, 2025
7bcba8a
run lint that vi broke with her suggestion
Snipy7374 Aug 28, 2025
d9d2bbd
Merge branch 'master' into ft/pins
Snipy7374 Aug 28, 2025
0a856e8
Update changelog/1305.feature.rst
Snipy7374 Aug 30, 2025
c836e36
Update disnake/abc.py
Snipy7374 Aug 30, 2025
2861c48
Update disnake/abc.py
Snipy7374 Aug 30, 2025
ec4c144
Update disnake/abc.py
Snipy7374 Aug 30, 2025
f589246
Update disnake/http.py
Snipy7374 Aug 30, 2025
a818a43
remove handle pinned at method
Snipy7374 Sep 1, 2025
8d91fe6
Merge branch 'master' into ft/pins
Snipy7374 Sep 1, 2025
2f79f27
Merge branch 'master' into ft/pins
onerandomusername Sep 2, 2025
cd6a610
change deprecation notice wording
Snipy7374 Sep 3, 2025
ebdc689
Merge branch 'master' into ft/pins
Snipy7374 Sep 3, 2025
6fb35ca
mention that now you need the pin_messages permission to pin a message
Snipy7374 Sep 3, 2025
36b9fbd
update docstrings about the new pin messages permission
Snipy7374 Sep 3, 2025
39ed638
Merge branch 'master' into ft/pins
Snipy7374 Sep 3, 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
1 change: 1 addition & 0 deletions changelog/1305.deprecate.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Deprecate awaiting :meth:`.Messageable.pins` in favour of ``async for msg in channel.pins()``.
1 change: 1 addition & 0 deletions changelog/1305.feature.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Update the :meth:`.Messageable.pins`, :meth:`Message.pin` and :meth:`Message.unpin` methods to use the new API endpoints. :meth:`.Messageable.pins` returns now an asynchronous iterator to yield all pinned messages.
57 changes: 45 additions & 12 deletions disnake/abc.py
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@
from .enums import InviteTarget
from .guild import Guild, GuildChannel as AnyGuildChannel, GuildMessageable
from .guild_scheduled_event import GuildScheduledEvent
from .iterators import HistoryIterator
from .iterators import ChannelPinsIterator, HistoryIterator
from .member import Member
from .message import Message, MessageReference, PartialMessage
from .poll import Poll
Expand Down Expand Up @@ -1863,31 +1863,64 @@ async def fetch_message(self, id: int, /) -> Message:
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|
def pins(
self, *, limit: Optional[int] = 50, before: Optional[SnowflakeTime] = None
) -> ChannelPinsIterator:
"""Returns an :class:`.AsyncIterator` that enables receiving the destination's pinned messages.

Retrieves all messages that are currently pinned in the channel.
You must have the :attr:`.Permissions.read_message_history` and :attr:`.Permissions.view_channel` permissions to use this.

.. note::

Due to a limitation with the Discord API, the :class:`.Message`
objects returned by this method do not contain complete
:attr:`.Message.reactions` data.

.. versionchanged:: 2.11
Now returns an :class:`.AsyncIterator` to support changes in Discord's API.
``await``\\ing the result of this method remains supported, but only returns the
last 50 pins and is deprecated in favor of ``async for msg in channel.pins()``.

Examples
--------
Usage ::

counter = 0
async for message in channel.pins(limit=100):
if message.author == client.user:
counter += 1

Flattening to a list ::

pinned_messages = await channel.pins(limit=100).flatten()
# pinned_messages is now a list of Message...

All parameters are optional.

Parameters
----------
limit: Optional[:class:`int`]
The number of pinned messages to retrieve.
If ``None``, retrieves every pinned message in the channel. Note, however,
that this would make it a slow operation.
before: Optional[Union[:class:`.abc.Snowflake`, :class:`datetime.datetime`]]
Retrieve messages pinned 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.

Raises
------
HTTPException
Retrieving the pinned messages failed.

Returns
-------
List[:class:`.Message`]
The messages that are currently pinned.
Yields
------
:class:`.Message`
The pinned message from the parsed message data.
"""
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]
from .iterators import ChannelPinsIterator # due to cyclic imports

return ChannelPinsIterator(self, limit=limit, before=before)

def history(
self,
Expand Down
23 changes: 19 additions & 4 deletions disnake/http.py
Original file line number Diff line number Diff line change
Expand Up @@ -876,7 +876,7 @@ def pin_message(
) -> 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 @@ -887,14 +887,29 @@ def unpin_message(
) -> 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 get_pins(
self,
channel_id: Snowflake,
limit: int,
before: Optional[Snowflake] = None,
) -> Response[channel.ChannelPins]:
r = Route(
"GET",
"/channels/{channel_id}/messages/pins",
channel_id=channel_id,
)
params: Dict[str, Any] = {"limit": limit}

if before is not None:
params["before"] = before

return self.request(r, params=params)

# Member management

Expand Down
76 changes: 75 additions & 1 deletion disnake/iterators.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
Awaitable,
Callable,
Dict,
Generator,
List,
Optional,
TypeVar,
Expand All @@ -29,7 +30,7 @@
from .object import Object
from .subscription import Subscription
from .threads import Thread
from .utils import maybe_coroutine, snowflake_time, time_snowflake
from .utils import deprecated, maybe_coroutine, parse_time, snowflake_time, time_snowflake

__all__ = (
"ReactionIterator",
Expand Down Expand Up @@ -1308,3 +1309,76 @@ async def fill_users(self) -> None:
if not (self.guild is None or isinstance(self.guild, Object)):
member = self.guild.get_member(int(element["id"]))
await self.users.put(member or self.state.create_user(data=element))


class ChannelPinsIterator(_AsyncIterator["Message"]):
def __init__(
self,
messageable: Messageable,
*,
limit: Optional[int],
before: Optional[Union[Snowflake, datetime.datetime]] = None,
) -> None:
before_ = None
if before is not None:
if isinstance(before, datetime.datetime):
before_ = before.isoformat()
elif isinstance(before, Object):
before_ = snowflake_time(before.id).isoformat()
else:
raise TypeError(
f"Expected either `disnake.Snowflake` or `datetime.datetime` for `before`. Got `{before.__class__.__name__!r}`."
)

self.messageable = messageable
self._state = messageable._state
self.limit = limit
self.before: Optional[str] = before_

self.getter = self._state.http.get_pins
self.messages: asyncio.Queue[Message] = asyncio.Queue()

# defined to maintain backward compatibility with the old `pins` method
@deprecated("async for msg in channel.pins()")
def __await__(self) -> Generator[None, None, List[Message]]:
return self.flatten().__await__()

async def next(self) -> Message:
if self.messages.empty():
await self.fill_messages()

try:
return self.messages.get_nowait()
except asyncio.QueueEmpty:
raise NoMoreItems from None

def _get_retrieve(self) -> bool:
self.retrieve = min(self.limit, 50) if self.limit is not None else 50
return self.retrieve > 0

async def fill_messages(self) -> None:
if not hasattr(self, "channel"):
channel = await self.messageable._get_channel()
self.channel = channel

if self._get_retrieve():
data = await self.getter(
channel_id=self.channel.id,
before=self.before,
limit=self.retrieve,
)

if len(data):
if self.limit is not None:
self.limit -= self.retrieve

if data["items"]:
self.before = data["items"][-1]["pinned_at"]

if not data["has_more"]:
self.limit = 0 # terminate loop

for element in data["items"]:
message = self._state.create_message(channel=self.channel, data=element["message"])
message._pinned_at = parse_time(element["pinned_at"])
await self.messages.put(message)
17 changes: 15 additions & 2 deletions disnake/message.py
Original file line number Diff line number Diff line change
Expand Up @@ -1157,6 +1157,7 @@ class Message(Hashable):
"poll",
"_edited_timestamp",
"_role_subscription_data",
"_pinned_at",
)

if TYPE_CHECKING:
Expand Down Expand Up @@ -1197,6 +1198,7 @@ def __init__(
)
self.type: MessageType = try_enum(MessageType, data["type"])
self.pinned: bool = data["pinned"]
self._pinned_at: Optional[datetime.datetime] = None
self.flags: MessageFlags = MessageFlags._from_value(data.get("flags", 0))
self.mention_everyone: bool = data["mention_everyone"]
self.tts: bool = data["tts"]
Expand Down Expand Up @@ -1556,6 +1558,17 @@ def edited_at(self) -> Optional[datetime.datetime]:
"""Optional[:class:`datetime.datetime`]: An aware UTC datetime object containing the edited time of the message."""
return self._edited_timestamp

@property
def pinned_at(self) -> Optional[datetime.datetime]:
"""Optional[:class:`datetime.datetime`]: An aware UTC datetime object containing the pin time of the message.

.. note::
This is only set on messages retrieved using :meth:`abc.Messageable.pins`.

.. versionadded:: 2.11
"""
return self._pinned_at

@property
def jump_url(self) -> str:
""":class:`str`: Returns a URL that allows the client to jump to this message."""
Expand Down Expand Up @@ -2137,7 +2150,7 @@ async def pin(self, *, reason: Optional[str] = None) -> None:

Pins the message.

You must have the :attr:`~Permissions.manage_messages` permission to do
You must have the :attr:`~Permissions.pin_messages` permission to do
this in a non-private channel context.

This does not work with messages sent in a :class:`VoiceChannel` or :class:`StageChannel`.
Expand Down Expand Up @@ -2167,7 +2180,7 @@ async def unpin(self, *, reason: Optional[str] = None) -> None:

Unpins the message.

You must have the :attr:`~Permissions.manage_messages` permission to do
You must have the :attr:`~Permissions.pin_messages` permission to do
this in a non-private channel context.

Parameters
Expand Down
2 changes: 1 addition & 1 deletion disnake/permissions.py
Original file line number Diff line number Diff line change
Expand Up @@ -760,7 +760,7 @@ def send_tts_messages(self) -> int:

@flag_value
def manage_messages(self) -> int:
""":class:`bool`: Returns ``True`` if a user can delete or pin messages in a text channel.
""":class:`bool`: Returns ``True`` if a user can delete messages in a text channel.

.. note::

Expand Down
6 changes: 6 additions & 0 deletions disnake/types/channel.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

from typing_extensions import NotRequired

from .message import MessagePin
from .snowflake import Snowflake
from .threads import ForumTag, ThreadArchiveDurationLiteral, ThreadMember, ThreadMetadata
from .user import PartialUser
Expand Down Expand Up @@ -196,3 +197,8 @@ class CreateGuildChannel(TypedDict):
rtc_region: NotRequired[Optional[str]]
video_quality_mode: NotRequired[Optional[VideoQualityMode]]
default_auto_archive_duration: NotRequired[Optional[ThreadArchiveDurationLiteral]]


class ChannelPins(TypedDict):
items: List[MessagePin]
has_more: bool
5 changes: 5 additions & 0 deletions disnake/types/message.py
Original file line number Diff line number Diff line change
Expand Up @@ -159,3 +159,8 @@ class AllowedMentions(TypedDict):
roles: SnowflakeList
users: SnowflakeList
replied_user: bool


class MessagePin(TypedDict):
pinned_at: str
message: Message
Loading