From 488bcbd73936dbf9e9576e172980d7ec61d5a7c3 Mon Sep 17 00:00:00 2001
From: lovetox <philipp@hoerist.com>
Date: Sat, 18 Apr 2020 11:28:13 +0200
Subject: [PATCH] Rewrite CapsCache

- Simplify implementation
- Add cached entrys from the CapsCache to the DiscoInfo cache when
receiving presence. This allows us to use get_last_disco_info() for all
contacts (Group Chats and Roster Contacts)
- Remove old/broken tests
---
 gajim/application.py                     |   4 +-
 gajim/common/app.py                      |   3 +-
 gajim/common/caps_cache.py               | 335 -----------------------
 gajim/common/contacts.py                 |  33 ++-
 gajim/common/logger.py                   |  93 ++-----
 gajim/common/modules/caps.py             |  98 ++-----
 gajim/common/modules/discovery.py        |   4 -
 gajim/common/optparser.py                |   3 -
 gajim/common/structs.py                  |   3 -
 test/broken/no_gui/test_protocol_caps.py |  55 ----
 test/gtk/htmltextview.py                 |   2 -
 test/no_gui/unit/test_account.py         |  19 --
 test/no_gui/unit/test_caps_cache.py      | 149 ----------
 test/no_gui/unit/test_contacts.py        | 117 --------
 14 files changed, 79 insertions(+), 839 deletions(-)
 delete mode 100644 gajim/common/caps_cache.py
 delete mode 100644 test/broken/no_gui/test_protocol_caps.py
 delete mode 100644 test/no_gui/unit/test_account.py
 delete mode 100644 test/no_gui/unit/test_caps_cache.py
 delete mode 100644 test/no_gui/unit/test_contacts.py

diff --git a/gajim/application.py b/gajim/application.py
index 71e5b55082..7c719f703a 100644
--- a/gajim/application.py
+++ b/gajim/application.py
@@ -52,9 +52,9 @@
 from gajim.common import configpaths
 from gajim.common import logging_helpers
 from gajim.common import exceptions
-from gajim.common import caps_cache
 from gajim.common import logger
 from gajim.common.i18n import _
+from gajim.common.contacts import LegacyContactsAPI
 
 
 class GajimApplication(Gtk.Application):
@@ -199,7 +199,7 @@ def _startup(self, _application):
         configpaths.create_paths()
         try:
             app.logger = logger.Logger()
-            caps_cache.initialize(app.logger)
+            app.contacts = LegacyContactsAPI()
         except exceptions.DatabaseMalformed as error:
             dlg = Gtk.MessageDialog(
                 transient_for=None,
diff --git a/gajim/common/app.py b/gajim/common/app.py
index 2a45242823..ed8f145b73 100644
--- a/gajim/common/app.py
+++ b/gajim/common/app.py
@@ -46,7 +46,6 @@
 from gajim.common import ged as ged_module
 from gajim.common.i18n import LANG
 from gajim.common.const import Display
-from gajim.common.contacts import LegacyContactsAPI
 from gajim.common.events import Events
 from gajim.common.types import NetworkEventsControllerT  # pylint: disable=unused-import
 from gajim.common.types import InterfaceT  # pylint: disable=unused-import
@@ -79,7 +78,7 @@
 # {acct1: {jid1: time1, jid2: time2}, }
 last_message_time = {}  # type: Dict[str, Dict[str, float]]
 
-contacts = LegacyContactsAPI()
+contacts = None
 
 # tell if we are connected to the room or not
 # {acct: {room_jid: True}}
diff --git a/gajim/common/caps_cache.py b/gajim/common/caps_cache.py
deleted file mode 100644
index fc8530c003..0000000000
--- a/gajim/common/caps_cache.py
+++ /dev/null
@@ -1,335 +0,0 @@
-# Copyright (C) 2007 Tomasz Melcer <liori AT exroot.org>
-#                    Travis Shirk <travis AT pobox.com>
-# Copyright (C) 2007-2014 Yann Leboulanger <asterix AT lagaule.org>
-# Copyright (C) 2008 Brendan Taylor <whateley AT gmail.com>
-#                    Jonathan Schleifer <js-gajim AT webkeks.org>
-# Copyright (C) 2008-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 all XEP-115 (Entity Capabilities) related classes
-
-Basic Idea:
-CapsCache caches features to hash relationships. The cache is queried
-through ClientCaps objects which are hold by contact instances.
-"""
-
-import logging
-
-from nbxmpp import (NS_ESESSION, NS_CHATSTATES,
-    NS_JINGLE_ICE_UDP, NS_JINGLE_RTP_AUDIO, NS_JINGLE_RTP_VIDEO,
-    NS_JINGLE_FILE_TRANSFER_5)
-# Features where we cannot safely assume that the other side supports them
-FEATURE_BLACKLIST = [NS_CHATSTATES, NS_ESESSION,
-    NS_JINGLE_ICE_UDP, NS_JINGLE_RTP_AUDIO, NS_JINGLE_RTP_VIDEO,
-    NS_JINGLE_FILE_TRANSFER_5]
-
-log = logging.getLogger('gajim.c.caps_cache')
-
-# Query entry status codes
-NEW = 0
-QUERIED = 1
-CACHED = 2 # got the answer
-FAKED = 3 # allow NullClientCaps to behave as it has a cached item
-
-################################################################################
-### Public API of this module
-################################################################################
-
-capscache = None
-
-def initialize(logger):
-    """
-    Initialize this module
-    """
-    global capscache
-    capscache = CapsCache(logger)
-
-def client_supports(client_caps, requested_feature):
-    lookup_item = client_caps.get_cache_lookup_strategy()
-    cache_item = lookup_item(capscache)
-
-    supported_features = cache_item.features
-    if requested_feature in supported_features:
-        return True
-    if not supported_features and cache_item.status in (NEW, QUERIED, FAKED):
-        # assume feature is supported, if we don't know yet, what the client
-        # is capable of
-        return requested_feature not in FEATURE_BLACKLIST
-    return False
-
-def get_client_identity(client_caps):
-    lookup_item = client_caps.get_cache_lookup_strategy()
-    cache_item = lookup_item(capscache)
-
-    for identity in cache_item.identities:
-        if identity.category == 'client':
-            return identity.type
-
-def create_suitable_client_caps(node, caps_hash, hash_method, fjid=None):
-    """
-    Create and return a suitable ClientCaps object for the given node,
-    caps_hash, hash_method combination.
-    """
-    if not node or not caps_hash:
-        if fjid:
-            client_caps = NoClientCaps(fjid)
-        else:
-            # improper caps, ignore client capabilities.
-            client_caps = NullClientCaps()
-    elif not hash_method:
-        client_caps = OldClientCaps(caps_hash, node)
-    else:
-        client_caps = ClientCaps(caps_hash, node, hash_method)
-    return client_caps
-
-
-################################################################################
-### Internal classes of this module
-################################################################################
-
-class AbstractClientCaps:
-    """
-    Base class representing a client and its capabilities as advertised by a
-    caps tag in a presence
-    """
-    def __init__(self, caps_hash, node):
-        self._hash = caps_hash
-        self._node = node
-        self._hash_method = None
-
-    @property
-    def hash_method(self):
-        return self._hash_method
-
-    def get_discover_strategy(self):
-        return self._discover
-
-    def _discover(self, connection, jid):
-        """
-        To be implemented by subclasses
-        """
-        raise NotImplementedError
-
-    def get_cache_lookup_strategy(self):
-        return self._lookup_in_cache
-
-    def _lookup_in_cache(self, caps_cache):
-        """
-        To be implemented by subclasses
-        """
-        raise NotImplementedError
-
-
-class ClientCaps(AbstractClientCaps):
-    """
-    The current XEP-115 implementation
-    """
-    def __init__(self, caps_hash, node, hash_method):
-        AbstractClientCaps.__init__(self, caps_hash, node)
-        assert hash_method != 'old'
-        self._hash_method = hash_method
-
-    def _lookup_in_cache(self, caps_cache):
-        return caps_cache[(self._hash_method, self._hash)]
-
-    def _discover(self, connection, jid):
-        connection.get_module('Discovery').disco_contact(
-            jid, '%s#%s' % (self._node, self._hash))
-
-
-class OldClientCaps(AbstractClientCaps):
-    """
-    Old XEP-115 implementation. Kept around for background compatibility
-    """
-    def __init__(self, caps_hash, node):
-        AbstractClientCaps.__init__(self, caps_hash, node)
-        self._hash_method = 'old'
-
-    def _lookup_in_cache(self, caps_cache):
-        return caps_cache[('old', self._node + '#' + self._hash)]
-
-    def _discover(self, connection, jid):
-        connection.get_module('Discovery').disco_contact(jid)
-
-
-class NoClientCaps(AbstractClientCaps):
-    """
-    For clients that don't support XEP-0115
-    """
-    def __init__(self, fjid):
-        AbstractClientCaps.__init__(self, fjid, fjid)
-        self._hash_method = 'no'
-
-    def _lookup_in_cache(self, caps_cache):
-        return caps_cache[('no', self._node)]
-
-    def _discover(self, connection, jid):
-        connection.get_module('Discovery').disco_contact(jid)
-
-
-class NullClientCaps(AbstractClientCaps):
-    """
-    This is a NULL-Object to streamline caps handling if a client has not
-    advertised any caps or has advertised them in an improper way
-
-    Assumes (almost) everything is supported.
-    """
-    _instance = None
-    def __new__(cls, *args, **kwargs):
-        """
-        Make it a singleton.
-        """
-        if not cls._instance:
-            cls._instance = super(NullClientCaps, cls).__new__(
-                    cls, *args, **kwargs)
-        return cls._instance
-
-    def __init__(self):
-        AbstractClientCaps.__init__(self, None, None)
-        self._hash_method = 'dummy'
-
-    def _lookup_in_cache(self, caps_cache):
-        # lookup something which does not exist to get a new CacheItem created
-        cache_item = caps_cache[('dummy', '')]
-        # Mark the item as cached so that protocol/caps.py does not update it
-        cache_item.status = FAKED
-        return cache_item
-
-    def _discover(self, connection, jid):
-        pass
-
-
-class CapsCache:
-    """
-    This object keeps the mapping between caps data and real disco features they
-    represent, and provides simple way to query that info
-    """
-    def __init__(self, logger=None):
-        # our containers:
-        # __cache is a dictionary mapping: pair of hash method and hash maps
-        #   to CapsCacheItem object
-        # __CacheItem is a class that stores data about particular
-        #   client (hash method/hash pair)
-        self.__cache = {}
-
-        class CacheItem:
-            # __names is a string cache; every string long enough is given
-            #   another object, and we will have plenty of identical long
-            #   strings. therefore we can cache them
-            __names = {}
-
-            def __init__(self, hash_method, hash_, logger):
-                # cached into db
-                self.hash_method = hash_method
-                self.hash = hash_
-                self._features = []
-                self._identities = []
-                self._logger = logger
-
-                self.status = NEW
-                self._recently_seen = False
-
-            def _get_features(self):
-                return self._features
-
-            def _set_features(self, value):
-                self._features = []
-                for feature in value:
-                    self._features.append(self.__names.setdefault(feature, feature))
-
-            features = property(_get_features, _set_features)
-
-            def _get_identities(self):
-                return self._identities
-
-            def _set_identities(self, value):
-                self._identities = value
-
-            identities = property(_get_identities, _set_identities)
-
-            def set_and_store(self, info):
-                self.identities = info.identities
-                self.features = info.features
-                if self.hash_method != 'no':
-                    self._logger.add_caps_entry(self.hash_method, self.hash, info)
-                self.status = CACHED
-
-            def update_last_seen(self):
-                if not self._recently_seen:
-                    self._recently_seen = True
-                    if self.hash_method != 'no':
-                        self._logger.update_caps_time(self.hash_method,
-                            self.hash)
-
-            def is_valid(self):
-                """
-                Returns True if identities and features for this cache item
-                are known.
-                """
-                return self.status in (CACHED, FAKED)
-
-        self.__CacheItem = CacheItem
-        self.logger = logger
-
-    def initialize_from_db(self):
-        self._remove_outdated_caps()
-        data = self.logger.load_caps_data()
-        for key, item in data.items():
-            x = self[key]
-            x.identities = item.identities
-            x.features = item.features
-            x.status = CACHED
-
-    def _remove_outdated_caps(self):
-        """
-        Remove outdated values from the db
-        """
-        self.logger.clean_caps_table()
-
-    def __getitem__(self, caps):
-        if caps in self.__cache:
-            return self.__cache[caps]
-
-        hash_method, hash_ = caps
-
-        x = self.__CacheItem(hash_method, hash_, self.logger)
-        self.__cache[(hash_method, hash_)] = x
-        return x
-
-    def query_client_of_jid_if_unknown(self, connection, jid, client_caps):
-        """
-        Start a disco query to determine caps (node, ver, exts). Won't query if
-        the data is already in cache
-        """
-        lookup_cache_item = client_caps.get_cache_lookup_strategy()
-        q = lookup_cache_item(self)
-
-        if q.status == NEW:
-            # do query for bare node+hash pair
-            # this will create proper object
-            q.status = QUERIED
-            discover = client_caps.get_discover_strategy()
-            discover(connection, jid)
-        else:
-            q.update_last_seen()
-
-    def forget_caps(self, client_caps):
-        hash_method = client_caps._hash_method
-        hash_ = client_caps._hash
-        key = (hash_method, hash_)
-        if key in self.__cache:
-            del self.__cache[key]
diff --git a/gajim/common/contacts.py b/gajim/common/contacts.py
index c628b1f5d6..6f2ae7df15 100644
--- a/gajim/common/contacts.py
+++ b/gajim/common/contacts.py
@@ -25,7 +25,7 @@
 # along with Gajim. If not, see <http://www.gnu.org/licenses/>.
 
 try:
-    from gajim.common import caps_cache
+    from gajim.common import app
     from gajim.common.i18n import _
     from gajim.common.account import Account
     from gajim import common
@@ -47,7 +47,7 @@ def __init__(self, jid, account, resource):
 class CommonContact(XMPPEntity):
 
     def __init__(self, jid, account, resource, show, presence, status, name,
-                 chatstate, client_caps=None):
+                 chatstate):
 
         XMPPEntity.__init__(self, jid, account, resource)
 
@@ -56,8 +56,6 @@ def __init__(self, jid, account, resource, show, presence, status, name,
         self.status = status
         self.name = name
 
-        self.client_caps = client_caps or caps_cache.NullClientCaps()
-
         # this is contact's chatstate
         self._chatstate = chatstate
 
@@ -129,11 +127,20 @@ def supports(self, requested_feature):
             # show, so we can be sure it's existant. Otherwise, we still
             # return caps for a contact that has no resources left.
             return False
-        return caps_cache.client_supports(self.client_caps, requested_feature)
+
+        disco_info = app.logger.get_last_disco_info(self.get_full_jid())
+        if disco_info is None:
+            return False
+
+        return disco_info.supports(requested_feature)
 
     @property
     def uses_phone(self):
-        return caps_cache.get_client_identity(self.client_caps) == 'phone'
+        disco_info = app.logger.get_last_disco_info(self.get_full_jid())
+        if disco_info is None:
+            return False
+
+        return disco_info.has_category('phone')
 
 
 class Contact(CommonContact):
@@ -141,7 +148,7 @@ class Contact(CommonContact):
     Information concerning a contact
     """
     def __init__(self, jid, account, name='', groups=None, show='', status='',
-    sub='', ask='', resource='', priority=0, client_caps=None,
+    sub='', ask='', resource='', priority=0,
     chatstate=None, idle_time=None, avatar_sha=None, groupchat=False,
     is_pm_contact=False):
         if not isinstance(jid, str):
@@ -150,8 +157,7 @@ def __init__(self, jid, account, name='', groups=None, show='', status='',
             groups = []
 
         CommonContact.__init__(self, jid, account, resource, show,
-                               None, status, name,
-                               chatstate, client_caps=client_caps)
+                               None, status, name, chatstate)
 
         self.contact_name = '' # nick choosen by contact
         self.groups = [i if i else _('General') for i in set(groups)] # filter duplicate values
@@ -182,7 +188,6 @@ def get_shown_name(self):
         return self.jid.split('@')[0]
 
     def _get_groupchat_name(self):
-        from gajim.common import app
         from gajim.common.helpers import get_groupchat_name
         con = app.connections[self.account.name]
         return get_groupchat_name(con, self.jid)
@@ -229,7 +234,6 @@ def is_groupchat(self):
 
     @property
     def is_connected(self):
-        from gajim.common import app
         try:
             return app.gc_connected[self.account.name][self.jid]
         except Exception:
@@ -272,7 +276,7 @@ def as_contact(self):
         """
         return Contact(jid=self.get_full_jid(), account=self.account,
             name=self.name, groups=[], show=self.show, status=self.status,
-            sub='none', client_caps=self.client_caps, avatar_sha=self.avatar_sha,
+            sub='none', avatar_sha=self.avatar_sha,
             is_pm_contact=True)
 
 
@@ -312,7 +316,7 @@ def remove_account(self, account):
 
     def create_contact(self, jid, account, name='', groups=None, show='',
     status='', sub='', ask='', resource='', priority=0,
-    client_caps=None, chatstate=None, idle_time=None,
+    chatstate=None, idle_time=None,
     avatar_sha=None, groupchat=False):
         if groups is None:
             groups = []
@@ -320,7 +324,7 @@ def create_contact(self, jid, account, name='', groups=None, show='',
         account = self._accounts.get(account, account)
         return Contact(jid=jid, account=account, name=name, groups=groups,
             show=show, status=status, sub=sub, ask=ask, resource=resource,
-            priority=priority, client_caps=client_caps,
+            priority=priority,
             chatstate=chatstate, idle_time=idle_time, avatar_sha=avatar_sha,
             groupchat=groupchat)
 
@@ -349,7 +353,6 @@ def copy_contact(self, contact):
             name=contact.name, groups=contact.groups, show=contact.show,
             status=contact.status, sub=contact.sub, ask=contact.ask,
             resource=contact.resource, priority=contact.priority,
-            client_caps=contact.client_caps,
             chatstate=contact.chatstate_enum,
             idle_time=contact.idle_time, avatar_sha=contact.avatar_sha)
 
diff --git a/gajim/common/logger.py b/gajim/common/logger.py
index 92eb7f74ce..232d41a0c2 100644
--- a/gajim/common/logger.py
+++ b/gajim/common/logger.py
@@ -38,11 +38,9 @@
 
 from gi.repository import GLib
 
-from nbxmpp.protocol import Node
 from nbxmpp.protocol import Iq
 from nbxmpp.structs import DiscoInfo
 from nbxmpp.structs import CommonError
-from nbxmpp.modules.dataforms import extend_form
 from nbxmpp.modules.discovery import parse_disco_info
 
 from gajim.common import exceptions
@@ -53,8 +51,6 @@
 from gajim.common.const import (
     JIDConstant, KindConstant, ShowConstant, TypeConstant,
     SubscriptionConstant)
-from gajim.common.structs import CapsData
-from gajim.common.structs import CapsIdentity
 
 
 LOGS_SQL_STATEMENT = '''
@@ -135,55 +131,12 @@
             jid TEXT PRIMARY KEY UNIQUE,
             avatar_sha TEXT
     );
-    PRAGMA user_version=4;
+    PRAGMA user_version=5;
     '''
 
 log = logging.getLogger('gajim.c.logger')
 
 
-
-class CapsEncoder(json.JSONEncoder):
-    def encode(self, obj):
-        if isinstance(obj, DiscoInfo):
-            identities = []
-            for identity in obj.identities:
-                identities.append(
-                    {'category': identity.category,
-                     'type': identity.type,
-                     'name': identity.name,
-                     'lang': identity.lang})
-
-            dataforms = []
-            for dataform in obj.dataforms:
-                # Filter out invalid forms according to XEP-0115
-                form_type = dataform.vars.get('FORM_TYPE')
-                if form_type is None or form_type.type_ != 'hidden':
-                    continue
-                dataforms.append(str(dataform))
-
-            obj = {'identities': identities,
-                   'features': obj.features,
-                   'dataforms': dataforms}
-        return json.JSONEncoder.encode(self, obj)
-
-
-def caps_decoder(dict_):
-    if 'identities' not in dict_:
-        return dict_
-
-    identities = []
-    for identity in dict_['identities']:
-        identities.append(CapsIdentity(**identity))
-
-    features = dict_['features']
-
-    dataforms = []
-    for dataform in dict_['dataforms']:
-        dataforms.append(extend_form(node=Node(node=dataform)))
-    return CapsData(identities=identities,
-                    features=features,
-                    dataforms=dataforms)
-
 def timeit(func):
     def func_wrapper(self, *args, **kwargs):
         start = time.time()
@@ -228,6 +181,7 @@ def __init__(self):
         self._log_db_path = configpaths.get('LOG_DB')
         self._cache_db_path = configpaths.get('CACHE_DB')
 
+        self._entity_caps_cache = {}
         self._disco_info_cache = {}
         self._muc_avatar_sha_cache = {}
 
@@ -237,6 +191,8 @@ def __init__(self):
         self._get_jid_ids_from_db()
         self._fill_disco_info_cache()
         self._fill_muc_avatar_sha_cache()
+        self._clean_caps_table()
+        self._load_caps_data()
 
     def _create_databases(self):
         if os.path.isdir(self._log_db_path):
@@ -388,6 +344,13 @@ def _migrate_cache(self, con):
                 ]
             self._execute_multiple(con, statements)
 
+        if self._get_user_version(con) < 5:
+            statements = [
+                'DELETE FROM caps_cache',
+                'PRAGMA user_version=5'
+                ]
+            self._execute_multiple(con, statements)
+
     @staticmethod
     def _execute_multiple(con, statements):
         """
@@ -1083,32 +1046,32 @@ def get_transports_type(self):
         return answer
 
     @timeit
-    def load_caps_data(self):
+    def _load_caps_data(self):
         '''
         Load caps cache data
         '''
         rows = self._con.execute(
-            'SELECT hash_method, hash, data FROM caps_cache')
+            'SELECT hash_method, hash, data as "data [disco_info]" '
+            'FROM caps_cache')
 
-        cache = {}
         for row in rows:
-            try:
-                data = json.loads(row.data, object_hook=caps_decoder)
-            except Exception:
-                log.exception('')
-                continue
-            cache[(row.hash_method, row.hash)] = data
-        return cache
+            self._entity_caps_cache[(row.hash_method, row.hash)] = row.data
 
     @timeit
-    def add_caps_entry(self, hash_method, hash_, caps_data):
-        serialized = json.dumps(caps_data, cls=CapsEncoder)
+    def add_caps_entry(self, jid, hash_method, hash_, caps_data):
+        self._entity_caps_cache[(hash_method, hash_)] = caps_data
+
+        self._disco_info_cache[jid] = caps_data
+
         self._con.execute('''
                 INSERT INTO caps_cache (hash_method, hash, data, last_seen)
                 VALUES (?, ?, ?, ?)
-                ''', (hash_method, hash_, serialized, int(time.time())))
+                ''', (hash_method, hash_, caps_data, int(time.time())))
         self._timeout_commit()
 
+    def get_caps_entry(self, hash_method, hash_):
+        return self._entity_caps_cache.get((hash_method, hash_))
+
     @timeit
     def update_caps_time(self, method, hash_):
         sql = '''UPDATE caps_cache SET last_seen = ?
@@ -1117,7 +1080,7 @@ def update_caps_time(self, method, hash_):
         self._timeout_commit()
 
     @timeit
-    def clean_caps_table(self):
+    def _clean_caps_table(self):
         """
         Remove caps which was not seen for 3 months
         """
@@ -1689,7 +1652,7 @@ def get_last_disco_info(self, jid, max_age=0):
         return disco_info
 
     @timeit
-    def set_last_disco_info(self, jid, disco_info):
+    def set_last_disco_info(self, jid, disco_info, cache_only=False):
         """
         Get last disco info from jid
 
@@ -1701,6 +1664,10 @@ def set_last_disco_info(self, jid, disco_info):
 
         log.info('Save disco info from %s', jid)
 
+        if cache_only:
+            self._disco_info_cache[jid] = disco_info
+            return
+
         disco_exists = self.get_last_disco_info(jid) is not None
         if disco_exists:
             sql = '''UPDATE last_seen_disco_info SET
diff --git a/gajim/common/modules/caps.py b/gajim/common/modules/caps.py
index 6faf63cee5..51834c47c4 100644
--- a/gajim/common/modules/caps.py
+++ b/gajim/common/modules/caps.py
@@ -23,7 +23,6 @@
 from nbxmpp.util import is_error_result
 from nbxmpp.util import compute_caps_hash
 
-from gajim.common import caps_cache
 from gajim.common import app
 from gajim.common.const import COMMON_FEATURES
 from gajim.common.helpers import get_optional_features
@@ -49,10 +48,6 @@ def __init__(self, con):
                           priority=51),
         ]
 
-        self._capscache = caps_cache.capscache
-        self._create_suitable_client_caps = \
-            caps_cache.create_suitable_client_caps
-
         self._identities = [
             DiscoIdentity(category='client', type='pc', name='Gajim')
         ]
@@ -74,83 +69,46 @@ def _entity_caps(self, _con, _stanza, properties):
             'Received from %s, type: %s, method: %s, node: %s, hash: %s',
             jid, properties.type, hash_method, node, caps_hash)
 
-        client_caps = self._create_suitable_client_caps(
-            node, caps_hash, hash_method, jid)
-
-        # Type is None means 'available'
-        if properties.type.is_available 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, jid, client_caps)
-
-        self._update_client_caps_of_contact(properties.jid, client_caps)
-
-        app.nec.push_incoming_event(
-            NetworkEvent('caps-update',
-                         account=self._account,
-                         fjid=jid,
-                         jid=properties.jid.getBare()))
+        disco_info = app.logger.get_caps_entry(hash_method, caps_hash)
+        if disco_info is None:
+            self._con.get_module('Discovery').disco_info(
+                jid,
+                '%s#%s' % (node, caps_hash),
+                callback=self._on_disco_info,
+                user_data=hash_method)
 
-    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:
-            self._log.info('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, info):
-        """
-        callback to update our caps cache with queried information after
-        we have retrieved an unknown caps hash via a disco
-        """
-
-        if is_error_result(info):
-            self._log.info(info)
+            app.logger.set_last_disco_info(jid, disco_info, cache_only=True)
+            app.nec.push_incoming_event(
+                NetworkEvent('caps-update',
+                             account=self._account,
+                             fjid=jid,
+                             jid=properties.jid.getBare()))
+
+    def _on_disco_info(self, disco_info, hash_method):
+        if is_error_result(disco_info):
+            self._log.info(disco_info)
             return
 
-        bare_jid = info.jid.getBare()
-
-        contact = self._get_contact_or_gc_contact_for_jid(info.jid)
-        if not contact:
-            self._log.info('Received Disco from unknown contact %s', info.jid)
-            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
+        bare_jid = disco_info.jid.getBare()
 
         try:
-            compute_caps_hash(info)
+            compute_caps_hash(disco_info)
         except Exception as error:
             self._log.warning('Disco info malformed: %s %s',
-                              contact.get_full_jid(), error)
-            node = caps_hash = hash_method = None
-            contact.client_caps = self._create_suitable_client_caps(
-                node, caps_hash, hash_method)
-        else:
-            cache_item.set_and_store(info)
+                              disco_info.jid, error)
+            return
+
+        app.logger.add_caps_entry(
+            str(disco_info.jid),
+            hash_method,
+            disco_info.get_caps_hash(),
+            disco_info)
 
         app.nec.push_incoming_event(
             NetworkEvent('caps-update',
                          account=self._account,
-                         fjid=str(info.jid),
+                         fjid=str(disco_info.jid),
                          jid=bare_jid))
 
     def update_caps(self):
diff --git a/gajim/common/modules/discovery.py b/gajim/common/modules/discovery.py
index 7dbf8feafa..b443bcc2d3 100644
--- a/gajim/common/modules/discovery.py
+++ b/gajim/common/modules/discovery.py
@@ -57,10 +57,6 @@ def account_info(self):
     def server_info(self):
         return self._server_info
 
-    def disco_contact(self, jid, node=None):
-        success_cb = self._con.get_module('Caps').contact_info_received
-        self.disco_info(jid, node, callback=success_cb)
-
     def discover_server_items(self):
         server = self._con.get_own_jid().getDomain()
         self.disco_items(server, callback=self._server_items_received)
diff --git a/gajim/common/optparser.py b/gajim/common/optparser.py
index 4f4d79ede4..cf22fb686a 100644
--- a/gajim/common/optparser.py
+++ b/gajim/common/optparser.py
@@ -30,7 +30,6 @@
 from pathlib import Path
 
 from gajim.common import app
-from gajim.common import caps_cache
 from gajim.common.i18n import _
 
 
@@ -153,8 +152,6 @@ def update_config(self, old_version, new_version):
 
         app.config.set('version', new_version)
 
-        caps_cache.capscache.initialize_from_db()
-
     @staticmethod
     def update_ft_proxies(to_remove=None, to_add=None):
         if to_remove is None:
diff --git a/gajim/common/structs.py b/gajim/common/structs.py
index c92a4cb7e4..651969701a 100644
--- a/gajim/common/structs.py
+++ b/gajim/common/structs.py
@@ -23,9 +23,6 @@
 URI = namedtuple('URI', 'type action data')
 URI.__new__.__defaults__ = (None, None)  # type: ignore
 
-CapsData = namedtuple('CapsData', 'identities features dataforms')
-CapsIdentity = namedtuple('CapsIdentity', 'category type name lang')
-
 
 class MUCData:
     def __init__(self, room_jid, nick, password, config=None):
diff --git a/test/broken/no_gui/test_protocol_caps.py b/test/broken/no_gui/test_protocol_caps.py
deleted file mode 100644
index eef096a43d..0000000000
--- a/test/broken/no_gui/test_protocol_caps.py
+++ /dev/null
@@ -1,55 +0,0 @@
-'''
-Tests for caps network coding
-'''
-
-import unittest
-from unittest.mock import MagicMock
-
-import nbxmpp
-
-from gajim.common import app
-from gajim.common import nec
-from gajim.common import ged
-from gajim.common import caps_cache
-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@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,
-            self._nec_caps_presence_received)
-
-        self.module = Caps(MagicMock())
-        self.module._account = 'account'
-        self.module._capscache = MagicMock()
-
-    def tearDown(self):
-        app.contacts.remove_account('account')
-
-    def _nec_caps_presence_received(self, obj):
-        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@server.com/a"
-
-        xml = """<presence from='user@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))
-        self.module._presence_received(None, msg)
-
-
-if __name__ == '__main__':
-    unittest.main()
diff --git a/test/gtk/htmltextview.py b/test/gtk/htmltextview.py
index a106925429..cdc28fa516 100644
--- a/test/gtk/htmltextview.py
+++ b/test/gtk/htmltextview.py
@@ -7,13 +7,11 @@
 from gajim.common import app
 from gajim.common import configpaths
 configpaths.init()
-from gajim.common import caps_cache
 from gajim.common.helpers import AdditionalDataDict
 
 from gajim.conversation_textview import ConversationTextview
 from gajim.gui_interface import Interface
 
-caps_cache.capscache = MagicMock()
 app.plugin_manager = MagicMock()
 app.logger = MagicMock()
 app.cert_store = MagicMock()
diff --git a/test/no_gui/unit/test_account.py b/test/no_gui/unit/test_account.py
deleted file mode 100644
index 75d94542e6..0000000000
--- a/test/no_gui/unit/test_account.py
+++ /dev/null
@@ -1,19 +0,0 @@
-'''
-Tests for Account classes
-'''
-import unittest
-
-from gajim.common.account import Account
-
-
-class Test(unittest.TestCase):
-
-    def testInstantiate(self):
-        account = Account(name='MyAcc', contacts=None, gc_contacts=None)
-
-        self.assertEqual('MyAcc', account.name)
-        self.assertTrue(account.gc_contacts is None)
-        self.assertTrue(account.contacts is None)
-
-if __name__ == "__main__":
-    unittest.main()
diff --git a/test/no_gui/unit/test_caps_cache.py b/test/no_gui/unit/test_caps_cache.py
deleted file mode 100644
index bf086761af..0000000000
--- a/test/no_gui/unit/test_caps_cache.py
+++ /dev/null
@@ -1,149 +0,0 @@
-'''
-Tests for capabilities and the capabilities cache
-'''
-import unittest
-from unittest.mock import MagicMock, Mock
-
-from nbxmpp import NS_MUC, NS_PING, NS_XHTML_IM, NS_JINGLE_FILE_TRANSFER_5
-from nbxmpp.structs import DiscoIdentity
-from nbxmpp.structs import DiscoInfo
-from gajim.common import caps_cache as caps
-from gajim.common.structs import CapsData
-
-
-class CommonCapsTest(unittest.TestCase):
-
-    def setUp(self):
-        self.caps_method = 'sha-1'
-        self.caps_hash = 'm3P2WeXPMGVH2tZPe7yITnfY0Dw='
-        self.client_caps = (self.caps_method, self.caps_hash)
-
-        self.node = "http://gajim.org"
-        self.identity = DiscoIdentity(category='client',
-                                      type='pc',
-                                      name='Gajim')
-
-        self.identities = [self.identity]
-        self.features = [NS_MUC, NS_XHTML_IM, NS_JINGLE_FILE_TRANSFER_5]
-
-        # Simulate a filled db
-        db_caps_cache = {
-            (self.caps_method, self.caps_hash): CapsData(self.identities, self.features, []),
-            ('old', self.node + '#' + self.caps_hash): CapsData(self.identities, self.features, [])
-        }
-
-        self.logger = Mock()
-        self.logger.load_caps_data = Mock(return_value=db_caps_cache)
-
-        self.cc = caps.CapsCache(self.logger)
-        caps.capscache = self.cc
-
-
-class TestCapsCache(CommonCapsTest):
-
-    def test_set_retrieve(self):
-        ''' Test basic set / retrieve cycle '''
-
-        self.cc[self.client_caps].identities = self.identities
-        self.cc[self.client_caps].features = self.features
-
-        self.assertTrue(NS_MUC in self.cc[self.client_caps].features)
-        self.assertTrue(NS_PING not in self.cc[self.client_caps].features)
-
-        identities = self.cc[self.client_caps].identities
-
-        self.assertEqual(1, len(identities))
-
-        identity = identities[0]
-        self.assertEqual('client', identity.category)
-        self.assertEqual('pc', identity.type)
-
-    def test_set_and_store(self):
-        ''' Test client_caps update gets logged into db '''
-
-        disco_info = DiscoInfo(None, self.identities, self.features, [])
-
-        item = self.cc[self.client_caps]
-        item.set_and_store(disco_info)
-
-        self.logger.add_caps_entry.assert_called_once_with(self.caps_method,
-                                                           self.caps_hash,
-                                                           disco_info)
-
-    def test_initialize_from_db(self):
-        ''' Read cashed dummy data from db '''
-        self.assertEqual(self.cc[self.client_caps].status, caps.NEW)
-        self.cc.initialize_from_db()
-        self.assertEqual(self.cc[self.client_caps].status, caps.CACHED)
-
-    def test_preload_triggering_query(self):
-        ''' Make sure that preload issues a disco '''
-        connection = MagicMock()
-        client_caps = caps.ClientCaps(self.caps_hash, self.node, self.caps_method)
-
-        self.cc.query_client_of_jid_if_unknown(
-            connection, "test@gajim.org", client_caps)
-
-        self.assertEqual(1, connection.get_module('Discovery').disco_contact.call_count)
-
-    def test_no_preload_query_if_cashed(self):
-        ''' Preload must not send a query if the data is already cached '''
-        connection = MagicMock()
-        client_caps = caps.ClientCaps(self.caps_hash, self.node, self.caps_method)
-
-        self.cc.initialize_from_db()
-        self.cc.query_client_of_jid_if_unknown(
-            connection, "test@gajim.org", client_caps)
-
-        self.assertEqual(0, connection.get_module('Discovery').disco_contact.call_count)
-
-
-class TestClientCaps(CommonCapsTest):
-
-    def setUp(self):
-        CommonCapsTest.setUp(self)
-        self.client_caps = caps.ClientCaps(self.caps_hash, self.node, self.caps_method)
-
-    def test_query_by_get_discover_strategy(self):
-        ''' Client must be queried if the data is unkown '''
-        connection = MagicMock()
-        discover = self.client_caps.get_discover_strategy()
-        discover(connection, "test@gajim.org")
-        connection.get_module('Discovery').disco_contact.assert_called_once_with(
-            'test@gajim.org', 'http://gajim.org#m3P2WeXPMGVH2tZPe7yITnfY0Dw=')
-
-    def test_client_supports(self):
-        self.assertTrue(caps.client_supports(self.client_caps, NS_PING),
-                        msg="Assume supported, if we don't have caps")
-
-        self.assertFalse(caps.client_supports(self.client_caps, NS_JINGLE_FILE_TRANSFER_5),
-                msg="Must not assume blacklisted feature is supported on default")
-
-        self.cc.initialize_from_db()
-
-        self.assertFalse(caps.client_supports(self.client_caps, NS_PING),
-                        msg="Must return false on unsupported feature")
-
-        self.assertTrue(caps.client_supports(self.client_caps, NS_XHTML_IM),
-                        msg="Must return True on supported feature")
-
-        self.assertTrue(caps.client_supports(self.client_caps, NS_MUC),
-                        msg="Must return True on supported feature")
-
-
-class TestOldClientCaps(TestClientCaps):
-
-    def setUp(self):
-        TestClientCaps.setUp(self)
-        self.client_caps = caps.OldClientCaps(self.caps_hash, self.node)
-
-    def test_query_by_get_discover_strategy(self):
-        ''' Client must be queried if the data is unknown '''
-        connection = MagicMock()
-        discover = self.client_caps.get_discover_strategy()
-        discover(connection, "test@gajim.org")
-
-        connection.get_module('Discovery').disco_contact.assert_called_once_with('test@gajim.org')
-
-if __name__ == '__main__':
-    unittest.main()
diff --git a/test/no_gui/unit/test_contacts.py b/test/no_gui/unit/test_contacts.py
deleted file mode 100644
index b3233a422f..0000000000
--- a/test/no_gui/unit/test_contacts.py
+++ /dev/null
@@ -1,117 +0,0 @@
-'''
-Test for Contact, GC_Contact and Contacts
-'''
-import unittest
-from nbxmpp import NS_MUC
-
-from gajim.common.contacts import CommonContact, Contact, GC_Contact, LegacyContactsAPI
-
-from gajim.common import caps_cache
-
-class TestCommonContact(unittest.TestCase):
-
-    def setUp(self):
-        self.contact = CommonContact(
-            jid='', account="", resource='', show='', presence=None,
-            status='', name='', chatstate=None, client_caps=None)
-
-    def test_default_client_supports(self):
-        '''
-        Test the caps support method of contacts.
-        See test_caps for more enhanced tests.
-        '''
-        caps_cache.capscache = caps_cache.CapsCache()
-        self.assertTrue(self.contact.supports(NS_MUC),
-                msg="Must not backtrace on simple check for supported feature")
-
-        self.contact.client_caps = caps_cache.NullClientCaps()
-
-        self.assertTrue(self.contact.supports(NS_MUC),
-                msg="Must not backtrace on simple check for supported feature")
-
-
-class TestContact(TestCommonContact):
-
-    def setUp(self):
-        TestCommonContact.setUp(self)
-        self.contact = Contact(jid="test@gajim.org", account="account")
-
-    def test_attributes_available(self):
-        '''This test supports the migration from the old to the new contact
-        domain model by smoke testing that no attribute values are lost'''
-
-        attributes = ["jid", "resource", "show", "status", "name",
-                      "chatstate", "client_caps", "priority", "sub"]
-        for attr in attributes:
-            self.assertTrue(hasattr(self.contact, attr), msg="expected: " + attr)
-
-
-class TestGC_Contact(TestCommonContact):
-
-    def setUp(self):
-        TestCommonContact.setUp(self)
-        self.contact = GC_Contact(room_jid="confernce@gajim.org", account="account")
-
-    def test_attributes_available(self):
-        '''This test supports the migration from the old to the new contact
-        domain model by asserting no attributes have been lost'''
-
-        attributes = ["jid", "resource", "show", "status", "name",
-                      "chatstate", "client_caps", "role", "room_jid"]
-        for attr in attributes:
-            self.assertTrue(hasattr(self.contact, attr), msg="expected: " + attr)
-
-
-class TestContacts(unittest.TestCase):
-
-    def setUp(self):
-        self.contacts = LegacyContactsAPI()
-
-    def test_create_add_get_contact(self):
-        jid = 'test@gajim.org'
-        account = "account"
-
-        contact = self.contacts.create_contact(jid=jid, account=account)
-        self.contacts.add_contact(account, contact)
-
-        retrieved_contact = self.contacts.get_contact(account, jid)
-        self.assertEqual(contact, retrieved_contact, "Contact must be known")
-
-        self.contacts.remove_contact(account, contact)
-
-        retrieved_contact = self.contacts.get_contact(account, jid)
-        self.assertNotEqual(contact, retrieved_contact,
-                msg="Contact must not be known any longer")
-
-
-    def test_copy_contact(self):
-        jid = 'test@gajim.org'
-        account = "account"
-
-        contact = self.contacts.create_contact(jid=jid, account=account)
-        copy = self.contacts.copy_contact(contact)
-        self.assertFalse(contact is copy, msg="Must not be the same")
-
-        # Not yet implemented to remain backwart compatible
-        # self.assertEqual(contact, copy, msg="Must be equal")
-
-    def test_legacy_contacts_from_groups(self):
-        jid1 = "test1@gajim.org"
-        jid2 = "test2@gajim.org"
-        account = "account"
-        group = "GroupA"
-
-        contact1 = self.contacts.create_contact(jid=jid1, account=account,
-                groups=[group])
-        self.contacts.add_contact(account, contact1)
-
-        contact2 = self.contacts.create_contact(jid=jid2, account=account,
-                groups=[group])
-        self.contacts.add_contact(account, contact2)
-
-        self.assertEqual(2, len(self.contacts.get_contacts_from_group(account, group)))
-        self.assertEqual(0, len(self.contacts.get_contacts_from_group(account, '')))
-
-
-if __name__ == "__main__":
-    unittest.main()
-- 
GitLab