Skip to content

Commit e038fc2

Browse files
NeloBlivionpre-commit-ci[bot]DA-344plun1331
authored
feat: Message pin updates (#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]>
1 parent a383590 commit e038fc2

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
@@ -1754,32 +1754,64 @@ async def fetch_message(self, id: int, /) -> Message:
17541754
data = await self._state.http.get_message(channel.id, id)
17551755
return self._state.create_message(channel=channel, data=data)
17561756

1757-
async def pins(self) -> list[Message]:
1758-
"""|coro|
1757+
def pins(
1758+
self,
1759+
*,
1760+
limit: int | None = 50,
1761+
before: SnowflakeTime | None = None,
1762+
) -> MessagePinIterator:
1763+
"""Returns a :class:`~discord.MessagePinIterator` that enables receiving the destination's pinned messages.
17591764
1760-
Retrieves all messages that are currently pinned in the channel.
1765+
You must have :attr:`~discord.Permissions.read_message_history` permissions to use this.
17611766
1762-
.. note::
1767+
.. warning::
17631768
1764-
Due to a limitation with the Discord API, the :class:`.Message`
1765-
objects returned by this method do not contain complete
1766-
:attr:`.Message.reactions` data.
1769+
Starting from version 3.0, `await channel.pins()` will no longer return a list of :class:`Message`. See examples below for new usage instead.
17671770
1768-
Returns
1769-
-------
1770-
List[:class:`~discord.Message`]
1771-
The messages that are currently pinned.
1771+
Parameters
1772+
----------
1773+
limit: Optional[:class:`int`]
1774+
The number of pinned messages to retrieve.
1775+
If ``None``, retrieves every pinned message in the channel.
1776+
before: Optional[Union[:class:`~discord.abc.Snowflake`, :class:`datetime.datetime`]]
1777+
Retrieve messages pinned before this datetime.
1778+
If a datetime is provided, it is recommended to use a UTC aware datetime.
1779+
If the datetime is naive, it is assumed to be local time.
1780+
1781+
Yields
1782+
------
1783+
:class:`~discord.MessagePin`
1784+
The pinned message.
17721785
17731786
Raises
17741787
------
1788+
~discord.Forbidden
1789+
You do not have permissions to get pinned messages.
17751790
~discord.HTTPException
1776-
Retrieving the pinned messages failed.
1777-
"""
1791+
The request to get pinned messages failed.
17781792
1779-
channel = await self._get_channel()
1780-
state = self._state
1781-
data = await state.http.pins_from(channel.id)
1782-
return [state.create_message(channel=channel, data=m) for m in data]
1793+
Examples
1794+
--------
1795+
1796+
Usage ::
1797+
1798+
counter = 0
1799+
async for pin in channel.pins(limit=250):
1800+
if pin.message.author == client.user:
1801+
counter += 1
1802+
1803+
Flattening into a list: ::
1804+
1805+
pins = await channel.pins(limit=None).flatten()
1806+
# pins is now a list of MessagePin...
1807+
1808+
All parameters are optional.
1809+
"""
1810+
return MessagePinIterator(
1811+
self,
1812+
limit=limit,
1813+
before=before,
1814+
)
17831815

17841816
def can_send(self, *objects) -> bool:
17851817
"""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
@@ -888,7 +888,7 @@ def pin_message(
888888
) -> Response[None]:
889889
r = Route(
890890
"PUT",
891-
"/channels/{channel_id}/pins/{message_id}",
891+
"/channels/{channel_id}/messages/pins/{message_id}",
892892
channel_id=channel_id,
893893
message_id=message_id,
894894
)
@@ -899,13 +899,30 @@ def unpin_message(
899899
) -> Response[None]:
900900
r = Route(
901901
"DELETE",
902-
"/channels/{channel_id}/pins/{message_id}",
902+
"/channels/{channel_id}/messages/pins/{message_id}",
903903
channel_id=channel_id,
904904
message_id=message_id,
905905
)
906906
return self.request(r, reason=reason)
907907

908-
def pins_from(self, channel_id: Snowflake) -> Response[list[message.Message]]:
908+
def pins_from(
909+
self,
910+
channel_id: Snowflake,
911+
limit: int | None = None,
912+
before: str | None = None,
913+
) -> Response[message.MessagePinPagination]:
914+
r = Route("GET", "/channels/{channel_id}/messages/pins", channel_id=channel_id)
915+
params = {}
916+
if limit:
917+
params["limit"] = limit
918+
if before:
919+
params["before"] = before
920+
921+
return self.request(r, params=params)
922+
923+
def legacy_pins_from(
924+
self, channel_id: Snowflake
925+
) -> Response[list[message.Message]]:
909926
return self.request(
910927
Route("GET", "/channels/{channel_id}/pins", channel_id=channel_id)
911928
)

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,
@@ -41,7 +42,7 @@
4142
from .audit_logs import AuditLogEntry
4243
from .errors import NoMoreItems
4344
from .object import Object
44-
from .utils import maybe_coroutine, snowflake_time, time_snowflake
45+
from .utils import maybe_coroutine, snowflake_time, time_snowflake, warn_deprecated
4546

4647
__all__ = (
4748
"ReactionIterator",
@@ -56,15 +57,17 @@
5657

5758
if TYPE_CHECKING:
5859
from .abc import Snowflake
60+
from .channel import MessageableChannel
5961
from .guild import BanEntry, Guild
6062
from .member import Member
61-
from .message import Message
63+
from .message import Message, MessagePin
6264
from .monetization import Entitlement, Subscription
6365
from .scheduled_events import ScheduledEvent
6466
from .threads import Thread
6567
from .types.audit_log import AuditLog as AuditLogPayload
6668
from .types.guild import Guild as GuildPayload
6769
from .types.message import Message as MessagePayload
70+
from .types.message import MessagePin as MessagePinPayload
6871
from .types.monetization import Entitlement as EntitlementPayload
6972
from .types.monetization import Subscription as SubscriptionPayload
7073
from .types.threads import Thread as ThreadPayload
@@ -1198,3 +1201,85 @@ async def _retrieve_subscriptions_after_strategy(self, retrieve):
11981201
self.limit -= retrieve
11991202
self.after = Object(id=int(data[0]["id"]))
12001203
return data
1204+
1205+
1206+
class MessagePinIterator(_AsyncIterator["MessagePin"]):
1207+
def __init__(
1208+
self,
1209+
channel: MessageableChannel,
1210+
limit: int | None,
1211+
before: Snowflake | datetime.datetime | None = None,
1212+
):
1213+
self._channel = channel
1214+
self.limit = limit
1215+
self.http = channel._state.http
1216+
1217+
self.before: str | None
1218+
if before is None:
1219+
self.before = None
1220+
elif isinstance(before, datetime.datetime):
1221+
self.before = before.isoformat()
1222+
else:
1223+
self.before = snowflake_time(before.id).isoformat()
1224+
1225+
self.update_before: Callable[[MessagePinPayload], str] = self.get_last_pinned
1226+
1227+
self.endpoint = self.http.pins_from
1228+
1229+
self.queue: asyncio.Queue[MessagePin] = asyncio.Queue()
1230+
self.has_more: bool = True
1231+
1232+
async def next(self) -> MessagePin:
1233+
if self.queue.empty():
1234+
await self.fill_queue()
1235+
1236+
try:
1237+
return self.queue.get_nowait()
1238+
except asyncio.QueueEmpty:
1239+
raise NoMoreItems()
1240+
1241+
@staticmethod
1242+
def get_last_pinned(data: MessagePinPayload) -> str:
1243+
return data["pinned_at"]
1244+
1245+
async def fill_queue(self) -> None:
1246+
if not self.has_more:
1247+
raise NoMoreItems()
1248+
1249+
if not hasattr(self, "channel"):
1250+
channel = await self._channel._get_channel()
1251+
self.channel = channel
1252+
1253+
limit = 50 if self.limit is None else min(self.limit, 50)
1254+
data = await self.endpoint(self.channel.id, before=self.before, limit=limit)
1255+
1256+
pins: list[MessagePinPayload] = data.get("items", [])
1257+
for d in pins:
1258+
self.queue.put_nowait(self.create_pin(d))
1259+
1260+
self.has_more = data.get("has_more", False)
1261+
if self.limit is not None:
1262+
self.limit -= len(pins)
1263+
if self.limit <= 0:
1264+
self.has_more = False
1265+
1266+
if self.has_more:
1267+
self.before = self.update_before(pins[-1])
1268+
1269+
def create_pin(self, data: MessagePinPayload) -> MessagePin:
1270+
from .message import MessagePin
1271+
1272+
return MessagePin(state=self.channel._state, channel=self.channel, data=data)
1273+
1274+
async def retrieve_inner(self) -> list[Message]:
1275+
pins = await self.flatten()
1276+
return [p.message for p in pins]
1277+
1278+
def __await__(self) -> Generator[Any, Any, MessagePin]:
1279+
warn_deprecated(
1280+
f"Messageable.pins() returning a list of Message",
1281+
since="2.7",
1282+
removed="3.0",
1283+
reference="The documentation of pins()",
1284+
)
1285+
return self.retrieve_inner().__await__()

discord/message.py

Lines changed: 35 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,7 @@
8484
from .types.message import MessageActivity as MessageActivityPayload
8585
from .types.message import MessageApplication as MessageApplicationPayload
8686
from .types.message import MessageCall as MessageCallPayload
87+
from .types.message import MessagePin as MessagePinPayload
8788
from .types.message import MessageReference as MessageReferencePayload
8889
from .types.message import MessageSnapshot as MessageSnapshotPayload
8990
from .types.message import Reaction as ReactionPayload
@@ -793,6 +794,38 @@ def flatten_handlers(cls):
793794
return cls
794795

795796

797+
class MessagePin:
798+
"""Represents information about a pinned message.
799+
800+
.. versionadded:: 2.7
801+
"""
802+
803+
def __init__(
804+
self,
805+
state: ConnectionState,
806+
channel: MessageableChannel,
807+
data: MessagePinPayload,
808+
):
809+
self._state: ConnectionState = state
810+
self._pinned_at: datetime.datetime = utils.parse_time(data["pinned_at"])
811+
self._message: Message = state.create_message(
812+
channel=channel, data=data["message"]
813+
)
814+
815+
@property
816+
def message(self) -> Message:
817+
"""The pinned message."""
818+
return self._message
819+
820+
@property
821+
def pinned_at(self) -> datetime.datetime:
822+
"""An aware timestamp of when the message was pinned."""
823+
return self._pinned_at
824+
825+
def __repr__(self) -> str:
826+
return f"<MessagePin pinned_at={self.pinned_at!r} message={self.message!r}>"
827+
828+
796829
@flatten_handlers
797830
class Message(Hashable):
798831
r"""Represents a message from Discord.
@@ -1843,7 +1876,7 @@ async def pin(self, *, reason: str | None = None) -> None:
18431876
18441877
Pins the message.
18451878
1846-
You must have the :attr:`~Permissions.manage_messages` permission to do
1879+
You must have the :attr:`~Permissions.pin_messages` permission to do
18471880
this in a non-private channel context.
18481881
18491882
Parameters
@@ -1872,7 +1905,7 @@ async def unpin(self, *, reason: str | None = None) -> None:
18721905
18731906
Unpins the message.
18741907
1875-
You must have the :attr:`~Permissions.manage_messages` permission to do
1908+
You must have the :attr:`~Permissions.pin_messages` permission to do
18761909
this in a non-private channel context.
18771910
18781911
Parameters

discord/permissions.py

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

185185
@classmethod
186186
def all_channel(cls: type[P]) -> P:
@@ -394,11 +394,11 @@ def send_tts_messages(self) -> int:
394394

395395
@flag_value
396396
def manage_messages(self) -> int:
397-
""":class:`bool`: Returns ``True`` if a user can delete or pin messages in a text channel.
397+
""":class:`bool`: Returns ``True`` if a user can delete messages in a text channel.
398398
399-
.. note::
399+
.. warning::
400400
401-
Note that there are currently no ways to edit other people's messages.
401+
Starting from January 12th 2026, this will no longer grant the ability to pin/unpin messages. Use :attr:`pin_messages` instead.
402402
"""
403403
return 1 << 13
404404

@@ -672,6 +672,14 @@ def use_external_apps(self) -> int:
672672
"""
673673
return 1 << 50
674674

675+
@flag_value
676+
def pin_messages(self) -> int:
677+
""":class:`bool`: Returns ``True`` if a member can pin/unpin messages.
678+
679+
.. versionadded:: 2.7
680+
"""
681+
return 1 << 51
682+
675683

676684
PO = TypeVar("PO", bound="PermissionOverwrite")
677685

@@ -795,6 +803,7 @@ class PermissionOverwrite:
795803
set_voice_channel_status: bool | None
796804
send_polls: bool | None
797805
use_external_apps: bool | None
806+
pin_messages: bool | None
798807

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

0 commit comments

Comments
 (0)