Skip to content

Commit 5f5b0fd

Browse files
NeloBlivionPaillat-dev
authored andcommitted
feat: Message pin updates (Pycord-Development#2872)
Signed-off-by: UK <[email protected]> Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> Co-authored-by: DA344 <[email protected]> Co-authored-by: plun1331 <[email protected]> (cherry picked from commit e038fc2) Signed-off-by: Paillat-dev <[email protected]>
1 parent 921260d commit 5f5b0fd

File tree

7 files changed

+220
-29
lines changed

7 files changed

+220
-29
lines changed

CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -167,6 +167,8 @@ These changes are available on the `master` branch, but have not yet been releas
167167
([#2797](https://github.com/Pycord-Development/pycord/pull/2797))
168168
- Upgraded voice websocket version to v8.
169169
([#2812](https://github.com/Pycord-Development/pycord/pull/2812))
170+
- `Messageable.pins()` now returns a `MessagePinIterator` and has new arguments.
171+
([#2872](https://github.com/Pycord-Development/pycord/pull/2872))
170172

171173
### Deprecated
172174

@@ -176,6 +178,9 @@ These changes are available on the `master` branch, but have not yet been releas
176178
([#2501](https://github.com/Pycord-Development/pycord/pull/2501))
177179
- Deprecated `Interaction.cached_channel` in favor of `Interaction.channel`.
178180
([#2658](https://github.com/Pycord-Development/pycord/pull/2658))
181+
- Deprecated `Messageable.pins()` returning a list of `Message`; it should be used as an
182+
iterator of `MessagePin` instead.
183+
([#2872](https://github.com/Pycord-Development/pycord/pull/2872))
179184

180185
### Removed
181186

discord/abc.py

Lines changed: 50 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@
4848
from .file import File, VoiceMessage
4949
from .flags import ChannelFlags, MessageFlags
5050
from .invite import Invite
51-
from .iterators import HistoryIterator
51+
from .iterators import HistoryIterator, MessagePinIterator
5252
from .mentions import AllowedMentions
5353
from .partial_emoji import PartialEmoji, _EmojiTag
5454
from .permissions import PermissionOverwrite, Permissions
@@ -1679,32 +1679,64 @@ async def fetch_message(self, id: int, /) -> Message:
16791679
data = await self._state.http.get_message(channel.id, id)
16801680
return self._state.create_message(channel=channel, data=data)
16811681

1682-
async def pins(self) -> list[Message]:
1683-
"""|coro|
1682+
def pins(
1683+
self,
1684+
*,
1685+
limit: int | None = 50,
1686+
before: SnowflakeTime | None = None,
1687+
) -> MessagePinIterator:
1688+
"""Returns a :class:`~discord.MessagePinIterator` that enables receiving the destination's pinned messages.
16841689
1685-
Retrieves all messages that are currently pinned in the channel.
1690+
You must have :attr:`~discord.Permissions.read_message_history` permissions to use this.
16861691
1687-
.. note::
1692+
.. warning::
16881693
1689-
Due to a limitation with the Discord API, the :class:`.Message`
1690-
objects returned by this method do not contain complete
1691-
:attr:`.Message.reactions` data.
1694+
Starting from version 3.0, `await channel.pins()` will no longer return a list of :class:`Message`. See examples below for new usage instead.
16921695
1693-
Returns
1694-
-------
1695-
List[:class:`~discord.Message`]
1696-
The messages that are currently pinned.
1696+
Parameters
1697+
----------
1698+
limit: Optional[:class:`int`]
1699+
The number of pinned messages to retrieve.
1700+
If ``None``, retrieves every pinned message in the channel.
1701+
before: Optional[Union[:class:`~discord.abc.Snowflake`, :class:`datetime.datetime`]]
1702+
Retrieve messages pinned before this datetime.
1703+
If a datetime is provided, it is recommended to use a UTC aware datetime.
1704+
If the datetime is naive, it is assumed to be local time.
1705+
1706+
Yields
1707+
------
1708+
:class:`~discord.MessagePin`
1709+
The pinned message.
16971710
16981711
Raises
16991712
------
1713+
~discord.Forbidden
1714+
You do not have permissions to get pinned messages.
17001715
~discord.HTTPException
1701-
Retrieving the pinned messages failed.
1702-
"""
1716+
The request to get pinned messages failed.
17031717
1704-
channel = await self._get_channel()
1705-
state = self._state
1706-
data = await state.http.pins_from(channel.id)
1707-
return [state.create_message(channel=channel, data=m) for m in data]
1718+
Examples
1719+
--------
1720+
1721+
Usage ::
1722+
1723+
counter = 0
1724+
async for pin in channel.pins(limit=250):
1725+
if pin.message.author == client.user:
1726+
counter += 1
1727+
1728+
Flattening into a list: ::
1729+
1730+
pins = await channel.pins(limit=None).flatten()
1731+
# pins is now a list of MessagePin...
1732+
1733+
All parameters are optional.
1734+
"""
1735+
return MessagePinIterator(
1736+
self,
1737+
limit=limit,
1738+
before=before,
1739+
)
17081740

17091741
def can_send(self, *objects) -> bool:
17101742
"""Returns a :class:`bool` indicating whether you have the permissions to send the object(s).

discord/http.py

Lines changed: 20 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -848,7 +848,7 @@ def publish_message(self, channel_id: Snowflake, message_id: Snowflake) -> Respo
848848
def pin_message(self, channel_id: Snowflake, message_id: Snowflake, reason: str | None = None) -> Response[None]:
849849
r = Route(
850850
"PUT",
851-
"/channels/{channel_id}/pins/{message_id}",
851+
"/channels/{channel_id}/messages/pins/{message_id}",
852852
channel_id=channel_id,
853853
message_id=message_id,
854854
)
@@ -857,13 +857,30 @@ def pin_message(self, channel_id: Snowflake, message_id: Snowflake, reason: str
857857
def unpin_message(self, channel_id: Snowflake, message_id: Snowflake, reason: str | None = None) -> Response[None]:
858858
r = Route(
859859
"DELETE",
860-
"/channels/{channel_id}/pins/{message_id}",
860+
"/channels/{channel_id}/messages/pins/{message_id}",
861861
channel_id=channel_id,
862862
message_id=message_id,
863863
)
864864
return self.request(r, reason=reason)
865865

866-
def pins_from(self, channel_id: Snowflake) -> Response[list[message.Message]]:
866+
def pins_from(
867+
self,
868+
channel_id: Snowflake,
869+
limit: int | None = None,
870+
before: str | None = None,
871+
) -> Response[message.MessagePinPagination]:
872+
r = Route("GET", "/channels/{channel_id}/messages/pins", channel_id=channel_id)
873+
params = {}
874+
if limit:
875+
params["limit"] = limit
876+
if before:
877+
params["before"] = before
878+
879+
return self.request(r, params=params)
880+
881+
def legacy_pins_from(
882+
self, channel_id: Snowflake
883+
) -> Response[list[message.Message]]:
867884
return self.request(Route("GET", "/channels/{channel_id}/pins", channel_id=channel_id))
868885

869886
# Member management

discord/iterators.py

Lines changed: 87 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@
3333
AsyncIterator,
3434
Awaitable,
3535
Callable,
36+
Generator,
3637
List,
3738
TypeVar,
3839
Union,
@@ -42,7 +43,7 @@
4243
from .errors import NoMoreItems
4344
from .object import Object
4445
from .utils import generate_snowflake, snowflake_time
45-
from .utils.private import maybe_awaitable
46+
from .utils.private import maybe_awaitable, warn_deprecated
4647

4748
__all__ = (
4849
"ReactionIterator",
@@ -57,15 +58,17 @@
5758

5859
if TYPE_CHECKING:
5960
from .abc import Snowflake
61+
from .channel import MessageableChannel
6062
from .guild import BanEntry, Guild
6163
from .member import Member
62-
from .message import Message
64+
from .message import Message, MessagePin
6365
from .monetization import Entitlement, Subscription
6466
from .scheduled_events import ScheduledEvent
6567
from .threads import Thread
6668
from .types.audit_log import AuditLog as AuditLogPayload
6769
from .types.guild import Guild as GuildPayload
6870
from .types.message import Message as MessagePayload
71+
from .types.message import MessagePin as MessagePinPayload
6972
from .types.monetization import Entitlement as EntitlementPayload
7073
from .types.monetization import Subscription as SubscriptionPayload
7174
from .types.threads import Thread as ThreadPayload
@@ -1157,3 +1160,85 @@ async def _retrieve_subscriptions_after_strategy(self, retrieve):
11571160
self.limit -= retrieve
11581161
self.after = Object(id=int(data[0]["id"]))
11591162
return data
1163+
1164+
1165+
class MessagePinIterator(_AsyncIterator["MessagePin"]):
1166+
def __init__(
1167+
self,
1168+
channel: MessageableChannel,
1169+
limit: int | None,
1170+
before: Snowflake | datetime.datetime | None = None,
1171+
):
1172+
self._channel = channel
1173+
self.limit = limit
1174+
self.http = channel._state.http
1175+
1176+
self.before: str | None
1177+
if before is None:
1178+
self.before = None
1179+
elif isinstance(before, datetime.datetime):
1180+
self.before = before.isoformat()
1181+
else:
1182+
self.before = snowflake_time(before.id).isoformat()
1183+
1184+
self.update_before: Callable[[MessagePinPayload], str] = self.get_last_pinned
1185+
1186+
self.endpoint = self.http.pins_from
1187+
1188+
self.queue: asyncio.Queue[MessagePin] = asyncio.Queue()
1189+
self.has_more: bool = True
1190+
1191+
async def next(self) -> MessagePin:
1192+
if self.queue.empty():
1193+
await self.fill_queue()
1194+
1195+
try:
1196+
return self.queue.get_nowait()
1197+
except asyncio.QueueEmpty:
1198+
raise NoMoreItems()
1199+
1200+
@staticmethod
1201+
def get_last_pinned(data: MessagePinPayload) -> str:
1202+
return data["pinned_at"]
1203+
1204+
async def fill_queue(self) -> None:
1205+
if not self.has_more:
1206+
raise NoMoreItems()
1207+
1208+
if not hasattr(self, "channel"):
1209+
channel = await self._channel._get_channel()
1210+
self.channel = channel
1211+
1212+
limit = 50 if self.limit is None else min(self.limit, 50)
1213+
data = await self.endpoint(self.channel.id, before=self.before, limit=limit)
1214+
1215+
pins: list[MessagePinPayload] = data.get("items", [])
1216+
for d in pins:
1217+
self.queue.put_nowait(self.create_pin(d))
1218+
1219+
self.has_more = data.get("has_more", False)
1220+
if self.limit is not None:
1221+
self.limit -= len(pins)
1222+
if self.limit <= 0:
1223+
self.has_more = False
1224+
1225+
if self.has_more:
1226+
self.before = self.update_before(pins[-1])
1227+
1228+
def create_pin(self, data: MessagePinPayload) -> MessagePin:
1229+
from .message import MessagePin
1230+
1231+
return MessagePin(state=self.channel._state, channel=self.channel, data=data)
1232+
1233+
async def retrieve_inner(self) -> list[Message]:
1234+
pins = await self.flatten()
1235+
return [p.message for p in pins]
1236+
1237+
def __await__(self) -> Generator[Any, Any, MessagePin]:
1238+
warn_deprecated(
1239+
f"Messageable.pins() returning a list of Message",
1240+
since="2.7",
1241+
removed="3.0",
1242+
reference="The documentation of pins()",
1243+
)
1244+
return self.retrieve_inner().__await__()

discord/message.py

Lines changed: 35 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,7 @@
8585
from .types.message import MessageActivity as MessageActivityPayload
8686
from .types.message import MessageApplication as MessageApplicationPayload
8787
from .types.message import MessageCall as MessageCallPayload
88+
from .types.message import MessagePin as MessagePinPayload
8889
from .types.message import MessageReference as MessageReferencePayload
8990
from .types.message import MessageSnapshot as MessageSnapshotPayload
9091
from .types.message import Reaction as ReactionPayload
@@ -769,6 +770,38 @@ def flatten_handlers(cls):
769770
return cls
770771

771772

773+
class MessagePin:
774+
"""Represents information about a pinned message.
775+
776+
.. versionadded:: 2.7
777+
"""
778+
779+
def __init__(
780+
self,
781+
state: ConnectionState,
782+
channel: MessageableChannel,
783+
data: MessagePinPayload,
784+
):
785+
self._state: ConnectionState = state
786+
self._pinned_at: datetime.datetime = utils.parse_time(data["pinned_at"])
787+
self._message: Message = state.create_message(
788+
channel=channel, data=data["message"]
789+
)
790+
791+
@property
792+
def message(self) -> Message:
793+
"""The pinned message."""
794+
return self._message
795+
796+
@property
797+
def pinned_at(self) -> datetime.datetime:
798+
"""An aware timestamp of when the message was pinned."""
799+
return self._pinned_at
800+
801+
def __repr__(self) -> str:
802+
return f"<MessagePin pinned_at={self.pinned_at!r} message={self.message!r}>"
803+
804+
772805
@flatten_handlers
773806
class Message(Hashable):
774807
r"""Represents a message from Discord.
@@ -1743,7 +1776,7 @@ async def pin(self, *, reason: str | None = None) -> None:
17431776
17441777
Pins the message.
17451778
1746-
You must have the :attr:`~Permissions.manage_messages` permission to do
1779+
You must have the :attr:`~Permissions.pin_messages` permission to do
17471780
this in a non-private channel context.
17481781
17491782
Parameters
@@ -1772,7 +1805,7 @@ async def unpin(self, *, reason: str | None = None) -> None:
17721805
17731806
Unpins the message.
17741807
1775-
You must have the :attr:`~Permissions.manage_messages` permission to do
1808+
You must have the :attr:`~Permissions.pin_messages` permission to do
17761809
this in a non-private channel context.
17771810
17781811
Parameters

discord/permissions.py

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -171,7 +171,7 @@ def all(cls: type[P]) -> P:
171171
"""A factory method that creates a :class:`Permissions` with all
172172
permissions set to ``True``.
173173
"""
174-
return cls(0b1111111111111111111111111111111111111111111111111)
174+
return cls(~(~1 << 51))
175175

176176
@classmethod
177177
def all_channel(cls: type[P]) -> P:
@@ -385,11 +385,11 @@ def send_tts_messages(self) -> int:
385385

386386
@flag_value
387387
def manage_messages(self) -> int:
388-
""":class:`bool`: Returns ``True`` if a user can delete or pin messages in a text channel.
388+
""":class:`bool`: Returns ``True`` if a user can delete messages in a text channel.
389389
390-
.. note::
390+
.. warning::
391391
392-
Note that there are currently no ways to edit other people's messages.
392+
Starting from January 12th 2026, this will no longer grant the ability to pin/unpin messages. Use :attr:`pin_messages` instead.
393393
"""
394394
return 1 << 13
395395

@@ -663,6 +663,14 @@ def use_external_apps(self) -> int:
663663
"""
664664
return 1 << 50
665665

666+
@flag_value
667+
def pin_messages(self) -> int:
668+
""":class:`bool`: Returns ``True`` if a member can pin/unpin messages.
669+
670+
.. versionadded:: 2.7
671+
"""
672+
return 1 << 51
673+
666674

667675
PO = TypeVar("PO", bound="PermissionOverwrite")
668676

@@ -786,6 +794,7 @@ class PermissionOverwrite:
786794
set_voice_channel_status: bool | None
787795
send_polls: bool | None
788796
use_external_apps: bool | None
797+
pin_messages: bool | None
789798

790799
def __init__(self, **kwargs: bool | None):
791800
self._values: dict[str, bool | None] = {}

0 commit comments

Comments
 (0)