[Git][gajim/gajim][master] Move caps code into own module

Philipp Hörist gitlab at dev.gajim.org
Sun Jul 22 21:35:37 CEST 2018


Philipp Hörist pushed to branch master at gajim / gajim


Commits:
a943a35a by Philipp Hörist at 2018-07-22T20:49:53+02:00
Move caps code into own module

- - - - -


9 changed files:

- gajim/chat_control.py
- gajim/common/connection_handlers.py
- gajim/common/connection_handlers_events.py
- + gajim/common/modules/caps.py
- gajim/common/modules/discovery.py
- + gajim/common/modules/presence.py
- − gajim/common/protocol/caps.py
- gajim/groupchat_control.py
- test/unit/test_protocol_caps.py


Changes:

=====================================
gajim/chat_control.py
=====================================
--- a/gajim/chat_control.py
+++ b/gajim/chat_control.py
@@ -231,7 +231,7 @@ class ChatControl(ChatControlBase):
                 self._nec_update_avatar)
         app.ged.register_event_handler('chatstate-received', ged.GUI1,
             self._nec_chatstate_received)
-        app.ged.register_event_handler('caps-received', ged.GUI1,
+        app.ged.register_event_handler('caps-update', ged.GUI1,
             self._nec_caps_received)
         app.ged.register_event_handler('message-sent', ged.OUT_POSTCORE,
             self._message_sent)
@@ -1154,7 +1154,7 @@ class ChatControl(ChatControlBase):
                 self._nec_update_avatar)
         app.ged.remove_event_handler('chatstate-received', ged.GUI1,
             self._nec_chatstate_received)
-        app.ged.remove_event_handler('caps-received', ged.GUI1,
+        app.ged.remove_event_handler('caps-update', ged.GUI1,
             self._nec_caps_received)
         app.ged.remove_event_handler('message-sent', ged.OUT_POSTCORE,
             self._message_sent)


=====================================
gajim/common/connection_handlers.py
=====================================
--- a/gajim/common/connection_handlers.py
+++ b/gajim/common/connection_handlers.py
@@ -29,24 +29,18 @@
 ##
 
 import operator
-
 from time import time as time_time
 
-from gi.repository import GLib
-
 import nbxmpp
-from gajim.common import caps_cache as capscache
 
 from gajim.common import modules
 from gajim.common import helpers
 from gajim.common import app
 from gajim.common import jingle_xtls
 from gajim.common.caps_cache import muc_caps_cache
-from gajim.common.protocol.caps import ConnectionCaps
 from gajim.common.protocol.bytestream import ConnectionSocks5Bytestream
 from gajim.common.protocol.bytestream import ConnectionIBBytestream
 from gajim.common.connection_handlers_events import *
-from gajim.common.modules.misc import parse_eme
 
 from gajim.common import ged
 from gajim.common.nec import NetworkEvent
@@ -455,7 +449,7 @@ class ConnectionHandlersBase:
         return sess
 
 class ConnectionHandlers(ConnectionSocks5Bytestream, ConnectionDisco,
-                         ConnectionCaps, ConnectionHandlersBase,
+                         ConnectionHandlersBase,
                          ConnectionJingle, ConnectionIBBytestream):
     def __init__(self):
         ConnectionSocks5Bytestream.__init__(self)
@@ -464,9 +458,6 @@ class ConnectionHandlers(ConnectionSocks5Bytestream, ConnectionDisco,
         # Handle presences BEFORE caps
         app.nec.register_incoming_event(PresenceReceivedEvent)
 
-        ConnectionCaps.__init__(self, account=self.name,
-            capscache=capscache.capscache,
-            client_caps_factory=capscache.create_suitable_client_caps)
         ConnectionJingle.__init__(self)
         ConnectionHandlersBase.__init__(self)
 
@@ -502,7 +493,6 @@ class ConnectionHandlers(ConnectionSocks5Bytestream, ConnectionDisco,
 
     def cleanup(self):
         ConnectionHandlersBase.cleanup(self)
-        ConnectionCaps.cleanup(self)
         app.ged.remove_event_handler('roster-set-received',
             ged.CORE, self._nec_roster_set_received)
         app.ged.remove_event_handler('roster-received', ged.CORE,
@@ -681,14 +671,6 @@ class ConnectionHandlers(ConnectionSocks5Bytestream, ConnectionDisco,
         self.connection.SendAndCallForResponse(iq, self._on_bob_received,
             {'cid': cid})
 
-    def _presenceCB(self, con, prs):
-        """
-        Called when we receive a presence
-        """
-        log.debug('PresenceCB')
-        app.nec.push_incoming_event(NetworkEvent('raw-pres-received',
-            conn=self, stanza=prs))
-
     def _nec_subscribe_presence_received(self, obj):
         account = obj.conn.name
         if account != self.name:
@@ -918,7 +900,6 @@ class ConnectionHandlers(ConnectionSocks5Bytestream, ConnectionDisco,
     def _register_handlers(self, con, con_type):
         # try to find another way to register handlers in each class
         # that defines handlers
-        con.RegisterHandler('presence', self._presenceCB)
         con.RegisterHandler('iq', self._rosterSetCB, 'set', nbxmpp.NS_ROSTER)
         con.RegisterHandler('iq', self._siSetCB, 'set', nbxmpp.NS_SI)
         con.RegisterHandler('iq', self._siErrorCB, 'error', nbxmpp.NS_SI)


=====================================
gajim/common/connection_handlers_events.py
=====================================
--- a/gajim/common/connection_handlers_events.py
+++ b/gajim/common/connection_handlers_events.py
@@ -900,48 +900,6 @@ class ConnectionLostEvent(nec.NetworkIncomingEvent):
             show='offline'))
         return True
 
-class CapsPresenceReceivedEvent(nec.NetworkIncomingEvent, HelperEvent,
-PresenceHelperEvent):
-    name = 'caps-presence-received'
-    base_network_events = ['raw-pres-received']
-
-    def _extract_caps_from_presence(self):
-        caps_tag = self.stanza.getTag('c', namespace=nbxmpp.NS_CAPS)
-        if caps_tag:
-            self.hash_method = caps_tag['hash']
-            self.node = caps_tag['node']
-            self.caps_hash = caps_tag['ver']
-        else:
-            self.hash_method = self.node = self.caps_hash = None
-
-    def generate(self):
-        self.conn = self.base_event.conn
-        self.stanza = self.base_event.stanza
-        try:
-            self.get_jid_resource()
-        except Exception:
-            return
-        self._generate_ptype()
-        self._generate_show()
-        self._extract_caps_from_presence()
-        return True
-
-class CapsDiscoReceivedEvent(nec.NetworkIncomingEvent):
-    name = 'caps-disco-received'
-    base_network_events = []
-
-class CapsReceivedEvent(nec.NetworkIncomingEvent):
-    name = 'caps-received'
-    base_network_events = ['caps-presence-received', 'caps-disco-received']
-
-    def generate(self):
-        self.conn = self.base_event.conn
-        self.fjid = self.base_event.fjid
-        self.jid = self.base_event.jid
-        self.resource = self.base_event.resource
-        self.client_caps = self.base_event.client_caps
-        return True
-
 class GPGTrustKeyEvent(nec.NetworkIncomingEvent):
     name = 'gpg-trust-key'
     base_network_events = []


=====================================
gajim/common/modules/caps.py
=====================================
--- /dev/null
+++ b/gajim/common/modules/caps.py
@@ -0,0 +1,153 @@
+# Copyright (C) 2009 Stephan Erb <steve-e AT h3c.de>
+#
+# This file is part of Gajim.
+#
+# Gajim is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published
+# by the Free Software Foundation; version 3 only.
+#
+# Gajim is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with Gajim. If not, see <http://www.gnu.org/licenses/>.
+
+# XEP-0115: Entity Capabilities
+
+import logging
+
+import nbxmpp
+
+from gajim.common import caps_cache
+from gajim.common import app
+from gajim.common.nec import NetworkEvent
+from gajim.common.modules.presence import parse_show
+from gajim.common.modules.presence import parse_type
+
+log = logging.getLogger('gajim.c.m.caps')
+
+
+class Caps:
+    def __init__(self, con):
+        self._con = con
+        self._account = con.name
+
+        self.handlers = [
+            ('presence', self._presence_received, '', nbxmpp.NS_CAPS)
+        ]
+
+        self._capscache = caps_cache.capscache
+        self._create_suitable_client_caps = caps_cache.create_suitable_client_caps
+
+    def _presence_received(self, con, stanza):
+        hash_method = node = caps_hash = None
+
+        caps = stanza.getTag('c', namespace=nbxmpp.NS_CAPS)
+        if caps is not None:
+            hash_method = caps['hash']
+            node = caps['node']
+            caps_hash = caps['ver']
+
+        from_ = stanza.getFrom()
+        full_jid = str(from_)
+        show = parse_show(stanza)
+        type_ = parse_type(stanza)
+
+        log.info('Received from %s, type: %s, method: %s, node: %s, hash: %s',
+                 from_, stanza.getType(), hash_method, node, caps_hash)
+
+        client_caps = self._create_suitable_client_caps(
+            node, caps_hash, hash_method, full_jid)
+
+        # Type is None means 'available'
+        if stanza.getType() is None and client_caps._hash_method == 'no':
+            self._capscache.forget_caps(client_caps)
+            client_caps = self._create_suitable_client_caps(
+                node, caps_hash, hash_method)
+        else:
+            self._capscache.query_client_of_jid_if_unknown(
+                self._con, full_jid, client_caps)
+
+        self._update_client_caps_of_contact(from_, client_caps)
+
+        # Event is only used by ClientIcons Plugin
+        app.nec.push_incoming_event(NetworkEvent(
+                                    'caps-presence-received',
+                                    conn=self._con,
+                                    fjid=full_jid,
+                                    jid=from_.getStripped(),
+                                    resource=from_.getResource(),
+                                    hash_method=hash_method,
+                                    node=node,
+                                    caps_hash=caps_hash,
+                                    client_caps=client_caps,
+                                    show=show,
+                                    ptype=type_,
+                                    stanza=stanza))
+
+        app.nec.push_incoming_event(NetworkEvent('caps-update',
+                                                 conn=self._con,
+                                                 fjid=full_jid,
+                                                 jid=from_.getStripped()))
+
+    def _update_client_caps_of_contact(self, from_, client_caps):
+        contact = self._get_contact_or_gc_contact_for_jid(from_)
+        if contact is not None:
+            contact.client_caps = client_caps
+        else:
+            log.warning('Received Caps from unknown contact %s' % from_)
+
+    def _get_contact_or_gc_contact_for_jid(self, from_):
+        contact = app.contacts.get_contact_from_full_jid(self._account,
+                                                         str(from_))
+
+        if contact is None:
+            room_jid, resource = from_.getStripped(), from_.getResource()
+            contact = app.contacts.get_gc_contact(
+                self._account, room_jid, resource)
+        return contact
+
+    def contact_info_received(self, from_, identities, features, data, node):
+        """
+        callback to update our caps cache with queried information after
+        we have retrieved an unknown caps hash via a disco
+        """
+        bare_jid = from_.getStripped()
+
+        contact = self._get_contact_or_gc_contact_for_jid(from_)
+        if not contact:
+            log.info('Received Disco from unknown contact %s' % from_)
+            return
+
+        lookup = contact.client_caps.get_cache_lookup_strategy()
+        cache_item = lookup(self._capscache)
+
+        if cache_item.is_valid():
+            # we already know that the hash is fine and have already cached
+            # the identities and features
+            return
+        else:
+            validate = contact.client_caps.get_hash_validation_strategy()
+            hash_is_valid = validate(identities, features, data)
+
+            if hash_is_valid:
+                cache_item.set_and_store(identities, features)
+            else:
+                node = caps_hash = hash_method = None
+                contact.client_caps = self._create_suitable_client_caps(
+                    node, caps_hash, hash_method)
+                log.warning(
+                    'Computed and retrieved caps hash differ. Ignoring '
+                    'caps of contact %s' % contact.get_full_jid())
+
+            app.nec.push_incoming_event(
+                NetworkEvent('caps-update',
+                             conn=self._con,
+                             fjid=str(from_),
+                             jid=bare_jid))
+
+
+def get_instance(*args, **kwargs):
+    return Caps(*args, **kwargs), 'Caps'


=====================================
gajim/common/modules/discovery.py
=====================================
--- a/gajim/common/modules/discovery.py
+++ b/gajim/common/modules/discovery.py
@@ -39,7 +39,7 @@ class Discovery:
         ]
 
     def disco_contact(self, jid, node=None):
-        success_cb = self._con._nec_agent_info_received_caps
+        success_cb = self._con.get_module('Caps').contact_info_received
         self._disco(nbxmpp.NS_DISCO_INFO, jid, node, success_cb, None)
 
     def disco_items(self, jid, node=None, success_cb=None, error_cb=None):


=====================================
gajim/common/modules/presence.py
=====================================
--- /dev/null
+++ b/gajim/common/modules/presence.py
@@ -0,0 +1,70 @@
+# This file is part of Gajim.
+#
+# Gajim is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published
+# by the Free Software Foundation; version 3 only.
+#
+# Gajim is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with Gajim. If not, see <http://www.gnu.org/licenses/>.
+
+# Presence handler
+
+import logging
+
+from gajim.common import app
+from gajim.common.nec import NetworkEvent
+
+log = logging.getLogger('gajim.c.m.presence')
+
+
+class Presence:
+    def __init__(self, con):
+        self._con = con
+        self._account = con.name
+
+        self.handlers = [('presence', self._presence_received)]
+
+    def _presence_received(self, con, stanza):
+        log.info('Received from %s', stanza.getFrom())
+        app.nec.push_incoming_event(
+            NetworkEvent('raw-pres-received',
+                         conn=self._con,
+                         stanza=stanza))
+
+
+def parse_show(stanza):
+    show = stanza.getShow()
+    type_ = parse_type(stanza)
+    if show is None and type_ is None:
+        return 'online'
+
+    if type_ == 'unavailable':
+        return 'offline'
+
+    if show not in (None, 'chat', 'away', 'xa', 'dnd'):
+        log.warning('Invalid show element: %s', stanza)
+        if type_ is None:
+            return 'online'
+        return 'offline'
+
+    if show is None:
+        return 'online'
+    return show
+
+
+def parse_type(stanza):
+    type_ = stanza.getType()
+    if type_ not in (None, 'unavailable', 'error', 'subscribe',
+                     'subscribed', 'unsubscribe', 'unsubscribed'):
+        log.warning('Invalid type: %s', stanza)
+        return None
+    return type_
+
+
+def get_instance(*args, **kwargs):
+    return Presence(*args, **kwargs), 'Presence'


=====================================
gajim/common/protocol/caps.py deleted
=====================================
--- a/gajim/common/protocol/caps.py
+++ /dev/null
@@ -1,121 +0,0 @@
-# -*- coding:utf-8 -*-
-## src/common/protocol/caps.py
-##
-## Copyright (C) 2009 Stephan Erb <steve-e AT h3c.de>
-##
-## This file is part of Gajim.
-##
-## Gajim is free software; you can redistribute it and/or modify
-## it under the terms of the GNU General Public License as published
-## by the Free Software Foundation; version 3 only.
-##
-## Gajim is distributed in the hope that it will be useful,
-## but WITHOUT ANY WARRANTY; without even the implied warranty of
-## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-## GNU General Public License for more details.
-##
-## You should have received a copy of the GNU General Public License
-## along with Gajim. If not, see <http://www.gnu.org/licenses/>.
-##
-
-"""
-Module containing the network portion of XEP-115 (Entity Capabilities)
-"""
-
-import logging
-log = logging.getLogger('gajim.c.p.caps')
-
-from gajim.common import app
-from gajim.common import ged
-from gajim.common.connection_handlers_events import CapsPresenceReceivedEvent, \
-    CapsDiscoReceivedEvent, CapsReceivedEvent
-
-
-class ConnectionCaps(object):
-
-    def __init__(self, account, capscache, client_caps_factory):
-        self._account = account
-        self._capscache = capscache
-        self._create_suitable_client_caps = client_caps_factory
-        app.nec.register_incoming_event(CapsPresenceReceivedEvent)
-        app.nec.register_incoming_event(CapsReceivedEvent)
-        app.ged.register_event_handler('caps-presence-received', ged.GUI1,
-            self._nec_caps_presence_received)
-
-    def cleanup(self):
-        app.ged.remove_event_handler('caps-presence-received', ged.GUI1,
-            self._nec_caps_presence_received)
-
-    def caps_change_account_name(self, new_name):
-        self._account = new_name
-
-    def _nec_caps_presence_received(self, obj):
-        if obj.conn.name != self._account:
-            return
-        obj.client_caps = self._create_suitable_client_caps(obj.node,
-            obj.caps_hash, obj.hash_method, obj.fjid)
-        if obj.show == 'offline' and obj.client_caps._hash_method == 'no':
-            self._capscache.forget_caps(obj.client_caps)
-            obj.client_caps = self._create_suitable_client_caps(obj.node,
-                obj.caps_hash, obj.hash_method)
-        else:
-            self._capscache.query_client_of_jid_if_unknown(self, obj.fjid,
-                obj.client_caps)
-        self._update_client_caps_of_contact(obj)
-
-    def _update_client_caps_of_contact(self, obj):
-        contact = self._get_contact_or_gc_contact_for_jid(obj.fjid)
-        if contact:
-            contact.client_caps = obj.client_caps
-        else:
-            log.info('Received Caps from unknown contact %s' % obj.fjid)
-
-    def _get_contact_or_gc_contact_for_jid(self, jid):
-        contact = app.contacts.get_contact_from_full_jid(self._account, jid)
-        if contact is None:
-            room_jid, nick = app.get_room_and_nick_from_fjid(jid)
-            contact = app.contacts.get_gc_contact(self._account, room_jid, nick)
-        return contact
-
-    def _nec_agent_info_received_caps(self, from_, identities, features,
-                                      data, node):
-        """
-        callback to update our caps cache with queried information after
-        we have retrieved an unknown caps hash and issued a disco
-        """
-        fjid = str(from_)
-        bare_jid = from_.getStripped()
-        resource = from_.getResource()
-
-        contact = self._get_contact_or_gc_contact_for_jid(fjid)
-        if not contact:
-            log.info('Received Disco from unknown contact %s' % fjid)
-            return
-
-        lookup = contact.client_caps.get_cache_lookup_strategy()
-        cache_item = lookup(self._capscache)
-
-        if cache_item.is_valid():
-            # we already know that the hash is fine and have already cached
-            # the identities and features
-            return
-        else:
-            validate = contact.client_caps.get_hash_validation_strategy()
-            hash_is_valid = validate(identities, features, data)
-
-            if hash_is_valid:
-                cache_item.set_and_store(identities, features)
-            else:
-                node = caps_hash = hash_method = None
-                contact.client_caps = self._create_suitable_client_caps(
-                    node, caps_hash, hash_method)
-                log.info('Computed and retrieved caps hash differ.'
-                         'Ignoring caps of contact %s' % contact.get_full_jid())
-
-            app.nec.push_incoming_event(
-                CapsDiscoReceivedEvent(None,
-                                       conn=self,
-                                       fjid=fjid,
-                                       jid=bare_jid,
-                                       resource=resource,
-                                       client_caps=contact.client_caps))


=====================================
gajim/groupchat_control.py
=====================================
--- a/gajim/groupchat_control.py
+++ b/gajim/groupchat_control.py
@@ -169,7 +169,7 @@ class PrivateChatControl(ChatControl):
         self.TYPE_ID = 'pm'
         app.ged.register_event_handler('update-gc-avatar', ged.GUI1,
             self._nec_update_avatar)
-        app.ged.register_event_handler('caps-received', ged.GUI1,
+        app.ged.register_event_handler('caps-update', ged.GUI1,
             self._nec_caps_received_pm)
         app.ged.register_event_handler('gc-presence-received', ged.GUI1,
             self._nec_gc_presence_received)
@@ -181,7 +181,7 @@ class PrivateChatControl(ChatControl):
         super(PrivateChatControl, self).shutdown()
         app.ged.remove_event_handler('update-gc-avatar', ged.GUI1,
             self._nec_update_avatar)
-        app.ged.remove_event_handler('caps-received', ged.GUI1,
+        app.ged.remove_event_handler('caps-update', ged.GUI1,
             self._nec_caps_received_pm)
         app.ged.remove_event_handler('gc-presence-received', ged.GUI1,
             self._nec_gc_presence_received)


=====================================
test/unit/test_protocol_caps.py
=====================================
--- a/test/unit/test_protocol_caps.py
+++ b/test/unit/test_protocol_caps.py
@@ -1,7 +1,11 @@
 '''
 Tests for caps network coding
 '''
+
 import unittest
+from unittest.mock import MagicMock
+
+import nbxmpp
 
 import lib
 lib.setup_env()
@@ -10,64 +14,42 @@ from gajim.common import app
 from gajim.common import nec
 from gajim.common import ged
 from gajim.common import caps_cache
-from gajim.common.connection_handlers import ConnectionHandlers
-from gajim.common.protocol import caps
-from gajim.common.contacts import Contact
-from gajim.common.connection_handlers_events import CapsPresenceReceivedEvent
-
-from mock import Mock
-
-import nbxmpp
-
-class TestableConnectionCaps(ConnectionHandlers, caps.ConnectionCaps):
-
-    def __init__(self, *args, **kwargs):
-        self.name = 'account'
-        self._mocked_contacts = {}
-        caps.ConnectionCaps.__init__(self, *args, **kwargs)
-
-    def _get_contact_or_gc_contact_for_jid(self, jid):
-        """
-        Overwrite to decouple form contact handling
-        """
-        if jid not in self._mocked_contacts:
-            self._mocked_contacts[jid] = Mock(realClass=Contact)
-            self._mocked_contacts[jid].jid = jid
-        return self._mocked_contacts[jid]
-
-    def discoverInfo(self, *args, **kwargs):
-        pass
-
-    def get_mocked_contact_for_jid(self, jid):
-        return self._mocked_contacts[jid]
+from gajim.common.modules.caps import Caps
 
 
 class TestConnectionCaps(unittest.TestCase):
 
     def setUp(self):
+        app.contacts.add_account('account')
+        contact = app.contacts.create_contact(
+            'user at server.com', 'account', resource='a')
+        app.contacts.add_contact('account', contact)
+
         app.nec = nec.NetworkEventsController()
-        app.ged.register_event_handler('caps-presence-received', ged.GUI2,
+        app.ged.register_event_handler(
+            'caps-presence-received', ged.GUI2,
             self._nec_caps_presence_received)
 
+        self.module = Caps(MagicMock())
+        self.module._account = 'account'
+        self.module._capscache = MagicMock()
+
     def _nec_caps_presence_received(self, obj):
-        self.assertFalse(isinstance(obj.client_caps, caps_cache.NullClientCaps),
-            msg="On receive of proper caps, we must not use the fallback")
+        self.assertTrue(
+            isinstance(obj.client_caps, caps_cache.ClientCaps),
+            msg="On receive of valid caps, ClientCaps should be returned")
 
     def test_capsPresenceCB(self):
         fjid = "user at server.com/a"
 
-        connection_caps = TestableConnectionCaps("account", Mock(),
-            caps_cache.create_suitable_client_caps)
-
-        contact = connection_caps._get_contact_or_gc_contact_for_jid(fjid)
-
         xml = """<presence from='user at server.com/a' to='%s' id='123'>
             <c node='http://gajim.org' ver='pRCD6cgQ4SDqNMCjdhRV6TECx5o='
             hash='sha-1' xmlns='http://jabber.org/protocol/caps'/>
             </presence>
         """ % (fjid)
         msg = nbxmpp.protocol.Presence(node=nbxmpp.simplexml.XML2Node(xml))
-        connection_caps._presenceCB(None, msg)
+        self.module._presence_received(None, msg)
+
 
 if __name__ == '__main__':
     unittest.main()



View it on GitLab: https://dev.gajim.org/gajim/gajim/commit/a943a35a5da5577b47dcd929afc1d233b3e4c850

-- 
View it on GitLab: https://dev.gajim.org/gajim/gajim/commit/a943a35a5da5577b47dcd929afc1d233b3e4c850
You're receiving this email because of your account on dev.gajim.org.
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <http://lists.gajim.org/pipermail/commits/attachments/20180722/8e3100f4/attachment-0001.html>


More information about the Commits mailing list