using Gee; using Dino.Entities; using Qlite; using Xmpp; using Xmpp.Xep; using Xmpp.Xep.ServiceDiscovery; namespace Dino { public class EntityInfo : StreamInteractionModule, Object { public static ModuleIdentity IDENTITY = new ModuleIdentity("entity_info"); public string id { get { return IDENTITY.id; } } private StreamInteractor stream_interactor; private Database db; private EntityCapabilitiesStorage entity_capabilities_storage; private HashMap entity_caps_hashes = new HashMap(Jid.hash_func, Jid.equals_func); private HashMap> entity_features = new HashMap>(); private HashMap> jid_features = new HashMap>(Jid.hash_func, Jid.equals_func); private HashMap> entity_identity = new HashMap>(); private HashMap> jid_identity = new HashMap>(Jid.hash_func, Jid.equals_func); public static void start(StreamInteractor stream_interactor, Database db) { EntityInfo m = new EntityInfo(stream_interactor, db); stream_interactor.add_module(m); } private EntityInfo(StreamInteractor stream_interactor, Database db) { this.stream_interactor = stream_interactor; this.db = db; this.entity_capabilities_storage = new EntityCapabilitiesStorage(db); stream_interactor.account_added.connect(on_account_added); stream_interactor.connection_manager.stream_opened.connect((account, stream) => { stream.received_features_node.connect(() => { string? hash = EntityCapabilities.get_server_caps_hash(stream); if (hash != null) { entity_caps_hashes[account.bare_jid.domain_jid] = hash; } }); }); stream_interactor.module_manager.initialize_account_modules.connect(initialize_modules); remove_old_entities(); Timeout.add_seconds(60 * 60, () => { remove_old_entities(); return true; }); } public async Gee.Set? get_identities(Account account, Jid jid) { if (jid_identity.has_key(jid)) { return jid_identity[jid]; } string? hash = entity_caps_hashes[jid]; if (hash != null) { Gee.Set? identities = get_stored_identities(hash); if (identities != null) return identities; } ServiceDiscovery.InfoResult? info_result = yield get_info_result(account, jid, hash); if (info_result != null) { return info_result.identities; } return null; } public async Identity? get_identity(Account account, Jid jid) { Gee.Set? identities = yield get_identities(account, jid); if (identities == null) return null; foreach (var identity in identities) { if (identity.category == Identity.CATEGORY_CLIENT) { return identity; } } return null; } public async bool has_feature(Account account, Jid jid, string feature) { int has_feature_cached = has_feature_cached_int(account, jid, feature); if (has_feature_cached != -1) { return has_feature_cached == 1; } ServiceDiscovery.InfoResult? info_result = yield get_info_result(account, jid, entity_caps_hashes[jid]); if (info_result == null) return false; return info_result.features.contains(feature); } public bool has_feature_offline(Account account, Jid jid, string feature) { int ret = has_feature_cached_int(account, jid, feature); if (ret == -1) { return db.entity.select() .with(db.entity.account_id, "=", account.id) .with(db.entity.jid_id, "=", db.get_jid_id(jid)) .with(db.entity.resource, "=", jid.resourcepart ?? "") .join_with(db.entity_feature, db.entity.caps_hash, db.entity_feature.entity) .with(db.entity_feature.feature, "=", feature) .count() > 0; } return ret == 1; } public bool has_feature_cached(Account account, Jid jid, string feature) { return has_feature_cached_int(account, jid, feature) == 1; } private int has_feature_cached_int(Account account, Jid jid, string feature) { if (jid_features.has_key(jid)) { return jid_features[jid].contains(feature) ? 1 : 0; } string? hash = entity_caps_hashes[jid]; if (hash != null) { Gee.List? features = get_stored_features(hash); if (features != null) { return features.contains(feature) ? 1 : 0; } } return -1; } private void on_received_available_presence(Account account, Presence.Stanza presence) { bool is_gc = stream_interactor.get_module(MucManager.IDENTITY).might_be_groupchat(presence.from.bare_jid, account); if (is_gc) return; string? caps_hash = EntityCapabilities.get_caps_hash(presence); if (caps_hash == null) return; db.entity.upsert() .value(db.entity.account_id, account.id, true) .value(db.entity.jid_id, db.get_jid_id(presence.from), true) .value(db.entity.resource, presence.from.resourcepart, true) .value(db.entity.last_seen, (long)(new DateTime.now_local()).to_unix()) .value(db.entity.caps_hash, caps_hash) .perform(); if (caps_hash != null) { entity_caps_hashes[presence.from] = caps_hash; } } private void remove_old_entities() { long timestamp = (long)(new DateTime.now_local().add_days(-14)).to_unix(); db.entity.delete().with(db.entity.last_seen, "<", timestamp).perform(); } private void store_features(string entity, Gee.List features) { if (entity_features.has_key(entity)) return; foreach (string feature in features) { db.entity_feature.insert() .value(db.entity_feature.entity, entity) .value(db.entity_feature.feature, feature) .perform(); } entity_features[entity] = features; } private void store_identities(string entity, Gee.Set identities) { foreach (Identity identity in identities) { db.entity_identity.insert() .value(db.entity_identity.entity, entity) .value(db.entity_identity.category, identity.category) .value(db.entity_identity.type, identity.type_) .value(db.entity_identity.entity_name, identity.name) .perform(); } entity_identity[entity] = identities; } private Gee.List? get_stored_features(string entity) { Gee.List? features = entity_features[entity]; if (features != null) { return features; } features = new ArrayList(); foreach (Row row in db.entity_feature.select({db.entity_feature.feature}).with(db.entity_feature.entity, "=", entity)) { features.add(row[db.entity_feature.feature]); } if (features.size == 0) { return null; } entity_features[entity] = features; return features; } private Gee.Set? get_stored_identities(string entity) { Gee.Set? identities = entity_identity[entity]; if (identities != null) { return identities; } identities = new HashSet(Identity.hash_func, Identity.equals_func); var qry = db.entity_identity.select().with(db.entity_identity.entity, "=", entity); foreach (Row row in qry) { var identity = new Identity(row[db.entity_identity.category], row[db.entity_identity.type], row[db.entity_identity.entity_name]); identities.add(identity); } if (identities.size == 0) { return null; } entity_identity[entity] = identities; return identities; } private async ServiceDiscovery.InfoResult? get_info_result(Account account, Jid jid, string? hash = null) { XmppStream? stream = stream_interactor.get_stream(account); if (stream == null) return null; ServiceDiscovery.InfoResult? info_result = yield stream.get_module(ServiceDiscovery.Module.IDENTITY).request_info(stream, jid); if (info_result == null) return null; var computed_hash = EntityCapabilities.Module.compute_hash_for_info_result(info_result); if (hash == null || computed_hash == hash) { db.entity.upsert() .value(db.entity.account_id, account.id, true) .value(db.entity.jid_id, db.get_jid_id(jid), true) .value(db.entity.resource, jid.resourcepart ?? "", true) .value(db.entity.last_seen, (long)(new DateTime.now_local()).to_unix()) .value(db.entity.caps_hash, computed_hash) .perform(); store_features(computed_hash, info_result.features); store_identities(computed_hash, info_result.identities); } else { warning("Claimed entity caps hash from %s doesn't match computed one", jid.to_string()); } jid_features[jid] = info_result.features; jid_identity[jid] = info_result.identities; return info_result; } private void on_account_added(Account account) { var cache = new CapsCacheImpl(account, this); stream_interactor.module_manager.get_module(account, ServiceDiscovery.Module.IDENTITY).cache = cache; stream_interactor.module_manager.get_module(account, Presence.Module.IDENTITY).received_available.connect((stream, presence) => on_received_available_presence(account, presence)); } private void initialize_modules(Account account, ArrayList modules) { modules.add(new Xep.EntityCapabilities.Module(entity_capabilities_storage)); } } public class CapsCacheImpl : CapsCache, Object { private Account account; private EntityInfo entity_info; public CapsCacheImpl(Account account, EntityInfo entity_info) { this.account = account; this.entity_info = entity_info; } public async bool has_entity_feature(Jid jid, string feature) { return yield entity_info.has_feature(account, jid, feature); } public async Gee.Set get_entity_identities(Jid jid) { return yield entity_info.get_identities(account, jid); } } }