diff options
author | Patiga <dev@patiga.eu> | 2022-06-28 12:11:17 +0200 |
---|---|---|
committer | fiaxh <git@lightrise.org> | 2024-11-14 10:20:12 -0600 |
commit | aaf4542e6208460c305db4be36b15dc832ddc95a (patch) | |
tree | ec7b60b0f0ea74e21403788e8345336bd0f3939b /libdino | |
parent | 909f569318835d11703c49fba7dbe49996759f38 (diff) | |
download | dino-aaf4542e6208460c305db4be36b15dc832ddc95a.tar.gz dino-aaf4542e6208460c305db4be36b15dc832ddc95a.zip |
Implement XEP-0447: Stateless file sharing
Diffstat (limited to 'libdino')
-rw-r--r-- | libdino/src/entity/file_transfer.vala | 158 | ||||
-rw-r--r-- | libdino/src/entity/message.vala | 1 | ||||
-rw-r--r-- | libdino/src/service/database.vala | 56 | ||||
-rw-r--r-- | libdino/src/service/file_manager.vala | 263 | ||||
-rw-r--r-- | libdino/src/service/jingle_file_transfers.vala | 2 | ||||
-rw-r--r-- | libdino/src/service/module_manager.vala | 1 |
6 files changed, 452 insertions, 29 deletions
diff --git a/libdino/src/entity/file_transfer.vala b/libdino/src/entity/file_transfer.vala index 20bc1a7a..c9c7916e 100644 --- a/libdino/src/entity/file_transfer.vala +++ b/libdino/src/entity/file_transfer.vala @@ -14,6 +14,22 @@ public class FileTransfer : Object { FAILED } + public class SerializedSfsSource: Object { + public string type; + public string data; + + public SerializedSfsSource.from_sfs_source(Xep.StatelessFileSharing.SfsSource source) { + this.type = source.type(); + this.data = source.serialize(); + } + + public async Xep.StatelessFileSharing.SfsSource to_sfs_source() { + assert(this.type == Xep.StatelessFileSharing.HttpSource.SOURCE_TYPE); + Xep.StatelessFileSharing.HttpSource http_source = yield Xep.StatelessFileSharing.HttpSource.deserialize(this.data); + return http_source; + } + } + public int id { get; set; default=-1; } public Account account { get; set; } public Jid counterpart { get; set; } @@ -64,13 +80,19 @@ public class FileTransfer : Object { } public string path { get; set; } public string? mime_type { get; set; } - // TODO(hrxi): expand to 64 bit - public int size { get; set; default=-1; } - + public int64 size { get; set; } public State state { get; set; default=State.NOT_STARTED; } public int provider { get; set; } public string info { get; set; } public Cancellable cancellable { get; default=new Cancellable(); } + public string? desc { get; set; } + public DateTime? modification_date { get; set; } + public int width { get; set; default=-1; } + public int height { get; set; default=-1; } + public int64 length { get; set; default=-1; } + public Xep.CryptographicHashes.Hashes hashes { get; set; default=new Xep.CryptographicHashes.Hashes();} + public ListStore sfs_sources { get; set; default=new ListStore(typeof(SerializedSfsSource)); } + public Gee.List<Xep.JingleContentThumbnails.Thumbnail> thumbnails = new Gee.ArrayList<Xep.JingleContentThumbnails.Thumbnail>(); private Database? db; private string storage_dir; @@ -99,10 +121,37 @@ public class FileTransfer : Object { file_name = row[db.file_transfer.file_name]; path = row[db.file_transfer.path]; mime_type = row[db.file_transfer.mime_type]; - size = row[db.file_transfer.size]; + size = (int64) row[db.file_transfer.size]; state = (State) row[db.file_transfer.state]; provider = row[db.file_transfer.provider]; info = row[db.file_transfer.info]; + modification_date = new DateTime.from_unix_utc(row[db.file_transfer.modification_date]); + width = row[db.file_transfer.width]; + height = row[db.file_transfer.height]; + length = (int64) row[db.file_transfer.length]; + + foreach(var hash_row in db.file_hashes.select().with(db.file_hashes.id, "=", id)) { + Xep.CryptographicHashes.Hash hash = new Xep.CryptographicHashes.Hash(); + hash.algo = hash_row[db.file_hashes.algo]; + hash.val = hash_row[db.file_hashes.value]; + hashes.hashes.add(hash); + } + + foreach(var thumbnail_row in db.file_thumbnails.select().with(db.file_thumbnails.id, "=", id)) { + Xep.JingleContentThumbnails.Thumbnail thumbnail = new Xep.JingleContentThumbnails.Thumbnail(); + thumbnail.uri = thumbnail_row[db.file_thumbnails.uri]; + thumbnail.media_type = thumbnail_row[db.file_thumbnails.mime_type]; + thumbnail.width = thumbnail_row[db.file_thumbnails.width]; + thumbnail.height = thumbnail_row[db.file_thumbnails.height]; + thumbnails.add(thumbnail); + } + + foreach(Qlite.Row source_row in db.sfs_sources.select().with(db.sfs_sources.id, "=", id)) { + SerializedSfsSource source = new SerializedSfsSource(); + source.type = source_row[db.sfs_sources.type]; + source.data = source_row[db.sfs_sources.data]; + sfs_sources.append(source as Object); + } notify.connect(on_update); } @@ -121,17 +170,52 @@ public class FileTransfer : Object { .value(db.file_transfer.local_time, (long) local_time.to_unix()) .value(db.file_transfer.encryption, encryption) .value(db.file_transfer.file_name, file_name) - .value(db.file_transfer.size, size) + .value(db.file_transfer.size, (long) size) .value(db.file_transfer.state, state) .value(db.file_transfer.provider, provider) .value(db.file_transfer.info, info); - if (file_name != null) builder.value(db.file_transfer.file_name, file_name); if (path != null) builder.value(db.file_transfer.path, path); if (mime_type != null) builder.value(db.file_transfer.mime_type, mime_type); + if (path != null) builder.value(db.file_transfer.path, path); + if (modification_date != null) builder.value(db.file_transfer.modification_date, (long) modification_date.to_unix()); + if (width != -1) builder.value(db.file_transfer.width, width); + if (height != -1) builder.value(db.file_transfer.height, height); + if (length != -1) builder.value(db.file_transfer.length, (long) length); id = (int) builder.perform(); + + foreach (Xep.CryptographicHashes.Hash hash in hashes.hashes) { + db.file_hashes.insert() + .value(db.file_hashes.id, id) + .value(db.file_hashes.algo, hash.algo) + .value(db.file_hashes.value, hash.val) + .perform(); + } + foreach (Xep.JingleContentThumbnails.Thumbnail thumbnail in thumbnails) { + db.file_thumbnails.insert() + .value(db.file_thumbnails.id, id) + .value(db.file_thumbnails.uri, thumbnail.uri) + .value(db.file_thumbnails.mime_type, thumbnail.media_type) + .value(db.file_thumbnails.width, thumbnail.width) + .value(db.file_thumbnails.height, thumbnail.height) + .perform(); + } + + for(int i = 0; i < sfs_sources.get_n_items(); i++) { + Object source_object = sfs_sources.get_item(i); + SerializedSfsSource source = source_object as SerializedSfsSource; + db.sfs_sources.insert() + .value(db.sfs_sources.id, id) + .value(db.sfs_sources.type, source.type) + .value(db.sfs_sources.data, source.data) + .perform(); + } + notify.connect(on_update); + sfs_sources.items_changed.connect((position, removed, added) => { + on_update_sources_items(this, position, removed, added); + }); } public File get_file() { @@ -161,7 +245,7 @@ public class FileTransfer : Object { case "mime-type": update_builder.set(db.file_transfer.mime_type, mime_type); break; case "size": - update_builder.set(db.file_transfer.size, size); break; + update_builder.set(db.file_transfer.size, (long) size); break; case "state": if (state == State.IN_PROGRESS) return; update_builder.set(db.file_transfer.state, state); break; @@ -169,9 +253,69 @@ public class FileTransfer : Object { update_builder.set(db.file_transfer.provider, provider); break; case "info": update_builder.set(db.file_transfer.info, info); break; + case "modification-date": + update_builder.set(db.file_transfer.modification_date, (long) modification_date.to_unix()); break; + case "width": + update_builder.set(db.file_transfer.width, width); break; + case "height": + update_builder.set(db.file_transfer.height, height); break; + case "length": + update_builder.set(db.file_transfer.length, (long) length); break; } update_builder.perform(); } + + private void on_update_sources_items(FileTransfer file_transfer, uint position, uint removed, uint added) { + for(uint i = position; i < position + added; i++) { + Object source_object = file_transfer.sfs_sources.get_item(i); + SerializedSfsSource source = source_object as SerializedSfsSource; + db.sfs_sources.insert() + .value(db.sfs_sources.id, id) + .value(db.sfs_sources.type, source.type) + .value(db.sfs_sources.data, source.data) + .perform(); + } + } + + public Xep.FileMetadataElement.FileMetadata to_metadata_element() { + Xep.FileMetadataElement.FileMetadata metadata = new Xep.FileMetadataElement.FileMetadata(); + metadata.name = this.file_name; + metadata.mime_type = this.mime_type; + metadata.size = this.size; + metadata.desc = this.desc; + metadata.date = this.modification_date; + metadata.width = this.width; + metadata.height = this.height; + metadata.length = this.length; + metadata.hashes = this.hashes; + metadata.thumbnails = this.thumbnails; + return metadata; + } + + public async Xep.StatelessFileSharing.SfsElement to_sfs_element() { + Xep.StatelessFileSharing.SfsElement sfs_element = new Xep.StatelessFileSharing.SfsElement(); + sfs_element.metadata = this.to_metadata_element(); + for(int i = 0; i < sfs_sources.get_n_items(); i++) { + Object source_object = sfs_sources.get_item(i); + SerializedSfsSource source = source_object as SerializedSfsSource; + sfs_element.sources.add(yield source.to_sfs_source()); + } + + return sfs_element; + } + + public void with_metadata_element(Xep.FileMetadataElement.FileMetadata metadata) { + this.file_name = metadata.name; + this.mime_type = metadata.mime_type; + this.size = metadata.size; + this.desc = metadata.desc; + this.modification_date = metadata.date; + this.width = metadata.width; + this.height = metadata.height; + this.length = metadata.length; + this.hashes = metadata.hashes; + this.thumbnails = metadata.thumbnails; + } } } diff --git a/libdino/src/entity/message.vala b/libdino/src/entity/message.vala index e5aad25f..d48f999b 100644 --- a/libdino/src/entity/message.vala +++ b/libdino/src/entity/message.vala @@ -1,5 +1,6 @@ using Gee; using Xmpp; +using Xmpp.Xep; namespace Dino.Entities { diff --git a/libdino/src/service/database.vala b/libdino/src/service/database.vala index eba8b7ca..30d6d55f 100644 --- a/libdino/src/service/database.vala +++ b/libdino/src/service/database.vala @@ -7,7 +7,7 @@ using Dino.Entities; namespace Dino { public class Database : Qlite.Database { - private const int VERSION = 27; + private const int VERSION = 28; public class AccountTable : Table { public Column<int> id = new Column.Integer("id") { primary_key = true, auto_increment = true }; @@ -191,15 +191,57 @@ public class Database : Qlite.Database { public Column<string> file_name = new Column.Text("file_name"); public Column<string> path = new Column.Text("path"); public Column<string> mime_type = new Column.Text("mime_type"); - public Column<int> size = new Column.Integer("size"); + public Column<long> size = new Column.Long("size") { default = "-1", min_version=28 }; public Column<int> state = new Column.Integer("state"); public Column<int> provider = new Column.Integer("provider"); public Column<string> info = new Column.Text("info"); + public Column<long> modification_date = new Column.Long("modification_date") { default = "-1", min_version=28 }; + public Column<int> width = new Column.Integer("width") { default = "-1", min_version=28 }; + public Column<int> height = new Column.Integer("height") { default = "-1", min_version=28 }; + public Column<long> length = new Column.Integer("length") { default = "-1", min_version=28 }; internal FileTransferTable(Database db) { base(db, "file_transfer"); init({id, account_id, counterpart_id, counterpart_resource, our_resource, direction, time, local_time, - encryption, file_name, path, mime_type, size, state, provider, info}); + encryption, file_name, path, mime_type, size, state, provider, info, modification_date, width, height, + length}); + } + } + + public class FileHashesTable : Table { + public Column<int> id = new Column.Integer("id"); + public Column<string> algo = new Column.Text("algo") { not_null = true }; + public Column<string> value = new Column.Text("value") { not_null = true }; + + internal FileHashesTable(Database db) { + base(db, "file_hashes"); + init({id, algo, value}); + unique({id, algo}, "REPLACE"); + } + } + + public class FileThumbnailsTable : Table { + public Column<int> id = new Column.Integer("id"); + public Column<string> uri = new Column.Text("uri") { not_null = true }; + public Column<string> mime_type = new Column.Text("mime_type"); + public Column<int> width = new Column.Integer("width"); + public Column<int> height = new Column.Integer("height"); + + internal FileThumbnailsTable(Database db) { + base(db, "file_thumbnails"); + init({id, uri, mime_type, width, height}); + } + } + + public class SfsSourcesTable : Table { + public Column<int> id = new Column.Integer("id"); + public Column<string> type = new Column.Text("type") { not_null = true }; + public Column<string> data = new Column.Text("data") { not_null = true }; + + internal SfsSourcesTable(Database db) { + base(db, "sfs_sources"); + init({id, type, data}); + unique({id, type, data}, "REPLACE"); } } @@ -401,6 +443,9 @@ 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 FileHashesTable file_hashes { get; private set; } + public FileThumbnailsTable file_thumbnails { get; private set; } + public SfsSourcesTable sfs_sources { get; private set; } public CallTable call { get; private set; } public CallCounterpartTable call_counterpart { get; private set; } public ConversationTable conversation { get; private set; } @@ -431,6 +476,9 @@ public class Database : Qlite.Database { occupantid = new OccupantIdTable(this); real_jid = new RealJidTable(this); file_transfer = new FileTransferTable(this); + file_hashes = new FileHashesTable(this); + file_thumbnails = new FileThumbnailsTable(this); + sfs_sources = new SfsSourcesTable(this); call = new CallTable(this); call_counterpart = new CallCounterpartTable(this); conversation = new ConversationTable(this); @@ -443,7 +491,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, body_meta, message_correction, reply, real_jid, occupantid, file_transfer, call, call_counterpart, conversation, avatar, entity_identity, entity_feature, roster, mam_catchup, reaction, settings, account_settings, conversation_settings }); + init({ account, jid, entity, content_item, message, 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, settings, account_settings, conversation_settings }); try { exec("PRAGMA journal_mode = WAL"); diff --git a/libdino/src/service/file_manager.vala b/libdino/src/service/file_manager.vala index 984fe5fd..32cf23c4 100644 --- a/libdino/src/service/file_manager.vala +++ b/libdino/src/service/file_manager.vala @@ -2,6 +2,7 @@ using Gdk; using Gee; using Xmpp; +using Xmpp.Xep; using Dino.Entities; namespace Dino { @@ -19,6 +20,7 @@ public class FileManager : StreamInteractionModule, Object { private Gee.List<FileEncryptor> file_encryptors = new ArrayList<FileEncryptor>(); private 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 static void start(StreamInteractor stream_interactor, Database db) { FileManager m = new FileManager(stream_interactor, db); @@ -36,9 +38,121 @@ public class FileManager : StreamInteractionModule, Object { this.add_provider(new JingleFileProvider(stream_interactor)); this.add_sender(new JingleFileSender(stream_interactor)); + this.add_metadata_provider(new GenericFileMetadataProvider()); + this.add_metadata_provider(new ImageFileMetadataProvider()); + this.stream_interactor.account_added.connect((account) => { + on_account_added(account); + }); + } + + public const int HTTP_PROVIDER_ID = 0; + public const int SFS_PROVIDER_ID = 2; + + private FileProvider? select_file_provider(FileTransfer file_transfer) { + bool http_usable = file_transfer.provider == SFS_PROVIDER_ID; + foreach (FileProvider file_provider in this.file_providers) { + if (file_transfer.provider == file_provider.get_id()) { + return file_provider; + } + if (http_usable && file_provider.get_id() == HTTP_PROVIDER_ID) { + return file_provider; + } + } + return null; } - public async HashMap<int, long> get_file_size_limits(Conversation conversation) { + // For receiving out of band data as sfs + private async void on_backwards_compatible_sfs(FileProvider file_provider, Jid from, DateTime time, DateTime local_time, Conversation conversation, FileReceiveData receive_data, FileMeta file_meta) { + Xep.StatelessFileSharing.SfsElement sfs_element = new Xep.StatelessFileSharing.SfsElement(); + + Xep.StatelessFileSharing.HttpSource source = new Xep.StatelessFileSharing.HttpSource(); + HttpFileReceiveData http_receive_data = receive_data as HttpFileReceiveData; + source.url = http_receive_data.url; + sfs_element.sources.add(source); + + FileTransfer file_transfer = new FileTransfer(); + + if (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("Can't accept oob data as stateless file sharing due to failed http request\n"); + } + } + + sfs_element.metadata.size = file_meta.size; + sfs_element.metadata.name = file_meta.file_name; + sfs_element.metadata.mime_type = file_meta.mime_type; + // Encryption unused in http file transfers + + yield on_receive_sfs(from, conversation, sfs_element, null); + } + + private async void on_receive_sfs(Jid from, Conversation conversation, Xep.StatelessFileSharing.SfsElement sfs_element, string? id) { + FileTransfer file_transfer = new FileTransfer(); + file_transfer.account = conversation.account; + file_transfer.counterpart = file_transfer.direction == FileTransfer.DIRECTION_RECEIVED ? from : conversation.counterpart; + if (conversation.type_.is_muc_semantic()) { + file_transfer.ourpart = stream_interactor.get_module(MucManager.IDENTITY).get_own_jid(conversation.counterpart, conversation.account) ?? conversation.account.bare_jid; + file_transfer.direction = from.equals(file_transfer.ourpart) ? FileTransfer.DIRECTION_SENT : FileTransfer.DIRECTION_RECEIVED; + } else { + file_transfer.ourpart = conversation.account.full_jid; + file_transfer.direction = from.equals_bare(file_transfer.ourpart) ? FileTransfer.DIRECTION_SENT : FileTransfer.DIRECTION_RECEIVED; + } + file_transfer.time = new DateTime.now_utc(); + // TODO: get time from message + file_transfer.local_time = new DateTime.now_utc(); + file_transfer.provider = SFS_PROVIDER_ID; + file_transfer.with_metadata_element(sfs_element.metadata); + foreach (Xep.StatelessFileSharing.SfsSource source in sfs_element.sources) { + file_transfer.sfs_sources.append(new FileTransfer.SerializedSfsSource.from_sfs_source(source) as Object); + } + // FileTransfer.info stores the id of the MessageStanza for future SfsSourceAttachments + // Prior to sfs, info stored the id of the Message entity for oob + file_transfer.info = id; + + stream_interactor.get_module(FileTransferStorage.IDENTITY).add_file(file_transfer); + + if (is_sender_trustworthy(file_transfer, conversation)) { + if (file_transfer.size >= 0 && file_transfer.size < 500) { + FileProvider? file_provider = this.select_file_provider(file_transfer); + download_file_internal.begin(file_provider, file_transfer, conversation, (_, res) => { + download_file_internal.end(res); + }); + } + } + + conversation.last_active = file_transfer.time; + received_file(file_transfer, conversation); + } + + private void on_receive_sfs_attachment(Jid from, Conversation conversation, Xep.StatelessFileSharing.SfsSourceAttachment attachment) { + foreach (Qlite.Row file_transfer_row in this.db.file_transfer.select() + .with(db.file_transfer.info, "=", attachment.sfs_id)) { + FileTransfer file_transfer = new FileTransfer.from_row(this.db, file_transfer_row, FileManager.get_storage_dir()); + if (file_transfer.hashes.supported_hashes().is_empty) { + return; + } + foreach (StatelessFileSharing.SfsSource source in attachment.sources) { + file_transfer.sfs_sources.append(new FileTransfer.SerializedSfsSource.from_sfs_source(source) as Object); + } + } + + } + + private void on_account_added(Account account) { + Xep.StatelessFileSharing.Module fsf_module = stream_interactor.module_manager.get_module(account, Xep.StatelessFileSharing.Module.IDENTITY); + fsf_module.received_sfs.connect((from, to, sfs_element, message) => { + Conversation? conversation = stream_interactor.get_module(ConversationManager.IDENTITY).approx_conversation_for_stanza(from, to, account, message.type_); + on_receive_sfs(from, conversation, sfs_element, message.id); + }); + fsf_module.received_sfs_attachment.connect((from, to, sfs_attachment, message) => { + Conversation? conversation = stream_interactor.get_module(ConversationManager.IDENTITY).approx_conversation_for_stanza(from, to, account, message.type_); + on_receive_sfs_attachment(from, conversation, sfs_attachment); + }); + } + + public async HashMap<int, long> get_file_size_limits(Conversation conversation) { HashMap<int, long> ret = new HashMap<int, long>(); foreach (FileSender sender in file_senders) { ret[sender.get_id()] = yield sender.get_file_size_limit(conversation); @@ -60,11 +174,15 @@ public class FileManager : StreamInteractionModule, Object { file_transfer.local_time = new DateTime.now_utc(); file_transfer.encryption = conversation.encryption; + Xep.FileMetadataElement.FileMetadata metadata = new Xep.FileMetadataElement.FileMetadata(); + foreach (FileMetadataProvider file_metadata_provider in this.file_metadata_providers) { + if (file_metadata_provider.supports_file(file)) { + yield file_metadata_provider.fill_metadata(file, metadata); + } + } + file_transfer.with_metadata_element(metadata); + try { - FileInfo file_info = file.query_info("*", FileQueryInfoFlags.NONE); - file_transfer.file_name = file_info.get_display_name(); - file_transfer.mime_type = file_info.get_content_type(); - file_transfer.size = (int)file_info.get_size(); file_transfer.input_stream = yield file.read_async(); yield save_file(file_transfer); @@ -130,12 +248,7 @@ public class FileManager : StreamInteractionModule, Object { public async void download_file(FileTransfer file_transfer) { Conversation conversation = stream_interactor.get_module(ConversationManager.IDENTITY).get_conversation(file_transfer.counterpart.bare_jid, file_transfer.account); - FileProvider? file_provider = null; - foreach (FileProvider fp in file_providers) { - if (file_transfer.provider == fp.get_id()) { - file_provider = fp; - } - } + FileProvider? file_provider = this.select_file_provider(file_transfer); yield download_file_internal(file_provider, file_transfer, conversation); } @@ -152,7 +265,12 @@ public class FileManager : StreamInteractionModule, Object { public void add_provider(FileProvider file_provider) { file_providers.add(file_provider); file_provider.file_incoming.connect((info, from, time, local_time, conversation, receive_data, file_meta) => { - handle_incoming_file.begin(file_provider, info, from, time, local_time, conversation, receive_data, file_meta); + if (receive_data is HttpFileReceiveData) { + printerr("Handling oob data as stateless file sharing"); + this.on_backwards_compatible_sfs.begin(file_provider, from, time, local_time, conversation, receive_data, file_meta); + } else { + handle_incoming_file.begin(file_provider, info, from, time, local_time, conversation, receive_data, file_meta); + } }); } @@ -174,12 +292,14 @@ public class FileManager : StreamInteractionModule, Object { file_decryptors.add(decryptor); } - public bool is_sender_trustworthy(FileTransfer file_transfer, Conversation conversation) { - if (file_transfer.direction == FileTransfer.DIRECTION_SENT) return true; + public void add_metadata_provider(FileMetadataProvider file_metadata_provider) { + file_metadata_providers.add(file_metadata_provider); + } + private bool is_jid_trustworthy(Jid from, Conversation conversation) { 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); + relevant_jid = stream_interactor.get_module(MucManager.IDENTITY).get_real_jid(from, conversation.account); } if (relevant_jid == null) return false; @@ -187,6 +307,12 @@ public class FileManager : StreamInteractionModule, Object { return in_roster; } + public bool is_sender_trustworthy(FileTransfer file_transfer, Conversation conversation) { + if (file_transfer.direction == FileTransfer.DIRECTION_SENT) return true; + + return is_jid_trustworthy(file_transfer.from, conversation); + } + 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); @@ -211,7 +337,7 @@ public class FileManager : StreamInteractionModule, Object { private async void download_file_internal(FileProvider file_provider, FileTransfer file_transfer, Conversation conversation) { try { // Get meta info - FileReceiveData receive_data = file_provider.get_file_receive_data(file_transfer); + FileReceiveData receive_data = yield file_provider.get_file_receive_data(file_transfer); FileDecryptor? file_decryptor = null; foreach (FileDecryptor decryptor in file_decryptors) { if (decryptor.can_decrypt_file(conversation, file_transfer, receive_data)) { @@ -379,7 +505,7 @@ public interface FileProvider : Object { public abstract Encryption get_encryption(FileTransfer file_transfer, FileReceiveData receive_data, FileMeta file_meta); public abstract FileMeta get_file_meta(FileTransfer file_transfer) throws FileReceiveError; - public abstract FileReceiveData? get_file_receive_data(FileTransfer file_transfer); + public abstract async FileReceiveData? get_file_receive_data(FileTransfer file_transfer); public abstract async FileMeta get_meta_info(FileTransfer file_transfer, FileReceiveData receive_data, FileMeta file_meta) throws FileReceiveError; public abstract async InputStream download(FileTransfer file_transfer, FileReceiveData receive_data, FileMeta file_meta) throws FileReceiveError; @@ -415,4 +541,107 @@ public interface FileDecryptor : Object { public abstract async InputStream decrypt_file(InputStream encrypted_stream, Conversation conversation, FileTransfer file_transfer, FileReceiveData receive_data) throws FileReceiveError; } +public interface FileMetadataProvider : Object { + public abstract bool supports_file(File file); + + public abstract async void fill_metadata(File file, Xep.FileMetadataElement.FileMetadata metadata); +} + +class GenericFileMetadataProvider: Dino.FileMetadataProvider, Object { + public bool supports_file(File file) { + return true; + } + + public async void fill_metadata(File file, Xep.FileMetadataElement.FileMetadata metadata) { + FileInfo info = file.query_info("*", FileQueryInfoFlags.NONE); + + metadata.name = info.get_display_name(); + metadata.mime_type = info.get_content_type(); + metadata.size = info.get_size(); + metadata.date = info.get_modification_date_time(); + + Bytes file_data = file.load_bytes(); + metadata.hashes.hashes.add(new CryptographicHashes.Hash.from_data(GLib.ChecksumType.SHA256, file_data.get_data())); + metadata.hashes.hashes.add(new CryptographicHashes.Hash.from_data(GLib.ChecksumType.SHA512, file_data.get_data())); + } +} + +public class ImageFileMetadataProvider: Dino.FileMetadataProvider, Object { + public bool supports_file(File file) { + return file.query_info("*", FileQueryInfoFlags.NONE).get_content_type().has_prefix("image"); + } + + private const int[] THUMBNAIL_DIMS = { 1, 2, 3, 4, 8 }; + private const string IMAGE_TYPE = "png"; + private const string MIME_TYPE = "image/png"; + + public async void fill_metadata(File file, Xep.FileMetadataElement.FileMetadata metadata) { + Pixbuf pixbuf = new Pixbuf.from_stream(yield file.read_async()); + metadata.width = pixbuf.get_width(); + metadata.height = pixbuf.get_height(); + float ratio = (float)metadata.width / (float) metadata.height; + + int thumbnail_width = -1; + int thumbnail_height = -1; + float diff = float.INFINITY; + for (int i = 0; i < THUMBNAIL_DIMS.length; i++) { + int test_width = THUMBNAIL_DIMS[i]; + int test_height = THUMBNAIL_DIMS[THUMBNAIL_DIMS.length - 1 - i]; + float test_ratio = (float)test_width / (float)test_height; + float test_diff = (test_ratio - ratio).abs(); + if (test_diff < diff) { + thumbnail_width = test_width; + thumbnail_height = test_height; + diff = test_diff; + } + } + + Pixbuf thumbnail_pixbuf = pixbuf.scale_simple(thumbnail_width, thumbnail_height, InterpType.BILINEAR); + uint8[] buffer; + thumbnail_pixbuf.save_to_buffer(out buffer, IMAGE_TYPE); + string base_64 = GLib.Base64.encode(buffer); + string uri = @"data:$MIME_TYPE;base64,$base_64"; + Xep.JingleContentThumbnails.Thumbnail thumbnail = new Xep.JingleContentThumbnails.Thumbnail(); + thumbnail.uri = uri; + thumbnail.media_type = MIME_TYPE; + thumbnail.width = thumbnail_width; + thumbnail.height = thumbnail_height; + metadata.thumbnails.add(thumbnail); + } + + public static Pixbuf? parse_thumbnail(Xep.JingleContentThumbnails.Thumbnail thumbnail) { + string[] splits = thumbnail.uri.split(":", 2); + if (splits.length != 2) { + printerr("Thumbnail parsing error: ':' not found"); + return null; + } + if (splits[0] != "data") { + printerr("Unsupported thumbnail: unimplemented uri type\n"); + return null; + } + splits = splits[1].split(";", 2); + if (splits.length != 2) { + printerr("Thumbnail parsing error: ';' not found"); + return null; + } + if (splits[0] != MIME_TYPE) { + printerr("Unsupported thumbnail: unsupported mime-type\n"); + return null; + } + splits = splits[1].split(",", 2); + if (splits.length != 2) { + printerr("Thumbnail parsing error: ',' not found"); + return null; + } + if (splits[0] != "base64") { + printerr("Unsupported thumbnail: data is not base64 encoded\n"); + return null; + } + uint8[] data = Base64.decode(splits[1]); + MemoryInputStream input_stream = new MemoryInputStream.from_data(data); + Pixbuf pixbuf = new Pixbuf.from_stream(input_stream); + return pixbuf; + } +} + } diff --git a/libdino/src/service/jingle_file_transfers.vala b/libdino/src/service/jingle_file_transfers.vala index 624be607..daccd309 100644 --- a/libdino/src/service/jingle_file_transfers.vala +++ b/libdino/src/service/jingle_file_transfers.vala @@ -74,7 +74,7 @@ public class JingleFileProvider : FileProvider, Object { return file_meta; } - public FileReceiveData? get_file_receive_data(FileTransfer file_transfer) { + public async FileReceiveData? get_file_receive_data(FileTransfer file_transfer) { return new FileReceiveData(); } diff --git a/libdino/src/service/module_manager.vala b/libdino/src/service/module_manager.vala index eb9e4fbc..28ccf8ef 100644 --- a/libdino/src/service/module_manager.vala +++ b/libdino/src/service/module_manager.vala @@ -86,6 +86,7 @@ public class ModuleManager { 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.StatelessFileSharing.Module()); initialize_account_modules(account, module_map[account]); } } |