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)