diff options
Diffstat (limited to 'libdino/src/service')
27 files changed, 938 insertions, 395 deletions
diff --git a/libdino/src/service/avatar_manager.vala b/libdino/src/service/avatar_manager.vala index 0c98a8f3..d079b70d 100644 --- a/libdino/src/service/avatar_manager.vala +++ b/libdino/src/service/avatar_manager.vala @@ -1,9 +1,9 @@ using Gdk; using Gee; -using Qlite; -using Xmpp; using Dino.Entities; +using Dino.Qlite; +using Xmpp; namespace Dino { @@ -142,7 +142,7 @@ public class AvatarManager : StreamInteractionModule, Object { public void unset_avatar(Account account) { XmppStream stream = stream_interactor.get_stream(account); if (stream == null) return; - Xmpp.Xep.UserAvatars.unset_avatar(stream); + Xmpp.Xep.UserAvatars.unset_avatar(stream, get_avatar_hash(account, account.bare_jid)); } private void on_account_added(Account account) { @@ -157,19 +157,19 @@ public class AvatarManager : StreamInteractionModule, Object { ); foreach (var entry in get_avatar_hashes(account, Source.USER_AVATARS).entries) { - on_user_avatar_received(account, entry.key, entry.value); + on_user_avatar_received(account, entry.key, entry.value, false); } foreach (var entry in get_avatar_hashes(account, Source.VCARD).entries) { - on_vcard_avatar_received(account, entry.key, entry.value); + on_vcard_avatar_received(account, entry.key, entry.value, false); } } - private void on_user_avatar_received(Account account, Jid jid_, string id) { + private void on_user_avatar_received(Account account, Jid jid_, string id, bool is_live = true) { Jid jid = jid_.bare_jid; if (!user_avatars.has_key(jid) || user_avatars[jid] != id) { user_avatars[jid] = id; - set_avatar_hash(account, jid, id, Source.USER_AVATARS); + if (is_live) set_avatar_hash(account, jid, id, Source.USER_AVATARS); } received_avatar(jid, account); } @@ -181,13 +181,13 @@ public class AvatarManager : StreamInteractionModule, Object { received_avatar(jid, account); } - private void on_vcard_avatar_received(Account account, Jid jid_, string id) { + private void on_vcard_avatar_received(Account account, Jid jid_, string id, bool is_live = true) { bool is_gc = stream_interactor.get_module(MucManager.IDENTITY).might_be_groupchat(jid_.bare_jid, account); Jid jid = is_gc ? jid_ : jid_.bare_jid; if (!vcard_avatars.has_key(jid) || vcard_avatars[jid] != id) { vcard_avatars[jid] = id; - if (jid.is_bare()) { // don't save MUC occupant avatars + if (is_live && jid.is_bare()) { // don't save MUC occupant avatars set_avatar_hash(account, jid, id, Source.VCARD); } } diff --git a/libdino/src/service/call_store.vala b/libdino/src/service/call_store.vala index bfc8255f..9e32517f 100644 --- a/libdino/src/service/call_store.vala +++ b/libdino/src/service/call_store.vala @@ -1,8 +1,8 @@ using Xmpp; using Gee; -using Qlite; using Dino.Entities; +using Dino.Qlite; namespace Dino { diff --git a/libdino/src/service/chat_interaction.vala b/libdino/src/service/chat_interaction.vala index b3ec1b93..def9ea86 100644 --- a/libdino/src/service/chat_interaction.vala +++ b/libdino/src/service/chat_interaction.vala @@ -170,7 +170,7 @@ public class ChatInteraction : StreamInteractionModule, Object { if (conversation == null) return false; Entities.Message? message = stream_interactor.get_module(MessageStorage.IDENTITY).get_last_message(conversation); if (message == null || message.direction == Entities.Message.DIRECTION_SENT) return false; - if (message.equals(conversation.read_up_to)) return false; + if (message.id == conversation.read_up_to_message_id) return false; bool sent_marker = yield send_and_mark_message_displayed(conversation, message); return sent_marker; @@ -242,7 +242,7 @@ public class ChatInteraction : StreamInteractionModule, Object { if (message.type_ == Message.Type.GROUPCHAT) return false; if (message.stanza_id == null) return false; if (message.direction == Entities.Message.DIRECTION_SENT) return false; - if (message.equals(conversation.read_up_to)) return false; + if (message.id == conversation.read_up_to_message_id) return false; if (conversation.get_send_marker_setting(stream_interactor) != Conversation.Setting.ON) return false; XmppStream? stream = stream_interactor.get_stream(conversation.account); diff --git a/libdino/src/service/contact_model.vala b/libdino/src/service/contact_model.vala index 33f6126b..3e52e040 100644 --- a/libdino/src/service/contact_model.vala +++ b/libdino/src/service/contact_model.vala @@ -1,6 +1,5 @@ using Xmpp; using Gee; -using Qlite; using Dino.Entities; diff --git a/libdino/src/service/content_item_store.vala b/libdino/src/service/content_item_store.vala index d74f9b05..5e440c76 100644 --- a/libdino/src/service/content_item_store.vala +++ b/libdino/src/service/content_item_store.vala @@ -1,7 +1,7 @@ using Gee; using Dino.Entities; -using Qlite; +using Dino.Qlite; using Xmpp; namespace Dino { @@ -26,6 +26,8 @@ public class ContentItemStore : StreamInteractionModule, Object { this.db = db; stream_interactor.get_module(FileManager.IDENTITY).received_file.connect(insert_file_transfer); + stream_interactor.get_module(FileManager2.IDENTITY).received_file.connect(insert_file_transfer); + stream_interactor.get_module(FileManager2.IDENTITY).received_file_group.connect(insert_file_group); stream_interactor.get_module(MessageProcessor.IDENTITY).message_received.connect(announce_message); stream_interactor.get_module(MessageProcessor.IDENTITY).message_sent.connect(announce_message); stream_interactor.get_module(Calls.IDENTITY).call_incoming.connect(insert_call); @@ -88,6 +90,13 @@ public class ContentItemStore : StreamInteractionModule, Object { return call_item; } break; + case 4: + FileTransferGroup? file_group = stream_interactor.get_module(FileTransferStorage.IDENTITY).get_file_group_by_id(foreign_id, conversation); + if (file_group != null) { + var file_group_item = new FileGroupItem(file_group, conversation, id); + return file_group_item; + } + break; default: warning("Unknown content item type: %i", content_type); break; @@ -300,6 +309,16 @@ public class ContentItemStore : StreamInteractionModule, Object { new_item(item, conversation); } + private void insert_file_group(FileTransferGroup file_group, Conversation conversation) { + FileGroupItem item = new FileGroupItem(file_group, conversation, file_group.id); + // TODO fix local time + item.id = db.add_content_item(conversation, item.time, item.time, 4, file_group.id, false); + if (collection_conversations.has_key(conversation)) { + collection_conversations.get(conversation).insert_item(item); + } + new_item(item, conversation); + } + private void insert_call(Call call, CallState call_state, Conversation conversation) { CallItem item = new CallItem(call, conversation, -1); item.id = db.add_content_item(conversation, call.time, call.local_time, 3, call.id, false); @@ -415,6 +434,21 @@ public class FileItem : ContentItem { } } +public class FileGroupItem : ContentItem { + public const string TYPE = "file_group"; + + public FileTransferGroup file_group; + public Conversation conversation; + + public FileGroupItem(FileTransferGroup file_group, Conversation conversation, int id) { + var file_transfer = file_group.file_transfers.get(0); + base(id, TYPE, file_transfer.from, file_transfer.time, file_transfer.encryption, Message.Marked.NONE); + + this.file_group = file_group; + this.conversation = conversation; + } +} + public class CallItem : ContentItem { public const string TYPE = "call"; diff --git a/libdino/src/service/counterpart_interaction_manager.vala b/libdino/src/service/counterpart_interaction_manager.vala index f60cfb20..d6ca73a9 100644 --- a/libdino/src/service/counterpart_interaction_manager.vala +++ b/libdino/src/service/counterpart_interaction_manager.vala @@ -159,8 +159,13 @@ public class CounterpartInteractionManager : StreamInteractionModule, Object { public bool mark_displayed_up_to_message(Conversation conversation, Message message) { // Don't move read marker backwards because we get old info from another client - if (conversation.read_up_to != null && conversation.read_up_to.local_time.compare(message.local_time) > 0) return false; - conversation.read_up_to = message; + if (conversation.read_up_to_message_id != -1) { + Message read_up_to_message = stream_interactor.get_module(MessageStorage.IDENTITY).get_message_by_id(conversation.read_up_to_message_id, conversation); + if (read_up_to_message.local_time.compare(message.local_time) > 0) { + return false; + } + } + conversation.read_up_to_message_id = message.id; ContentItem? content_item = stream_interactor.get_module(ContentItemStore.IDENTITY).get_content_item_for_message(conversation, message); if (content_item == null) return true; diff --git a/libdino/src/service/database.vala b/libdino/src/service/database.vala index b38ccfca..30032e2c 100644 --- a/libdino/src/service/database.vala +++ b/libdino/src/service/database.vala @@ -1,13 +1,13 @@ using Gee; -using Qlite; using Xmpp; using Dino.Entities; +using Dino.Qlite; namespace Dino { public class Database : Qlite.Database { - private const int VERSION = 33; + private const int VERSION = 35; public class AccountTable : Table { public Column<int> id = new Column.Integer("id") { primary_key = true, auto_increment = true }; @@ -49,6 +49,7 @@ public class Database : Qlite.Database { base(db, "entity"); init({id, account_id, jid_id, resource, caps_hash, last_seen}); unique({account_id, jid_id, resource}, "IGNORE"); + index("entity_account_jid_resource_idx", {account_id, jid_id, resource}); } } @@ -225,6 +226,28 @@ public class Database : Qlite.Database { } } + public class FileTransferGroupTable : Table { + public Column<int> id = new Column.Integer("id") { primary_key = true, auto_increment = true }; + public Column<int> message_id = new Column.Integer("message_id"); + + internal FileTransferGroupTable(Database db) { + base(db, "file_transfer_group"); + init({id, message_id}); + index("file_transfer_group_message_id_idx", {message_id}); + } + } + + public class FileTransferGroupFileTransferTable : Table { + public Column<int> file_transfer_group_id = new Column.Integer("file_transfer_group_id"); + public Column<int> file_transfer_id = new Column.Integer("file_transfer_id"); + + internal FileTransferGroupFileTransferTable(Database db) { + base(db, "file_transfer_group_file_transfer"); + init({file_transfer_group_id, file_transfer_id}); + index("file_transfer_group_file_transfer_id_idx", {file_transfer_group_id}); + } + } + public class FileHashesTable : Table { public Column<int> id = new Column.Integer("id"); public Column<string> algo = new Column.Text("algo") { not_null = true }; @@ -354,6 +377,7 @@ public class Database : Qlite.Database { init({entity, feature}); unique({entity, feature}, "IGNORE"); index("entity_feature_idx", {entity}); + index("entity_feature_feature_idx", {entity, feature}); } } @@ -489,6 +513,8 @@ public class Database : Qlite.Database { public RealJidTable real_jid { get; private set; } public OccupantIdTable occupantid { get; private set; } public FileTransferTable file_transfer { get; private set; } + public FileTransferGroupTable file_transfer_group { get; private set; } + public FileTransferGroupFileTransferTable file_transfer_group_file_transfer { get; private set; } public FileHashesTable file_hashes { get; private set; } public FileThumbnailsTable file_thumbnails { get; private set; } public SourcesTable sfs_sources { get; private set; } @@ -525,6 +551,8 @@ public class Database : Qlite.Database { occupantid = new OccupantIdTable(this); real_jid = new RealJidTable(this); file_transfer = new FileTransferTable(this); + file_transfer_group = new FileTransferGroupTable(this); + file_transfer_group_file_transfer = new FileTransferGroupFileTransferTable(this); file_hashes = new FileHashesTable(this); file_thumbnails = new FileThumbnailsTable(this); sfs_sources = new SourcesTable(this); @@ -542,7 +570,7 @@ public class Database : Qlite.Database { settings = new SettingsTable(this); account_settings = new AccountSettingsTable(this); conversation_settings = new ConversationSettingsTable(this); - init({ account, jid, entity, content_item, message, message_occupant_id, body_meta, message_correction, reply, real_jid, occupantid, file_transfer, file_hashes, file_thumbnails, sfs_sources, call, call_counterpart, conversation, avatar, entity_identity, entity_feature, roster, mam_catchup, reaction, muc_affiliation, muc_affiliation_version, settings, account_settings, conversation_settings }); + init({ account, jid, entity, content_item, message, message_occupant_id, body_meta, message_correction, reply, real_jid, occupantid, file_transfer, file_transfer_group, file_transfer_group_file_transfer, file_hashes, file_thumbnails, sfs_sources, call, call_counterpart, conversation, avatar, entity_identity, entity_feature, roster, mam_catchup, reaction, muc_affiliation, muc_affiliation_version, settings, account_settings, conversation_settings }); try { exec("PRAGMA journal_mode = WAL"); @@ -741,65 +769,6 @@ public class Database : Qlite.Database { .perform(); } - public Gee.List<Message> get_messages(Jid jid, Account account, Message.Type? type, int count, DateTime? before, DateTime? after, int id) { - QueryBuilder select = message.select(); - - if (before != null) { - if (id > 0) { - select.where(@"time < ? OR (time = ? AND message.id < ?)", { before.to_unix().to_string(), before.to_unix().to_string(), id.to_string() }); - } else { - select.with(message.id, "<", id); - } - } - if (after != null) { - if (id > 0) { - select.where(@"time > ? OR (time = ? AND message.id > ?)", { after.to_unix().to_string(), after.to_unix().to_string(), id.to_string() }); - } else { - select.with(message.time, ">", (long) after.to_unix()); - } - if (id > 0) { - select.with(message.id, ">", id); - } - } else { - select.order_by(message.time, "DESC"); - } - - select.with(message.counterpart_id, "=", get_jid_id(jid)) - .with(message.account_id, "=", account.id) - .limit(count); - if (jid.resourcepart != null) { - select.with(message.counterpart_resource, "=", jid.resourcepart); - } - if (type != null) { - select.with(message.type_, "=", (int) type); - } - - select.outer_join_with(real_jid, real_jid.message_id, message.id); - select.outer_join_with(message_correction, message_correction.message_id, message.id); - - LinkedList<Message> ret = new LinkedList<Message>(); - foreach (Row row in select) { - try { - ret.insert(0, new Message.from_row(this, row)); - } catch (InvalidJidError e) { - warning("Ignoring message with invalid Jid: %s", e.message); - } - } - return ret; - } - - public Message? get_message_by_id(int id) { - Row? row = message.row_with(message.id, id).inner; - if (row != null) { - try { - return new Message.from_row(this, row); - } catch (InvalidJidError e) { - warning("Ignoring message with invalid Jid: %s", e.message); - } - } - return null; - } - public ArrayList<Conversation> get_conversations(Account account) { ArrayList<Conversation> ret = new ArrayList<Conversation>(); foreach (Row row in conversation.select().with(conversation.account_id, "=", account.id)) { diff --git a/libdino/src/service/entity_capabilities_storage.vala b/libdino/src/service/entity_capabilities_storage.vala deleted file mode 100644 index c6d0bbbd..00000000 --- a/libdino/src/service/entity_capabilities_storage.vala +++ /dev/null @@ -1,71 +0,0 @@ -using Gee; -using Qlite; -using Xmpp; -using Xmpp.Xep.ServiceDiscovery; - -namespace Dino { - -public class EntityCapabilitiesStorage : Xep.EntityCapabilities.Storage, Object { - - private Database db; - private HashMap<string, Gee.List<string>> features_cache = new HashMap<string, Gee.List<string>>(); - private HashMap<string, Identity> identity_cache = new HashMap<string, Identity>(); - - public EntityCapabilitiesStorage(Database db) { - this.db = db; - } - - public void store_features(string entity, Gee.List<string> features) { - if (features_cache.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(); - } - } - - public void store_identities(string entity, Gee.Set<Identity> identities) { - foreach (Identity identity in identities) { - if (identity.category == Identity.CATEGORY_CLIENT) { - 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(); - return; - } - } - } - - public Gee.List<string> get_features(string entity) { - Gee.List<string>? features = features_cache[entity]; - if (features != null) { - return features; - } - - features = new ArrayList<string>(); - 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]); - } - features_cache[entity] = features; - return features; - } - - public Identity? get_identities(string entity) { - Identity? identity = identity_cache[entity]; - if (identity != null) { - return identity; - } - - RowOption row = db.entity_identity.select().with(db.entity_identity.entity, "=", entity).single().row(); - if (row.is_present()) { - identity = new Identity(row[db.entity_identity.category], row[db.entity_identity.type], row[db.entity_identity.entity_name]); - } - identity_cache[entity] = identity; - return identity; - } -} -} diff --git a/libdino/src/service/entity_info.vala b/libdino/src/service/entity_info.vala index 53d14b15..e9a20df1 100644 --- a/libdino/src/service/entity_info.vala +++ b/libdino/src/service/entity_info.vala @@ -1,6 +1,6 @@ using Gee; using Dino.Entities; -using Qlite; +using Dino.Qlite; using Xmpp; using Xmpp.Xep; using Xmpp.Xep.ServiceDiscovery; @@ -12,7 +12,6 @@ public class EntityInfo : StreamInteractionModule, Object { private StreamInteractor stream_interactor; private Database db; - private EntityCapabilitiesStorage entity_capabilities_storage; private Dino.Entities.Settings settings; private HashMap<Jid, string> entity_caps_hashes = new HashMap<Jid, string>(Jid.hash_func, Jid.equals_func); @@ -40,7 +39,6 @@ public class EntityInfo : StreamInteractionModule, Object { private EntityInfo(StreamInteractor stream_interactor, Database db, Dino.Entities.Settings settings) { this.stream_interactor = stream_interactor; this.db = db; - this.entity_capabilities_storage = new EntityCapabilitiesStorage(db); this.settings = settings; stream_interactor.account_added.connect(on_account_added); @@ -58,18 +56,32 @@ public class EntityInfo : StreamInteractionModule, Object { Timeout.add_seconds(60 * 60, () => { remove_old_entities(); return true; }); } - public async Gee.Set<Identity>? get_identities(Account account, Jid jid) { + public Gee.Set<Identity>? get_identities_offline(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<Identity>? identities = get_stored_identities(hash); - if (identities != null) return identities; + var qry = 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_identity, db.entity.caps_hash, db.entity_identity.entity); + + HashSet<Identity> identities = new HashSet<Identity>(Identity.hash_func, Identity.equals_func); + + 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); } - ServiceDiscovery.InfoResult? info_result = yield get_info_result(account, jid, hash); + return identities; + } + + public async Gee.Set<Identity>? get_identities(Account account, Jid jid) { + var identities = get_identities_offline(account, jid); + if (identities != null) return identities; + + ServiceDiscovery.InfoResult? info_result = yield get_info_result(account, jid, entity_caps_hashes[jid]); if (info_result != null) { return info_result.identities; } @@ -261,7 +273,7 @@ public class EntityInfo : StreamInteractionModule, Object { return identities; } - private async ServiceDiscovery.InfoResult? get_info_result(Account account, Jid jid, string? hash = null) { + public 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; @@ -298,7 +310,6 @@ public class EntityInfo : StreamInteractionModule, Object { } private void initialize_modules(Account account, ArrayList<XmppStreamModule> modules) { - modules.add(new Xep.EntityCapabilities.Module(entity_capabilities_storage)); var entity_time_module = modules.first_match((it) => it is EntityTime.Module) as EntityTime.Module; if (entity_time_module != null) { settings.bind_property("share-time", entity_time_module, "enabled", BindingFlags.SYNC_CREATE); diff --git a/libdino/src/service/fallback_body.vala b/libdino/src/service/fallback_body.vala index 0ce89ade..09aa839a 100644 --- a/libdino/src/service/fallback_body.vala +++ b/libdino/src/service/fallback_body.vala @@ -1,5 +1,4 @@ using Gee; -using Qlite; using Xmpp; using Xmpp.Xep; diff --git a/libdino/src/service/file_manager.vala b/libdino/src/service/file_manager.vala index 964278a5..60b7fafa 100644 --- a/libdino/src/service/file_manager.vala +++ b/libdino/src/service/file_manager.vala @@ -16,17 +16,22 @@ public class FileManager : StreamInteractionModule, Object { private StreamInteractor stream_interactor; private Database db; - private Gee.List<FileSender> file_senders = new ArrayList<FileSender>(); + public Gee.List<FileSender> file_senders = new ArrayList<FileSender>(); private Gee.List<FileEncryptor> file_encryptors = new ArrayList<FileEncryptor>(); - private Gee.List<FileDecryptor> file_decryptors = new ArrayList<FileDecryptor>(); + public Gee.List<FileDecryptor> file_decryptors = new ArrayList<FileDecryptor>(); private Gee.List<FileProvider> file_providers = new ArrayList<FileProvider>(); - private Gee.List<FileMetadataProvider> file_metadata_providers = new ArrayList<FileMetadataProvider>(); + public Gee.List<FileMetadataProvider> file_metadata_providers = new ArrayList<FileMetadataProvider>(); public StatelessFileSharing sfs { owned get { return stream_interactor.get_module(StatelessFileSharing.IDENTITY); } private set { } } + public FileManager2 fm2 { + owned get { return stream_interactor.get_module(FileManager2.IDENTITY); } + private set { } + } + public static void start(StreamInteractor stream_interactor, Database db) { FileManager m = new FileManager(stream_interactor, db); stream_interactor.add_module(m); @@ -97,7 +102,7 @@ public class FileManager : StreamInteractionModule, Object { try { file_transfer.input_stream = yield file.read_async(); - yield save_file(file_transfer); + yield fm2.save_file(file_transfer); stream_interactor.get_module(FileTransferStorage.IDENTITY).add_file(file_transfer); conversation.last_active = file_transfer.time; @@ -151,7 +156,7 @@ public class FileManager : StreamInteractionModule, Object { file_transfer.state = FileTransfer.State.IN_PROGRESS; - // Update current download progress in the FileTransfer + // Update current upload progress in the FileTransfer LimitInputStream? limit_stream = file_transfer.input_stream as LimitInputStream; if (limit_stream == null) { limit_stream = new LimitInputStream(file_transfer.input_stream, file_meta.size); @@ -216,19 +221,6 @@ public class FileManager : StreamInteractionModule, Object { file_metadata_providers.add(file_metadata_provider); } - public bool is_sender_trustworthy(FileTransfer file_transfer, Conversation conversation) { - if (file_transfer.direction == FileTransfer.DIRECTION_SENT) return true; - - Jid relevant_jid = conversation.counterpart; - if (conversation.type_ == Conversation.Type.GROUPCHAT) { - relevant_jid = stream_interactor.get_module(MucManager.IDENTITY).get_real_jid(file_transfer.from, conversation.account); - } - if (relevant_jid == null) return false; - - bool in_roster = stream_interactor.get_module(RosterManager.IDENTITY).get_roster_item(conversation.account, relevant_jid) != null; - return in_roster; - } - private async FileMeta get_file_meta(FileProvider file_provider, FileTransfer file_transfer, Conversation conversation, FileReceiveData receive_data_) throws FileReceiveError { FileReceiveData receive_data = receive_data_; FileMeta file_meta = file_provider.get_file_meta(file_transfer); @@ -390,7 +382,7 @@ public class FileManager : StreamInteractionModule, Object { FileTransfer file_transfer = create_file_transfer_from_provider_incoming(file_provider, info, from, time, local_time, conversation, receive_data, file_meta); stream_interactor.get_module(FileTransferStorage.IDENTITY).add_file(file_transfer); - if (is_sender_trustworthy(file_transfer, conversation)) { + if (fm2.is_sender_trustworthy(file_transfer, conversation)) { try { yield get_file_meta(file_provider, file_transfer, conversation, receive_data); } catch (Error e) { @@ -408,20 +400,6 @@ public class FileManager : StreamInteractionModule, Object { received_file(file_transfer, conversation); } - private async void save_file(FileTransfer file_transfer) throws FileSendError { - try { - string filename = Random.next_int().to_string("%x") + "_" + file_transfer.file_name; - File file = File.new_for_path(Path.build_filename(get_storage_dir(), filename)); - OutputStream os = file.create(FileCreateFlags.REPLACE_DESTINATION); - yield os.splice_async(file_transfer.input_stream, OutputStreamSpliceFlags.CLOSE_SOURCE | OutputStreamSpliceFlags.CLOSE_TARGET); - file_transfer.state = FileTransfer.State.COMPLETE; - file_transfer.path = filename; - file_transfer.input_stream = new LimitInputStream(yield file.read_async(), file_transfer.size); - } catch (Error e) { - throw new FileSendError.SAVE_FAILED("Saving file error: %s".printf(e.message)); - } - } - public Message? get_message_for_file_transfer(FileTransfer file_transfer, Conversation conversation) { if ((file_transfer.provider == 0 || file_transfer.provider == 2) && file_transfer.info != null) { return stream_interactor.get_module(MessageStorage.IDENTITY).get_message_by_id(int.parse(file_transfer.info), conversation); diff --git a/libdino/src/service/file_transfer_storage.vala b/libdino/src/service/file_transfer_storage.vala index 64bb6b81..33ebcbce 100644 --- a/libdino/src/service/file_transfer_storage.vala +++ b/libdino/src/service/file_transfer_storage.vala @@ -1,8 +1,8 @@ using Xmpp; using Gee; -using Qlite; using Dino.Entities; +using Dino.Qlite; namespace Dino { @@ -15,6 +15,8 @@ namespace Dino { private WeakMap<int, FileTransfer> files_by_db_id = new WeakMap<int, FileTransfer>(); private WeakMap<int, FileTransfer> files_by_message_id = new WeakMap<int, FileTransfer>(); + private WeakMap<int, FileTransferGroup> file_group_by_db_id = new WeakMap<int, FileTransferGroup>(); + private WeakMap<int, FileTransferGroup> file_group_by_message_id = new WeakMap<int, FileTransferGroup>(); private WeakMap<string, FileTransfer> files_by_message_and_file_id = new WeakMap<string, FileTransfer>(); public static void start(StreamInteractor stream_interactor, Database db) { @@ -32,7 +34,13 @@ namespace Dino { cache_file(file_transfer); } - public FileTransfer? get_file_by_id(int id, Conversation conversation) { + public void add_file_group(FileTransferGroup file_group) { + file_group.persist(db); + cache_file_group(file_group); + } + + // `conversation` should be provided, otherwise the FileTransfer.ourpart might be wrong + public FileTransfer? get_file_by_id(int id, Conversation? conversation) { FileTransfer? file_transfer = files_by_db_id[id]; if (file_transfer != null) { return file_transfer; @@ -58,6 +66,29 @@ namespace Dino { return create_file_from_row_opt(row_option, conversation); } + public FileTransferGroup? get_file_group_by_id(int id, Conversation conversation) { + FileTransferGroup? file_group = file_group_by_db_id[id]; + if (file_group != null) { + return file_group; + } + + RowOption row_option = db.file_transfer_group.select().with(db.file_transfer_group.id, "=", id).row(); + return row_option.is_present() ? create_file_group_from_row(row_option.inner, conversation) : null; + } + + public FileTransferGroup get_file_group_by_message_id(int id, Conversation conversation) { + FileTransferGroup? file_group = file_group_by_message_id[id]; + if (file_group != null) { + return file_group; + } + + foreach (Row row in db.file_transfer_group.select().with(db.file_transfer_group.message_id, "=", id)) { + return create_file_group_from_row(row, conversation); + } + + return null; + } + public FileTransfer get_files_by_message_and_file_id(int message_id, string file_sharing_id, Conversation conversation) { string combined_identifier = message_id.to_string() + file_sharing_id; FileTransfer? file_transfer = files_by_message_and_file_id[combined_identifier]; @@ -79,13 +110,13 @@ namespace Dino { return null; } - private FileTransfer? create_file_from_row_opt(RowOption row_opt, Conversation conversation) { + private FileTransfer? create_file_from_row_opt(RowOption row_opt, Conversation? conversation) { if (!row_opt.is_present()) return null; try { FileTransfer file_transfer = new FileTransfer.from_row(db, row_opt.inner, FileManager.get_storage_dir()); - if (conversation.type_.is_muc_semantic()) { + if (conversation != null && conversation.type_.is_muc_semantic()) { file_transfer.ourpart = conversation.counterpart.with_resource(file_transfer.ourpart.resourcepart); } @@ -97,6 +128,21 @@ namespace Dino { return null; } + private FileTransferGroup? create_file_group_from_row(Row file_transfer_group_row, Conversation conversation) { + QueryBuilder query = db.file_transfer_group_file_transfer.select() + .with(db.file_transfer_group_file_transfer.file_transfer_group_id, "=", file_transfer_group_row[db.file_transfer_group.id]); + var file_transfers = new ArrayList<FileTransfer>(); + foreach (Qlite.Row row in query) { + FileTransfer file_transfer = get_file_by_id(row[db.file_transfer_group_file_transfer.file_transfer_id], conversation); + file_transfers.add(file_transfer); + } + if (file_transfers.size == 0) return null; + + FileTransferGroup file_group = new FileTransferGroup.from_row(db, file_transfer_group_row, file_transfers); + cache_file_group(file_group); + return file_group; + } + private void cache_file(FileTransfer file_transfer) { files_by_db_id[file_transfer.id] = file_transfer; @@ -109,5 +155,10 @@ namespace Dino { } } } + + private void cache_file_group(FileTransferGroup file_group) { + file_group_by_db_id[file_group.id] = file_group; +// file_group_by_message_id[file_group.message_id] = file_group; + } } }
\ No newline at end of file diff --git a/libdino/src/service/history_sync.vala b/libdino/src/service/history_sync.vala index 9bd3f54f..a7a99d2d 100644 --- a/libdino/src/service/history_sync.vala +++ b/libdino/src/service/history_sync.vala @@ -3,7 +3,7 @@ using Gee; using Xmpp; using Xmpp.Xep; using Dino.Entities; -using Qlite; +using Dino.Qlite; public class Dino.HistorySync { diff --git a/libdino/src/service/ice.vala b/libdino/src/service/ice.vala new file mode 100644 index 00000000..0e168be9 --- /dev/null +++ b/libdino/src/service/ice.vala @@ -0,0 +1,538 @@ +using Gee; + +using Dino.Crypto; +using Xmpp; +using Xmpp.Xep; + +private extern const size_t NICE_ADDRESS_STRING_LEN; + +public class Dino.IceUdpModule : Xmpp.Xep.JingleIceUdp.Module { + + public string? stun_ip = null; + public uint stun_port = 0; + public string? turn_ip = null; + public Xep.ExternalServiceDiscovery.Service? turn_service = null; + + private static weak Nice.Agent? agent; + private HashMap<string, DtlsSrtp.CredentialsCapsule> cerds = new HashMap<string, DtlsSrtp.CredentialsCapsule>(); + + private uint timeout = 0; + + public override void attach(XmppStream stream) { + base.attach(stream); + external_discovery_refresh_services.begin(stream); + } + public override void detach(XmppStream stream) { + base.detach(stream); + stop_external_discovery_refresh_services(); + } + + private Nice.Agent get_agent() { + Nice.Agent? agent = this.agent; + if (agent == null) { + Nice.debug_enable(true); + agent = new Nice.Agent(MainContext.@default(), Nice.Compatibility.RFC5245); + if (stun_ip != null) { + agent.stun_server = stun_ip; + agent.stun_server_port = stun_port; + } + agent.ice_tcp = false; + agent.set_software("Dino"); + agent.weak_ref(agent_unweak); + this.agent = agent; + debug("STUN server for libnice %s %u", agent.stun_server, agent.stun_server_port); + } + return agent; + } + + private void agent_unweak() { + this.agent = null; + } + + public override Jingle.TransportParameters create_transport_parameters(XmppStream stream, uint8 components, Jid local_full_jid, Jid peer_full_jid) { + DtlsSrtp.CredentialsCapsule? cred = get_create_credentials(local_full_jid, peer_full_jid); + return new IceUdpTransportParameters(get_agent(), cred, turn_service, turn_ip, components, local_full_jid, peer_full_jid); + } + + public override Jingle.TransportParameters parse_transport_parameters(XmppStream stream, uint8 components, Jid local_full_jid, Jid peer_full_jid, StanzaNode transport) throws Jingle.IqError { + DtlsSrtp.CredentialsCapsule? cred = get_create_credentials(local_full_jid, peer_full_jid); + return new IceUdpTransportParameters(get_agent(), cred, turn_service, turn_ip, components, local_full_jid, peer_full_jid, transport); + } + + private DtlsSrtp.CredentialsCapsule? get_create_credentials(Jid local_full_jid, Jid peer_full_jid) { + string from_to_id = local_full_jid.to_string() + peer_full_jid.to_string(); + try { + if (!cerds.has_key(from_to_id)) cerds[from_to_id] = DtlsSrtp.generate_credentials(); + } catch (GLib.Error e) { + warning("Error creating dtls credentials: %s", e.message); + } + return cerds[from_to_id]; + } + + private void stop_external_discovery_refresh_services() { + if (timeout != 0) { + Source.remove(timeout); + timeout = 0; + } + } + + private async void external_discovery_refresh_services(XmppStream stream) { + stop_external_discovery_refresh_services(); + Gee.List<Xep.ExternalServiceDiscovery.Service> services = yield ExternalServiceDiscovery.request_services(stream); + foreach (Xep.ExternalServiceDiscovery.Service service in services) { + if (service.transport == "udp" && (service.ty == "stun" || service.ty == "turn")) { + InetAddress? ip = yield lookup_ipv4_addess(service.host); + if (ip == null) continue; + + if (ip.is_any || ip.is_link_local || ip.is_loopback || ip.is_multicast || ip.is_site_local) { + warning("Ignoring STUN/TURN server at %s", service.host); + continue; + } + + if (service.ty == "stun") { + debug("Server offers STUN server: %s:%u, resolved to %s", service.host, service.port, ip.to_string()); + stun_ip = ip.to_string(); + stun_port = service.port; + } else if (service.ty == "turn") { + debug("Server offers TURN server: %s:%u, resolved to %s", service.host, service.port, ip.to_string()); + turn_ip = ip.to_string(); + turn_service = service; + } + } + } + if (stun_ip == null) { + InetAddress ip = yield lookup_ipv4_addess("stun.dino.im"); + if (ip == null) return; + + debug("Using fallback STUN server: stun.dino.im:7886, resolved to %s", ip.to_string()); + + stun_ip = ip.to_string(); + stun_port = 7886; + } + + // Refresh TURN credentials before they expire + if (turn_service != null) { + DateTime? expires = turn_service.expires; + if (expires != null) { + uint refresh_delay = (uint) (expires.to_unix() - new DateTime.now_utc().to_unix()) / 2; + + if (refresh_delay >= 300 && refresh_delay <= (int64) uint.MAX) { // 5 min < refetch < max + debug("Re-fetching TURN credentials in %u sec", refresh_delay); + + stop_external_discovery_refresh_services(); + timeout = Timeout.add_seconds((uint) refresh_delay, () => { + timeout = 0; + external_discovery_refresh_services.begin(stream); + return false; + }); + } else { + warning("TURN credentials' expiry time = %u, *not* re-fetching", refresh_delay); + } + } + } + } + + private async InetAddress? lookup_ipv4_addess(string host) { + try { + Resolver resolver = Resolver.get_default(); + GLib.List<GLib.InetAddress>? ips = yield resolver.lookup_by_name_async(host); + foreach (GLib.InetAddress ina in ips) { + if (ina.get_family() != SocketFamily.IPV4) continue; + return ina; + } + } catch (GLib.Error e) { + warning("Failed looking up IP address of %s", host); + } + return null; + } +} + +public class Dino.IceUdpTransportParameters : JingleIceUdp.IceUdpTransportParameters { + private Nice.Agent agent; + private uint stream_id; + private bool we_want_connection; + private bool remote_credentials_set; + private Map<uint8, DatagramConnection> connections = new HashMap<uint8, DatagramConnection>(); + private DtlsSrtp.Handler? dtls_srtp_handler; + private MainContext thread_context; + private MainLoop thread_loop; + + private static bool is_component_ready(Nice.Agent agent, uint stream_id, uint component_id) { + var state = agent.get_component_state(stream_id, component_id); + return state == Nice.ComponentState.CONNECTED || state == Nice.ComponentState.READY; + } + + private class DatagramConnection : Jingle.DatagramConnection { + private Nice.Agent agent; + private DtlsSrtp.Handler? dtls_srtp_handler; + private uint stream_id; + private string? error; + private ulong datagram_received_id; + + internal DatagramConnection(Nice.Agent agent, DtlsSrtp.Handler? dtls_srtp_handler, uint stream_id, uint8 component_id) { + this.agent = agent; + this.dtls_srtp_handler = dtls_srtp_handler; + this.stream_id = stream_id; + this.component_id = component_id; + this.datagram_received_id = this.datagram_received.connect((datagram) => { + bytes_received += datagram.length; + }); + } + + public override async void terminate(bool we_terminated, string? reason_string = null, string? reason_text = null) { + yield base.terminate(we_terminated, reason_string, reason_text); + this.disconnect(datagram_received_id); + agent = null; + dtls_srtp_handler = null; + } + + public override void send_datagram(Bytes datagram) { + if (this.agent != null && is_component_ready(agent, stream_id, component_id)) { + try { + if (dtls_srtp_handler != null) { + uint8[] encrypted_data = dtls_srtp_handler.process_outgoing_data(component_id, datagram.get_data()); + if (encrypted_data == null) return; + GLib.OutputVector vector = { encrypted_data, encrypted_data.length }; + GLib.OutputVector[] vectors = { vector }; + Nice.OutputMessage message = { vectors }; + Nice.OutputMessage[] messages = { message }; + agent.send_messages_nonblocking(stream_id, component_id, messages); + } else { + GLib.OutputVector vector = { datagram.get_data(), datagram.get_size() }; + GLib.OutputVector[] vectors = { vector }; + Nice.OutputMessage message = { vectors }; + Nice.OutputMessage[] messages = { message }; + agent.send_messages_nonblocking(stream_id, component_id, messages); + } + bytes_sent += datagram.length; + } catch (GLib.Error e) { + warning("%s while send_datagram stream %u component %u", e.message, stream_id, component_id); + } + } + } + } + + internal IceUdpTransportParameters(Nice.Agent agent, DtlsSrtp.CredentialsCapsule? credentials, Xep.ExternalServiceDiscovery.Service? turn_service, string? turn_ip, uint8 components, Jid local_full_jid, Jid peer_full_jid, StanzaNode? node = null) { + base(components, local_full_jid, peer_full_jid, node); + this.we_want_connection = (node == null); + this.agent = agent; + + if (this.peer_fingerprint != null || !incoming) { + dtls_srtp_handler = setup_dtls(this, credentials); + own_fingerprint = dtls_srtp_handler.own_fingerprint; + if (incoming) { + own_setup = "active"; + dtls_srtp_handler.mode = DtlsSrtp.Mode.CLIENT; + dtls_srtp_handler.peer_fingerprint = peer_fingerprint; + dtls_srtp_handler.peer_fp_algo = peer_fp_algo; + } else { + own_setup = "actpass"; + dtls_srtp_handler.mode = DtlsSrtp.Mode.SERVER; + dtls_srtp_handler.setup_dtls_connection.begin((_, res) => { + var content_encryption = dtls_srtp_handler.setup_dtls_connection.end(res); + if (content_encryption != null) { + this.content.encryptions[content_encryption.encryption_ns] = content_encryption; + } + }); + } + } + + agent.candidate_gathering_done.connect(on_candidate_gathering_done); + agent.initial_binding_request_received.connect(on_initial_binding_request_received); + agent.component_state_changed.connect(on_component_state_changed); + agent.new_selected_pair_full.connect(on_new_selected_pair_full); + agent.new_candidate_full.connect(on_new_candidate); + + agent.controlling_mode = !incoming; + stream_id = agent.add_stream(components); + thread_context = new MainContext(); + new Thread<void*>(@"ice-thread-$stream_id", () => { + thread_context.push_thread_default(); + thread_loop = new MainLoop(thread_context, false); + thread_loop.run(); + thread_context.pop_thread_default(); + return null; + }); + + if (turn_ip != null) { + for (uint8 component_id = 1; component_id <= components; component_id++) { + agent.set_relay_info(stream_id, component_id, turn_ip, turn_service.port, turn_service.username, turn_service.password, Nice.RelayType.UDP); + debug("TURN info (component %i) %s:%u", component_id, turn_ip, turn_service.port); + } + } + string ufrag; + string pwd; + agent.get_local_credentials(stream_id, out ufrag, out pwd); + init(ufrag, pwd); + + for (uint8 component_id = 1; component_id <= components; component_id++) { + // We don't properly get local candidates before this call + agent.attach_recv(stream_id, component_id, thread_context, on_recv); + } + + agent.gather_candidates(stream_id); + } + + private static DtlsSrtp.Handler setup_dtls(IceUdpTransportParameters tp, DtlsSrtp.CredentialsCapsule credentials) { + var weak_self = WeakRef(tp); + DtlsSrtp.Handler dtls_srtp = new DtlsSrtp.Handler.with_cert(credentials); + dtls_srtp.send_data.connect((data) => { + IceUdpTransportParameters self = (IceUdpTransportParameters) weak_self.get(); + if (self != null) self.agent.send(self.stream_id, 1, data); + }); + return dtls_srtp; + } + + private void on_candidate_gathering_done(uint stream_id) { + if (stream_id != this.stream_id) return; + debug("on_candidate_gathering_done in %u", stream_id); + + for (uint8 i = 1; i <= components; i++) { + foreach (unowned Nice.Candidate nc in agent.get_local_candidates(stream_id, i)) { + if (nc.transport == Nice.CandidateTransport.UDP) { + JingleIceUdp.Candidate? candidate = candidate_to_jingle(nc); + if (candidate == null) continue; + debug("Local candidate summary: %s", agent.generate_local_candidate_sdp(nc)); + } + } + } + } + + private void on_new_candidate(Nice.Candidate nc) { + if (nc.stream_id != stream_id) return; + JingleIceUdp.Candidate? candidate = candidate_to_jingle(nc); + if (candidate == null) return; + + if (nc.transport == Nice.CandidateTransport.UDP) { + // Execution was in the agent thread before + add_local_candidate_threadsafe(candidate); + } + } + + private bool bytes_equal(uint8[] a1, uint8[] a2) { + return a1.length == a2.length && Memory.cmp(a1, a2, a1.length) == 0; + } + + public override void handle_transport_accept(StanzaNode transport) throws Jingle.IqError { + debug("on_transport_accept from %s", peer_full_jid.to_string()); + base.handle_transport_accept(transport); + + if (dtls_srtp_handler != null && peer_fingerprint != null) { + if (dtls_srtp_handler.peer_fingerprint != null) { + if (!bytes_equal(dtls_srtp_handler.peer_fingerprint, peer_fingerprint)) { + warning("Tried to replace certificate fingerprint mid use. We don't allow that."); + peer_fingerprint = dtls_srtp_handler.peer_fingerprint; + peer_fp_algo = dtls_srtp_handler.peer_fp_algo; + } + } else { + dtls_srtp_handler.peer_fingerprint = peer_fingerprint; + dtls_srtp_handler.peer_fp_algo = peer_fp_algo; + } + if (peer_setup == "passive") { + dtls_srtp_handler.mode = DtlsSrtp.Mode.CLIENT; + dtls_srtp_handler.stop_dtls_connection(); + dtls_srtp_handler.setup_dtls_connection.begin((_, res) => { + var content_encryption = dtls_srtp_handler.setup_dtls_connection.end(res); + if (content_encryption != null) { + this.content.encryptions[content_encryption.encryption_ns] = content_encryption; + } + }); + } + } else { + dtls_srtp_handler = null; + } + } + + public override void handle_transport_info(StanzaNode transport) throws Jingle.IqError { + debug("on_transport_info from %s", peer_full_jid.to_string()); + base.handle_transport_info(transport); + + if (dtls_srtp_handler != null && peer_fingerprint != null) { + if (dtls_srtp_handler.peer_fingerprint != null) { + if (!bytes_equal(dtls_srtp_handler.peer_fingerprint, peer_fingerprint)) { + warning("Tried to replace certificate fingerprint mid use. We don't allow that."); + peer_fingerprint = dtls_srtp_handler.peer_fingerprint; + peer_fp_algo = dtls_srtp_handler.peer_fp_algo; + } + } else { + dtls_srtp_handler.peer_fingerprint = peer_fingerprint; + dtls_srtp_handler.peer_fp_algo = peer_fp_algo; + } + } + + if (!we_want_connection) return; + + if (remote_ufrag != null && remote_pwd != null && !remote_credentials_set) { + agent.set_remote_credentials(stream_id, remote_ufrag, remote_pwd); + remote_credentials_set = true; + } + for (uint8 i = 1; i <= components; i++) { + SList<Nice.Candidate> candidates = new SList<Nice.Candidate>(); + foreach (JingleIceUdp.Candidate candidate in remote_candidates) { + if (candidate.component == i) { + candidates.append(candidate_to_nice(candidate)); + } + } + int new_candidates = agent.set_remote_candidates(stream_id, i, candidates); + debug("Updated to %i remote candidates for candidate %u via transport info", new_candidates, i); + } + } + + public override void create_transport_connection(XmppStream stream, Jingle.Content content) { + debug("create_transport_connection: %s", content.session.sid); + debug("local_credentials: %s %s", local_ufrag, local_pwd); + debug("remote_credentials: %s %s", remote_ufrag, remote_pwd); + debug("expected incoming credentials: %s %s", local_ufrag + ":" + remote_ufrag, local_pwd); + debug("expected outgoing credentials: %s %s", remote_ufrag + ":" + local_ufrag, remote_pwd); + + we_want_connection = true; + + if (remote_ufrag != null && remote_pwd != null && !remote_credentials_set) { + agent.set_remote_credentials(stream_id, remote_ufrag, remote_pwd); + remote_credentials_set = true; + } + for (uint8 i = 1; i <= components; i++) { + SList<Nice.Candidate> candidates = new SList<Nice.Candidate>(); + foreach (JingleIceUdp.Candidate candidate in remote_candidates) { + if (candidate.ip.has_prefix("fe80::")) continue; + if (candidate.component == i) { + candidates.append(candidate_to_nice(candidate)); + debug("remote candidate: %s", agent.generate_local_candidate_sdp(candidate_to_nice(candidate))); + } + } + int new_candidates = agent.set_remote_candidates(stream_id, i, candidates); + debug("Initiated component %u with %i remote candidates", i, new_candidates); + + connections[i] = new DatagramConnection(agent, dtls_srtp_handler, stream_id, i); + content.set_transport_connection(connections[i], i); + } + + base.create_transport_connection(stream, content); + } + + private void on_component_state_changed(uint stream_id, uint component_id, uint state) { + if (stream_id != this.stream_id) return; + debug("stream %u component %u state changed to %s", stream_id, component_id, agent.get_component_state(stream_id, component_id).to_string()); + may_consider_ready(stream_id, component_id); + if (incoming && dtls_srtp_handler != null && !dtls_srtp_handler.ready && is_component_ready(agent, stream_id, component_id) && dtls_srtp_handler.mode == DtlsSrtp.Mode.CLIENT) { + dtls_srtp_handler.setup_dtls_connection.begin((_, res) => { + Jingle.ContentEncryption? encryption = dtls_srtp_handler.setup_dtls_connection.end(res); + if (encryption != null) { + this.content.encryptions[encryption.encryption_ns] = encryption; + } + }); + } + } + + private void may_consider_ready(uint stream_id, uint component_id) { + if (stream_id != this.stream_id) return; + if (connections.has_key((uint8) component_id) && !connections[(uint8)component_id].ready && is_component_ready(agent, stream_id, component_id) && (dtls_srtp_handler == null || dtls_srtp_handler.ready)) { + connections[(uint8)component_id].ready = true; + } + } + + private void on_initial_binding_request_received(uint stream_id) { + if (stream_id != this.stream_id) return; + debug("initial_binding_request_received"); + } + + private void on_new_selected_pair_full(uint stream_id, uint component_id, Nice.Candidate p1, Nice.Candidate p2) { + if (stream_id != this.stream_id) return; + debug("new_selected_pair_full %u [%s, %s]", component_id, agent.generate_local_candidate_sdp(p1), agent.generate_local_candidate_sdp(p2)); + } + + private void on_recv(Nice.Agent agent, uint stream_id, uint component_id, uint8[] data) { + if (stream_id != this.stream_id) return; + uint8[] decrypt_data = null; + if (dtls_srtp_handler != null) { + try { + decrypt_data = dtls_srtp_handler.process_incoming_data(component_id, data); + if (decrypt_data == null) return; + } catch (Crypto.Error e) { + warning("%s while on_recv stream %u component %u", e.message, stream_id, component_id); + return; + } + } + may_consider_ready(stream_id, component_id); + if (connections.has_key((uint8) component_id)) { + if (!connections[(uint8) component_id].ready) { + debug("on_recv stream %u component %u when state %s", stream_id, component_id, agent.get_component_state(stream_id, component_id).to_string()); + } + connections[(uint8) component_id].datagram_received(new Bytes(decrypt_data ?? data)); + } else { + debug("on_recv stream %u component %u length %u", stream_id, component_id, data.length); + } + } + + private static Nice.Candidate candidate_to_nice(JingleIceUdp.Candidate c) { + Nice.CandidateType type; + switch (c.type_) { + case JingleIceUdp.Candidate.Type.HOST: type = Nice.CandidateType.HOST; break; + case JingleIceUdp.Candidate.Type.PRFLX: type = Nice.CandidateType.PEER_REFLEXIVE; break; + case JingleIceUdp.Candidate.Type.RELAY: type = Nice.CandidateType.RELAYED; break; + case JingleIceUdp.Candidate.Type.SRFLX: type = Nice.CandidateType.SERVER_REFLEXIVE; break; + default: assert_not_reached(); + } + + Nice.Candidate candidate = new Nice.Candidate(type); + candidate.component_id = c.component; + char[] foundation = new char[Nice.CANDIDATE_MAX_FOUNDATION]; + Memory.copy(foundation, c.foundation.data, size_t.min(c.foundation.length, Nice.CANDIDATE_MAX_FOUNDATION - 1)); + candidate.foundation = foundation; + candidate.addr = Nice.Address(); + candidate.addr.init(); + candidate.addr.set_from_string(c.ip); + candidate.addr.set_port(c.port); + candidate.priority = c.priority; + if (c.rel_addr != null) { + candidate.base_addr = Nice.Address(); + candidate.base_addr.init(); + candidate.base_addr.set_from_string(c.rel_addr); + candidate.base_addr.set_port(c.rel_port); + } + candidate.transport = Nice.CandidateTransport.UDP; + return candidate; + } + + private static JingleIceUdp.Candidate? candidate_to_jingle(Nice.Candidate nc) { + JingleIceUdp.Candidate candidate = new JingleIceUdp.Candidate(); + switch (nc.type) { + case Nice.CandidateType.HOST: candidate.type_ = JingleIceUdp.Candidate.Type.HOST; break; + case Nice.CandidateType.PEER_REFLEXIVE: candidate.type_ = JingleIceUdp.Candidate.Type.PRFLX; break; + case Nice.CandidateType.RELAYED: candidate.type_ = JingleIceUdp.Candidate.Type.RELAY; break; + case Nice.CandidateType.SERVER_REFLEXIVE: candidate.type_ = JingleIceUdp.Candidate.Type.SRFLX; break; + default: assert_not_reached(); + } + candidate.component = (uint8) nc.component_id; + candidate.foundation = ((string)nc.foundation).dup(); + candidate.generation = 0; + candidate.id = Random.next_int().to_string("%08x"); // TODO + + char[] res = new char[NICE_ADDRESS_STRING_LEN]; + nc.addr.to_string(res); + candidate.ip = (string) res; + candidate.network = 0; // TODO + candidate.port = (uint16) nc.addr.get_port(); + candidate.priority = nc.priority; + candidate.protocol = "udp"; + if (nc.base_addr.is_valid() && !nc.base_addr.equal(nc.addr)) { + res = new char[NICE_ADDRESS_STRING_LEN]; + nc.base_addr.to_string(res); + candidate.rel_addr = (string) res; + candidate.rel_port = (uint16) nc.base_addr.get_port(); + } + if (candidate.ip.has_prefix("fe80::")) return null; + + return candidate; + } + + public override void dispose() { + base.dispose(); + agent = null; + dtls_srtp_handler = null; + connections.clear(); + if (thread_loop != null) { + thread_loop.quit(); + } + } +} diff --git a/libdino/src/service/message_correction.vala b/libdino/src/service/message_correction.vala index 77625c72..5f04bdbc 100644 --- a/libdino/src/service/message_correction.vala +++ b/libdino/src/service/message_correction.vala @@ -3,7 +3,7 @@ using Gee; using Xmpp; using Xmpp.Xep; using Dino.Entities; -using Qlite; +using Dino.Qlite; namespace Dino { @@ -29,7 +29,6 @@ public class MessageCorrection : StreamInteractionModule, MessageListener { public MessageCorrection(StreamInteractor stream_interactor, Database db) { this.stream_interactor = stream_interactor; this.db = db; - stream_interactor.account_added.connect(on_account_added); stream_interactor.get_module(MessageProcessor.IDENTITY).received_pipeline.connect(this); stream_interactor.get_module(MessageProcessor.IDENTITY).build_message_stanza.connect(check_add_correction_node); stream_interactor.get_module(PresenceManager.IDENTITY).received_offline_presence.connect((jid, account) => { @@ -232,23 +231,6 @@ public class MessageCorrection : StreamInteractionModule, MessageListener { return -1; } - private void on_account_added(Account account) { - Gee.List<Conversation> conversations = stream_interactor.get_module(ConversationManager.IDENTITY).get_active_conversations(account); - foreach (Conversation conversation in conversations) { - if (conversation.type_ != Conversation.Type.CHAT) continue; - - HashMap<Jid, Message> last_conversation_messages = new HashMap<Jid, Message>(Jid.hash_func, Jid.equals_func); - Gee.List<Message> messages = stream_interactor.get_module(MessageStorage.IDENTITY).get_messages(conversation); - for (int i = messages.size - 1; i > 0; i--) { - Message message = messages[i]; - if (!last_conversation_messages.has_key(message.from) && message.edit_to == null) { - last_conversation_messages[message.from] = message; - } - } - last_messages[conversation] = last_conversation_messages; - } - } - private void cache_unmatched_correction(ContentItem content_item, Conversation conversation) { MessageItem message_item = content_item as MessageItem; if (message_item == null || message_item.message.edit_to == null) return; diff --git a/libdino/src/service/message_deletion.vala b/libdino/src/service/message_deletion.vala index 4467311e..0060a7e2 100644 --- a/libdino/src/service/message_deletion.vala +++ b/libdino/src/service/message_deletion.vala @@ -3,7 +3,6 @@ using Gee; using Xmpp; using Xmpp.Xep; using Dino.Entities; -using Qlite; namespace Dino { diff --git a/libdino/src/service/message_processor.vala b/libdino/src/service/message_processor.vala index b9c23240..7997af67 100644 --- a/libdino/src/service/message_processor.vala +++ b/libdino/src/service/message_processor.vala @@ -3,7 +3,7 @@ using Gee; using Xmpp; using Xmpp.Xep; using Dino.Entities; -using Qlite; +using Dino.Qlite; namespace Dino { @@ -472,12 +472,8 @@ public class MessageProcessor : StreamInteractionModule, Object { // Set an origin ID if a MUC doen't guarantee to keep IDs if (conversation.type_ == Conversation.Type.GROUPCHAT) { - Xep.Muc.Flag? flag = stream.get_flag(Xep.Muc.Flag.IDENTITY); - if (flag == null) { - message.marked = Entities.Message.Marked.UNSENT; - return; - } - if(!flag.has_room_feature(conversation.counterpart, Xep.Muc.Feature.STABLE_ID)) { + var supports_stable_ids = stream_interactor.get_module(EntityInfo.IDENTITY).has_feature_cached(conversation.account, conversation.counterpart, Xep.Muc.FEATURE_STABLE_ID); + if(!supports_stable_ids) { UniqueStableStanzaIDs.set_origin_id(new_message, message.stanza_id); } } diff --git a/libdino/src/service/message_storage.vala b/libdino/src/service/message_storage.vala index d6b8d6e3..0bcbe533 100644 --- a/libdino/src/service/message_storage.vala +++ b/libdino/src/service/message_storage.vala @@ -1,8 +1,8 @@ using Xmpp; using Gee; -using Qlite; using Dino.Entities; +using Dino.Qlite; namespace Dino { @@ -35,41 +35,82 @@ public class MessageStorage : StreamInteractionModule, Object { cache_message(message, conversation); } - public Gee.List<Message> get_messages(Conversation conversation, int count = 50) { - var query = db.message.select() + public Message? get_last_message(Conversation conversation) { + Gee.List<Message> messages = get_messages(conversation, 1); + + if (messages.size > 0) { + return messages[0]; + } + + return null; + } + + public Message? get_last_message_by_sent_resource(Conversation conversation, string resource) { + RowOption row = db.message.select({db.message.id}) .with(db.message.account_id, "=", conversation.account.id) .with(db.message.counterpart_id, "=", db.get_jid_id(conversation.counterpart)) .with(db.message.type_, "=", (int) Util.get_message_type_for_conversation(conversation)) + .with(db.message.our_resource, "=", resource) + .with(db.message.direction, "=", Message.DIRECTION_SENT) .order_by(db.message.time, "DESC") - .outer_join_with(db.message_occupant_id, db.message_occupant_id.message_id, db.message.id) - .outer_join_with(db.message_correction, db.message_correction.message_id, db.message.id) - .outer_join_with(db.reply, db.reply.message_id, db.message.id) - .limit(count); + .single().row(); + return get_message_by_id_row_opt(row, conversation); + } - Gee.List<Message> ret = new LinkedList<Message>(Message.equals_func); - foreach (Row row in query) { - Message? message = messages_by_db_id[row[db.message.id]]; - if (message == null) { - message = create_message_from_row(row, conversation); + public Gee.List<Message> get_messages(Conversation conversation, int count = 50, Message.Type? type = null, DateTime? before = null, DateTime? after = null, int id = 0) { + QueryBuilder select = db.message.select(); + + if (before != null) { + if (id > 0) { + select.where(@"time < ? OR (time = ? AND message.id < ?)", { before.to_unix().to_string(), before.to_unix().to_string(), id.to_string() }); + } else { + select.with(db.message.id, "<", id); } - ret.insert(0, message); + } + if (after != null) { + if (id > 0) { + select.where(@"time > ? OR (time = ? AND message.id > ?)", { after.to_unix().to_string(), after.to_unix().to_string(), id.to_string() }); + } else { + select.with(db.message.time, ">", (long) after.to_unix()); + } + if (id > 0) { + select.with(db.message.id, ">", id); + } + } else { + select.order_by(db.message.time, "DESC"); } - return ret; - } + select.with(db.message.counterpart_id, "=", db.get_jid_id(conversation.counterpart)) + .with(db.message.account_id, "=", conversation.account.id) + .limit(count); + if (conversation.counterpart.resourcepart != null) { + select.with(db.message.counterpart_resource, "=", conversation.counterpart.resourcepart); + } + if (type != null) { + select.with(db.message.type_, "=", (int) type); + } - public Message? get_last_message(Conversation conversation) { - Gee.List<Message> messages = get_messages(conversation, 1); + select.outer_join_with(db.real_jid, db.real_jid.message_id, db.message.id); + select.outer_join_with(db.message_correction, db.message_correction.message_id, db.message.id); + select.outer_join_with(db.message_occupant_id, db.message_occupant_id.message_id, db.message.id); + select.outer_join_with(db.reply, db.reply.message_id, db.message.id); - if (messages.size > 0) { - return messages[0]; + LinkedList<Message> ret = new LinkedList<Message>(); + foreach (Row row in select) { + try { + Message? message = messages_by_db_id[row[db.message.id]]; + if (message == null) { + message = create_message_from_row(row, conversation); + } + } catch (InvalidJidError e) { + warning("Ignoring message with invalid Jid: %s", e.message); + } } - - return null; + return ret; } public Gee.List<MessageItem> get_messages_before_message(Conversation? conversation, DateTime before, int id, int count = 20) { - Gee.List<Message> db_messages = db.get_messages(conversation.counterpart, conversation.account, Util.get_message_type_for_conversation(conversation), count, before, null, id); + Gee.List<Message> db_messages = get_messages(conversation, count, Util.get_message_type_for_conversation(conversation), before, null, id); Gee.List<MessageItem> ret = new ArrayList<MessageItem>(); foreach (Message message in db_messages) { ret.add(new MessageItem(message, conversation, -1)); @@ -78,7 +119,7 @@ public class MessageStorage : StreamInteractionModule, Object { } public Gee.List<MessageItem> get_messages_after_message(Conversation? conversation, DateTime after, int id, int count = 20) { - Gee.List<Message> db_messages = db.get_messages(conversation.counterpart, conversation.account, Util.get_message_type_for_conversation(conversation), count, null, after, id); + Gee.List<Message> db_messages = get_messages(conversation, count, Util.get_message_type_for_conversation(conversation), null, after, id); Gee.List<MessageItem> ret = new ArrayList<MessageItem>(); foreach (Message message in db_messages) { ret.add(new MessageItem(message, conversation, -1)); @@ -86,6 +127,11 @@ public class MessageStorage : StreamInteractionModule, Object { return ret; } + private Message? get_message_by_id_row_opt(RowOption row_option, Conversation conversation) { + if (!row_option.is_present()) return null; + return get_message_by_id(row_option[db.message.id], conversation); + } + public Message? get_message_by_id(int id, Conversation conversation) { Message? message = messages_by_db_id[id]; if (message != null) { @@ -93,9 +139,11 @@ public class MessageStorage : StreamInteractionModule, Object { } RowOption row_option = db.message.select().with(db.message.id, "=", id) + .outer_join_with(db.real_jid, db.real_jid.message_id, db.message.id) .outer_join_with(db.message_occupant_id, db.message_occupant_id.message_id, db.message.id) .outer_join_with(db.message_correction, db.message_correction.message_id, db.message.id) .outer_join_with(db.reply, db.reply.message_id, db.message.id) + .single() .row(); return create_message_from_row_opt(row_option, conversation); @@ -103,9 +151,9 @@ public class MessageStorage : StreamInteractionModule, Object { public Message? get_message_by_referencing_id(string id, Conversation conversation) { if (conversation.type_ == Conversation.Type.CHAT) { - return stream_interactor.get_module(MessageStorage.IDENTITY).get_message_by_stanza_id(id, conversation); + return get_message_by_stanza_id(id, conversation); } else { - return stream_interactor.get_module(MessageStorage.IDENTITY).get_message_by_server_id(id, conversation); + return get_message_by_server_id(id, conversation); } } @@ -117,15 +165,12 @@ public class MessageStorage : StreamInteractionModule, Object { } } - var query = db.message.select() + var query = db.message.select({db.message.id}) .with(db.message.account_id, "=", conversation.account.id) .with(db.message.counterpart_id, "=", db.get_jid_id(conversation.counterpart)) .with(db.message.type_, "=", (int) Util.get_message_type_for_conversation(conversation)) .with(db.message.stanza_id, "=", stanza_id) - .order_by(db.message.time, "DESC") - .outer_join_with(db.message_occupant_id, db.message_occupant_id.message_id, db.message.id) - .outer_join_with(db.message_correction, db.message_correction.message_id, db.message.id) - .outer_join_with(db.reply, db.reply.message_id, db.message.id); + .order_by(db.message.time, "DESC"); if (conversation.counterpart.resourcepart != null) { query.with(db.message.counterpart_resource, "=", conversation.counterpart.resourcepart); @@ -133,7 +178,7 @@ public class MessageStorage : StreamInteractionModule, Object { RowOption row_option = query.single().row(); - return create_message_from_row_opt(row_option, conversation); + return get_message_by_id_row_opt(row_option, conversation); } public Message? get_message_by_server_id(string server_id, Conversation conversation) { @@ -144,25 +189,20 @@ public class MessageStorage : StreamInteractionModule, Object { } } - var query = db.message.select() + var query = db.message.select({db.message.id}) .with(db.message.account_id, "=", conversation.account.id) .with(db.message.counterpart_id, "=", db.get_jid_id(conversation.counterpart)) .with(db.message.type_, "=", (int) Util.get_message_type_for_conversation(conversation)) .with(db.message.server_id, "=", server_id) - .order_by(db.message.time, "DESC") - .outer_join_with(db.message_occupant_id, db.message_occupant_id.message_id, db.message.id) - .outer_join_with(db.message_correction, db.message_correction.message_id, db.message.id) - .outer_join_with(db.reply, db.reply.message_id, db.message.id); + .order_by(db.message.time, "DESC"); - if (conversation.counterpart.resourcepart == null) { - query.with_null(db.message.counterpart_resource); - } else { + if (conversation.counterpart.resourcepart != null) { query.with(db.message.counterpart_resource, "=", conversation.counterpart.resourcepart); } RowOption row_option = query.single().row(); - return create_message_from_row_opt(row_option, conversation); + return get_message_by_id_row_opt(row_option, conversation); } private Message? create_message_from_row_opt(RowOption row_option, Conversation conversation) { diff --git a/libdino/src/service/module_manager.vala b/libdino/src/service/module_manager.vala index 5ba4fe80..a6efe629 100644 --- a/libdino/src/service/module_manager.vala +++ b/libdino/src/service/module_manager.vala @@ -69,6 +69,7 @@ public class ModuleManager { module_map[account].add(new Presence.Module()); module_map[account].add(new Xmpp.MessageModule()); module_map[account].add(new Xmpp.MessageArchiveManagement.Module()); + module_map[account].add(new Xep.EntityCapabilities.Module()); module_map[account].add(new Xep.MessageCarbons.Module()); module_map[account].add(new Xep.BitsOfBinary.Module()); module_map[account].add(new Xep.Muc.Module()); @@ -84,7 +85,7 @@ public class ModuleManager { module_map[account].add(new Xep.InBandRegistration.Module()); module_map[account].add(new Xep.HttpFileUpload.Module()); module_map[account].add(new Xep.Reactions.Module()); - module_map[account].add(new Xep.Socks5Bytestreams.Module()); + module_map[account].add(new Xep.Socks5Bytestreams.Module.with_local_ip_address_handler(Util.get_local_ip_addresses)); module_map[account].add(new Xep.InBandBytestreams.Module()); module_map[account].add(new Xep.Jingle.Module()); module_map[account].add(new Xep.JingleSocks5Bytestreams.Module()); @@ -96,11 +97,12 @@ public class ModuleManager { module_map[account].add(new Xep.JingleMessageInitiation.Module()); module_map[account].add(new Xep.OccupantIds.Module()); module_map[account].add(new Xep.MessageRetraction.Module()); - module_map[account].add(new Xep.JingleRawUdp.Module()); + module_map[account].add(new Xep.JingleRawUdp.Module.with_local_ip_address_handler(Util.get_local_ip_addresses)); module_map[account].add(new Xep.Muji.Module()); module_map[account].add(new Xep.CallInvites.Module()); module_map[account].add(new Xep.Coin.Module()); module_map[account].add(new Xep.MessageDisplayedSynchronization.Module()); + module_map[account].add(new IceUdpModule()); initialize_account_modules(account, module_map[account]); } } diff --git a/libdino/src/service/muc_manager.vala b/libdino/src/service/muc_manager.vala index b47a52bf..4959074b 100644 --- a/libdino/src/service/muc_manager.vala +++ b/libdino/src/service/muc_manager.vala @@ -108,19 +108,11 @@ public class MucManager : StreamInteractionModule, Object { joined_conversation.nickname = nick; stream_interactor.get_module(ConversationManager.IDENTITY).start_conversation(joined_conversation); - bool can_do_affiliation_versioning = yield entity_info.has_feature(account, jid, Xmpp.Xep.MucAffiliationsVersioning.NS_URI); - if (!can_do_affiliation_versioning) { - stream.get_module(Xep.Muc.Module.IDENTITY).fetch_affiliations.begin(stream, jid.bare_jid, () => { - muc_affiliations.clear_affiliations(account, jid); - persist_muc_affiliations(account, jid.bare_jid); - }); - } - if (can_do_mam) { var history_sync = stream_interactor.get_module(MessageProcessor.IDENTITY).history_sync; if (conversation == null) { // We never joined the conversation before, just fetch the latest MAM page - yield history_sync.fetch_latest_page(account, jid.bare_jid, null, new DateTime.from_unix_utc(0), cancellable); + history_sync.fetch_latest_page.begin(account, jid.bare_jid, null, new DateTime.from_unix_utc(0), cancellable); } else { // Fetch everything up to the last time the user actively joined if (!mucs_sync_cancellables.has_key(account)) { @@ -137,6 +129,17 @@ public class MucManager : StreamInteractionModule, Object { } } } + + bool can_do_affiliation_versioning = yield entity_info.has_feature(account, jid, Xmpp.Xep.MucAffiliationsVersioning.NS_URI); + if (!can_do_affiliation_versioning) { + stream.get_module(Xep.Muc.Module.IDENTITY).fetch_affiliations.begin(stream, jid.bare_jid, () => { + muc_affiliations.clear_affiliations(account, jid); + persist_muc_affiliations(account, jid.bare_jid); + }); + } + + room_info_updated(account, jid); // EntityInfo.has_feature trigged a disco query + } else if (res.muc_error != null) { // Join failed enter_errors[jid] = res.muc_error; @@ -254,19 +257,11 @@ public class MucManager : StreamInteractionModule, Object { //the term `private room` is a short hand for members-only+non-anonymous rooms public bool is_private_room(Account account, Jid jid) { var entity_info = stream_interactor.get_module(EntityInfo.IDENTITY); - return entity_info.has_feature_offline(account, jid, "muc_membersonly") && entity_info.has_feature_offline(account, jid, "muc_nonanonymous"); + return entity_info.has_feature_offline(account, jid, Xep.Muc.FEATURE_MEMBERS_ONLY) && entity_info.has_feature_offline(account, jid, Xep.Muc.FEATURE_NON_ANONYMOUS); } public bool is_moderated_room(Account account, Jid jid) { - XmppStream? stream = stream_interactor.get_stream(account); - if (stream == null) { - return false; - } - Xep.Muc.Flag? flag = stream.get_flag(Xep.Muc.Flag.IDENTITY); - if (flag == null) { - return false; - } - return flag.has_room_feature(jid, Xep.Muc.Feature.MODERATED); + return stream_interactor.get_module(EntityInfo.IDENTITY).has_feature_cached(account, jid, Xep.Muc.FEATURE_MODERATED); } public bool is_public_room(Account account, Jid jid) { @@ -332,9 +327,13 @@ public class MucManager : StreamInteractionModule, Object { } public string? get_room_name(Account account, Jid jid) { - Xep.Muc.Flag? flag = get_muc_flag(account); - if (flag != null) { - return flag.get_room_name(jid); + var identities = stream_interactor.get_module(EntityInfo.IDENTITY).get_identities_offline(account, jid); + if (identities == null) return null; + + foreach (var identity in identities) { + if (identity.category == "conference") { + return identity.name; + } } return null; } @@ -471,8 +470,10 @@ public class MucManager : StreamInteractionModule, Object { private_room_occupants_updated(account, muc_jid); } }); - stream_interactor.module_manager.get_module(account, Xep.Muc.Module.IDENTITY).room_info_updated.connect( (stream, muc_jid) => { - room_info_updated(account, muc_jid); + stream_interactor.module_manager.get_module(account, Xep.Muc.Module.IDENTITY).room_info_changed.connect( (stream, muc_jid) => { + stream_interactor.get_module(EntityInfo.IDENTITY).get_info_result.begin(account, muc_jid, null, () => { + room_info_updated(account, muc_jid); + }); }); stream_interactor.module_manager.get_module(account, Xep.OccupantIds.Module.IDENTITY).received_own_occupant_id.connect( (stream, jid, occupant_id) => { if (!own_occupant_ids.has_key(account)) { diff --git a/libdino/src/service/occupant_id_store.vala b/libdino/src/service/occupant_id_store.vala index 662b9cbb..d2849652 100644 --- a/libdino/src/service/occupant_id_store.vala +++ b/libdino/src/service/occupant_id_store.vala @@ -1,8 +1,8 @@ using Xmpp; using Gee; -using Qlite; using Dino.Entities; +using Dino.Qlite; namespace Dino { diff --git a/libdino/src/service/reactions.vala b/libdino/src/service/reactions.vala index 16849c68..a8df614e 100644 --- a/libdino/src/service/reactions.vala +++ b/libdino/src/service/reactions.vala @@ -1,9 +1,9 @@ using Gee; -using Qlite; +using Dino.Entities; +using Dino.Qlite; using Xmpp; using Xmpp.Xep; -using Dino.Entities; public class Dino.Reactions : StreamInteractionModule, Object { public static ModuleIdentity<Reactions> IDENTITY = new ModuleIdentity<Reactions>("reactions"); @@ -296,15 +296,6 @@ public class Dino.Reactions : StreamInteractionModule, Object { } } - private Message? get_message_for_reaction(Conversation conversation, string message_id) { - // Query message from a specific account and counterpart. This also makes sure it's a valid reaction for the message. - if (conversation.type_ == Conversation.Type.CHAT) { - return stream_interactor.get_module(MessageStorage.IDENTITY).get_message_by_stanza_id(message_id, conversation); - } else { - return stream_interactor.get_module(MessageStorage.IDENTITY).get_message_by_server_id(message_id, conversation); - } - } - private void process_reaction_for_message(int content_item_id, ReactionInfo reaction_info) { Account account = reaction_info.conversation.account; MessageStanza stanza = reaction_info.stanza; diff --git a/libdino/src/service/replies.vala b/libdino/src/service/replies.vala index ea10cf88..95c28ef1 100644 --- a/libdino/src/service/replies.vala +++ b/libdino/src/service/replies.vala @@ -1,9 +1,9 @@ using Gee; -using Qlite; using Xmpp; using Xmpp.Xep; using Dino.Entities; +using Dino.Qlite; public class Dino.Replies : StreamInteractionModule, Object { public static ModuleIdentity<Replies> IDENTITY = new ModuleIdentity<Replies>("reply"); diff --git a/libdino/src/service/search_processor.vala b/libdino/src/service/search_processor.vala index 2b5ebe49..aaec990a 100644 --- a/libdino/src/service/search_processor.vala +++ b/libdino/src/service/search_processor.vala @@ -1,8 +1,8 @@ using Gee; -using Xmpp; -using Qlite; using Dino.Entities; +using Dino.Qlite; +using Xmpp; namespace Dino { diff --git a/libdino/src/service/stateless_file_sharing.vala b/libdino/src/service/stateless_file_sharing.vala index 15e1ddfe..6e97a5ce 100644 --- a/libdino/src/service/stateless_file_sharing.vala +++ b/libdino/src/service/stateless_file_sharing.vala @@ -16,11 +16,16 @@ public class Dino.StatelessFileSharing : StreamInteractionModule, Object { private set { } } - public FileManager file_manager { + public FileManager fm { owned get { return stream_interactor.get_module(FileManager.IDENTITY); } private set { } } + public FileManager2 fm2 { + owned get { return stream_interactor.get_module(FileManager2.IDENTITY); } + private set { } + } + public Database db { owned get { return Application.get_default().db; } private set { } @@ -30,7 +35,7 @@ public class Dino.StatelessFileSharing : StreamInteractionModule, Object { this.stream_interactor = stream_interactor; this.db = db; - stream_interactor.get_module(MessageProcessor.IDENTITY).received_pipeline.connect(new ReceivedMessageListener(this)); + stream_interactor.get_module(MessageProcessor.IDENTITY).build_message_stanza.connect(check_add_sfs_element); } public static void start(StreamInteractor stream_interactor, Database db) { @@ -38,7 +43,81 @@ public class Dino.StatelessFileSharing : StreamInteractionModule, Object { stream_interactor.add_module(m); } - public async void create_file_transfer(Conversation conversation, Message message, string? file_sharing_id, Xep.FileMetadataElement.FileMetadata metadata, Gee.List<Xep.StatelessFileSharing.Source>? sources) { + public async Entities.Message announce_files(FileTransferGroup file_group, Conversation conversation) { + Entities.Message file_share_message = stream_interactor.get_module(MessageProcessor.IDENTITY).create_out_message(null, conversation); + stream_interactor.get_module(MessageStorage.IDENTITY).add_message(file_share_message, conversation); + file_group.message_id = file_share_message.id; + + foreach (var file_transfer in file_group.file_transfers) { + file_transfer.provider = SFS_PROVIDER_ID; + file_transfer.info = file_share_message.id.to_string(); + file_transfer.file_sharing_id = Xmpp.random_uuid(); + } + + stream_interactor.get_module(MessageProcessor.IDENTITY).send_xmpp_message(file_share_message, conversation); + + return file_share_message; + } + + public async void attach_source(FileTransfer file_transfer, Conversation conversation, string http_url, string to_sfs_id) { + MessageStanza stanza = new MessageStanza() { to = conversation.counterpart, type_ = conversation.type_ == GROUPCHAT ? MessageStanza.TYPE_GROUPCHAT : MessageStanza.TYPE_CHAT }; + stanza.body = http_url; + Xep.OutOfBandData.add_url_to_message(stanza, http_url); + var sources = new ArrayList<Xep.StatelessFileSharing.Source>(); + sources.add(new Xep.StatelessFileSharing.HttpSource() { url = http_url }); + Xep.StatelessFileSharing.set_sfs_attachment(stanza, to_sfs_id, file_transfer.file_sharing_id, sources); + + var stream = stream_interactor.get_stream(conversation.account); + if (stream == null) throw new FileSendError.UPLOAD_FAILED("No stream"); + + stream.get_module(MessageModule.IDENTITY).send_message.begin(stream, stanza); + } + + public async bool try_process_sfs(Entities.Message message, Xmpp.MessageStanza stanza, Conversation conversation) { + Gee.List<Xep.StatelessFileSharing.FileShare> file_shares = Xep.StatelessFileSharing.get_file_shares(stanza); + if (file_shares != null) { + // For now, only accept file shares that have at least one supported hash + foreach (Xep.StatelessFileSharing.FileShare file_share in file_shares) { + if (!Xep.CryptographicHashes.has_supported_hashes(file_share.metadata.hashes)) { + return false; + } + } + + if (file_shares.size > 0) { + var file_transfers = new ArrayList<FileTransfer>(); + foreach (Xep.StatelessFileSharing.FileShare file_share in file_shares) { + var file_transfer = yield create_incoming_file_transfer(conversation, message, file_share.id, file_share.metadata, file_share.sources); + file_transfers.add(file_transfer); + stream_interactor.get_module(FileTransferStorage.IDENTITY).add_file(file_transfer); + } + var file_group = new FileTransferGroup() { + message_id = message.id, + file_transfers = file_transfers + }; + stream_interactor.get_module(FileTransferStorage.IDENTITY).add_file_group(file_group); + + fm2.received_file_group(file_group, conversation); + } + return true; + } + + var source_attachments = Xep.StatelessFileSharing.get_source_attachments(stanza); + if (source_attachments != null) { + foreach (var source_attachment in source_attachments) { + on_received_sources(stanza.from, conversation, source_attachment.to_message_id, source_attachment.to_file_transfer_id, source_attachment.sources); + return true; + } + } + + // Don't process messages that are fallback for legacy clients + if (Xep.StatelessFileSharing.is_sfs_fallback_message(stanza)) { + return true; + } + + return false; + } + + public async FileTransfer create_incoming_file_transfer(Conversation conversation, Message message, string? file_sharing_id, Xep.FileMetadataElement.FileMetadata metadata, Gee.List<Xep.StatelessFileSharing.Source>? sources) { FileTransfer file_transfer = new FileTransfer(); file_transfer.file_sharing_id = file_sharing_id; file_transfer.account = message.account; @@ -57,7 +136,8 @@ public class Dino.StatelessFileSharing : StreamInteractionModule, Object { stream_interactor.get_module(FileTransferStorage.IDENTITY).add_file(file_transfer); conversation.last_active = file_transfer.time; - file_manager.received_file(file_transfer, conversation); + + return file_transfer; } public void on_received_sources(Jid from, Conversation conversation, string attach_to_message_id, string? attach_to_file_id, Gee.List<Xep.StatelessFileSharing.Source> sources) { @@ -74,9 +154,9 @@ public class Dino.StatelessFileSharing : StreamInteractionModule, Object { // "If no <hash/> is provided or the <hash/> elements provided use unsupported algorithms, receiving clients MUST ignore // any attached sources from other senders and only obtain the file from the sources announced by the original sender." - // For now we only allow the original sender - if (from.equals(file_transfer.from) && Xep.CryptographicHashes.get_supported_hashes(file_transfer.hashes).is_empty) { - warning("Ignoring sfs source: Not from original sender or no known file hashes"); + // For now we don't allow source attachments for file shares without hashes. TODO extend this + if (Xep.CryptographicHashes.get_supported_hashes(file_transfer.hashes).is_empty) { + warning("Ignoring sfs source: No known file hashes"); return; } @@ -84,85 +164,26 @@ public class Dino.StatelessFileSharing : StreamInteractionModule, Object { file_transfer.add_sfs_source(source); } - if (file_manager.is_sender_trustworthy(file_transfer, conversation) && file_transfer.state == FileTransfer.State.NOT_STARTED && file_transfer.size >= 0 && file_transfer.size < 5000000) { - file_manager.download_file(file_transfer); + if (fm2.is_sender_trustworthy(file_transfer, conversation) && file_transfer.state == FileTransfer.State.NOT_STARTED && file_transfer.size >= 0 && file_transfer.size < 5000000) { + fm.download_file(file_transfer); } } - /* - public async void create_sfs_for_legacy_transfer(FileProvider file_provider, string info, Jid from, DateTime time, DateTime local_time, Conversation conversation, FileReceiveData receive_data, FileMeta file_meta) { - FileTransfer file_transfer = file_manager.create_file_transfer_from_provider_incoming(file_provider, info, from, time, local_time, conversation, receive_data, file_meta); - - HttpFileReceiveData? http_receive_data = receive_data as HttpFileReceiveData; - if (http_receive_data == null) return; + private void check_add_sfs_element(Entities.Message message, Xmpp.MessageStanza message_stanza, Conversation conversation) { + if (message.encryption != Encryption.NONE) return; - var sources = new ArrayList<Xep.StatelessFileSharing.Source>(); - Xep.StatelessFileSharing.HttpSource source = new Xep.StatelessFileSharing.HttpSource(); - source.url = http_receive_data.url; - sources.add(source); - - if (file_manager.is_jid_trustworthy(from, conversation)) { - try { - file_meta = yield file_provider.get_meta_info(file_transfer, http_receive_data, file_meta); - } catch (Error e) { - warning("Http meta request failed: %s", e.message); + // Check if it's a FileTransferGroup + FileTransferGroup? file_group = stream_interactor.get_module(FileTransferStorage.IDENTITY).get_file_group_by_message_id(message.id, conversation); + if (file_group != null) { + print("check added file group\n"); + foreach (FileTransfer file_transfer in file_group.file_transfers) { + Xep.StatelessFileSharing.set_sfs_element(message_stanza, file_transfer.file_sharing_id, file_transfer.file_metadata, file_transfer.sfs_sources); } - } - var metadata = new Xep.FileMetadataElement.FileMetadata(); - metadata.size = file_meta.size; - metadata.name = file_meta.file_name; - metadata.mime_type = file_meta.mime_type; + Xep.MessageProcessingHints.set_message_hint(message_stanza, Xep.MessageProcessingHints.HINT_STORE); - file_transfer.provider = SFS_PROVIDER_ID; - file_transfer.file_metadata = metadata; - file_transfer.sfs_sources = sources; - } - */ - - private class ReceivedMessageListener : MessageListener { - - public string[] after_actions_const = new string[]{ "STORE" }; - public override string action_group { get { return "MESSAGE_REINTERPRETING"; } } - public override string[] after_actions { get { return after_actions_const; } } - - private StatelessFileSharing outer; - private StreamInteractor stream_interactor; - - public ReceivedMessageListener(StatelessFileSharing outer) { - this.outer = outer; - this.stream_interactor = outer.stream_interactor; + return; } - public override async bool run(Entities.Message message, Xmpp.MessageStanza stanza, Conversation conversation) { - Gee.List<Xep.StatelessFileSharing.FileShare> file_shares = Xep.StatelessFileSharing.get_file_shares(stanza); - if (file_shares != null) { - // For now, only accept file shares that have at least one supported hash - foreach (Xep.StatelessFileSharing.FileShare file_share in file_shares) { - if (!Xep.CryptographicHashes.has_supported_hashes(file_share.metadata.hashes)) { - return false; - } - } - foreach (Xep.StatelessFileSharing.FileShare file_share in file_shares) { - outer.create_file_transfer(conversation, message, file_share.id, file_share.metadata, file_share.sources); - } - return true; - } - - var source_attachments = Xep.StatelessFileSharing.get_source_attachments(stanza); - if (source_attachments != null) { - foreach (var source_attachment in source_attachments) { - outer.on_received_sources(stanza.from, conversation, source_attachment.to_message_id, source_attachment.to_file_transfer_id, source_attachment.sources); - return true; - } - } - - // Don't process messages that are fallback for legacy clients - if (Xep.StatelessFileSharing.is_sfs_fallback_message(stanza)) { - return true; - } - - return false; - } } }
\ No newline at end of file diff --git a/libdino/src/service/stream_interactor.vala b/libdino/src/service/stream_interactor.vala index 2ac13103..2cd0c45c 100644 --- a/libdino/src/service/stream_interactor.vala +++ b/libdino/src/service/stream_interactor.vala @@ -100,12 +100,7 @@ public class ModuleIdentity<T> : Object { } public T? cast(StreamInteractionModule module) { -#if VALA_0_56_11 - // We can't typecheck due to compiler bug - return (T) module; -#else return module.get_type().is_a(typeof(T)) ? (T?) module : null; -#endif } public bool matches(StreamInteractionModule module) { diff --git a/libdino/src/service/util.vala b/libdino/src/service/util.vala index e85e2005..577bc6cc 100644 --- a/libdino/src/service/util.vala +++ b/libdino/src/service/util.vala @@ -1,9 +1,6 @@ using Dino.Entities; -using Qlite; -namespace Dino { - -public class Util { +namespace Dino.Util { public static Message.Type get_message_type_for_conversation(Conversation conversation) { switch (conversation.type_) { case Conversation.Type.CHAT: @@ -42,6 +39,12 @@ public class Util { } return false; } -} + public static Gee.List<string> get_local_ip_addresses() { + Gee.List<string> result = new Gee.ArrayList<string>(); + foreach (string ip_address in Nice.interfaces_get_local_ips(false)) { + result.add(ip_address); + } + return result; + } } |
