Skip to content
Merged
Show file tree
Hide file tree
Changes from 16 commits
Commits
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
68 changes: 50 additions & 18 deletions discord/abc.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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.fetch_pins(limit=250):
if pin.message.author == client.user:
counter += 1

Flattening into a list: ::

pins = await channel.fetch_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).
Expand Down
23 changes: 20 additions & 3 deletions discord/http.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
)
Expand All @@ -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[list[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)
)
Expand Down
89 changes: 87 additions & 2 deletions discord/iterators.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
AsyncIterator,
Awaitable,
Callable,
Generator,
List,
TypeVar,
Union,
Expand All @@ -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",
Expand All @@ -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
Expand Down Expand Up @@ -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[Thread] = asyncio.Queue()
self.has_more: bool = True

async def next(self) -> Thread:
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__()
37 changes: 35 additions & 2 deletions discord/message.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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"<MessagePin pinned_at={self.pinned_at!r} message={self.message!r}>"


@flatten_handlers
class Message(Hashable):
r"""Represents a message from Discord.
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
15 changes: 12 additions & 3 deletions discord/permissions.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -396,9 +396,9 @@ def send_tts_messages(self) -> int:
def manage_messages(self) -> int:
""":class:`bool`: Returns ``True`` if a user can delete or pin 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

Expand Down Expand Up @@ -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")

Expand Down Expand Up @@ -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] = {}
Expand Down
10 changes: 10 additions & 0 deletions discord/types/message.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"]


Expand Down
Loading