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

Commits:

4 changed files:

Changes:

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

  • nbxmpp/modules/pubsub.py
    ... ... @@ -76,3 +76,14 @@ class PubSub:
    76 76
                 return CommonResult(jid=stanza.getFrom(),
    
    77 77
                                     error=stanza.getError())
    
    78 78
             return CommonResult(jid=stanza.getFrom())
    
    79
    +
    
    80
    +
    
    81
    +def get_pubsub_request(jid, node, id_=None, max_items=None):
    
    82
    +    query = Iq('get', to=jid)
    
    83
    +    pubsub = query.addChild('pubsub', namespace=NS_PUBSUB)
    
    84
    +    items = pubsub.addChild('items', {'node': node})
    
    85
    +    if max_items is not None:
    
    86
    +        items.setAttr('max_items', max_items)
    
    87
    +    if id_ is not None:
    
    88
    +        items.addChild('item', {'id': id_})
    
    89
    +    return query

  • nbxmpp/modules/user_avatar.py
    1
    +# Copyright (C) 2019 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
    +import base64
    
    20
    +
    
    21
    +from nbxmpp.protocol import NS_AVATAR_DATA
    
    22
    +from nbxmpp.protocol import NS_AVATAR_METADATA
    
    23
    +from nbxmpp.protocol import NS_PUBSUB_EVENT
    
    24
    +from nbxmpp.protocol import NodeProcessed
    
    25
    +from nbxmpp.protocol import isResultNode
    
    26
    +from nbxmpp.protocol import JID
    
    27
    +from nbxmpp.structs import StanzaHandler
    
    28
    +from nbxmpp.structs import AvatarMetaData
    
    29
    +from nbxmpp.structs import AvatarData
    
    30
    +from nbxmpp.util import call_on_response
    
    31
    +from nbxmpp.util import callback
    
    32
    +from nbxmpp.modules.pubsub import get_pubsub_request
    
    33
    +
    
    34
    +log = logging.getLogger('nbxmpp.m.user_avatar')
    
    35
    +
    
    36
    +
    
    37
    +class UserAvatar:
    
    38
    +    def __init__(self, client):
    
    39
    +        self._client = client
    
    40
    +        self.handlers = [
    
    41
    +            StanzaHandler(name='message',
    
    42
    +                          callback=self._process_pubsub_avatar,
    
    43
    +                          ns=NS_PUBSUB_EVENT,
    
    44
    +                          priority=16),
    
    45
    +        ]
    
    46
    +
    
    47
    +    def _process_pubsub_avatar(self, _con, stanza, properties):
    
    48
    +        if not properties.is_pubsub_event:
    
    49
    +            return
    
    50
    +
    
    51
    +        if properties.pubsub_event.node != NS_AVATAR_METADATA:
    
    52
    +            return
    
    53
    +
    
    54
    +        item = properties.pubsub_event.item
    
    55
    +
    
    56
    +        metadata = item.getTag('metadata', namespace=NS_AVATAR_METADATA)
    
    57
    +        if not metadata.getChildren():
    
    58
    +            pubsub_event = properties.pubsub_event._replace(empty=True)
    
    59
    +            log.info('Received avatar metadata: %s - no avatar set',
    
    60
    +                     properties.jid)
    
    61
    +        else:
    
    62
    +            info = metadata.getTags('info', one=True)
    
    63
    +            try:
    
    64
    +                data = AvatarMetaData(**info.getAttrs())
    
    65
    +            except Exception:
    
    66
    +                log.warning('Malformed user avatar data')
    
    67
    +                log.warning(stanza)
    
    68
    +                raise NodeProcessed
    
    69
    +
    
    70
    +            pubsub_event = properties.pubsub_event._replace(data=data)
    
    71
    +            log.info('Received avatar metadata: %s - %s', properties.jid, data)
    
    72
    +
    
    73
    +        properties.pubsub_event = pubsub_event
    
    74
    +
    
    75
    +    @call_on_response('_avatar_data_received')
    
    76
    +    def request_avatar(self, jid, id_):
    
    77
    +        return get_pubsub_request(jid, NS_AVATAR_DATA, id_=id_)
    
    78
    +
    
    79
    +    @callback
    
    80
    +    def _avatar_data_received(self, stanza):
    
    81
    +        jid = stanza.getFrom()
    
    82
    +        if jid is None:
    
    83
    +            jid = JID(self._client.get_bound_jid().getBare())
    
    84
    +
    
    85
    +        if not isResultNode(stanza):
    
    86
    +            log.info('Error: %s', stanza.getError())
    
    87
    +            raise NodeProcessed
    
    88
    +
    
    89
    +        pubsub_node = stanza.getTag('pubsub')
    
    90
    +        items_node = pubsub_node.getTag('items')
    
    91
    +        item = items_node.getTag('item')
    
    92
    +
    
    93
    +        sha = item.getAttr('id')
    
    94
    +        data_node = item.getTag('data', namespace=NS_AVATAR_DATA)
    
    95
    +        if data_node is None:
    
    96
    +            log.warning('No data node found')
    
    97
    +            log.warning(stanza)
    
    98
    +            raise NodeProcessed
    
    99
    +
    
    100
    +        data = data_node.getData()
    
    101
    +        if data is None:
    
    102
    +            log.warning('Data node empty')
    
    103
    +            log.warning(stanza)
    
    104
    +            raise NodeProcessed
    
    105
    +
    
    106
    +        data = base64.b64decode(data.encode('utf-8'))
    
    107
    +        log.info('Received avatar data: %s %s', jid, sha)
    
    108
    +        return AvatarData(jid, sha, data)

  • nbxmpp/structs.py
    ... ... @@ -68,6 +68,12 @@ ActivityData = namedtuple('ActivityData', 'activity subactivity text')
    68 68
     LocationData = namedtuple('LocationData', LOCATION_DATA)
    
    69 69
     LocationData.__new__.__defaults__ = (None,) * len(LocationData._fields)
    
    70 70
     
    
    71
    +AvatarMetaData = namedtuple('AvatarMetaData', 'bytes height width id type url')
    
    72
    +AvatarMetaData.__new__.__defaults__ = (None,) * len(AvatarMetaData._fields)
    
    73
    +
    
    74
    +AvatarData = namedtuple('AvatarData', 'jid sha data')
    
    75
    +AvatarData.__new__.__defaults__ = (None,) * len(AvatarData._fields)
    
    76
    +
    
    71 77
     
    
    72 78
     class TuneData(namedtuple('TuneData', 'artist length rating source title track uri')):
    
    73 79