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

Commits:

3 changed files:

Changes:

  • nbxmpp/modules/presence.py
    ... ... @@ -18,6 +18,8 @@
    18 18
     from nbxmpp.protocol import Error as ErrorStanza
    
    19 19
     from nbxmpp.protocol import ERR_BAD_REQUEST
    
    20 20
     from nbxmpp.protocol import NodeProcessed
    
    21
    +from nbxmpp.protocol import Presence
    
    22
    +from nbxmpp.namespaces import Namespace
    
    21 23
     from nbxmpp.structs import StanzaHandler
    
    22 24
     from nbxmpp.util import error_factory
    
    23 25
     from nbxmpp.const import PresenceType
    
    ... ... @@ -90,3 +92,62 @@ class BasePresence(BaseModule):
    90 92
                 self._log.warning('Presence with invalid show')
    
    91 93
                 self._log.warning(stanza)
    
    92 94
                 return PresenceShow.ONLINE
    
    95
    +
    
    96
    +    def unsubscribe(self, jid):
    
    97
    +        self.send(jid=jid, typ='unsubscribe')
    
    98
    +
    
    99
    +    def unsubscribed(self, jid):
    
    100
    +        self.send(jid=jid, typ='unsubscribed')
    
    101
    +
    
    102
    +    def subscribed(self, jid):
    
    103
    +        self.send(jid=jid, typ='subscribed')
    
    104
    +
    
    105
    +    def subscribe(self, jid, status=None, nick=None):
    
    106
    +        self.send(jid=jid, typ='subscribe', status=status, nick=nick)
    
    107
    +
    
    108
    +    def send(self,
    
    109
    +             jid=None,
    
    110
    +             typ=None,
    
    111
    +             priority=None,
    
    112
    +             show=None,
    
    113
    +             status=None,
    
    114
    +             nick=None,
    
    115
    +             caps=None,
    
    116
    +             idle_time=None,
    
    117
    +             signed=None,
    
    118
    +             muc=False,
    
    119
    +             muc_history=None,
    
    120
    +             muc_password=None,
    
    121
    +             extend=None):
    
    122
    +
    
    123
    +        if show is not None and show not in ('chat', 'away', 'xa', 'dnd'):
    
    124
    +            raise ValueError('Invalid show value: %s' % show)
    
    125
    +
    
    126
    +        presence = Presence(jid, typ, priority, show, status)
    
    127
    +        if nick is not None:
    
    128
    +            nick_tag = presence.setTag('nick', namespace=Namespace.NICK)
    
    129
    +            nick_tag.setData(nick)
    
    130
    +
    
    131
    +        if idle_time is not None:
    
    132
    +            idle_node = presence.setTag('idle', namespace=Namespace.IDLE)
    
    133
    +            idle_node.setAttr('since', idle_time)
    
    134
    +
    
    135
    +        if caps is not None and typ != 'unavailable':
    
    136
    +            presence.setTag('c', namespace=Namespace.CAPS, attrs=caps)
    
    137
    +
    
    138
    +        if signed is not None:
    
    139
    +            presence.setTag(Namespace.SIGNED + ' x').setData(signed)
    
    140
    +
    
    141
    +        if muc or muc_history is not None or muc_password is not None:
    
    142
    +            muc_x = presence.setTag(Namespace.MUC + ' x')
    
    143
    +            if muc_history is not None:
    
    144
    +                muc_x.setTag('history', muc_history)
    
    145
    +
    
    146
    +            if muc_password is not None:
    
    147
    +                muc_x.setTagData('password', muc_password)
    
    148
    +
    
    149
    +        if extend is not None:
    
    150
    +            for node in extend:
    
    151
    +                presence.addChild(node=node)
    
    152
    +
    
    153
    +        self._con.send_stanza(presence)

  • nbxmpp/modules/roster.py
    1
    +# Copyright (C) 2021 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
    +
    
    19
    +from nbxmpp.namespaces import Namespace
    
    20
    +from nbxmpp.simplexml import Node
    
    21
    +from nbxmpp.protocol import Iq
    
    22
    +from nbxmpp.protocol import JID
    
    23
    +from nbxmpp.protocol import NodeProcessed
    
    24
    +from nbxmpp.structs import RosterData
    
    25
    +from nbxmpp.structs import RosterItem
    
    26
    +from nbxmpp.structs import StanzaHandler
    
    27
    +from nbxmpp.errors import StanzaError
    
    28
    +from nbxmpp.task import iq_request_task
    
    29
    +from nbxmpp.modules.base import BaseModule
    
    30
    +from nbxmpp.modules.util import process_response
    
    31
    +
    
    32
    +
    
    33
    +class Roster(BaseModule):
    
    34
    +    def __init__(self, client):
    
    35
    +        BaseModule.__init__(self, client)
    
    36
    +
    
    37
    +        self._client = client
    
    38
    +        self.handlers = [
    
    39
    +            # StanzaHandler(name='iq',
    
    40
    +            #               callback=self._process_roster_push,
    
    41
    +            #               typ='set',
    
    42
    +            #               priority=15,
    
    43
    +            #               ns=Namespace.ROSTER),
    
    44
    +        ]
    
    45
    +
    
    46
    +    @iq_request_task
    
    47
    +    def request_roster(self, version=None):
    
    48
    +        _task = yield
    
    49
    +
    
    50
    +        self._log.info('Request Roster, version: %s', version)
    
    51
    +        response = yield _make_request(version)
    
    52
    +        if response.isError():
    
    53
    +            raise StanzaError(response)
    
    54
    +
    
    55
    +        yield _parse_push(response)
    
    56
    +
    
    57
    +    def _process_roster_push(self, _client, stanza, properties):
    
    58
    +        from_ = stanza.getFrom()
    
    59
    +        if from_ is not None:
    
    60
    +            if not self._con.get_bound_jid().bare == from_:
    
    61
    +                self._log.warning('Malicious Roster Push from %s', from_)
    
    62
    +                raise NodeProcessed
    
    63
    +
    
    64
    +        properties.roster = _parse_push(stanza)
    
    65
    +
    
    66
    +        self._log.info('Roster Push, version: %s', properties.roster.version)
    
    67
    +        for item in properties.roster.items:
    
    68
    +            self._log.info(item)
    
    69
    +
    
    70
    +        self._ack_roster_push(stanza)
    
    71
    +
    
    72
    +    def _ack_roster_push(self, stanza):
    
    73
    +        iq = Iq('result',
    
    74
    +                to=stanza.getFrom(),
    
    75
    +                frm=stanza.getTo(),
    
    76
    +                attrs={'id': stanza.getID()})
    
    77
    +        self._con.send_stanza(iq)
    
    78
    +
    
    79
    +    @iq_request_task
    
    80
    +    def delete_item(self, jid):
    
    81
    +        _task = yield
    
    82
    +
    
    83
    +        response = yield _make_delete(jid)
    
    84
    +        yield process_response(response)
    
    85
    +
    
    86
    +    @iq_request_task
    
    87
    +    def set_item(self, jid, name, groups=None):
    
    88
    +        _task = yield
    
    89
    +
    
    90
    +        response = yield _make_set(jid, name, groups)
    
    91
    +        yield process_response(response)
    
    92
    +
    
    93
    +
    
    94
    +def _make_delete(jid):
    
    95
    +    return Iq('set',
    
    96
    +              Namespace.ROSTER,
    
    97
    +              payload=[Node('item', {'jid': jid, 'subscription': 'remove'})])
    
    98
    +
    
    99
    +
    
    100
    +def _make_set(jid, name, groups=None):
    
    101
    +    if groups is None:
    
    102
    +        groups = []
    
    103
    +
    
    104
    +    infos = {'jid': jid}
    
    105
    +    if name:
    
    106
    +        infos['name'] = name
    
    107
    +    iq = Iq('set', Namespace.ROSTER)
    
    108
    +    query = iq.setQuery()
    
    109
    +    item = query.addChild('item', attrs=infos)
    
    110
    +    for group in groups:
    
    111
    +        item.addChild('group').setData(group)
    
    112
    +    return iq
    
    113
    +
    
    114
    +
    
    115
    +def _make_request(version):
    
    116
    +    iq = Iq('get', Namespace.ROSTER)
    
    117
    +    if version is not None:
    
    118
    +        iq.setTagAttr('query', 'ver', version)
    
    119
    +    return iq
    
    120
    +
    
    121
    +
    
    122
    +def _get_item_attrs(item):
    
    123
    +    default_attrs = {'name': None,
    
    124
    +                     'ask': None,
    
    125
    +                     'subscription': None,
    
    126
    +                     'groups': []}
    
    127
    +
    
    128
    +    attrs = item.getAttrs()
    
    129
    +    del attrs['jid']
    
    130
    +    groups = {group.getData() for group in item.getTags('group')}
    
    131
    +    attrs['groups'] = list(groups)
    
    132
    +
    
    133
    +    default_attrs.update(attrs)
    
    134
    +    return default_attrs
    
    135
    +
    
    136
    +
    
    137
    +def _parse_push(stanza):
    
    138
    +    query = stanza.getTag('query', namespace=Namespace.ROSTER)
    
    139
    +    version = query.getAttr('ver')
    
    140
    +    pushed_items = []
    
    141
    +
    
    142
    +    for item in query.getTags('item'):
    
    143
    +        jid = JID.from_string(item.getAttr('jid'))
    
    144
    +        attrs = _get_item_attrs(item)
    
    145
    +        pushed_items.append(RosterItem(attrs, jid))
    
    146
    +
    
    147
    +    return RosterData(pushed_items, version)

  • nbxmpp/structs.py
    ... ... @@ -126,6 +126,9 @@ MAMQueryData = namedtuple('MAMQueryData', 'jid rsm complete')
    126 126
     
    
    127 127
     MAMPreferencesData = namedtuple('MAMPreferencesData', 'default always never')
    
    128 128
     
    
    129
    +RosterData = namedtuple('RosterData', 'items version')
    
    130
    +RosterItem = namedtuple('RosterItem', 'data jid')
    
    131
    +
    
    129 132
     
    
    130 133
     class DiscoInfo(namedtuple('DiscoInfo', 'stanza identities features dataforms timestamp')):
    
    131 134
     
    
    ... ... @@ -833,6 +836,7 @@ class IqProperties:
    833 836
             self.http_auth = None
    
    834 837
             self.ibb = None
    
    835 838
             self.blocking = None
    
    839
    +        self.roster = None
    
    836 840
     
    
    837 841
         @property
    
    838 842
         def is_http_auth(self):
    
    ... ... @@ -846,6 +850,10 @@ class IqProperties:
    846 850
         def is_blocking(self):
    
    847 851
             return self.blocking is not None
    
    848 852
     
    
    853
    +    @property
    
    854
    +    def is_roster(self):
    
    855
    +        return self.roster is not None
    
    856
    +
    
    849 857
     
    
    850 858
     class PresenceProperties:
    
    851 859
         def __init__(self, own_jid):