summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.gitlab-ci.yml3
-rwxr-xr-xsetup.py3
-rw-r--r--slixmpp/plugins/__init__.py4
-rw-r--r--slixmpp/plugins/protoxep_occupantid/__init__.py12
-rw-r--r--slixmpp/plugins/protoxep_occupantid/occupantid.py23
-rw-r--r--slixmpp/plugins/protoxep_occupantid/stanza.py16
-rw-r--r--slixmpp/plugins/protoxep_reactions/stanza.py31
-rw-r--r--slixmpp/plugins/xep_0045/muc.py357
-rw-r--r--slixmpp/plugins/xep_0045/stanza.py49
-rw-r--r--slixmpp/plugins/xep_0363/http_upload.py1
-rw-r--r--slixmpp/plugins/xep_0421/occupant_id.py3
-rw-r--r--slixmpp/plugins/xep_0421/stanza.py1
-rw-r--r--slixmpp/plugins/xep_0444/__init__.py (renamed from slixmpp/plugins/protoxep_reactions/__init__.py)6
-rw-r--r--slixmpp/plugins/xep_0444/reactions.py (renamed from slixmpp/plugins/protoxep_reactions/reactions.py)39
-rw-r--r--slixmpp/plugins/xep_0444/stanza.py60
-rw-r--r--slixmpp/xmlstream/stanzabase.py7
-rw-r--r--slixmpp/xmlstream/xmlstream.py17
-rw-r--r--tests/test_stanza_xep_0444.py69
18 files changed, 430 insertions, 271 deletions
diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index 839de025..0a4013b6 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -9,7 +9,8 @@ test:
image: ubuntu:latest
script:
- apt update
- - apt install -y python3 cython3 gpg
+ - apt install -y python3 python3-pip cython3 gpg
+ - pip3 install emoji
- ./run_tests.py
trigger_poezio:
diff --git a/setup.py b/setup.py
index 5b122388..9ba0b5df 100755
--- a/setup.py
+++ b/setup.py
@@ -30,6 +30,7 @@ CLASSIFIERS = [
'Programming Language :: Python',
'Programming Language :: Python :: 3.7',
'Programming Language :: Python :: 3.8',
+ 'Programming Language :: Python :: 3.9',
'Topic :: Internet :: XMPP',
'Topic :: Software Development :: Libraries :: Python Modules',
]
@@ -82,7 +83,7 @@ setup(
platforms=['any'],
packages=packages,
ext_modules=ext_modules,
- install_requires=['aiodns>=1.0', 'pyasn1', 'pyasn1_modules', 'aiohttp'],
+ install_requires=['aiodns>=1.0', 'pyasn1', 'pyasn1_modules', 'aiohttp', 'emoji'],
classifiers=CLASSIFIERS,
cmdclass={'test': TestCommand}
)
diff --git a/slixmpp/plugins/__init__.py b/slixmpp/plugins/__init__.py
index c7736adc..a89b10f6 100644
--- a/slixmpp/plugins/__init__.py
+++ b/slixmpp/plugins/__init__.py
@@ -86,6 +86,6 @@ __all__ = [
'xep_0325', # IoT Systems Control
'xep_0332', # HTTP Over XMPP Transport
'xep_0377', # Spam reporting
- 'protoxep_reactions', # https://dino.im/xeps/reactions.html
- 'protoxep_occupantid', # https://dino.im/xeps/occupant-id.html
+ 'xep_0421', # Anonymous unique occupant identifiers for MUCs
+ 'xep_0444', # Message Reactions
]
diff --git a/slixmpp/plugins/protoxep_occupantid/__init__.py b/slixmpp/plugins/protoxep_occupantid/__init__.py
deleted file mode 100644
index 1bd374b6..00000000
--- a/slixmpp/plugins/protoxep_occupantid/__init__.py
+++ /dev/null
@@ -1,12 +0,0 @@
-"""
- Slixmpp: The Slick XMPP Library
- Copyright (C) 2019 Mathieu Pasquet
- This file is part of Slixmpp.
-
- See the file LICENSE for copying permission.
-"""
-from slixmpp.plugins.base import register_plugin
-from slixmpp.plugins.protoxep_occupantid.occupantid import XEP_OccupantID
-from slixmpp.plugins.protoxep_occupantid.stanza import OccupantID
-
-register_plugin(XEP_OccupantID)
diff --git a/slixmpp/plugins/protoxep_occupantid/occupantid.py b/slixmpp/plugins/protoxep_occupantid/occupantid.py
deleted file mode 100644
index 7f4a9d4a..00000000
--- a/slixmpp/plugins/protoxep_occupantid/occupantid.py
+++ /dev/null
@@ -1,23 +0,0 @@
-"""
- Slixmpp: The Slick XMPP Library
- Copyright (C) 2019 Mathieu Pasquet
- This file is part of Slixmpp.
-
- See the file LICENSE for copying permission.
-"""
-from slixmpp.plugins import BasePlugin
-from slixmpp.stanza import Message, Presence
-from slixmpp.xmlstream import register_stanza_plugin
-
-from slixmpp.plugins.protoxep_occupantid import stanza
-
-
-class XEP_OccupantID(BasePlugin):
- name = 'protoxep_occupantid'
- description = 'XEP-XXXX: Anonymous unique occupant identifiers for MUCs'
- dependencies = set()
- stanza = stanza
-
- def plugin_init(self):
- register_stanza_plugin(Message, stanza.OccupantID)
- register_stanza_plugin(Presence, stanza.OccupantID)
diff --git a/slixmpp/plugins/protoxep_occupantid/stanza.py b/slixmpp/plugins/protoxep_occupantid/stanza.py
deleted file mode 100644
index e5853111..00000000
--- a/slixmpp/plugins/protoxep_occupantid/stanza.py
+++ /dev/null
@@ -1,16 +0,0 @@
-"""
- Slixmpp: The Slick XMPP Library
- Copyright (C) 2019 Mathieu Pasquet
- This file is part of Slixmpp.
-
- See the file LICENSE for copying permission.
-"""
-
-from slixmpp.xmlstream import ElementBase
-
-
-class OccupantID(ElementBase):
- name = 'occupant-id'
- plugin_attrib = 'occupant-id'
- namespace = 'urn:xmpp:occupant-id:0'
- interfaces = {'id'}
diff --git a/slixmpp/plugins/protoxep_reactions/stanza.py b/slixmpp/plugins/protoxep_reactions/stanza.py
deleted file mode 100644
index 45414a37..00000000
--- a/slixmpp/plugins/protoxep_reactions/stanza.py
+++ /dev/null
@@ -1,31 +0,0 @@
-"""
- Slixmpp: The Slick XMPP Library
- Copyright (C) 2019 Mathieu Pasquet
- This file is part of Slixmpp.
-
- See the file LICENSE for copying permission.
-"""
-
-from slixmpp.xmlstream import ElementBase, register_stanza_plugin
-
-
-class Reactions(ElementBase):
- name = 'reactions'
- plugin_attrib = 'reactions'
- namespace = 'urn:xmpp:reactions:0'
- interfaces = {'to'}
-
-
-class Reaction(ElementBase):
- name = 'reaction'
- namespace = 'urn:xmpp:reactions:0'
- interfaces = {'value'}
-
- def get_value(self) -> str:
- return self.xml.text
-
- def set_value(self, value: str):
- self.xml.text = value
-
-
-register_stanza_plugin(Reactions, Reaction, iterable=True)
diff --git a/slixmpp/plugins/xep_0045/muc.py b/slixmpp/plugins/xep_0045/muc.py
index 364c47fd..f310c03e 100644
--- a/slixmpp/plugins/xep_0045/muc.py
+++ b/slixmpp/plugins/xep_0045/muc.py
@@ -9,8 +9,18 @@
from __future__ import with_statement
import logging
-
-from slixmpp import Presence, Message
+from typing import (
+ List,
+ Tuple,
+ Optional,
+)
+
+from slixmpp import (
+ Presence,
+ Message,
+ Iq,
+ JID,
+)
from slixmpp.plugins import BasePlugin
from slixmpp.xmlstream import register_stanza_plugin, ET
from slixmpp.xmlstream.handler.callback import Callback
@@ -19,11 +29,23 @@ from slixmpp.xmlstream.matcher.xmlmask import MatchXMLMask
from slixmpp.exceptions import IqError, IqTimeout
from slixmpp.plugins.xep_0045 import stanza
-from slixmpp.plugins.xep_0045.stanza import MUCPresence, MUCMessage
+from slixmpp.plugins.xep_0045.stanza import (
+ MUCPresence,
+ MUCJoin,
+ MUCMessage,
+ MUCAdminQuery,
+ MUCAdminItem,
+ MUCHistory,
+ MUCOwnerQuery,
+ MUCOwnerDestroy,
+)
log = logging.getLogger(__name__)
+AFFILIATIONS = ('outcast', 'member', 'admin', 'owner', 'none')
+ROLES = ('moderator', 'participant', 'visitor', 'none')
+
class XEP_0045(BasePlugin):
@@ -39,19 +61,61 @@ class XEP_0045(BasePlugin):
def plugin_init(self):
self.rooms = {}
self.our_nicks = {}
- self.xep = '0045'
# load MUC support in presence stanzas
register_stanza_plugin(Presence, MUCPresence)
+ register_stanza_plugin(Presence, MUCJoin)
+ register_stanza_plugin(MUCJoin, MUCHistory)
register_stanza_plugin(Message, MUCMessage)
- self.xmpp.register_handler(Callback('MUCPresence', MatchXMLMask("<presence xmlns='%s' />" % self.xmpp.default_ns), self.handle_groupchat_presence))
- self.xmpp.register_handler(Callback('MUCError', MatchXMLMask("<message xmlns='%s' type='error'><error/></message>" % self.xmpp.default_ns), self.handle_groupchat_error_message))
- self.xmpp.register_handler(Callback('MUCMessage', MatchXMLMask("<message xmlns='%s' type='groupchat'><body/></message>" % self.xmpp.default_ns), self.handle_groupchat_message))
- self.xmpp.register_handler(Callback('MUCSubject', MatchXMLMask("<message xmlns='%s' type='groupchat'><subject/></message>" % self.xmpp.default_ns), self.handle_groupchat_subject))
- self.xmpp.register_handler(Callback('MUCConfig', MatchXMLMask("<message xmlns='%s' type='groupchat'><x xmlns='http://jabber.org/protocol/muc#user'><status/></x></message>" % self.xmpp.default_ns), self.handle_config_change))
- self.xmpp.register_handler(Callback('MUCInvite', MatchXPath("{%s}message/{%s}x/{%s}invite" % (
- self.xmpp.default_ns,
- stanza.NS_USER,
- stanza.NS_USER)), self.handle_groupchat_invite))
+ register_stanza_plugin(Iq, MUCAdminQuery)
+ register_stanza_plugin(Iq, MUCOwnerQuery)
+ register_stanza_plugin(MUCOwnerQuery, MUCOwnerDestroy)
+ register_stanza_plugin(MUCAdminQuery, MUCAdminItem, iterable=True)
+
+ # Register handlers
+ self.xmpp.register_handler(
+ Callback(
+ 'MUCPresence',
+ MatchXMLMask("<presence xmlns='%s' />" % self.xmpp.default_ns),
+ self.handle_groupchat_presence,
+ ))
+ self.xmpp.register_handler(
+ Callback(
+ 'MUCError',
+ MatchXMLMask("<message xmlns='%s' type='error'><error/></message>" % self.xmpp.default_ns),
+ self.handle_groupchat_error_message
+ ))
+ self.xmpp.register_handler(
+ Callback(
+ 'MUCMessage',
+ MatchXMLMask("<message xmlns='%s' type='groupchat'><body/></message>" % self.xmpp.default_ns),
+ self.handle_groupchat_message
+ ))
+ self.xmpp.register_handler(
+ Callback(
+ 'MUCSubject',
+ MatchXMLMask("<message xmlns='%s' type='groupchat'><subject/></message>" % self.xmpp.default_ns),
+ self.handle_groupchat_subject
+ ))
+ self.xmpp.register_handler(
+ Callback(
+ 'MUCConfig',
+ MatchXMLMask(
+ "<message xmlns='%s' type='groupchat'>"
+ "<x xmlns='http://jabber.org/protocol/muc#user'><status/></x>"
+ "</message>" % self.xmpp.default_ns
+ ),
+ self.handle_config_change
+ ))
+ self.xmpp.register_handler(
+ Callback(
+ 'MUCInvite',
+ MatchXPath("{%s}message/{%s}x/{%s}invite" % (
+ self.xmpp.default_ns,
+ stanza.NS_USER,
+ stanza.NS_USER
+ )),
+ self.handle_groupchat_invite
+ ))
def plugin_end(self):
self.xmpp.plugin['xep_0030'].del_feature(feature=stanza.NS)
@@ -112,7 +176,6 @@ class XEP_0045(BasePlugin):
self.xmpp.event("muc::%s::message_error" % msg['from'].bare, msg)
-
def handle_groupchat_subject(self, msg: Message) -> None:
""" Handle a message coming from a muc indicating
a change of subject (or announcing it when joining the room)
@@ -123,143 +186,96 @@ class XEP_0045(BasePlugin):
return None
self.xmpp.event('groupchat_subject', msg)
- def jid_in_room(self, room, jid):
+ def jid_in_room(self, room: JID, jid: JID) -> bool:
for nick in self.rooms[room]:
entry = self.rooms[room][nick]
if entry is not None and entry['jid'].full == jid:
return True
return False
- def get_nick(self, room, jid):
+ def get_nick(self, room: JID, jid: JID) -> Optional[str]:
for nick in self.rooms[room]:
entry = self.rooms[room][nick]
if entry is not None and entry['jid'].full == jid:
return nick
- def configure_room(self, room, form=None, ifrom=None):
- if form is None:
- form = self.get_room_config(room, ifrom=ifrom)
- iq = self.xmpp.make_iq_set()
- iq['to'] = room
- if ifrom is not None:
- iq['from'] = ifrom
- query = ET.Element('{http://jabber.org/protocol/muc#owner}query')
- form['type'] = 'submit'
- query.append(form)
- iq.append(query)
- # For now, swallow errors to preserve existing API
- try:
- result = iq.send()
- except IqError:
- return False
- except IqTimeout:
- return False
- return True
-
- def join_muc(self, room, nick, maxhistory="0", password='', wait=False, pstatus=None, pshow=None, pfrom=None):
+ def join_muc(self, room: JID, nick: str, maxhistory="0", password='',
+ pstatus='', pshow='', pfrom=''):
""" Join the specified room, requesting 'maxhistory' lines of history.
"""
- stanza = self.xmpp.make_presence(pto="%s/%s" % (room, nick), pstatus=pstatus, pshow=pshow, pfrom=pfrom)
- x = ET.Element('{http://jabber.org/protocol/muc}x')
+ stanza = self.xmpp.make_presence(
+ pto="%s/%s" % (room, nick), pstatus=pstatus,
+ pshow=pshow, pfrom=pfrom
+ )
+ stanza.enable('muc_join')
if password:
- passelement = ET.Element('{http://jabber.org/protocol/muc}password')
- passelement.text = password
- x.append(passelement)
+ stanza['muc_join']['password'] = password
if maxhistory:
- history = ET.Element('{http://jabber.org/protocol/muc}history')
- if maxhistory == "0":
- history.attrib['maxchars'] = maxhistory
+ if maxhistory == "0":
+ stanza['muc_join']['history']['maxchars'] = '0'
else:
- history.attrib['maxstanzas'] = maxhistory
- x.append(history)
- stanza.append(x)
- if not wait:
- self.xmpp.send(stanza)
- else:
- #wait for our own room presence back
- expect = ET.Element("{%s}presence" % self.xmpp.default_ns, {'from':"%s/%s" % (room, nick)})
- self.xmpp.send(stanza, expect)
+ stanza['muc_join']['history']['maxstanzas'] = str(maxhistory)
+ self.xmpp.send(stanza)
self.rooms[room] = {}
self.our_nicks[room] = nick
- def destroy(self, room, reason='', altroom = '', ifrom=None):
- iq = self.xmpp.make_iq_set()
- if ifrom is not None:
- iq['from'] = ifrom
- iq['to'] = room
- query = ET.Element('{http://jabber.org/protocol/muc#owner}query')
- destroy = ET.Element('{http://jabber.org/protocol/muc#owner}destroy')
+ async def destroy(self, room: JID, reason='', altroom='', *,
+ ifrom: Optional[JID] = None, **iqkwargs) -> Iq:
+ iq = self.xmpp.make_iq_set(ifrom=ifrom, ito=room)
+ iq.enable('mucowner_query')
+ iq['mucowner_query'].enable('destroy')
if altroom:
- destroy.attrib['jid'] = altroom
- xreason = ET.Element('{http://jabber.org/protocol/muc#owner}reason')
- xreason.text = reason
- destroy.append(xreason)
- query.append(destroy)
- iq.append(query)
- # For now, swallow errors to preserve existing API
- try:
- r = iq.send()
- except IqError:
- return False
- except IqTimeout:
- return False
- return True
-
- def set_affiliation(self, room, jid=None, nick=None, affiliation='member', ifrom=None):
+ iq['mucowner_query']['destroy']['jid'] = altroom
+ if reason:
+ iq['mucowner_query']['destroy']['reason'] = reason
+ await iq.send(**iqkwargs)
+
+ async def set_affiliation(self, room: JID, jid: Optional[JID] = None, nick: Optional[str] = None, *, affiliation: str,
+ ifrom: Optional[JID] = None, **iqkwargs):
""" Change room affiliation."""
- if affiliation not in ('outcast', 'member', 'admin', 'owner', 'none'):
- raise TypeError
- query = ET.Element('{http://jabber.org/protocol/muc#admin}query')
- if nick is not None:
- item = ET.Element('{http://jabber.org/protocol/muc#admin}item', {'affiliation':affiliation, 'nick':nick})
- else:
- item = ET.Element('{http://jabber.org/protocol/muc#admin}item', {'affiliation':affiliation, 'jid':jid})
- query.append(item)
- iq = self.xmpp.make_iq_set(query)
- iq['to'] = room
- iq['from'] = ifrom
- # For now, swallow errors to preserve existing API
- try:
- result = iq.send()
- except IqError:
- return False
- except IqTimeout:
- return False
- return True
-
- def set_role(self, room, nick, role):
+ if affiliation not in AFFILIATIONS:
+ raise ValueError('%s is not a valid affiliation' % affiliation)
+ if not any((jid, nick)):
+ raise ValueError('One of jid or nick must be set')
+ iq = self.xmpp.make_iq_set(ito=room, ifrom=ifrom)
+ iq.enable('mucadmin_query')
+ item = MUCAdminItem()
+ item['affiliation'] = affiliation
+ if nick:
+ item['nick'] = nick
+ if jid:
+ item['jid'] = jid
+ iq['mucadmin_query'].append(item)
+ await iq.send(**iqkwargs)
+
+ async def set_role(self, room: JID, nick: str, role: str, *,
+ ifrom: Optional[JID] = None, **iqkwargs) -> Iq:
""" Change role property of a nick in a room.
Typically, roles are temporary (they last only as long as you are in the
room), whereas affiliations are permanent (they last across groupchat
sessions).
"""
- if role not in ('moderator', 'participant', 'visitor', 'none'):
- raise TypeError
- query = ET.Element('{http://jabber.org/protocol/muc#admin}query')
- item = ET.Element('item', {'role':role, 'nick':nick})
- query.append(item)
- iq = self.xmpp.make_iq_set(query)
- iq['to'] = room
- result = iq.send()
- if result is False or result['type'] != 'result':
- raise ValueError
- return True
-
- def invite(self, room, jid, reason='', mfrom=''):
+ if role not in ROLES:
+ raise ValueError("Role %s does not exist" % role)
+ iq = self.xmpp.make_iq_set(ito=room, ifrom=ifrom)
+ iq.enable('mucadmin_query')
+ item = MUCAdminItem()
+ item['role'] = role
+ item['nick'] = nick
+ iq['mucadmin_query'].append(item)
+ await iq.send(**iqkwargs)
+
+ def invite(self, room: JID, jid: JID, reason='', *,
+ mfrom: Optional[JID] = None):
""" Invite a jid to a room."""
- msg = self.xmpp.make_message(room)
- msg['from'] = mfrom
- x = ET.Element('{http://jabber.org/protocol/muc#user}x')
- invite = ET.Element('{http://jabber.org/protocol/muc#user}invite', {'to': jid})
+ msg = self.xmpp.make_message(room, mfrom=mfrom)
+ msg.enable('muc')
+ msg['muc']['invite'] = jid
if reason:
- rxml = ET.Element('{http://jabber.org/protocol/muc#user}reason')
- rxml.text = reason
- invite.append(rxml)
- x.append(invite)
- msg.append(x)
+ msg['muc']['invite']['reason'] = reason
self.xmpp.send(msg)
- def leave_muc(self, room, nick, msg='', pfrom=None):
+ def leave_muc(self, room: JID, nick: str, msg='', pfrom=None):
""" Leave the specified room.
"""
if msg:
@@ -268,44 +284,77 @@ class XEP_0045(BasePlugin):
self.xmpp.send_presence(pshow='unavailable', pto="%s/%s" % (room, nick), pfrom=pfrom)
del self.rooms[room]
- def get_room_config(self, room, ifrom=''):
- iq = self.xmpp.make_iq_get('http://jabber.org/protocol/muc#owner')
- iq['to'] = room
- iq['from'] = ifrom
+
+ async def get_room_config(self, room: JID, ifrom=''):
+ """Get the room config form in 0004 plugin format """
+ iq = self.xmpp.make_iq_get(stanza.NS_OWNER, ito=room, ifrom=ifrom)
# For now, swallow errors to preserve existing API
- try:
- result = iq.send()
- except IqError:
- raise ValueError
- except IqTimeout:
- raise ValueError
+ result = await iq.send()
form = result.xml.find('{http://jabber.org/protocol/muc#owner}query/{jabber:x:data}x')
if form is None:
- raise ValueError
+ raise ValueError("Configuration form not found")
return self.xmpp.plugin['xep_0004'].build_form(form)
- def cancel_config(self, room, ifrom=None):
- query = ET.Element('{http://jabber.org/protocol/muc#owner}query')
+ async def cancel_config(self, room: JID, *,
+ ifrom: Optional[JID] = None, **iqkwargs) -> Iq:
+ """Cancel a requested config form"""
+ query = MUCOwnerQuery()
x = ET.Element('{jabber:x:data}x', type='cancel')
query.append(x)
- iq = self.xmpp.make_iq_set(query)
- iq['to'] = room
- iq['from'] = ifrom
- iq.send()
+ iq = self.xmpp.make_iq_set(query, ito=room, ifrom=ifrom)
+ return await iq.send(**iqkwargs)
- def set_room_config(self, room, config, ifrom=''):
- query = ET.Element('{http://jabber.org/protocol/muc#owner}query')
+ async def set_room_config(self, room: JID, config, *,
+ ifrom: Optional[JID] = None, **iqkwargs) -> Iq:
+ """Send a room config form"""
+ query = MUCOwnerQuery()
config['type'] = 'submit'
query.append(config)
- iq = self.xmpp.make_iq_set(query)
- iq['to'] = room
- iq['from'] = ifrom
- iq.send()
-
- def get_joined_rooms(self):
+ iq = self.xmpp.make_iq_set(query, ito=room, ifrom=ifrom)
+ return await iq.send(**iqkwargs)
+
+ async def get_affiliation_list(self, room: JID, affiliation: str, *,
+ ifrom: Optional[JID] = None, **iqkwargs) -> List[JID]:
+ """"Get a list of JIDs with the specified affiliation"""
+ iq = self.xmpp.make_iq_get(stanza.NS_ADMIN, ito=room, ifrom=ifrom)
+ iq['mucadmin_query']['item']['affiliation'] = affiliation
+ result = await iq.send(**iqkwargs)
+ return [item['jid'] for item in result['mucadmin_query']]
+
+ async def get_roles_list(self, room: JID, role: str, *,
+ ifrom: Optional[JID] = None, **iqkwargs) -> List[str]:
+ """"Get a list of JIDs with the specified role"""
+ iq = self.xmpp.make_iq_get(stanza.NS_ADMIN, ito=room, ifrom=ifrom)
+ iq['mucadmin_query']['item']['role'] = role
+ result = await iq.send(**iqkwargs)
+ return [item['nick'] for item in result['mucadmin_query']]
+
+ async def send_affiliation_list(self, room: JID, affiliations: List[Tuple[JID, str]], *,
+ ifrom: Optional[JID] = None, **iqkwargs) -> Iq:
+ """Send an affiliation delta list"""
+ iq = self.xmpp.make_iq_set(ito=room, ifrom=ifrom)
+ for jid, affiliation in affiliations:
+ item = MUCAdminItem()
+ item['jid'] = jid
+ item['affiliation'] = affiliation
+ iq['mucadmin_query'].append(item)
+ return await iq.send(**iqkwargs)
+
+ async def send_role_list(self, room: JID, roles: List[Tuple[str, str]], *,
+ ifrom: Optional[JID], **iqkwargs) -> Iq:
+ """Send a role delta list"""
+ iq = self.xmpp.make_iq_set(ito=room, ifrom=ifrom)
+ for nick, affiliation in roles:
+ item = MUCAdminItem()
+ item['nick'] = nick
+ item['affiliation'] = affiliation
+ iq['mucadmin_query'].append(item)
+ return await iq.send(**iqkwargs)
+
+ def get_joined_rooms(self) -> List[JID]:
return self.rooms.keys()
- def get_our_jid_in_room(self, room_jid):
+ def get_our_jid_in_room(self, room_jid: JID) -> str:
""" Return the jid we're using in a room.
"""
return "%s/%s" % (room_jid, self.our_nicks[room_jid])
@@ -319,19 +368,15 @@ class XEP_0045(BasePlugin):
else:
return None
- def get_roster(self, room):
+ def get_roster(self, room: JID) -> List[str]:
""" Get the list of nicks in a room.
"""
if room not in self.rooms.keys():
return None
return self.rooms[room].keys()
- def get_users_by_affiliation(cls, room, affiliation='member', ifrom=None):
- if affiliation not in ('outcast', 'member', 'admin', 'owner', 'none'):
+ def get_users_by_affiliation(self, room: JID, affiliation='member', *, ifrom: Optional[JID] = None):
+ # Preserve old API
+ if affiliation not in AFFILIATIONS:
raise TypeError
- query = ET.Element('{http://jabber.org/protocol/muc#admin}query')
- item = ET.Element('{http://jabber.org/protocol/muc#admin}item', {'affiliation': affiliation})
- query.append(item)
- iq = cls.xmpp.Iq(sto=room, sfrom=ifrom, stype='get')
- iq.append(query)
- return iq.send()
+ return self.get_affiliation_list(room, affiliation, ifrom=ifrom)
diff --git a/slixmpp/plugins/xep_0045/stanza.py b/slixmpp/plugins/xep_0045/stanza.py
index 2db83baf..9756790b 100644
--- a/slixmpp/plugins/xep_0045/stanza.py
+++ b/slixmpp/plugins/xep_0045/stanza.py
@@ -147,3 +147,52 @@ class MUCMessage(MUCBase):
</x>
</message>
'''
+
+class MUCJoin(ElementBase):
+ name = 'x'
+ namespace = NS
+ plugin_attrib = 'muc_join'
+ interfaces = {'password'}
+ sub_interfaces = {'password'}
+
+
+class MUCInvite(ElementBase):
+ name = 'invite'
+ plugin_attrib = 'invite'
+ namespace = NS_USER
+ interfaces = {'to', 'reason'}
+ sub_interfaces = {'reason'}
+
+
+class MUCHistory(ElementBase):
+ name = 'history'
+ plugin_attrib = 'history'
+ namespace = NS
+ interfaces = {'maxchars', 'maxstanzas', 'since', 'seconds'}
+
+
+class MUCOwnerQuery(ElementBase):
+ name = 'query'
+ plugin_attrib = 'mucowner_query'
+ namespace = NS_OWNER
+
+
+class MUCOwnerDestroy(ElementBase):
+ name = 'destroy'
+ plugin_attrib = 'destroy'
+ interfaces = {'reason', 'jid'}
+ sub_interfaces = {'reason'}
+
+
+class MUCAdminQuery(ElementBase):
+ name = 'query'
+ plugin_attrib = 'mucadmin_query'
+ namespace = NS_ADMIN
+
+
+class MUCAdminItem(ElementBase):
+ namespace = NS_ADMIN
+ name = 'item'
+ plugin_attrib = 'item'
+ interfaces = {'role', 'affiliation', 'nick', 'jid'}
+
diff --git a/slixmpp/plugins/xep_0363/http_upload.py b/slixmpp/plugins/xep_0363/http_upload.py
index a833a9c9..04b066cd 100644
--- a/slixmpp/plugins/xep_0363/http_upload.py
+++ b/slixmpp/plugins/xep_0363/http_upload.py
@@ -138,6 +138,7 @@ class XEP_0363(BasePlugin):
basename = os.path.basename(filename)
slot_iq = await self.request_slot(self.upload_service, basename, size,
content_type, ifrom, timeout,
+ callback=callback,
timeout_callback=timeout_callback)
slot = slot_iq['http_upload_slot']
diff --git a/slixmpp/plugins/xep_0421/occupant_id.py b/slixmpp/plugins/xep_0421/occupant_id.py
index 116bf2d9..4ee27a09 100644
--- a/slixmpp/plugins/xep_0421/occupant_id.py
+++ b/slixmpp/plugins/xep_0421/occupant_id.py
@@ -6,7 +6,7 @@
See the file LICENSE for copying permission.
"""
-from slixmpp import JID, Message
+from slixmpp import JID, Message, Presence
from slixmpp.plugins import BasePlugin
from slixmpp.xmlstream import register_stanza_plugin
from slixmpp.plugins.xep_0421 import stanza
@@ -25,6 +25,7 @@ class XEP_0421(BasePlugin):
def plugin_init(self) -> None:
# XXX: This should be MucMessage. Someday..
register_stanza_plugin(Message, OccupantId)
+ register_stanza_plugin(Presence, OccupantId)
async def has_feature(self, jid: JID) -> bool:
info = await self.xmpp['xep_0030'].get_info(jid)
diff --git a/slixmpp/plugins/xep_0421/stanza.py b/slixmpp/plugins/xep_0421/stanza.py
index 0cb93959..ab1128d6 100644
--- a/slixmpp/plugins/xep_0421/stanza.py
+++ b/slixmpp/plugins/xep_0421/stanza.py
@@ -36,5 +36,6 @@ class OccupantId(ElementBase):
'''
name = 'occupant-id'
+ plugin_attrib = 'occupant-id'
namespace = NS
interface = {'id'}
diff --git a/slixmpp/plugins/protoxep_reactions/__init__.py b/slixmpp/plugins/xep_0444/__init__.py
index e107bd16..dff4287c 100644
--- a/slixmpp/plugins/protoxep_reactions/__init__.py
+++ b/slixmpp/plugins/xep_0444/__init__.py
@@ -1,11 +1,11 @@
"""
Slixmpp: The Slick XMPP Library
- Copyright (C) 2019 Mathieu Pasquet
+ Copyright (C) 2020 Mathieu Pasquet
This file is part of Slixmpp.
See the file LICENSE for copying permission.
"""
from slixmpp.plugins.base import register_plugin
-from slixmpp.plugins.protoxep_reactions.reactions import XEP_Reactions
+from slixmpp.plugins.xep_0444.reactions import XEP_0444
-register_plugin(XEP_Reactions)
+register_plugin(XEP_0444)
diff --git a/slixmpp/plugins/protoxep_reactions/reactions.py b/slixmpp/plugins/xep_0444/reactions.py
index e7af8fcb..bfd12499 100644
--- a/slixmpp/plugins/protoxep_reactions/reactions.py
+++ b/slixmpp/plugins/xep_0444/reactions.py
@@ -1,26 +1,28 @@
"""
Slixmpp: The Slick XMPP Library
- Copyright (C) 2019 Mathieu Pasquet
+ Copyright (C) 2020 Mathieu Pasquet
This file is part of Slixmpp.
See the file LICENSE for copying permission.
"""
from typing import Iterable
+from slixmpp import JID
from slixmpp.plugins import BasePlugin
from slixmpp.stanza import Message
from slixmpp.xmlstream import register_stanza_plugin
from slixmpp.xmlstream.matcher import MatchXMLMask
from slixmpp.xmlstream.handler import Callback
-from slixmpp.plugins.protoxep_reactions import stanza
+from slixmpp.plugins.xep_0444 import stanza
-class XEP_Reactions(BasePlugin):
- name = 'protoxep_reactions'
- description = 'XEP-XXXX: Message Reactions'
- dependencies = {'xep_0030'}
+class XEP_0444(BasePlugin):
+ name = 'xep_0444'
+ description = 'XEP-0444: Message Reactions'
+ dependencies = {'xep_0030', 'xep_0334'}
stanza = stanza
+ namespace = stanza.NS
def plugin_init(self):
self.xmpp.register_handler(
@@ -30,25 +32,32 @@ class XEP_Reactions(BasePlugin):
self._handle_reactions,
)
)
- self.xmpp['xep_0030'].add_feature('urn:xmpp:reactions:0')
register_stanza_plugin(Message, stanza.Reactions)
+ register_stanza_plugin(stanza.Reactions, stanza.Reaction, iterable=True)
+
+ def session_bind(self, event):
+ self.xmpp['xep_0030'].add_feature(stanza.NS)
def plugin_end(self):
self.xmpp.remove_handler('Reaction received')
- self.xmpp['xep_0030'].remove_feature('urn:xmpp:reactions:0')
+ self.xmpp['xep_0030'].remove_feature(stanza.NS)
def _handle_reactions(self, message: Message):
self.xmpp.event('reactions', message)
+ def send_reactions(self, to: JID, to_id: str, reactions: Iterable[str], *, store=True):
+ """Send reactions related to a message"""
+ msg = self.xmpp.make_message(mto=to)
+ self.set_reactions(msg, to_id, reactions)
+ if store:
+ msg.enable('store')
+ msg.send()
+
@staticmethod
def set_reactions(message: Message, to_id: str, reactions: Iterable[str]):
- """
- Add reactions to a Message object.
- """
- reactions_stanza = stanza.Reactions()
- reactions_stanza['to'] = to_id
+ """Add reactions to a Message object."""
+ message['reactions']['id'] = to_id
for reaction in reactions:
reaction_stanza = stanza.Reaction()
reaction_stanza['value'] = reaction
- reactions_stanza.append(reaction_stanza)
- message.append(reactions_stanza)
+ message['reactions'].append(reaction_stanza)
diff --git a/slixmpp/plugins/xep_0444/stanza.py b/slixmpp/plugins/xep_0444/stanza.py
new file mode 100644
index 00000000..338a244e
--- /dev/null
+++ b/slixmpp/plugins/xep_0444/stanza.py
@@ -0,0 +1,60 @@
+"""
+ Slixmpp: The Slick XMPP Library
+ Copyright (C) 2020 Mathieu Pasquet
+ This file is part of Slixmpp.
+
+ See the file LICENSE for copying permission.
+"""
+
+from typing import Set, Iterable
+from slixmpp.xmlstream import ElementBase
+try:
+ from emoji import UNICODE_EMOJI
+except ImportError:
+ UNICODE_EMOJI = None
+
+
+NS = 'urn:xmpp:reactions:0'
+
+class Reactions(ElementBase):
+ name = 'reactions'
+ plugin_attrib = 'reactions'
+ namespace = NS
+ interfaces = {'id', 'values'}
+
+ def get_values(self, *, all_chars=False) -> Set[str]:
+ """"Get all reactions as str"""
+ reactions = set()
+ for reaction in self:
+ value = reaction['value']
+ if UNICODE_EMOJI and not all_chars:
+ if value in UNICODE_EMOJI:
+ reactions.add(reaction['value'])
+ else:
+ reactions.add(reaction['value'])
+ return reactions
+
+ def set_values(self, values: Iterable[str], *, all_chars=False):
+ """"Set all reactions as str"""
+ for element in self.xml.findall('reaction'):
+ self.xml.remove(element)
+ for reaction_txt in values:
+ reaction = Reaction()
+ reaction.set_value(reaction_txt, all_chars=all_chars)
+ self.append(reaction)
+
+
+class Reaction(ElementBase):
+ name = 'reaction'
+ namespace = NS
+ interfaces = {'value'}
+
+ def get_value(self) -> str:
+ return self.xml.text
+
+ def set_value(self, value: str, *, all_chars=False):
+ if UNICODE_EMOJI and not all_chars:
+ if not value in UNICODE_EMOJI:
+ raise ValueError("%s is not a valid emoji" % value)
+ self.xml.text = value
+
diff --git a/slixmpp/xmlstream/stanzabase.py b/slixmpp/xmlstream/stanzabase.py
index f45e4b96..7eaf78a5 100644
--- a/slixmpp/xmlstream/stanzabase.py
+++ b/slixmpp/xmlstream/stanzabase.py
@@ -1497,12 +1497,7 @@ class StanzaBase(ElementBase):
self.name)
def send(self):
- """Queue the stanza to be sent on the XML stream.
-
- :param bool now: Indicates if the queue should be skipped and the
- stanza sent immediately. Useful for stream
- initialization. Defaults to ``False``.
- """
+ """Queue the stanza to be sent on the XML stream."""
self.stream.send(self)
def __copy__(self):
diff --git a/slixmpp/xmlstream/xmlstream.py b/slixmpp/xmlstream/xmlstream.py
index 3aac8c8e..af494903 100644
--- a/slixmpp/xmlstream/xmlstream.py
+++ b/slixmpp/xmlstream/xmlstream.py
@@ -228,6 +228,8 @@ class XMLStream(asyncio.BaseProtocol):
self.add_event_handler('disconnected', self._remove_schedules)
self.add_event_handler('session_start', self._start_keepalive)
+
+ self._run_filters = None
@property
def loop(self):
@@ -271,10 +273,12 @@ class XMLStream(asyncio.BaseProtocol):
localhost
"""
- asyncio.ensure_future(
- self.run_filters(),
- loop=self.loop,
- )
+ if self._run_filters is None:
+ self._run_filters = asyncio.ensure_future(
+ self.run_filters(),
+ loop=self.loop,
+ )
+
self.disconnect_reason = None
self.cancel_connection_attempt()
self.connect_loop_wait = 0
@@ -463,6 +467,8 @@ class XMLStream(asyncio.BaseProtocol):
self.parser = None
self.transport = None
self.socket = None
+ if self._run_filters:
+ self._run_filters.cancel()
def cancel_connection_attempt(self):
"""
@@ -474,6 +480,9 @@ class XMLStream(asyncio.BaseProtocol):
if self._current_connection_attempt:
self._current_connection_attempt.cancel()
self._current_connection_attempt = None
+ if self._run_filters:
+ self._run_filters.cancel()
+
def disconnect(self, wait: float = 2.0, reason: Optional[str] = None, ignore_send_queue: bool = False) -> None:
"""Close the XML stream and wait for an acknowldgement from the server for
diff --git a/tests/test_stanza_xep_0444.py b/tests/test_stanza_xep_0444.py
new file mode 100644
index 00000000..b4d5739b
--- /dev/null
+++ b/tests/test_stanza_xep_0444.py
@@ -0,0 +1,69 @@
+"""
+ Slixmpp: The Slick XMPP Library
+ Copyright (C) 2020 Mathieu Pasquet
+ This file is part of Slixmpp.
+
+ See the file LICENSE for copying permission.
+"""
+
+import unittest
+from slixmpp import Message
+from slixmpp.test import SlixTest
+from slixmpp.plugins.xep_0444 import XEP_0444
+import slixmpp.plugins.xep_0444.stanza as stanza
+from slixmpp.xmlstream import register_stanza_plugin
+
+
+class TestReactions(SlixTest):
+
+ def setUp(self):
+ register_stanza_plugin(Message, stanza.Reactions)
+ register_stanza_plugin(stanza.Reactions, stanza.Reaction)
+
+ def testCreateReactions(self):
+ """Testing creating Reactions."""
+
+ xmlstring = """
+ <message>
+ <reactions xmlns="urn:xmpp:reactions:0" id="abcd">
+ <reaction>😃</reaction>
+ <reaction>🤗</reaction>
+ </reactions>
+ </message>
+ """
+
+ msg = self.Message()
+ msg['reactions']['id'] = 'abcd'
+ msg['reactions']['values'] = ['😃', '🤗']
+
+ self.check(msg, xmlstring, use_values=False)
+
+ self.assertEqual({'😃', '🤗'}, msg['reactions']['values'])
+
+
+ def testCreateReactionsUnrestricted(self):
+ """Testing creating Reactions with the extra all_chars arg."""
+
+ xmlstring = """
+ <message>
+ <reactions xmlns="urn:xmpp:reactions:0" id="abcd">
+ <reaction>😃</reaction>
+ <reaction>🤗</reaction>
+ <reaction>toto</reaction>
+ </reactions>
+ </message>
+ """
+
+ msg = self.Message()
+ msg['reactions']['id'] = 'abcd'
+ msg['reactions'].set_values(['😃', '🤗', 'toto'], all_chars=True)
+
+ self.check(msg, xmlstring, use_values=False)
+
+ self.assertEqual({'😃', '🤗'}, msg['reactions']['values'])
+ self.assertEqual({'😃', '🤗', 'toto'}, msg['reactions'].get_values(all_chars=True))
+ with self.assertRaises(ValueError):
+ msg['reactions'].set_values(['😃', '🤗', 'toto'], all_chars=False)
+
+
+suite = unittest.TestLoader().loadTestsFromTestCase(TestReactions)