diff --git a/CHANGELOG.md b/CHANGELOG.md index 7d76398448..4bb690976e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -161,6 +161,8 @@ These changes are available on the `master` branch, but have not yet been releas ([#2797](https://github.com/Pycord-Development/pycord/pull/2797)) - Upgraded voice websocket version to v8. ([#2812](https://github.com/Pycord-Development/pycord/pull/2812)) +- `Messageable.pins()` now returns a `MessagePinIterator` and has new arguments. + ([#2872](https://github.com/Pycord-Development/pycord/pull/2872)) ### Deprecated @@ -170,6 +172,9 @@ These changes are available on the `master` branch, but have not yet been releas ([#2501](https://github.com/Pycord-Development/pycord/pull/2501)) - Deprecated `Interaction.cached_channel` in favor of `Interaction.channel`. ([#2658](https://github.com/Pycord-Development/pycord/pull/2658)) +- Deprecated `Messageable.pins()` returning a list of `Message`; it should be used as an + iterator of `MessagePin` instead. + ([#2872](https://github.com/Pycord-Development/pycord/pull/2872)) ### Removed diff --git a/discord/abc.py b/discord/abc.py index 4f419a3942..1fd36948e1 100644 --- a/discord/abc.py +++ b/discord/abc.py @@ -48,7 +48,7 @@ from .file import File, VoiceMessage from .flags import ChannelFlags, MessageFlags from .invite import Invite -from .iterators import HistoryIterator +from .iterators import HistoryIterator, MessagePinIterator from .mentions import AllowedMentions from .partial_emoji import PartialEmoji, _EmojiTag from .permissions import PermissionOverwrite, Permissions @@ -1754,32 +1754,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: int | None = 50, + before: SnowflakeTime | None = None, + ) -> MessagePinIterator: + """Returns a :class:`~discord.MessagePinIterator` that enables receiving the destination's pinned messages. - Retrieves all messages that are currently pinned in the channel. + You must have :attr:`~discord.Permissions.read_message_history` permissions to use this. - .. note:: + .. warning:: - Due to a limitation with the Discord API, the :class:`.Message` - objects returned by this method do not contain complete - :attr:`.Message.reactions` data. + Starting from version 3.0, `await channel.pins()` will no longer return a list of :class:`Message`. See examples below for new usage instead. - Returns - ------- - List[:class:`~discord.Message`] - The messages that are currently pinned. + Parameters + ---------- + limit: Optional[:class:`int`] + The number of pinned messages to retrieve. + If ``None``, retrieves every pinned message in the channel. + before: Optional[Union[:class:`~discord.abc.Snowflake`, :class:`datetime.datetime`]] + Retrieve messages pinned before this datetime. + 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. + + Yields + ------ + :class:`~discord.MessagePin` + The pinned message. Raises ------ + ~discord.Forbidden + You do not have permissions to get pinned messages. ~discord.HTTPException - Retrieving the pinned messages failed. - """ + The request to get pinned messages failed. - 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] + Examples + -------- + + Usage :: + + counter = 0 + async for pin in channel.pins(limit=250): + if pin.message.author == client.user: + counter += 1 + + Flattening into a list: :: + + pins = await channel.pins(limit=None).flatten() + # pins is now a list of MessagePin... + + All parameters are optional. + """ + return MessagePinIterator( + self, + limit=limit, + before=before, + ) def can_send(self, *objects) -> bool: """Returns a :class:`bool` indicating whether you have the permissions to send the object(s). diff --git a/discord/http.py b/discord/http.py index 145a3c411b..030ecfa468 100644 --- a/discord/http.py +++ b/discord/http.py @@ -888,7 +888,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, ) @@ -899,13 +899,30 @@ 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]]: + def pins_from( + self, + channel_id: Snowflake, + limit: int | None = None, + before: str | None = None, + ) -> Response[message.MessagePinPagination]: + r = Route("GET", "/channels/{channel_id}/messages/pins", channel_id=channel_id) + params = {} + if limit: + params["limit"] = limit + if before: + params["before"] = before + + return self.request(r, params=params) + + def legacy_pins_from( + self, channel_id: Snowflake + ) -> Response[list[message.Message]]: return self.request( Route("GET", "/channels/{channel_id}/pins", channel_id=channel_id) ) diff --git a/discord/iterators.py b/discord/iterators.py index 193b2ac078..b074aefdc4 100644 --- a/discord/iterators.py +++ b/discord/iterators.py @@ -33,6 +33,7 @@ AsyncIterator, Awaitable, Callable, + Generator, List, TypeVar, Union, @@ -41,7 +42,7 @@ from .audit_logs import AuditLogEntry from .errors import NoMoreItems from .object import Object -from .utils import maybe_coroutine, snowflake_time, time_snowflake +from .utils import maybe_coroutine, snowflake_time, time_snowflake, warn_deprecated __all__ = ( "ReactionIterator", @@ -56,15 +57,17 @@ if TYPE_CHECKING: from .abc import Snowflake + from .channel import MessageableChannel from .guild import BanEntry, Guild from .member import Member - from .message import Message + from .message import Message, MessagePin from .monetization import Entitlement, Subscription from .scheduled_events import ScheduledEvent from .threads import Thread from .types.audit_log import AuditLog as AuditLogPayload from .types.guild import Guild as GuildPayload from .types.message import Message as MessagePayload + from .types.message import MessagePin as MessagePinPayload from .types.monetization import Entitlement as EntitlementPayload from .types.monetization import Subscription as SubscriptionPayload from .types.threads import Thread as ThreadPayload @@ -1198,3 +1201,85 @@ async def _retrieve_subscriptions_after_strategy(self, retrieve): self.limit -= retrieve self.after = Object(id=int(data[0]["id"])) return data + + +class MessagePinIterator(_AsyncIterator["MessagePin"]): + def __init__( + self, + channel: MessageableChannel, + limit: int | None, + before: Snowflake | datetime.datetime | None = None, + ): + self._channel = channel + self.limit = limit + self.http = channel._state.http + + self.before: str | None + if before is None: + self.before = None + elif isinstance(before, datetime.datetime): + self.before = before.isoformat() + else: + self.before = snowflake_time(before.id).isoformat() + + self.update_before: Callable[[MessagePinPayload], str] = self.get_last_pinned + + self.endpoint = self.http.pins_from + + self.queue: asyncio.Queue[MessagePin] = asyncio.Queue() + self.has_more: bool = True + + async def next(self) -> MessagePin: + if self.queue.empty(): + await self.fill_queue() + + try: + return self.queue.get_nowait() + except asyncio.QueueEmpty: + raise NoMoreItems() + + @staticmethod + def get_last_pinned(data: MessagePinPayload) -> str: + return data["pinned_at"] + + async def fill_queue(self) -> None: + if not self.has_more: + raise NoMoreItems() + + if not hasattr(self, "channel"): + channel = await self._channel._get_channel() + self.channel = channel + + limit = 50 if self.limit is None else min(self.limit, 50) + data = await self.endpoint(self.channel.id, before=self.before, limit=limit) + + pins: list[MessagePinPayload] = data.get("items", []) + for d in pins: + self.queue.put_nowait(self.create_pin(d)) + + self.has_more = data.get("has_more", False) + if self.limit is not None: + self.limit -= len(pins) + if self.limit <= 0: + self.has_more = False + + if self.has_more: + self.before = self.update_before(pins[-1]) + + def create_pin(self, data: MessagePinPayload) -> MessagePin: + from .message import MessagePin + + return MessagePin(state=self.channel._state, channel=self.channel, data=data) + + async def retrieve_inner(self) -> list[Message]: + pins = await self.flatten() + return [p.message for p in pins] + + def __await__(self) -> Generator[Any, Any, MessagePin]: + warn_deprecated( + f"Messageable.pins() returning a list of Message", + since="2.7", + removed="3.0", + reference="The documentation of pins()", + ) + return self.retrieve_inner().__await__() diff --git a/discord/message.py b/discord/message.py index 909453c1ad..cb38323bc6 100644 --- a/discord/message.py +++ b/discord/message.py @@ -84,6 +84,7 @@ from .types.message import MessageActivity as MessageActivityPayload from .types.message import MessageApplication as MessageApplicationPayload from .types.message import MessageCall as MessageCallPayload + from .types.message import MessagePin as MessagePinPayload from .types.message import MessageReference as MessageReferencePayload from .types.message import MessageSnapshot as MessageSnapshotPayload from .types.message import Reaction as ReactionPayload @@ -793,6 +794,38 @@ def flatten_handlers(cls): return cls +class MessagePin: + """Represents information about a pinned message. + + .. versionadded:: 2.7 + """ + + def __init__( + self, + state: ConnectionState, + channel: MessageableChannel, + data: MessagePinPayload, + ): + self._state: ConnectionState = state + self._pinned_at: datetime.datetime = utils.parse_time(data["pinned_at"]) + self._message: Message = state.create_message( + channel=channel, data=data["message"] + ) + + @property + def message(self) -> Message: + """The pinned message.""" + return self._message + + @property + def pinned_at(self) -> datetime.datetime: + """An aware timestamp of when the message was pinned.""" + return self._pinned_at + + def __repr__(self) -> str: + return f"" + + @flatten_handlers class Message(Hashable): r"""Represents a message from Discord. @@ -1843,7 +1876,7 @@ async def pin(self, *, reason: str | None = 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. Parameters @@ -1872,7 +1905,7 @@ async def unpin(self, *, reason: str | None = 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 diff --git a/discord/permissions.py b/discord/permissions.py index 110909384a..c8898a939a 100644 --- a/discord/permissions.py +++ b/discord/permissions.py @@ -180,7 +180,7 @@ def all(cls: type[P]) -> P: """A factory method that creates a :class:`Permissions` with all permissions set to ``True``. """ - return cls(0b1111111111111111111111111111111111111111111111111) + return cls(~(~1 << 51)) @classmethod def all_channel(cls: type[P]) -> P: @@ -394,11 +394,11 @@ 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:: + .. warning:: - Note that there are currently no ways to edit other people's messages. + Starting from January 12th 2026, this will no longer grant the ability to pin/unpin messages. Use :attr:`pin_messages` instead. """ return 1 << 13 @@ -672,6 +672,14 @@ def use_external_apps(self) -> int: """ return 1 << 50 + @flag_value + def pin_messages(self) -> int: + """:class:`bool`: Returns ``True`` if a member can pin/unpin messages. + + .. versionadded:: 2.7 + """ + return 1 << 51 + PO = TypeVar("PO", bound="PermissionOverwrite") @@ -795,6 +803,7 @@ class PermissionOverwrite: set_voice_channel_status: bool | None send_polls: bool | None use_external_apps: bool | None + pin_messages: bool | None def __init__(self, **kwargs: bool | None): self._values: dict[str, bool | None] = {} diff --git a/discord/types/message.py b/discord/types/message.py index 5baa988cd1..d9bc4f2f9d 100644 --- a/discord/types/message.py +++ b/discord/types/message.py @@ -175,6 +175,16 @@ class Message(TypedDict): message_snapshots: NotRequired[list[MessageSnapshot]] +class MessagePin(TypedDict): + pinned_at: str + message: Message + + +class MessagePinPagination(TypedDict): + items: list[MessagePin] + has_more: bool + + AllowedMentionType = Literal["roles", "users", "everyone"]