Skip to content

Commit 795d031

Browse files
SoheabPaillat-devDA-344NeloBlivionpre-commit-ci[bot]
authored
feat: add on_raw_member_update event (#3012)
Co-authored-by: Paillat <jeremiecotti@ik.me> Co-authored-by: DA344 <108473820+DA-344@users.noreply.github.com> Co-authored-by: Nelo <41271523+NeloBlivion@users.noreply.github.com> Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> Co-authored-by: Lala Sabathil <aiko@aitsys.dev> Co-authored-by: Lala Sabathil <lala@pycord.dev>
1 parent 25a1aee commit 795d031

File tree

7 files changed

+109
-30
lines changed

7 files changed

+109
-30
lines changed

CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,9 @@ These changes are available on the `master` branch, but have not yet been releas
2626
([#3107](https://github.com/Pycord-Development/pycord/pull/3107))
2727
- Added `Member.display_avatar_decoration` and `Member.guild_avatar_decoration`.
2828
([#3109](https://github.com/Pycord-Development/pycord/pull/3109))
29+
- Added a new event called `on_raw_member_update` that is dispatched when a member is
30+
updated, regardless of cache status.
31+
([#3012](https://github.com/Pycord-Development/pycord/pull/3012))
2932

3033
### Changed
3134

discord/flags.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -715,6 +715,7 @@ def members(self):
715715
- :func:`on_raw_member_remove`
716716
- :func:`on_member_update`
717717
- :func:`on_user_update`
718+
- :func:`on_raw_member_update`
718719
719720
This also corresponds to the following attributes and classes in terms of cache:
720721

discord/member.py

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -64,8 +64,8 @@
6464
from .role import Role
6565
from .state import ConnectionState
6666
from .types.activity import PartialPresenceUpdate
67-
from .types.member import Member
6867
from .types.member import Member as MemberPayload
68+
from .types.member import MemberUpdateEvent as MemberUpdateEventPayload
6969
from .types.member import MemberWithUser as MemberWithUserPayload
7070
from .types.member import UserWithMember as UserWithMemberPayload
7171
from .types.user import User as UserPayload
@@ -411,7 +411,7 @@ def _try_upgrade(
411411
def _copy(cls: type[M], member: M) -> M:
412412
self: M = cls.__new__(cls) # to bypass __init__
413413

414-
self._roles = utils.SnowflakeList(member._roles, is_sorted=True)
414+
self._roles = utils.SnowflakeList(member._roles, is_sorted=True) # type: ignore # the API is the same
415415
self.joined_at = member.joined_at
416416
self.premium_since = member.premium_since
417417
self._client_status = member._client_status.copy()
@@ -435,21 +435,21 @@ async def _get_channel(self):
435435
ch = await self.create_dm()
436436
return ch
437437

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

446446
try:
447-
self.pending = data["pending"]
447+
self.pending = data["pending"] # type: ignore # handled by the type-except
448448
except KeyError:
449449
pass
450450

451451
self.premium_since = utils.parse_time(data.get("premium_since"))
452-
self._roles = utils.SnowflakeList(map(int, data["roles"]))
452+
self._roles = utils.SnowflakeList(map(int, data["roles"])) # type: ignore # the API is the same
453453
self._avatar = data.get("avatar")
454454
self._banner = data.get("banner")
455455
self.communication_disabled_until = utils.parse_time(

discord/raw_models.py

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@
4747
from .state import ConnectionState
4848
from .threads import Thread
4949
from .types.channel import VoiceChannelEffectSendEvent as VoiceChannelEffectSend
50+
from .types.member import MemberUpdateEvent
5051
from .types.raw_models import (
5152
AuditLogEntryEvent,
5253
)
@@ -90,6 +91,7 @@
9091
"RawVoiceChannelStatusUpdateEvent",
9192
"RawMessagePollVoteEvent",
9293
"RawSoundboardSoundDeleteEvent",
94+
"RawMemberUpdateEvent",
9395
)
9496

9597

@@ -870,3 +872,28 @@ def __init__(self, data: PartialSoundboardSound) -> None:
870872
self.sound_id: int = int(data["sound_id"])
871873
self.guild_id: int = int(data["guild_id"])
872874
self.data: PartialSoundboardSound = data
875+
876+
877+
class RawMemberUpdateEvent(_RawReprMixin):
878+
"""Represents the payload for a :func:`on_raw_member_update` event.
879+
880+
.. versionadded:: 2.8
881+
882+
Attributes
883+
----------
884+
data: :class:`dict`
885+
The raw data sent by the `gateway <https://discord.com/developers/docs/topics/gateway-events#guild-member-update>`_
886+
cached_member: Optional[:class:`Member`]
887+
The cached member, if found in the internal member cache.
888+
member: :class:`Member`
889+
The new member object after the update.
890+
"""
891+
892+
__slots__ = ("guild_id", "user_id", "data", "cached_member", "member")
893+
894+
def __init__(self, data: MemberUpdateEvent, member: Member) -> None:
895+
self.guild_id: int = int(data["guild_id"])
896+
self.user_id: int = int(data["user"]["id"])
897+
self.data: MemberUpdateEvent = data
898+
self.cached_member: Member | None = None
899+
self.member: Member = member

discord/state.py

Lines changed: 38 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,7 @@
8585
from .types.channel import DMChannel as DMChannelPayload
8686
from .types.emoji import Emoji as EmojiPayload
8787
from .types.guild import Guild as GuildPayload
88+
from .types.member import MemberUpdateEvent
8889
from .types.message import Message as MessagePayload
8990
from .types.poll import Poll as PollPayload
9091
from .types.sticker import GuildSticker as GuildStickerPayload
@@ -918,7 +919,11 @@ def parse_message_poll_vote_add(self, data) -> None:
918919
counts[answer.id].count += 1
919920
else:
920921
counts[answer.id] = PollAnswerCount(
921-
{"id": answer.id, "count": 1, "me_voted": False}
922+
{
923+
"id": answer.id,
924+
"count": 1,
925+
"me_voted": False,
926+
}
922927
)
923928
if poll is not None and user is not None:
924929
answer = poll.get_answer(raw.answer_id)
@@ -1329,40 +1334,50 @@ def parse_guild_member_remove(self, data) -> None:
13291334
)
13301335
self.dispatch("raw_member_remove", raw)
13311336

1332-
def parse_guild_member_update(self, data) -> None:
1337+
def parse_guild_member_update(self, data: MemberUpdateEvent) -> None:
13331338
guild = self._get_guild(int(data["guild_id"]))
1334-
user = data["user"]
1335-
user_id = int(user["id"])
13361339
if guild is None:
13371340
_log.debug(
13381341
"GUILD_MEMBER_UPDATE referencing an unknown guild ID: %s. Discarding.",
13391342
data["guild_id"],
13401343
)
13411344
return
13421345

1343-
member = guild.get_member(user_id)
1344-
if member is not None:
1345-
old_member = Member._copy(member)
1346-
member._update(data)
1347-
user_update = member._update_inner_user(user)
1348-
if user_update:
1349-
self.dispatch("user_update", user_update[0], user_update[1])
1346+
user = data["user"]
1347+
user_id = int(user["id"])
1348+
1349+
# Try to get the old member from cache
1350+
old_member: Member | None = guild.get_member(user_id)
1351+
old_member_copy: Member | None = (
1352+
Member._copy(old_member) if old_member is not None else None
1353+
)
13501354

1351-
self.dispatch("member_update", old_member, member)
1355+
# Always create or update the member object
1356+
if old_member is not None:
1357+
old_member._update(data)
1358+
new_member: Member = old_member
13521359
else:
1353-
if self.member_cache_flags.joined:
1354-
member = Member(data=data, guild=guild, state=self)
1360+
new_member = Member(guild=guild, data=data, state=self) # type: ignore
13551361

1356-
# Force an update on the inner user if necessary
1357-
user_update = member._update_inner_user(user)
1358-
if user_update:
1359-
self.dispatch("user_update", user_update[0], user_update[1])
1362+
raw = RawMemberUpdateEvent(data, new_member)
1363+
raw.cached_member = old_member_copy
1364+
self.dispatch("raw_member_update", raw)
13601365

1361-
guild._add_member(member)
1362-
_log.debug(
1363-
"GUILD_MEMBER_UPDATE referencing an unknown member ID: %s. Discarding.",
1364-
user_id,
1365-
)
1366+
# Update the user cache if needed
1367+
user_update = None
1368+
if old_member_copy is not None:
1369+
user_update = old_member_copy._update_inner_user(user)
1370+
else:
1371+
user_update = new_member._update_inner_user(user)
1372+
1373+
if user_update:
1374+
self.dispatch("user_update", user_update[0], user_update[1])
1375+
1376+
if old_member_copy is not None:
1377+
self.dispatch("member_update", old_member_copy, new_member)
1378+
else:
1379+
if self.member_cache_flags.joined:
1380+
guild._add_member(new_member)
13661381

13671382
def parse_guild_emojis_update(self, data) -> None:
13681383
guild = self._get_guild(int(data["guild_id"]))

discord/types/member.py

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,8 +25,10 @@
2525

2626
from typing import TypedDict
2727

28+
from typing_extensions import NotRequired
29+
2830
from .collectibles import AvatarDecoration, Collectibles
29-
from .snowflake import SnowflakeList
31+
from .snowflake import Snowflake, SnowflakeList
3032
from .user import User
3133

3234

@@ -70,3 +72,21 @@ class MemberWithUser(_OptionalMemberWithUser):
7072

7173
class UserWithMember(User, total=False):
7274
member: _OptionalMemberWithUser
75+
76+
77+
class MemberUpdateEvent(TypedDict):
78+
guild_id: Snowflake
79+
user: User
80+
roles: list[Snowflake]
81+
nick: NotRequired[str | None]
82+
avatar: NotRequired[str | None]
83+
banner: NotRequired[str | None]
84+
joined_at: NotRequired[str | None]
85+
premium_since: NotRequired[str | None]
86+
deaf: NotRequired[bool | None]
87+
mute: NotRequired[bool | None]
88+
pending: NotRequired[bool | None]
89+
communication_disabled_until: NotRequired[str | None]
90+
flags: NotRequired[int | None]
91+
avatar_decoration_data: NotRequired[AvatarDecoration | None]
92+
# collectibles: Collectibles

docs/api/events.rst

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1496,3 +1496,16 @@ Soundboard Sound
14961496

14971497
:param sound: The soundboard sound that was created.
14981498
:type sound: :class:`SoundboardSound`
1499+
1500+
.. function:: on_raw_member_update(payload)
1501+
1502+
Called when a :class:`Member` updates their profile.
1503+
Unlike :func:`on_member_update`, this is called regardless of the
1504+
state of the internal member cache.
1505+
1506+
This requires :attr:`Intents.members` to be enabled.
1507+
1508+
.. versionadded:: 2.8
1509+
1510+
:param payload: The raw event payload data.
1511+
:type payload: :class:`RawMemberUpdateEvent`

0 commit comments

Comments
 (0)