diff --git a/aioxmpp/muc/service.py b/aioxmpp/muc/service.py index fb4a34d8..248202cb 100644 --- a/aioxmpp/muc/service.py +++ b/aioxmpp/muc/service.py @@ -406,7 +406,7 @@ class Room(aioxmpp.im.conversation.AbstractConversation): The :meth:`on_enter` signal does not receive any arguments anymore to make MUC comply with the :class:`AbstractConversation` spec. - .. signal:: on_muc_enter(presence, occupant, **kwargs) + .. signal:: on_muc_enter(presence, occupant, *, muc_status_codes=set(), **kwargs) This is an extended version of :meth:`on_enter` which adds MUC-specific arguments. @@ -414,6 +414,10 @@ class Room(aioxmpp.im.conversation.AbstractConversation): :param presence: The initial presence stanza. :param occupant: The :class:`Occupant` which will be used to track the local user. + :param muc_status_codes: The set of status codes received in the + initial join. + :type muc_status_codes: :class:`~.abc.Set` of :class:`int` or + :class:`~.StatusCode` .. versionadded:: 0.10 @@ -486,7 +490,7 @@ class Room(aioxmpp.im.conversation.AbstractConversation): :meth:`.AbstractConversation.on_presence_changed` for the full specification. - .. signal:: on_nick_changed(member, old_nick, new_nick, **kwargs) + .. signal:: on_nick_changed(member, old_nick, new_nick, *, muc_status_codes=set(), **kwargs) The nickname of an occupant has changed @@ -496,6 +500,10 @@ class Room(aioxmpp.im.conversation.AbstractConversation): :type old_nick: :class:`str` or :data:`None` :param new_nick: The new nickname of the member. :type new_nick: :class:`str` + :param muc_status_codes: The set of status codes received in the leave + notification. + :type muc_status_codes: :class:`~.abc.Set` of :class:`int` or + :class:`~.StatusCode` The new nickname is already set in the `member` object. Both `old_nick` and `new_nick` are not :data:`None`. @@ -505,6 +513,10 @@ class Room(aioxmpp.im.conversation.AbstractConversation): :meth:`.AbstractConversation.on_nick_changed` for the full specification. + .. versionchanged:: 0.10 + + The `muc_status_codes` argument was added. + .. signal:: on_topic_changed(member, new_topic, *, muc_nick=None, **kwargs) The topic of the conversation has changed. @@ -552,10 +564,18 @@ class Room(aioxmpp.im.conversation.AbstractConversation): :type muc_actor: :class:`~.xso.UserActor` :param muc_reason: The reason for the cause, as given by the actor. :type muc_reason: :class:`str` + :param muc_status_codes: The set of status codes received in the leave + notification. + :type muc_status_codes: :class:`~.abc.Set` of :class:`int` or + :class:`~.StatusCode` When this signal is called, the `member` has already been removed from the :attr:`members`. + .. versionchanged:: 0.10 + + The `muc_status_codes` argument was added. + .. signal:: on_muc_suspend() Emits when the stream used by this MUC gets destroyed (see @@ -597,7 +617,7 @@ class Room(aioxmpp.im.conversation.AbstractConversation): `submission_future` is already done before processing the form (as it is possible that multiple handlers are connected to this signal). - .. signal:: on_exit(*, muc_leave_mode=None, muc_actor=None, muc_reason=None, **kwargs) + .. signal:: on_exit(*, muc_leave_mode=None, muc_actor=None, muc_reason=None, muc_status_codes=set(), **kwargs) Emits when the unavailable :class:`~.Presence` stanza for the local JID is received. @@ -608,6 +628,20 @@ class Room(aioxmpp.im.conversation.AbstractConversation): :type muc_actor: :class:`~.xso.UserActor` :param muc_reason: The reason for the cause, as given by the actor. :type muc_reason: :class:`str` + :param muc_status_codes: The set of status codes received in the leave + notification. + :type muc_status_codes: :class:`~.abc.Set` of :class:`int` or + :class:`~.StatusCode` + + .. note:: + + The keyword arguments `muc_actor`, `muc_reason` and + `muc_status_codes` are not always given. Be sure to default them + accordingly. + + .. versionchanged:: 0.10 + + The `muc_status_codes` argument was added. The following signals inform users about state changes related to **other** occupants in the chat room. Note that different events may fire for the @@ -616,25 +650,49 @@ class Room(aioxmpp.im.conversation.AbstractConversation): ``"outcast"``) and then :meth:`on_leave` (with :attr:`LeaveMode.BANNED` `mode`). - .. signal:: on_muc_affiliation_changed(member, *, actor=None, reason=None, **kwargs) + .. signal:: on_muc_affiliation_changed(member, *, actor=None, reason=None, status_codes=set(), **kwargs) + + Emits when the affiliation of a `member` with the room changes. + + :param occupant: The member of the room. + :type occupant: :class:`Occupant` + :param actor: The actor object if available. + :type actor: :class:`~.xso.UserActor` + :param reason: The reason for the change, as given by the actor. + :type reason: :class:`str` + :param status_codes: The set of status codes received in the change + notification. + :type status_codes: :class:`~.abc.Set` of :class:`int` or + :class:`~.StatusCode` - Emits when the affiliation of a `member` with the room changes. + `occupant` is the :class:`Occupant` instance tracking the occupant whose + affiliation changed. - `occupant` is the :class:`Occupant` instance tracking the occupant whose - affiliation changed. + .. versionchanged:: 0.10 - There may be `actor` and/or `reason` keyword arguments which provide - details on who triggered the change in affiliation and for what reason. + The `status_codes` argument was added. - .. signal:: on_muc_role_changed(member, *, actor=None, reason=None, **kwargs) + .. signal:: on_muc_role_changed(member, *, actor=None, reason=None, status_codes=set(), , **kwargs) - Emits when the role of an `occupant` in the room changes. + Emits when the role of an `occupant` in the room changes. - `occupant` is the :class:`Occupant` instance tracking the occupant whose - role changed. + :param occupant: The member of the room. + :type occupant: :class:`Occupant` + :param actor: The actor object if available. + :type actor: :class:`~.xso.UserActor` + :param reason: The reason for the change, as given by the actor. + :type reason: :class:`str` + :param status_codes: The set of status codes received in the change + notification. + :type status_codes: :class:`~.abc.Set` of :class:`int` or + :class:`~.StatusCode` - There may be `actor` and/or `reason` keyword arguments which provide - details on who triggered the change in role and for what reason. + `occupant` is the :class:`Occupant` instance tracking the occupant whose + role changed. + + .. versionchanged:: 0.10 + + The `status_codes` argument was added. """ # this occupant state events @@ -998,6 +1056,7 @@ def _diff_presence(self, stanza, info, existing): { "actor": actor, "reason": reason, + "status_codes": stanza.xep0045_muc_user.status_codes, }, )) @@ -1011,6 +1070,7 @@ def _diff_presence(self, stanza, info, existing): { "actor": actor, "reason": reason, + "status_codes": stanza.xep0045_muc_user.status_codes, }, )) @@ -1039,7 +1099,12 @@ def _handle_self_presence(self, stanza): self._joined = True self._active = True self._state = RoomState.HISTORY - self.on_muc_enter(stanza, info) + self.on_muc_enter( + stanza, info, + muc_status_codes=frozenset( + stanza.xep0045_muc_user.status_codes + ) + ) self.on_enter() return @@ -1064,7 +1129,8 @@ def _handle_self_presence(self, stanza): existing.update(info) self.on_exit(muc_leave_mode=mode, muc_actor=actor, - muc_reason=reason) + muc_reason=reason, + muc_status_codes=stanza.xep0045_muc_user.status_codes) self._joined = False self._active = False @@ -1128,7 +1194,8 @@ def _inbound_muc_user_presence(self, stanza): self.on_leave(existing, muc_leave_mode=mode, muc_actor=actor, - muc_reason=reason) + muc_reason=reason, + muc_status_codes=stanza.xep0045_muc_user.status_codes) del self._occupant_info[existing.conversation_jid] def _handle_role_request(self, form): diff --git a/docs/api/changelog.rst b/docs/api/changelog.rst index 7b2ebfee..62354de6 100644 --- a/docs/api/changelog.rst +++ b/docs/api/changelog.rst @@ -371,6 +371,13 @@ Version 0.10 * :class:`aioxmpp.muc.StatusCode` +* The signals :meth:`aioxmpp.muc.Room.on_muc_enter`, + :meth:`~aioxmpp.muc.Room.on_exit`, + :meth:`~aioxmpp.muc.Room.on_leave`, :meth:`~aioxmpp.muc.Room.on_join`, + :meth:`~aioxmpp.muc.Room.on_muc_role_changed`, and + :meth:`~aioxmpp.muc.Room.on_muc_affiliation_changed` now receive the set of + status codes which was included in the triggering stanza as keyword argument. + .. _api-changelog-0.9: Version 0.9 diff --git a/tests/muc/test_service.py b/tests/muc/test_service.py index 9789e06a..64dea183 100644 --- a/tests/muc/test_service.py +++ b/tests/muc/test_service.py @@ -737,7 +737,7 @@ def test__disconnect(self): self.base.mock_calls, [ unittest.mock.call.on_exit( - muc_leave_mode=muc_service.LeaveMode.DISCONNECTED + muc_leave_mode=muc_service.LeaveMode.DISCONNECTED, ), ] ) @@ -850,7 +850,10 @@ def test__suspend__resume_cycle(self): [ unittest.mock.call.on_muc_suspend(), unittest.mock.call.on_muc_resume(), - unittest.mock.call.on_muc_enter(presence, self.jmuc.me), + unittest.mock.call.on_muc_enter( + presence, self.jmuc.me, + muc_status_codes={110}, + ), unittest.mock.call.on_enter(), ] ) @@ -964,7 +967,9 @@ def test__inbound_muc_user_presence_emits_on_leave_for_unavailable(self): first, muc_leave_mode=muc_service.LeaveMode.NORMAL, muc_actor=None, - muc_reason=None) + muc_reason=None, + muc_status_codes=set(), + ) ] ) @@ -1026,12 +1031,16 @@ def test__inbound_muc_user_presence_emits_on_leave_for_kick(self): presence, first, actor=actor, - reason="Avaunt, you cullion!"), + reason="Avaunt, you cullion!", + status_codes={307}, + ), unittest.mock.call.on_leave( first, muc_leave_mode=muc_service.LeaveMode.KICKED, muc_actor=actor, - muc_reason="Avaunt, you cullion!") + muc_reason="Avaunt, you cullion!", + muc_status_codes={307}, + ) ] ) @@ -1102,12 +1111,16 @@ def test__inbound_muc_user_presence_emits_on_leave_for_error_kick(self): presence, first, actor=actor, - reason="Error"), + reason="Error", + status_codes={307, 333}, + ), unittest.mock.call.on_leave( first, muc_leave_mode=muc_service.LeaveMode.ERROR, muc_actor=actor, - muc_reason="Error") + muc_reason="Error", + muc_status_codes={307, 333}, + ) ] ) @@ -1176,17 +1189,23 @@ def test__inbound_muc_user_presence_handles_itemless_role_change(self): presence, first, actor=None, - reason=None), + reason=None, + status_codes=set(), + ), unittest.mock.call.on_muc_affiliation_changed( presence, first, actor=None, - reason=None), + reason=None, + status_codes=set(), + ), unittest.mock.call.on_leave( first, muc_leave_mode=muc_service.LeaveMode.NORMAL, muc_actor=None, - muc_reason=None) + muc_reason=None, + muc_status_codes=set(), + ) ] ) @@ -1255,18 +1274,22 @@ def test__inbound_muc_user_presence_emits_on_leave_for_ban(self): first, actor=actor, reason="Treason", + status_codes={301}, ), unittest.mock.call.on_muc_affiliation_changed( presence, first, actor=actor, - reason="Treason" + reason="Treason", + status_codes={301}, ), unittest.mock.call.on_leave( first, muc_leave_mode=muc_service.LeaveMode.BANNED, muc_actor=actor, - muc_reason="Treason") + muc_reason="Treason", + muc_status_codes={301}, + ) ] ) self.assertEqual( @@ -1325,18 +1348,22 @@ def test__inbound_muc_user_presence_emits_on_leave_for_affiliation_change( first, actor=actor, reason="foo", + status_codes={321}, ), unittest.mock.call.on_muc_affiliation_changed( presence, first, actor=actor, - reason="foo" + reason="foo", + status_codes={321}, ), unittest.mock.call.on_leave( first, muc_leave_mode=muc_service.LeaveMode.AFFILIATION_CHANGE, muc_actor=actor, - muc_reason="foo") + muc_reason="foo", + muc_status_codes={321}, + ) ] ) self.assertEqual( @@ -1399,12 +1426,15 @@ def test__inbound_muc_user_presence_emits_on_leave_for_moderation_change( first, actor=actor, reason="foo", + status_codes={322}, ), unittest.mock.call.on_leave( first, muc_leave_mode=muc_service.LeaveMode.MODERATION_CHANGE, muc_actor=actor, - muc_reason="foo") + muc_reason="foo", + muc_status_codes={322}, + ) ] ) self.assertEqual( @@ -1458,7 +1488,9 @@ def test__inbound_muc_user_presence_emits_on_leave_for_system_shutdown( first, muc_leave_mode=muc_service.LeaveMode.SYSTEM_SHUTDOWN, muc_actor=None, - muc_reason="foo") + muc_reason="foo", + muc_status_codes={332}, + ) ] ) self.assertEqual( @@ -1624,7 +1656,10 @@ def test__inbound_muc_self_presence_emits_on_nick_changed(self): self.assertSequenceEqual( self.base.mock_calls, [ - unittest.mock.call.on_muc_enter(presence, first), + unittest.mock.call.on_muc_enter( + presence, first, + muc_status_codes={110}, + ), unittest.mock.call.on_enter(), ] ) @@ -1730,11 +1765,15 @@ def test__inbound_muc_user_presence_emits_on_various_changes(self): unittest.mock.call.on_muc_role_changed( presence, first, actor=None, - reason="foobar"), + reason="foobar", + status_codes=set(), + ), unittest.mock.call.on_muc_affiliation_changed( presence, first, actor=None, - reason="foobar"), + reason="foobar", + status_codes=set(), + ), ] ) @@ -1974,7 +2013,11 @@ def test_inbound_groupchat_message_with_body_emits_on_message_other_member( _, (occupant, ), _ = self.base.on_join.mock_calls[-1] self.base.mock_calls.clear() - self.base.on_muc_enter.assert_called_once_with(pres, self.jmuc.me) + self.base.on_muc_enter.assert_called_once_with( + pres, + self.jmuc.me, + muc_status_codes=unittest.mock.ANY, + ) self.base.on_enter.assert_called_once_with() self.base.mock_calls.clear() @@ -2019,7 +2062,11 @@ def test_invent_temporary_member_for_message_from_non_occupant(self): self.jmuc._inbound_muc_user_presence(pres) - self.base.on_muc_enter.assert_called_once_with(pres, self.jmuc.me) + self.base.on_muc_enter.assert_called_once_with( + pres, + self.jmuc.me, + muc_status_codes=unittest.mock.ANY, + ) self.base.on_enter.assert_called_once_with() self.base.mock_calls.clear() @@ -2078,8 +2125,11 @@ def test__inbound_muc_user_presence_emits_on_enter_and_on_exit(self): self.assertSequenceEqual( self.base.mock_calls, [ - unittest.mock.call.on_muc_enter(presence, - self.jmuc.me), + unittest.mock.call.on_muc_enter( + presence, + self.jmuc.me, + muc_status_codes={110}, + ), unittest.mock.call.on_enter(), ] ) @@ -2119,7 +2169,8 @@ def test__inbound_muc_user_presence_emits_on_enter_and_on_exit(self): unittest.mock.call.on_exit( muc_leave_mode=muc_service.LeaveMode.NORMAL, muc_actor=None, - muc_reason=None + muc_reason=None, + muc_status_codes={110} ) ] ) @@ -2137,6 +2188,37 @@ def test__inbound_muc_user_presence_emits_on_enter_and_on_exit(self): ) self.assertFalse(self.jmuc.muc_active) + def test_on_muc_enter_forwards_status_codes(self): + presence = aioxmpp.stanza.Presence( + type_=aioxmpp.structs.PresenceType.AVAILABLE, + from_=TEST_MUC_JID.replace(resource="thirdwitch") + ) + presence.xep0045_muc_user = muc_xso.UserExt( + status_codes={110, 1234, 375}, + items=[ + muc_xso.UserItem(affiliation="member", + role="none"), + ] + ) + + self.jmuc._inbound_muc_user_presence(presence) + + self.assertSequenceEqual( + self.base.mock_calls, + [ + unittest.mock.call.on_muc_enter( + presence, + self.jmuc.me, + muc_status_codes={ + 110, + 375, + 1234, + } + ), + unittest.mock.call.on_enter(), + ] + ) + def test_detect_self_presence_from_jid_if_status_is_missing(self): presence = aioxmpp.stanza.Presence( type_=aioxmpp.structs.PresenceType.AVAILABLE, @@ -2155,8 +2237,11 @@ def test_detect_self_presence_from_jid_if_status_is_missing(self): self.assertSequenceEqual( self.base.mock_calls, [ - unittest.mock.call.on_muc_enter(presence, - self.jmuc.me), + unittest.mock.call.on_muc_enter( + presence, + self.jmuc.me, + muc_status_codes={110}, + ), unittest.mock.call.on_enter(), ] ) @@ -2182,7 +2267,8 @@ def test_detect_self_presence_from_jid_if_status_is_missing(self): unittest.mock.call.on_exit( muc_leave_mode=muc_service.LeaveMode.KICKED, muc_actor=None, - muc_reason=None + muc_reason=None, + muc_status_codes={307}, ) ] ) @@ -2218,8 +2304,11 @@ def test_do_not_treat_unavailable_stanzas_as_join(self): self.assertSequenceEqual( self.base.mock_calls, [ - unittest.mock.call.on_muc_enter(presence, - self.jmuc.me), + unittest.mock.call.on_muc_enter( + presence, + self.jmuc.me, + muc_status_codes={110}, + ), unittest.mock.call.on_enter(), ] ) @@ -5412,7 +5501,8 @@ def test_stream_destruction_with_autorejoin(self): base.mock_calls, [ unittest.mock.call.enter1(unittest.mock.ANY, - unittest.mock.ANY), + unittest.mock.ANY, + muc_status_codes=unittest.mock.ANY), unittest.mock.call.suspend1(), ] ) @@ -5507,9 +5597,11 @@ def extract(items, op): base.mock_calls, [ unittest.mock.call.enter1(unittest.mock.ANY, - unittest.mock.ANY), + unittest.mock.ANY, + muc_status_codes=unittest.mock.ANY), unittest.mock.call.enter2(unittest.mock.ANY, - unittest.mock.ANY), + unittest.mock.ANY, + muc_status_codes=unittest.mock.ANY), ] ) @@ -5583,7 +5675,8 @@ def test_stream_destruction_without_autorejoin(self): base.mock_calls, [ unittest.mock.call.enter1(unittest.mock.ANY, - unittest.mock.ANY), + unittest.mock.ANY, + muc_status_codes=unittest.mock.ANY), unittest.mock.call.exit1( muc_leave_mode=muc_service.LeaveMode.DISCONNECTED ),