Skip to content
Open
Show file tree
Hide file tree
Changes from all 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
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,9 @@ These changes are available on the `master` branch, but have not yet been releas
- Added `Attachment.read_chunked` and added optional `chunksize` argument to
`Attachment.save` for retrieving attachments in chunks.
([#2956](https://github.com/Pycord-Development/pycord/pull/2956))
- Added a new event called `on_raw_member_update` that is dispatched when a member is
updated, regardless of cache status.
([#3012](https://github.com/Pycord-Development/pycord/pull/3012))

### Changed

Expand Down
1 change: 1 addition & 0 deletions discord/flags.py
Original file line number Diff line number Diff line change
Expand Up @@ -715,6 +715,7 @@ def members(self):
- :func:`on_raw_member_remove`
- :func:`on_member_update`
- :func:`on_user_update`
- :func:`on_raw_member_update`

This also corresponds to the following attributes and classes in terms of cache:

Expand Down
10 changes: 6 additions & 4 deletions discord/member.py
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@
from .state import ConnectionState
from .types.activity import PartialPresenceUpdate
from .types.member import Member as MemberPayload
from .types.member import MemberUpdateEvent as MemberUpdateEventPayload
from .types.member import MemberWithUser as MemberWithUserPayload
from .types.member import UserWithMember as UserWithMemberPayload
from .types.user import User as UserPayload
Expand Down Expand Up @@ -424,27 +425,28 @@ async def _get_channel(self):
ch = await self.create_dm()
return ch

def _update(self, data: MemberPayload) -> None:
def _update(self, data: MemberPayload | MemberUpdateEventPayload) -> None:
# the nickname change is optional,
# if it isn't in the payload then it didn't change
try:
self.nick = data["nick"]
self.nick = data["nick"] # type: ignore # handled by the type-except
except KeyError:
pass

try:
self.pending = data["pending"]
self.pending = data["pending"] # type: ignore # handled by the type-except
except KeyError:
pass

self.premium_since = utils.parse_time(data.get("premium_since"))
self._roles = utils.SnowflakeList(map(int, data["roles"]))
self._roles = utils.SnowflakeList(map(int, data["roles"])) # type: ignore # the API is the same
self._avatar = data.get("avatar")
self._banner = data.get("banner")
self.communication_disabled_until = utils.parse_time(
data.get("communication_disabled_until")
)
self.flags = MemberFlags._from_value(data.get("flags", 0))
self.joined_at = utils.parse_time(data.get("joined_at")) or self.joined_at

def _presence_update(
self, data: PartialPresenceUpdate, user: UserPayload
Expand Down
27 changes: 27 additions & 0 deletions discord/raw_models.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@
from .state import ConnectionState
from .threads import Thread
from .types.channel import VoiceChannelEffectSendEvent as VoiceChannelEffectSend
from .types.member import MemberUpdateEvent
from .types.raw_models import (
AuditLogEntryEvent,
)
Expand Down Expand Up @@ -90,6 +91,7 @@
"RawVoiceChannelStatusUpdateEvent",
"RawMessagePollVoteEvent",
"RawSoundboardSoundDeleteEvent",
"RawMemberUpdateEvent",
)


Expand Down Expand Up @@ -870,3 +872,28 @@ def __init__(self, data: PartialSoundboardSound) -> None:
self.sound_id: int = int(data["sound_id"])
self.guild_id: int = int(data["guild_id"])
self.data: PartialSoundboardSound = data


class RawMemberUpdateEvent(_RawReprMixin):
"""Represents the payload for a :func:`on_raw_member_update` event.

.. versionadded:: 2.7

Attributes
----------
data: :class:`dict`
The raw data sent by the `gateway <https://discord.com/developers/docs/topics/gateway-events#guild-member-update>`_
cached_member: Optional[:class:`Member`]
The cached member, if found in the internal member cache.
member: :class:`Member`
The new member object after the update.
"""

__slots__ = ("guild_id", "user_id", "data", "cached_member", "member")

def __init__(self, data: MemberUpdateEvent, member: Member) -> None:
self.guild_id: int = int(data["guild_id"])
self.user_id: int = int(data["user"]["id"])
self.data: MemberUpdateEvent = data
self.cached_member: Member | None = None
self.member: Member = member
61 changes: 38 additions & 23 deletions discord/state.py
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,7 @@
from .types.channel import DMChannel as DMChannelPayload
from .types.emoji import Emoji as EmojiPayload
from .types.guild import Guild as GuildPayload
from .types.member import MemberUpdateEvent
from .types.message import Message as MessagePayload
from .types.poll import Poll as PollPayload
from .types.sticker import GuildSticker as GuildStickerPayload
Expand Down Expand Up @@ -911,7 +912,11 @@ def parse_message_poll_vote_add(self, data) -> None:
counts[answer.id].count += 1
else:
counts[answer.id] = PollAnswerCount(
{"id": answer.id, "count": 1, "me_voted": False}
{
"id": answer.id,
"count": 1,
"me_voted": False,
}
)
if poll is not None and user is not None:
answer = poll.get_answer(raw.answer_id)
Expand Down Expand Up @@ -1322,40 +1327,50 @@ def parse_guild_member_remove(self, data) -> None:
)
self.dispatch("raw_member_remove", raw)

def parse_guild_member_update(self, data) -> None:
def parse_guild_member_update(self, data: MemberUpdateEvent) -> None:
guild = self._get_guild(int(data["guild_id"]))
user = data["user"]
user_id = int(user["id"])
if guild is None:
_log.debug(
"GUILD_MEMBER_UPDATE referencing an unknown guild ID: %s. Discarding.",
data["guild_id"],
)
return

member = guild.get_member(user_id)
if member is not None:
old_member = Member._copy(member)
member._update(data)
user_update = member._update_inner_user(user)
if user_update:
self.dispatch("user_update", user_update[0], user_update[1])
user = data["user"]
user_id = int(user["id"])

# Try to get the old member from cache
old_member: Member | None = guild.get_member(user_id)
old_member_copy: Member | None = (
Member._copy(old_member) if old_member is not None else None
)

self.dispatch("member_update", old_member, member)
# Always create or update the member object
if old_member is not None:
old_member._update(data)
new_member = old_member
else:
if self.member_cache_flags.joined:
member = Member(data=data, guild=guild, state=self)
new_member = Member(guild=guild, data=data, state=self) # type: ignore

# Force an update on the inner user if necessary
user_update = member._update_inner_user(user)
if user_update:
self.dispatch("user_update", user_update[0], user_update[1])
raw = RawMemberUpdateEvent(data, new_member)
raw.cached_member = old_member_copy
self.dispatch("raw_member_update", raw)

guild._add_member(member)
_log.debug(
"GUILD_MEMBER_UPDATE referencing an unknown member ID: %s. Discarding.",
user_id,
)
# Update the user cache if needed
user_update = None
if old_member_copy is not None:
user_update = old_member_copy._update_inner_user(user)
else:
user_update = new_member._update_inner_user(user)

if user_update:
self.dispatch("user_update", user_update[0], user_update[1])

if old_member_copy is not None:
self.dispatch("member_update", old_member_copy, new_member)
else:
if self.member_cache_flags.joined:
guild._add_member(new_member)

def parse_guild_emojis_update(self, data) -> None:
guild = self._get_guild(int(data["guild_id"]))
Expand Down
21 changes: 19 additions & 2 deletions discord/types/member.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,9 +23,9 @@
DEALINGS IN THE SOFTWARE.
"""

from typing import TypedDict
from typing import NotRequired, TypedDict

from .snowflake import SnowflakeList
from .snowflake import Snowflake, SnowflakeList
from .user import User


Expand Down Expand Up @@ -66,3 +66,20 @@ class MemberWithUser(_OptionalMemberWithUser):

class UserWithMember(User, total=False):
member: _OptionalMemberWithUser


class MemberUpdateEvent(TypedDict):
guild_id: Snowflake
user: User
roles: list[Snowflake]
nick: NotRequired[str | None]
avatar: NotRequired[str | None]
banner: NotRequired[str | None]
joined_at: NotRequired[str | None]
premium_since: NotRequired[str | None]
deaf: NotRequired[bool | None]
mute: NotRequired[bool | None]
pending: NotRequired[bool | None]
communication_disabled_until: NotRequired[str | None]
flags: NotRequired[int | None]
avatar_decoration_data: NotRequired[dict | None]
13 changes: 13 additions & 0 deletions docs/api/events.rst
Original file line number Diff line number Diff line change
Expand Up @@ -1495,3 +1495,16 @@ Soundboard Sound

:param sound: The soundboard sound that was created.
:type sound: :class:`SoundboardSound`

.. function:: on_raw_member_update(payload)

Called when a :class:`Member` updates their profile.
Unlike :func:`on_member_update`, this is called regardless of the
state of the internal member cache.

This requires :attr:`Intents.members` to be enabled.

.. versionadded:: 2.7

:param payload: The raw event payload data.
:type payload: :class:`RawMemberUpdateEvent`