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..c21cc343 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
+ 'xep_0444', # Message Reactions
'protoxep_occupantid', # https://dino.im/xeps/occupant-id.html
]
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/protoxep_reactions/__init__.py b/slixmpp/plugins/xep_0444/__init__.py
similarity index 55%
rename from slixmpp/plugins/protoxep_reactions/__init__.py
rename to 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
similarity index 54%
rename from slixmpp/plugins/protoxep_reactions/reactions.py
rename to 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/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 = """
+
+
+ 😃
+ 🤗
+
+
+ """
+
+ 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 = """
+
+
+ 😃
+ 🤗
+ toto
+
+
+ """
+
+ 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)