From 6a43559f4fb7542ed95968357fdc72abfd5e0bac Mon Sep 17 00:00:00 2001 From: Emmanuel Gil Peyrot Date: Thu, 23 Nov 2017 12:10:39 +0000 Subject: Add a Markup plugin. --- examples/markup.py | 120 ++++++++++++++++++++++++++ slixmpp/plugins/xep_0394/__init__.py | 15 ++++ slixmpp/plugins/xep_0394/markup.py | 161 +++++++++++++++++++++++++++++++++++ slixmpp/plugins/xep_0394/stanza.py | 123 ++++++++++++++++++++++++++ 4 files changed, 419 insertions(+) create mode 100755 examples/markup.py create mode 100644 slixmpp/plugins/xep_0394/__init__.py create mode 100644 slixmpp/plugins/xep_0394/markup.py create mode 100644 slixmpp/plugins/xep_0394/stanza.py diff --git a/examples/markup.py b/examples/markup.py new file mode 100755 index 00000000..a72cbdb8 --- /dev/null +++ b/examples/markup.py @@ -0,0 +1,120 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +""" + Slixmpp: The Slick XMPP Library + Copyright (C) 2010 Nathanael C. Fritz + This file is part of Slixmpp. + + See the file LICENSE for copying permission. +""" + +import logging +from getpass import getpass +from argparse import ArgumentParser + +import slixmpp +from slixmpp.plugins.xep_0394 import stanza as markup_stanza + + +class EchoBot(slixmpp.ClientXMPP): + + """ + A simple Slixmpp bot that will echo messages it + receives, along with a short thank you message. + """ + + def __init__(self, jid, password): + slixmpp.ClientXMPP.__init__(self, jid, password) + + # The session_start event will be triggered when + # the bot establishes its connection with the server + # and the XML streams are ready for use. We want to + # listen for this event so that we we can initialize + # our roster. + self.add_event_handler("session_start", self.start) + + # The message event is triggered whenever a message + # stanza is received. Be aware that that includes + # MUC messages and error messages. + self.add_event_handler("message", self.message) + + def start(self, event): + """ + Process the session_start event. + + Typical actions for the session_start event are + requesting the roster and broadcasting an initial + presence stanza. + + Arguments: + event -- An empty dictionary. The session_start + event does not provide any additional + data. + """ + self.send_presence() + self.get_roster() + + def message(self, msg): + """ + Process incoming message stanzas. Be aware that this also + includes MUC messages and error messages. It is usually + a good idea to check the messages's type before processing + or sending replies. + + Arguments: + msg -- The received message stanza. See the documentation + for stanza objects and the Message stanza to see + how it may be used. + """ + body = msg['body'] + new_body = self['xep_0394'].to_plain_text(body, msg['markup']) + xhtml = self['xep_0394'].to_xhtml_im(body, msg['markup']) + print('Plain text:', new_body) + print('XHTML-IM:', xhtml['body']) + message = msg.reply() + message['body'] = new_body + message['html']['body'] = xhtml['body'] + self.send(message) + + +if __name__ == '__main__': + # Setup the command line arguments. + parser = ArgumentParser(description=EchoBot.__doc__) + + # Output verbosity options. + parser.add_argument("-q", "--quiet", help="set logging to ERROR", + action="store_const", dest="loglevel", + const=logging.ERROR, default=logging.INFO) + parser.add_argument("-d", "--debug", help="set logging to DEBUG", + action="store_const", dest="loglevel", + const=logging.DEBUG, default=logging.INFO) + + # JID and password options. + parser.add_argument("-j", "--jid", dest="jid", + help="JID to use") + parser.add_argument("-p", "--password", dest="password", + help="password to use") + + args = parser.parse_args() + + # Setup logging. + logging.basicConfig(level=args.loglevel, + format='%(levelname)-8s %(message)s') + + if args.jid is None: + args.jid = input("Username: ") + if args.password is None: + args.password = getpass("Password: ") + + # Setup the EchoBot and register plugins. Note that while plugins may + # have interdependencies, the order in which you register them does + # not matter. + xmpp = EchoBot(args.jid, args.password) + xmpp.register_plugin('xep_0030') # Service Discovery + xmpp.register_plugin('xep_0199') # XMPP Ping + xmpp.register_plugin('xep_0394') # Message Markup + + # Connect to the XMPP server and start processing XMPP stanzas. + xmpp.connect() + xmpp.process() diff --git a/slixmpp/plugins/xep_0394/__init__.py b/slixmpp/plugins/xep_0394/__init__.py new file mode 100644 index 00000000..f3a9b635 --- /dev/null +++ b/slixmpp/plugins/xep_0394/__init__.py @@ -0,0 +1,15 @@ +""" + Slixmpp: The Slick XMPP Library + Copyright (C) 2017 Emmanuel Gil Peyrot + This file is part of Slixmpp. + + See the file LICENSE for copying permission. +""" + +from slixmpp.plugins.base import register_plugin + +from slixmpp.plugins.xep_0394.stanza import Markup, Span, BlockCode, List, Li, BlockQuote +from slixmpp.plugins.xep_0394.markup import XEP_0394 + + +register_plugin(XEP_0394) diff --git a/slixmpp/plugins/xep_0394/markup.py b/slixmpp/plugins/xep_0394/markup.py new file mode 100644 index 00000000..d3ec69e6 --- /dev/null +++ b/slixmpp/plugins/xep_0394/markup.py @@ -0,0 +1,161 @@ +""" + Slixmpp: The Slick XMPP Library + Copyright (C) 2017 Emmanuel Gil Peyrot + This file is part of Slixmpp. + + See the file LICENSE for copying permission. +""" + + +from slixmpp.stanza import Message +from slixmpp.plugins import BasePlugin +from slixmpp.xmlstream import register_stanza_plugin, ET, tostring +from slixmpp.plugins.xep_0394 import stanza, Markup, Span, BlockCode, List, Li, BlockQuote +from slixmpp.plugins.xep_0071 import XHTML_IM + + +class Start: + def __init__(self, elem): + self.elem = elem + + def __repr__(self): + return 'Start(%s)' % self.elem + + +class End: + def __init__(self, elem): + self.elem = elem + + def __repr__(self): + return 'End(%s)' % self.elem + + +class XEP_0394(BasePlugin): + + name = 'xep_0394' + description = 'XEP-0394: Message Markup' + dependencies = {'xep_0030', 'xep_0071'} + stanza = stanza + + def plugin_init(self): + register_stanza_plugin(Message, Markup) + + def session_bind(self, jid): + self.xmpp['xep_0030'].add_feature(feature=Markup.namespace) + + def plugin_end(self): + self.xmpp['xep_0030'].del_feature(feature=Markup.namespace) + + @staticmethod + def _split_first_level(body, markup_elem): + split_points = [] + elements = {} + for markup in markup_elem['substanzas']: + start = markup['start'] + end = markup['end'] + split_points.append(start) + split_points.append(end) + elements.setdefault(start, []).append(Start(markup)) + elements.setdefault(end, []).append(End(markup)) + if isinstance(markup, List): + lis = markup['lis'] + for i, li in enumerate(lis): + start = li['start'] + split_points.append(start) + li_end = lis[i + 1]['start'] if i < len(lis) - 1 else end + elements.setdefault(li_end, []).append(End(li)) + elements.setdefault(start, []).append(Start(li)) + split_points = set(split_points) + new_body = [[]] + for i, letter in enumerate(body + '\x00'): + if i in split_points: + body_elements = [] + for elem in elements[i]: + body_elements.append(elem) + new_body.append(body_elements) + new_body.append([]) + new_body[-1].append(letter) + new_body[-1] = new_body[-1][:-1] + final = [] + for chunk in new_body: + if not chunk: + continue + final.append(''.join(chunk) if isinstance(chunk[0], str) else chunk) + return final + + def to_plain_text(self, body, markup_elem): + chunks = self._split_first_level(body, markup_elem) + final = [] + for chunk in chunks: + if isinstance(chunk, str): + final.append(chunk) + return ''.join(final) + + def to_xhtml_im(self, body, markup_elem): + chunks = self._split_first_level(body, markup_elem) + final = [] + stack = [] + for chunk in chunks: + if isinstance(chunk, str): + chunk = (chunk.replace("&", '&') + .replace('<', '<') + .replace('>', '>') + .replace('"', '"') + .replace("'", ''') + .replace('\n', '
')) + final.append(chunk) + continue + num_end = 0 + for elem in chunk: + if isinstance(elem, End): + num_end += 1 + + for i in range(num_end): + stack_top = stack.pop() + for elem in chunk: + if not isinstance(elem, End): + continue + elem = elem.elem + if elem is stack_top: + if isinstance(elem, Span): + final.append('') + elif isinstance(elem, BlockCode): + final.append('') + elif isinstance(elem, List): + final.append('') + elif isinstance(elem, Li): + final.append('') + elif isinstance(elem, BlockQuote): + final.append('') + break + else: + assert False + for elem in chunk: + if not isinstance(elem, Start): + continue + elem = elem.elem + stack.append(elem) + if isinstance(elem, Span): + style = [] + for type_ in elem['types']: + if type_ == 'emphasis': + style.append('font-style: italic;') + if type_ == 'code': + style.append('font-family: monospace;') + if type_ == 'deleted': + style.append('text-decoration: line-through;') + final.append("" % ' '.join(style)) + elif isinstance(elem, BlockCode): + final.append('
')
+                elif isinstance(elem, List):
+                    final.append('
    ') + elif isinstance(elem, Li): + final.append('
  • ') + elif isinstance(elem, BlockQuote): + final.append('
    ') + p = "

    %s

    " % ''.join(final) + p2 = ET.fromstring(p) + print('coucou', p, tostring(p2)) + xhtml_im = XHTML_IM() + xhtml_im['body'] = p2 + return xhtml_im diff --git a/slixmpp/plugins/xep_0394/stanza.py b/slixmpp/plugins/xep_0394/stanza.py new file mode 100644 index 00000000..f15a9ba1 --- /dev/null +++ b/slixmpp/plugins/xep_0394/stanza.py @@ -0,0 +1,123 @@ +""" + Slixmpp: The Slick XMPP Library + Copyright (C) 2017 Emmanuel Gil Peyrot + This file is part of Slixmpp. + + See the file LICENSE for copying permission. +""" + +from slixmpp.xmlstream import ElementBase, register_stanza_plugin, ET + + +class Markup(ElementBase): + namespace = 'urn:xmpp:markup:0' + name = 'markup' + plugin_attrib = 'markup' + + +class _FirstLevel(ElementBase): + namespace = 'urn:xmpp:markup:0' + interfaces = {'start', 'end'} + + def get_start(self): + return int(self._get_attr('start')) + + def set_start(self, value): + self._set_attr('start', '%d' % value) + + def get_end(self): + return int(self._get_attr('end')) + + def set_end(self, value): + self._set_attr('end', '%d' % value) + +class Span(_FirstLevel): + name = 'span' + plugin_attrib = 'span' + plugin_multi_attrib = 'spans' + interfaces = {'start', 'end', 'types'} + + def get_types(self): + types = [] + if self.xml.find('{urn:xmpp:markup:0}emphasis') is not None: + types.append('emphasis') + if self.xml.find('{urn:xmpp:markup:0}code') is not None: + types.append('code') + if self.xml.find('{urn:xmpp:markup:0}deleted') is not None: + types.append('deleted') + return types + + def set_types(self, value): + del self['types'] + for type_ in value: + if type_ == 'emphasis': + self.xml.append(ET.Element('{urn:xmpp:markup:0}emphasis')) + elif type_ == 'code': + self.xml.append(ET.Element('{urn:xmpp:markup:0}code')) + elif type_ == 'deleted': + self.xml.append(ET.Element('{urn:xmpp:markup:0}deleted')) + + def det_types(self): + for child in self.xml: + self.xml.remove(child) + + +class _SpanType(ElementBase): + namespace = 'urn:xmpp:markup:0' + + +class EmphasisType(_SpanType): + name = 'emphasis' + plugin_attrib = 'emphasis' + + +class CodeType(_SpanType): + name = 'code' + plugin_attrib = 'code' + + +class DeletedType(_SpanType): + name = 'deleted' + plugin_attrib = 'deleted' + + +class BlockCode(_FirstLevel): + name = 'bcode' + plugin_attrib = 'bcode' + plugin_multi_attrib = 'bcodes' + + +class List(_FirstLevel): + name = 'list' + plugin_attrib = 'list' + plugin_multi_attrib = 'lists' + interfaces = {'start', 'end', 'li'} + + +class Li(ElementBase): + namespace = 'urn:xmpp:markup:0' + name = 'li' + plugin_attrib = 'li' + plugin_multi_attrib = 'lis' + interfaces = {'start'} + + def get_start(self): + return int(self._get_attr('start')) + + def set_start(self, value): + self._set_attr('start', '%d' % value) + + +class BlockQuote(_FirstLevel): + name = 'bquote' + plugin_attrib = 'bquote' + plugin_multi_attrib = 'bquotes' + +register_stanza_plugin(Markup, Span, iterable=True) +register_stanza_plugin(Markup, BlockCode, iterable=True) +register_stanza_plugin(Markup, List, iterable=True) +register_stanza_plugin(Markup, BlockQuote, iterable=True) +register_stanza_plugin(Span, EmphasisType) +register_stanza_plugin(Span, CodeType) +register_stanza_plugin(Span, DeletedType) +register_stanza_plugin(List, Li, iterable=True) -- cgit v1.2.3