Philipp Hörist pushed to branch master at gajim / python-nbxmpp

Commits:

6 changed files:

Changes:

  • nbxmpp/const.py
    ... ... @@ -285,6 +285,11 @@ class Error(Enum):
    285 285
             return self.value
    
    286 286
     
    
    287 287
     
    
    288
    +class BookmarkStoreType(Enum):
    
    289
    +    PUBSUB = 'pubsub'
    
    290
    +    PRIVATE = 'private'
    
    291
    +
    
    292
    +
    
    288 293
     MOODS = [
    
    289 294
         'afraid',
    
    290 295
         'amazed',
    

  • nbxmpp/dispatcher_nb.py
    ... ... @@ -62,6 +62,7 @@ from nbxmpp.modules.tune import Tune
    62 62
     from nbxmpp.modules.mood import Mood
    
    63 63
     from nbxmpp.modules.location import Location
    
    64 64
     from nbxmpp.modules.user_avatar import UserAvatar
    
    65
    +from nbxmpp.modules.bookmarks import Bookmarks
    
    65 66
     from nbxmpp.modules.misc import unwrap_carbon
    
    66 67
     from nbxmpp.modules.misc import unwrap_mam
    
    67 68
     from nbxmpp.util import get_properties_struct
    
    ... ... @@ -191,6 +192,7 @@ class XMPPDispatcher(PlugIn):
    191 192
             self._modules['Tune'] = Tune(self._owner)
    
    192 193
             self._modules['Location'] = Location(self._owner)
    
    193 194
             self._modules['UserAvatar'] = UserAvatar(self._owner)
    
    195
    +        self._modules['Bookmarks'] = Bookmarks(self._owner)
    
    194 196
     
    
    195 197
             for instance in self._modules.values():
    
    196 198
                 for handler in instance.handlers:
    

  • nbxmpp/modules/bookmarks.py
    1
    +# Copyright (C) 2018 Philipp Hörist <philipp AT hoerist.com>
    
    2
    +#
    
    3
    +# This file is part of nbxmpp.
    
    4
    +#
    
    5
    +# This program is free software; you can redistribute it and/or
    
    6
    +# modify it under the terms of the GNU General Public License
    
    7
    +# as published by the Free Software Foundation; either version 3
    
    8
    +# of the License, or (at your option) any later version.
    
    9
    +#
    
    10
    +# This program is distributed in the hope that it will be useful,
    
    11
    +# but WITHOUT ANY WARRANTY; without even the implied warranty of
    
    12
    +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
    
    13
    +# GNU General Public License for more details.
    
    14
    +#
    
    15
    +# You should have received a copy of the GNU General Public License
    
    16
    +# along with this program; If not, see <http://www.gnu.org/licenses/>.
    
    17
    +
    
    18
    +import logging
    
    19
    +
    
    20
    +from nbxmpp.protocol import NS_BOOKMARKS
    
    21
    +from nbxmpp.protocol import NS_PUBSUB_EVENT
    
    22
    +from nbxmpp.protocol import NS_PRIVATE
    
    23
    +from nbxmpp.protocol import isResultNode
    
    24
    +from nbxmpp.protocol import Node
    
    25
    +from nbxmpp.protocol import Iq
    
    26
    +from nbxmpp.structs import StanzaHandler
    
    27
    +from nbxmpp.structs import BookmarkData
    
    28
    +from nbxmpp.const import BookmarkStoreType
    
    29
    +from nbxmpp.util import from_xs_boolean
    
    30
    +from nbxmpp.util import to_xs_boolean
    
    31
    +from nbxmpp.util import validate_jid
    
    32
    +from nbxmpp.util import call_on_response
    
    33
    +from nbxmpp.util import callback
    
    34
    +from nbxmpp.modules.pubsub import get_pubsub_item
    
    35
    +from nbxmpp.modules.pubsub import get_pubsub_request
    
    36
    +from nbxmpp.modules.pubsub import get_bookmark_publish_options
    
    37
    +
    
    38
    +log = logging.getLogger('nbxmpp.m.bookmarks')
    
    39
    +
    
    40
    +
    
    41
    +class Bookmarks:
    
    42
    +    def __init__(self, client):
    
    43
    +        self._client = client
    
    44
    +        self.handlers = [
    
    45
    +            StanzaHandler(name='message',
    
    46
    +                          callback=self._process_pubsub_bookmarks,
    
    47
    +                          ns=NS_PUBSUB_EVENT,
    
    48
    +                          priority=16),
    
    49
    +        ]
    
    50
    +
    
    51
    +    def _process_pubsub_bookmarks(self, _con, _stanza, properties):
    
    52
    +        if not properties.is_pubsub_event:
    
    53
    +            return
    
    54
    +
    
    55
    +        if properties.pubsub_event.node != NS_BOOKMARKS:
    
    56
    +            return
    
    57
    +
    
    58
    +        item = properties.pubsub_event.item
    
    59
    +
    
    60
    +        bookmarks = []
    
    61
    +        storage_node = item.getTag('storage', namespace=NS_BOOKMARKS)
    
    62
    +        if storage_node.getChildren():
    
    63
    +            bookmarks = self._parse_bookmarks(storage_node)
    
    64
    +
    
    65
    +        pubsub_event = properties.pubsub_event._replace(data=bookmarks)
    
    66
    +        log.info('Received bookmarks from: %s', properties.jid)
    
    67
    +        for bookmark in bookmarks:
    
    68
    +            log.info(bookmark)
    
    69
    +
    
    70
    +        properties.pubsub_event = pubsub_event
    
    71
    +
    
    72
    +    @staticmethod
    
    73
    +    def _parse_bookmarks(storage):
    
    74
    +        bookmarks = []
    
    75
    +        confs = storage.getTags('conference')
    
    76
    +        for conf in confs:
    
    77
    +            autojoin = conf.getAttr('autojoin')
    
    78
    +            if autojoin is None:
    
    79
    +                autojoin = False
    
    80
    +            else:
    
    81
    +                try:
    
    82
    +                    autojoin = from_xs_boolean(autojoin)
    
    83
    +                except ValueError as error:
    
    84
    +                    log.warning(error)
    
    85
    +                    log.warning(storage)
    
    86
    +                    autojoin = False
    
    87
    +
    
    88
    +            try:
    
    89
    +                jid = validate_jid(conf.getAttr('jid'))
    
    90
    +            except Exception:
    
    91
    +                log.warning('Invalid JID: %s, ignoring it',
    
    92
    +                            conf.getAttr('jid'))
    
    93
    +                continue
    
    94
    +
    
    95
    +            bookmark = BookmarkData(
    
    96
    +                jid=jid,
    
    97
    +                name=conf.getAttr('name'),
    
    98
    +                autojoin=autojoin,
    
    99
    +                password=conf.getTagData('password'),
    
    100
    +                nick=conf.getTagData('nick'))
    
    101
    +            bookmarks.append(bookmark)
    
    102
    +
    
    103
    +        return bookmarks
    
    104
    +
    
    105
    +    @staticmethod
    
    106
    +    def get_private_request():
    
    107
    +        iq = Iq(typ='get')
    
    108
    +        query = iq.addChild(name='query', namespace=NS_PRIVATE)
    
    109
    +        query.addChild(name='storage', namespace=NS_BOOKMARKS)
    
    110
    +        return iq
    
    111
    +
    
    112
    +    @call_on_response('_bookmarks_received')
    
    113
    +    def request_bookmarks(self, type_):
    
    114
    +        jid = self._client.get_bound_jid().getBare()
    
    115
    +        if type_ == BookmarkStoreType.PUBSUB:
    
    116
    +            log.info('Request bookmarks (PubSub)')
    
    117
    +            request = get_pubsub_request(jid, NS_BOOKMARKS, max_items=1)
    
    118
    +            return request, {'type_': type_}
    
    119
    +        if type_ == BookmarkStoreType.PRIVATE:
    
    120
    +            log.info('Request bookmarks (Private Storage)')
    
    121
    +            return self.get_private_request(), {'type_': type_}
    
    122
    +
    
    123
    +    @callback
    
    124
    +    def _bookmarks_received(self, stanza, type_):
    
    125
    +        if not isResultNode(stanza):
    
    126
    +            log.info('Error: %s', stanza.getError())
    
    127
    +            return []
    
    128
    +
    
    129
    +        if type_ == BookmarkStoreType.PUBSUB:
    
    130
    +            item = get_pubsub_item(stanza)
    
    131
    +            bookmarks = []
    
    132
    +            storage_node = item.getTag('storage', namespace=NS_BOOKMARKS)
    
    133
    +
    
    134
    +        if type_ == BookmarkStoreType.PRIVATE:
    
    135
    +            query = stanza.getQuery()
    
    136
    +            storage_node = query.getTag('storage', namespace=NS_BOOKMARKS)
    
    137
    +
    
    138
    +        if storage_node.getChildren():
    
    139
    +            bookmarks = self._parse_bookmarks(storage_node)
    
    140
    +
    
    141
    +        from_ = stanza.getFrom()
    
    142
    +        if from_ is None:
    
    143
    +            from_ = self._client.get_bound_jid().getBare()
    
    144
    +        log.info('Received bookmarks from: %s', from_)
    
    145
    +        for bookmark in bookmarks:
    
    146
    +            log.info(bookmark)
    
    147
    +        return bookmarks
    
    148
    +
    
    149
    +    @staticmethod
    
    150
    +    def _build_storage_node(bookmarks):
    
    151
    +        storage_node = Node(tag='storage', attrs={'xmlns': NS_BOOKMARKS})
    
    152
    +        for bookmark in bookmarks:
    
    153
    +            conf_node = storage_node.addChild(name="conference")
    
    154
    +            conf_node.setAttr('jid', bookmark.jid)
    
    155
    +            conf_node.setAttr('autojoin', to_xs_boolean(bookmark.autojoin))
    
    156
    +            if bookmark.name:
    
    157
    +                conf_node.setAttr('name', bookmark.name)
    
    158
    +            if bookmark.nick:
    
    159
    +                conf_node.setTagData('nick', bookmark.nick)
    
    160
    +            if bookmark.password:
    
    161
    +                conf_node.setTagData('password', bookmark.password)
    
    162
    +        return storage_node
    
    163
    +
    
    164
    +    def store_bookmarks(self, bookmarks, type_):
    
    165
    +        if type_ == BookmarkStoreType.PUBSUB:
    
    166
    +            self._store_with_pubsub(bookmarks)
    
    167
    +        elif type_ == BookmarkStoreType.PRIVATE:
    
    168
    +            self._store_with_private(bookmarks)
    
    169
    +
    
    170
    +    def _store_with_pubsub(self, bookmarks):
    
    171
    +        log.info('Store Bookmarks (PubSub)')
    
    172
    +        jid = self._client.get_bound_jid().getBare()
    
    173
    +        item = self._build_storage_node(bookmarks)
    
    174
    +        options = get_bookmark_publish_options()
    
    175
    +        self._client.get_module('PubSub').publish(jid,
    
    176
    +                                                  NS_BOOKMARKS,
    
    177
    +                                                  item,
    
    178
    +                                                  id_='current',
    
    179
    +                                                  options=options)
    
    180
    +
    
    181
    +    @call_on_response('_on_private_store_result')
    
    182
    +    def _store_with_private(self, bookmarks):
    
    183
    +        log.info('Store Bookmarks (Private Storage)')
    
    184
    +        storage_node = self._build_storage_node(bookmarks)
    
    185
    +        return Iq('set', NS_PRIVATE, payload=storage_node)
    
    186
    +
    
    187
    +    @staticmethod
    
    188
    +    def _on_private_store_result(_con, stanza):
    
    189
    +        if not isResultNode(stanza):
    
    190
    +            log.info('Error: %s', stanza.getError())

  • nbxmpp/modules/pubsub.py
    ... ... @@ -19,6 +19,9 @@ import logging
    19 19
     
    
    20 20
     from nbxmpp.protocol import NS_PUBSUB
    
    21 21
     from nbxmpp.protocol import NS_PUBSUB_EVENT
    
    22
    +from nbxmpp.protocol import NS_PUBSUB_PUBLISH_OPTIONS
    
    23
    +from nbxmpp.protocol import NS_DATA
    
    24
    +from nbxmpp.protocol import Node
    
    22 25
     from nbxmpp.protocol import Iq
    
    23 26
     from nbxmpp.protocol import isResultNode
    
    24 27
     from nbxmpp.structs import StanzaHandler
    
    ... ... @@ -87,3 +90,21 @@ def get_pubsub_request(jid, node, id_=None, max_items=None):
    87 90
         if id_ is not None:
    
    88 91
             items.addChild('item', {'id': id_})
    
    89 92
         return query
    
    93
    +
    
    94
    +
    
    95
    +def get_pubsub_item(stanza):
    
    96
    +    pubsub_node = stanza.getTag('pubsub')
    
    97
    +    items_node = pubsub_node.getTag('items')
    
    98
    +    return items_node.getTag('item')
    
    99
    +
    
    100
    +
    
    101
    +def get_bookmark_publish_options():
    
    102
    +    # TODO: Make generic
    
    103
    +    options = Node(NS_DATA + ' x',
    
    104
    +                   attrs={'type': 'submit'})
    
    105
    +    field = options.addChild('field',
    
    106
    +                             attrs={'var': 'FORM_TYPE', 'type': 'hidden'})
    
    107
    +    field.setTagData('value', NS_PUBSUB_PUBLISH_OPTIONS)
    
    108
    +    field = options.addChild('field', attrs={'var': 'pubsub#access_model'})
    
    109
    +    field.setTagData('value', 'whitelist')
    
    110
    +    return options

  • nbxmpp/structs.py
    ... ... @@ -74,6 +74,9 @@ AvatarMetaData.__new__.__defaults__ = (None,) * len(AvatarMetaData._fields)
    74 74
     AvatarData = namedtuple('AvatarData', 'jid sha data')
    
    75 75
     AvatarData.__new__.__defaults__ = (None,) * len(AvatarData._fields)
    
    76 76
     
    
    77
    +BookmarkData = namedtuple('BookmarkData', 'jid name nick autojoin password')
    
    78
    +BookmarkData.__new__.__defaults__ = (None, None, None, None)
    
    79
    +
    
    77 80
     
    
    78 81
     class TuneData(namedtuple('TuneData', 'artist length rating source title track uri')):
    
    79 82
     
    

  • nbxmpp/util.py
    ... ... @@ -159,11 +159,35 @@ def callback(func):
    159 159
         @wraps(func)
    
    160 160
         def func_wrapper(self, _con, stanza, **kwargs):
    
    161 161
             cb = kwargs.pop('callback', None)
    
    162
    -        if cb is None:
    
    163
    -            return
    
    164 162
     
    
    165
    -        if cb() is not None:
    
    166
    -            result = func(self, stanza, **kwargs)
    
    163
    +        result = func(self, stanza, **kwargs)
    
    164
    +        if cb is not None and cb() is not None:
    
    167 165
                 cb()(result)
    
    168 166
     
    
    169 167
         return func_wrapper
    
    168
    +
    
    169
    +
    
    170
    +def from_xs_boolean(value):
    
    171
    +    if value in ('1', 'true', 'True'):
    
    172
    +        return True
    
    173
    +
    
    174
    +    if value in ('0', 'false', 'False', ''):
    
    175
    +        return False
    
    176
    +
    
    177
    +    raise ValueError('Cant convert %s to python boolean' % value)
    
    178
    +
    
    179
    +
    
    180
    +def to_xs_boolean(value):
    
    181
    +    # Convert to xs:boolean ('true', 'false')
    
    182
    +    # from a python boolean (True, False) or None
    
    183
    +    if value is True:
    
    184
    +        return 'true'
    
    185
    +
    
    186
    +    if value is False:
    
    187
    +        return 'false'
    
    188
    +
    
    189
    +    if value is None:
    
    190
    +        return 'false'
    
    191
    +
    
    192
    +    raise ValueError(
    
    193
    +        'Cant convert %s to xs:boolean' % value)