Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
e59e4d0
feat: add on_raw_member_update event
Soheab Nov 29, 2025
1808959
docs: document new payload and event
Soheab Nov 29, 2025
e13a20a
docs: correct pr number
Soheab Nov 29, 2025
b7ed1e1
style(pre-commit): auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Nov 29, 2025
036d568
docs: correct versionadded string
Soheab Nov 29, 2025
fe5fc97
docs: correct versionadded string
Soheab Nov 29, 2025
b288f61
chore: remove debug print
Soheab Nov 29, 2025
3d1070a
Merge branch 'feat/onrawmemberupdate' of https://github.com/Soheab/py…
Soheab Nov 29, 2025
0c9677a
refactor: parsing logic
Soheab Nov 29, 2025
907327b
Merge branch 'master' into feat/onrawmemberupdate
NeloBlivion Dec 24, 2025
7bef6c9
Update discord/types/member.py
Soheab Dec 28, 2025
7f9b805
style(pre-commit): auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Dec 28, 2025
d4faa3c
Merge remote-tracking branch 'upstream/master' into feat/onrawmemberu…
Soheab Feb 26, 2026
0b3dc05
Update member.py
Soheab Feb 26, 2026
4500312
style(pre-commit): auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Feb 26, 2026
948ee87
Fixes and versionadded
Soheab Feb 26, 2026
2a962e5
Merge branch 'feat/onrawmemberupdate' of https://github.com/Soheab/py…
Soheab Feb 26, 2026
49e6840
Merge branch 'master' into feat/onrawmemberupdate
Soheab Feb 26, 2026
0d82585
Update discord/state.py
Lulalaby Mar 2, 2026
419120a
Merge branch 'master' into feat/onrawmemberupdate
Lulalaby Mar 2, 2026
6b9ff17
Apply suggestions from code review
Lulalaby Mar 2, 2026
db8809c
Apply suggestion from @Lulalaby
Lulalaby Mar 2, 2026
a602b6f
Apply suggestion from @Lulalaby
Lulalaby Mar 2, 2026
cde8a4e
style(pre-commit): auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Mar 2, 2026
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 @@ -24,6 +24,9 @@ These changes are available on the `master` branch, but have not yet been releas
([#3107](https://github.com/Pycord-Development/pycord/pull/3107))
- Added `Member.display_avatar_decoration` and `Member.guild_avatar_decoration`.
([#3109](https://github.com/Pycord-Development/pycord/pull/3109))
- 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
12 changes: 6 additions & 6 deletions discord/member.py
Original file line number Diff line number Diff line change
Expand Up @@ -64,8 +64,8 @@
from .role import Role
from .state import ConnectionState
from .types.activity import PartialPresenceUpdate
from .types.member import Member
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 @@ -411,7 +411,7 @@ def _try_upgrade(
def _copy(cls: type[M], member: M) -> M:
self: M = cls.__new__(cls) # to bypass __init__

self._roles = utils.SnowflakeList(member._roles, is_sorted=True)
self._roles = utils.SnowflakeList(member._roles, is_sorted=True) # type: ignore # the API is the same
self.joined_at = member.joined_at
self.premium_since = member.premium_since
self._client_status = member._client_status.copy()
Expand All @@ -435,21 +435,21 @@ 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(
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.8

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 @@ -918,7 +919,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 @@ -1329,40 +1334,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: 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
22 changes: 21 additions & 1 deletion discord/types/member.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,10 @@

from typing import TypedDict

from typing_extensions import NotRequired

from .collectibles import AvatarDecoration, Collectibles
from .snowflake import SnowflakeList
from .snowflake import Snowflake, SnowflakeList
from .user import User


Expand Down Expand Up @@ -70,3 +72,21 @@ 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[AvatarDecoration | None]
# collectibles: Collectibles
13 changes: 13 additions & 0 deletions docs/api/events.rst
Original file line number Diff line number Diff line change
Expand Up @@ -1496,3 +1496,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.8

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