diff options
Diffstat (limited to 'libdino/src')
| -rw-r--r-- | libdino/src/application.vala | 31 | ||||
| -rw-r--r-- | libdino/src/entity/file_transfer_group.vala | 53 | ||||
| -rw-r--r-- | libdino/src/file_transfer/file_manager2.vala | 252 | ||||
| -rw-r--r-- | libdino/src/file_transfer/http_transfer.vala | 319 | ||||
| -rw-r--r-- | libdino/src/service/content_item_store.vala | 34 | ||||
| -rw-r--r-- | libdino/src/service/database.vala | 30 | ||||
| -rw-r--r-- | libdino/src/service/file_manager.vala | 44 | ||||
| -rw-r--r-- | libdino/src/service/file_transfer_storage.vala | 57 | ||||
| -rw-r--r-- | libdino/src/service/stateless_file_sharing.vala | 177 |
9 files changed, 881 insertions, 116 deletions
diff --git a/libdino/src/application.vala b/libdino/src/application.vala index 8571120e..35b706a4 100644 --- a/libdino/src/application.vala +++ b/libdino/src/application.vala @@ -46,6 +46,7 @@ public interface Application : GLib.Application { AvatarManager.start(stream_interactor, db); RosterManager.start(stream_interactor, db); FileManager.start(stream_interactor, db); + FileManager2.start(stream_interactor, db); CallStore.start(stream_interactor, db); ContentItemStore.start(stream_interactor, db); ChatInteraction.start(stream_interactor); @@ -131,6 +132,36 @@ public interface Application : GLib.Application { stream_interactor.get_module(PresenceManager.IDENTITY).request_subscription(conversation.account, conversation.counterpart); }); add_action(accept_subscription_action); + + SimpleAction open_file_action = new SimpleAction("file_open_externally", VariantType.INT32); + open_file_action.activate.connect((variant) => { + int file_transfer_id = variant.get_int32(); + FileTransfer file_transfer = stream_interactor.get_module(FileTransferStorage.IDENTITY).get_file_by_id(file_transfer_id, null); + + try{ + AppInfo.launch_default_for_uri(file_transfer.get_file().get_uri(), null); + } catch (Error err) { + warning("Failed to open %s - %s", file_transfer.get_file().get_uri(), err.message); + } + }); + add_action(open_file_action); + + SimpleAction download_file_action = new SimpleAction("file_start_download", VariantType.INT32); + download_file_action.activate.connect((variant) => { + int file_transfer_id = variant.get_int32(); + FileTransfer file_transfer = stream_interactor.get_module(FileTransferStorage.IDENTITY).get_file_by_id(file_transfer_id, null); + + stream_interactor.get_module(FileManager.IDENTITY).download_file.begin(file_transfer); + }); + add_action(download_file_action); + + SimpleAction cancel_file_action = new SimpleAction("file_cancel_download", VariantType.INT32); + cancel_file_action.activate.connect((variant) => { + int file_transfer_id = variant.get_int32(); + FileTransfer file_transfer = stream_interactor.get_module(FileTransferStorage.IDENTITY).get_file_by_id(file_transfer_id, null); + file_transfer.cancellable.cancel(); + }); + add_action(cancel_file_action); } protected void add_connection(Account account) { diff --git a/libdino/src/entity/file_transfer_group.vala b/libdino/src/entity/file_transfer_group.vala new file mode 100644 index 00000000..94b9ccb6 --- /dev/null +++ b/libdino/src/entity/file_transfer_group.vala @@ -0,0 +1,53 @@ +using Xmpp; + +namespace Dino.Entities { + + public class FileTransferGroup : Object { + public Gee.ArrayList<FileTransfer> file_transfers = new Gee.ArrayList<FileTransfer>(); + + public int id { get; set; default=-1; } + public int message_id { get; set; default=-1; } + + private Database? db; + + public FileTransferGroup.from_row(Database db, Qlite.Row row, Gee.ArrayList<FileTransfer> file_transfers) throws InvalidJidError { + this.db = db; + this.file_transfers = file_transfers; + + id = row[db.file_transfer_group.id]; + message_id = row[db.file_transfer_group.message_id]; + + notify.connect(on_update); + } + + public void persist(Database db) { + if (id != -1) return; + + this.db = db; + + id = (int) db.file_transfer_group.insert() + .value(db.file_transfer_group.message_id, message_id) + .perform(); + + + Qlite.InsertBuilder builder = db.file_transfer_group_file_transfer.insert(); + foreach (FileTransfer file_transfer in file_transfers) { + db.file_transfer_group_file_transfer.insert() + .value(db.file_transfer_group_file_transfer.file_transfer_group_id, id) + .value(db.file_transfer_group_file_transfer.file_transfer_id, file_transfer.id) + .perform(); + } + + notify.connect(on_update); + } + + private void on_update(Object o, ParamSpec sp) { + Qlite.UpdateBuilder update_builder = db.file_transfer_group.update().with(db.file_transfer_group.id, "=", id); + switch (sp.name) { + case "message-id": + update_builder.set(db.file_transfer_group.message_id, message_id); break; + } + update_builder.perform(); + } + } +}
\ No newline at end of file diff --git a/libdino/src/file_transfer/file_manager2.vala b/libdino/src/file_transfer/file_manager2.vala new file mode 100644 index 00000000..104a170e --- /dev/null +++ b/libdino/src/file_transfer/file_manager2.vala @@ -0,0 +1,252 @@ +using Gdk; +using Gee; + +using Xmpp; +using Xmpp.Xep; +using Dino.Entities; + +namespace Dino { + + public class FileManager2 : StreamInteractionModule, Object { + public static ModuleIdentity<FileManager2> IDENTITY = new ModuleIdentity<FileManager2>("file2"); + public string id { get { return IDENTITY.id; } } + + public signal void received_file(FileTransfer file_group, Conversation conversation); + public signal void received_file_group(FileTransferGroup file_group, Conversation conversation); + + private StreamInteractor stream_interactor; + private Database db; + + public const int HTTP_PROVIDER_ID = 0; + public const int SFS_PROVIDER_ID = 2; + + public StatelessFileSharing sfs { + owned get { return stream_interactor.get_module(StatelessFileSharing.IDENTITY); } + private set {} + } + + public FileManager fm { + owned get { return stream_interactor.get_module(FileManager.IDENTITY); } + private set {} + } + + public HttpFileTransfers http_ft = null; + + public static void start(StreamInteractor stream_interactor, Database db) { + FileManager2 m = new FileManager2(stream_interactor, db); + stream_interactor.add_module(m); + } + + public static string get_storage_dir() { + return Path.build_filename(Dino.get_storage_dir(), "files"); + } + + private FileManager2(StreamInteractor stream_interactor, Database db) { + this.stream_interactor = stream_interactor; + this.db = db; + this.http_ft = new HttpFileTransfers(stream_interactor, db); + + stream_interactor.get_module(MessageProcessor.IDENTITY).received_pipeline.connect(new ReceivedMessageListener(this)); + } + + public async void send_files(Gee.List<File> files, Conversation conversation) { + bool can_reference_element = conversation.type_ == Conversation.Type.CHAT || ( + // The stable stanza ID XEP is not clear about an announcing MUC having to attach stanza-ids, thus we also check for MAM, which requires this. + stream_interactor.get_module(EntityInfo.IDENTITY).has_feature_cached(conversation.account, conversation.counterpart, Xep.UniqueStableStanzaIDs.NS_URI) && + stream_interactor.get_module(EntityInfo.IDENTITY).has_feature_cached(conversation.account, conversation.counterpart, Xmpp.MessageArchiveManagement.NS_URI) + ); + + // Share unencrypted files via SFS (only if we'll be able to reference messages) + if (conversation.encryption == Encryption.NONE && can_reference_element) { + yield send_unencrypted_sfs(files, conversation); + } else { + foreach (File file in files) { + fm.send_file(file, conversation); + } + } + } + +// private async void send_file(File file, Conversation conversation) { +// FileTransfer file_transfer = yield create_init_outgoing_file_transfer(file, conversation); +// received_file(file_transfer, conversation); +// +// try { +// string file_name = file_transfer.file_name; +// int64 file_size = file_transfer.size; +// FileContentType? file_content_type = file_transfer.content_type; +// print(@"$(file_name) $(file_size) $(file_content_type.get_mime_type())\n"); +// +// Omemo.OmemoHttpFileMeta? encrypted_file_meta = null; +// +// if (conversation.encryption == Encryption.OMEMO) { +// encrypted_file_meta = omemo_file_encryption.encrypt_file(conversation, file_transfer); +//// file_name = encrypted_file_meta.file_name; +// file_size = encrypted_file_meta.size; +// file_content_type = encrypted_file_meta.content_type; +// } +// HttpFileSlot? upload_slot = yield http_ft.request_upload_slot(conversation.account, file_name, file_size, file_content_type); +// yield http_ft.upload(file_transfer, upload_slot.url_up, upload_slot.headers, file_size); +// +// string url_down = upload_slot.url_down; +// +// if (conversation.encryption == Encryption.OMEMO) { +// url_down = omemo_file_encryption.generate_aesgcm_url(upload_slot.url_down, encrypted_file_meta.iv, encrypted_file_meta.key); +// } +// +// // 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_size); +// file_transfer.input_stream = limit_stream; +// } +// if (limit_stream != null) { +// limit_stream.bind_property("retrieved-bytes", file_transfer, "transferred-bytes", BindingFlags.SYNC_CREATE); +// } +// +// file_transfer.state = FileTransfer.State.IN_PROGRESS; +// +// http_ft.send_http_file_message(conversation, file_transfer, url_down, true); // TODO true +// +// file_transfer.state = FileTransfer.State.COMPLETE; +// } catch (Error e) { +// warning("Send file error: %s", e.message); +// file_transfer.state = FileTransfer.State.FAILED; +// } +// } + + private async void send_unencrypted_sfs(Gee.List<File> files, Conversation conversation) { + FileTransferGroup file_transfer_group = new FileTransferGroup(); + foreach (var file in files) { + var file_transfer = yield create_init_outgoing_file_transfer(file, conversation); + file_transfer_group.file_transfers.add(file_transfer); + } + + stream_interactor.get_module(FileTransferStorage.IDENTITY).add_file_group(file_transfer_group); + received_file_group(file_transfer_group, conversation); + + // TODO message-id not known here -> not cached by it + var sfs_message = yield sfs.announce_files(file_transfer_group, conversation); + + foreach (var file_transfer in file_transfer_group.file_transfers) { + upload_unencrypted_sfs_file.begin(file_transfer, conversation, sfs_message); + } + } + + private async void upload_unencrypted_sfs_file(FileTransfer file_transfer, Conversation conversation, Message sfs_message) { + var upload_slot_data = yield http_ft.request_upload_slot(conversation.account, file_transfer.server_file_name, file_transfer.size, file_transfer.content_type); + + // Upload file + yield http_ft.upload(file_transfer, upload_slot_data.url_up, upload_slot_data.headers, file_transfer.size); + + // Wait until we know the server id of the file share message (in MUCs; we get that from the reflected message) + if (conversation.type_.is_muc_semantic()) { + if (sfs_message.server_id == null) { + ulong server_id_notify_id = sfs_message.notify["server-id"].connect(() => { + Idle.add(upload_unencrypted_sfs_file.callback); + }); + yield; + sfs_message.disconnect(server_id_notify_id); + } + } + + file_transfer.sfs_sources.add(new Xep.StatelessFileSharing.HttpSource() { url=upload_slot_data.url_down } ); + + string attach_to_id = stream_interactor.get_module(MessageStorage.IDENTITY).get_reference_id(sfs_message, conversation); + sfs.attach_source(file_transfer, conversation, upload_slot_data.url_down, attach_to_id); + } + + private async FileTransfer? create_init_outgoing_file_transfer(File file, Conversation conversation) { + FileTransfer file_transfer = yield create_outgoing_file_transfer(file, conversation); + + try { + file_transfer.input_stream = yield file.read_async(); + yield save_file(file_transfer); + + stream_interactor.get_module(FileTransferStorage.IDENTITY).add_file(file_transfer); + conversation.last_active = file_transfer.time; + + return file_transfer; + } catch (Error e) { + file_transfer.state = FileTransfer.State.FAILED; + warning("Error saving outgoing file: %s", e.message); + return null; + } + } + + private async FileTransfer create_outgoing_file_transfer(File file, Conversation conversation) { + FileTransfer file_transfer = new FileTransfer(); + file_transfer.account = conversation.account; + file_transfer.counterpart = 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; + } else { + file_transfer.ourpart = conversation.account.full_jid; + } + file_transfer.direction = FileTransfer.DIRECTION_SENT; + file_transfer.time = new DateTime.now_utc(); + 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 fm.file_metadata_providers) { + if (file_metadata_provider.supports_file(file)) { + yield file_metadata_provider.fill_metadata(file, metadata); + } + } + file_transfer.file_metadata = metadata; + return file_transfer; + } + + public 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 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 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 FileManager2 outer; + private StreamInteractor stream_interactor; + + public ReceivedMessageListener(FileManager2 outer) { + this.outer = outer; + this.stream_interactor = outer.stream_interactor; + } + + public override async bool run(Entities.Message message, Xmpp.MessageStanza stanza, Conversation conversation) { + bool is_sfs = yield outer.sfs.try_process_sfs(message, stanza, conversation); + if (is_sfs) return true; + + bool is_http = outer.http_ft.try_parse_http_transfer(message, stanza, conversation); + if (is_http) return true; + + return false; + } + } + } +}
\ No newline at end of file diff --git a/libdino/src/file_transfer/http_transfer.vala b/libdino/src/file_transfer/http_transfer.vala new file mode 100644 index 00000000..ec97dfb3 --- /dev/null +++ b/libdino/src/file_transfer/http_transfer.vala @@ -0,0 +1,319 @@ +using Gee; + +using Dino.Entities; +using Xmpp; + +namespace Dino { + + public class HttpFileSlot : FileSendData { + public string url_down { get; set; } + public string url_up { get; set; } + public HashMap<string, string> headers { get; set; } + } + + public class HttpFileTransfers : Object { + + private StreamInteractor stream_interactor; + private Dino.Database dino_db; + private Soup.Session session; + private HttpFileProvider http_file_provider; + private HttpFileSender http_file_sender; + public static Regex http_url_regex = /^https?:\/\/([^\s#]*)$/; // Spaces are invalid in URLs and we can't use fragments for downloads + public static Regex omemo_url_regex = /^aesgcm:\/\/(.*)#(([A-Fa-f0-9]{2}){48}|([A-Fa-f0-9]{2}){44})$/; + public HashMap<Account, long> max_file_sizes = new HashMap<Account, long>(Account.hash_func, Account.equals_func); + + public FileManager fm { + owned get { return stream_interactor.get_module(FileManager.IDENTITY); } + private set {} + } + + public HttpFileTransfers(StreamInteractor stream_interactor, Dino.Database dino_db) { + this.stream_interactor = stream_interactor; + this.dino_db = dino_db; + this.session = new Soup.Session(); + this.http_file_provider = new HttpFileProvider(this, stream_interactor); + this.http_file_sender = new HttpFileSender(stream_interactor, this); + fm.add_provider(http_file_provider); + fm.add_sender(http_file_sender); + + session.user_agent = @"Dino/$(Dino.get_short_version()) "; + + stream_interactor.stream_negotiated.connect(on_stream_negotiated); + } + + public HttpFileReceiveData? get_file_receive_data(FileTransfer file_transfer) { + if (file_transfer.provider == FileManager.SFS_PROVIDER_ID) { + if (!file_transfer.sfs_sources.is_empty) { + var http_source = file_transfer.sfs_sources.get(0) as Xep.StatelessFileSharing.HttpSource; + if (http_source != null) { + var receive_data = new HttpFileReceiveData(); + receive_data.url = http_source.url; + return receive_data; + } + } + return null; + } + + Conversation? conversation = stream_interactor.get_module(ConversationManager.IDENTITY).get_conversation(file_transfer.counterpart.bare_jid, file_transfer.account); + if (conversation == null) return null; + + Message? message = stream_interactor.get_module(MessageStorage.IDENTITY).get_message_by_id(int.parse(file_transfer.info), conversation); + if (message == null) return null; + + var receive_data = new HttpFileReceiveData(); + receive_data.url = message.body; + + return receive_data; + } + + public async InputStream download(FileTransfer file_transfer, HttpFileReceiveData receive_data, int64 file_size = -1) throws IOError { + var get_message = new Soup.Message("GET", receive_data.url); + + string transfer_host = Uri.parse(receive_data.url, UriFlags.NONE).get_host(); + get_message.accept_certificate.connect((peer_cert, errors) => { return ConnectionManager.on_invalid_certificate(transfer_host, peer_cert, errors); }); + InputStream stream = yield session.send_async(get_message, GLib.Priority.LOW, file_transfer.cancellable); + + if (file_size != -1) { + return new LimitInputStream(stream, file_size); + } else { + return stream; + } + } + + public bool try_parse_http_transfer(Entities.Message message, Xmpp.MessageStanza stanza, Conversation conversation) { + string? oob_url = Xmpp.Xep.OutOfBandData.get_url_from_message(stanza); + bool normal_file = oob_url != null && oob_url == message.body && HttpFileTransfers.http_url_regex.match(message.body); + bool omemo_file = HttpFileTransfers.omemo_url_regex.match(message.body); + if (normal_file || omemo_file) { + http_file_provider.on_file_message(message, conversation); + return true; + } + return false; + } + + public async HttpFileSlot? request_upload_slot(Account account, string file_name, int64 file_size, FileContentType file_content_type) throws FileSendError { + HttpFileSlot upload_slot = new HttpFileSlot(); + if (upload_slot == null) return null; + + Xmpp.XmppStream? stream = stream_interactor.get_stream(account); + if (stream == null) return null; + + try { + var slot_result = yield stream_interactor.module_manager.get_module(account, Xmpp.Xep.HttpFileUpload.Module.IDENTITY).request_slot(stream, file_name, file_size, file_content_type); + upload_slot.url_down = slot_result.url_get; + upload_slot.url_up = slot_result.url_put; + upload_slot.headers = slot_result.headers; + } catch (Xep.HttpFileUpload.HttpFileTransferError e) { + throw new FileSendError.UPLOAD_FAILED("Http file upload XMPP error: %s".printf(e.message)); + } + + return upload_slot; + } + + public async void upload(FileTransfer file_transfer, string upload_url, HashMap<string, string> header_fields, int64 upload_size) throws FileSendError { + Xmpp.XmppStream? stream = stream_interactor.get_stream(file_transfer.account); + if (stream == null) return; + + var put_message = new Soup.Message("PUT", upload_url); + + string transfer_host = Uri.parse(upload_url, UriFlags.NONE).get_host(); + put_message.accept_certificate.connect((peer_cert, errors) => { return ConnectionManager.on_invalid_certificate(transfer_host, peer_cert, errors); }); + put_message.set_request_body(file_transfer.content_type.get_mime_type(), file_transfer.input_stream, (ssize_t) upload_size); + + foreach (var entry in header_fields) { + put_message.request_headers.append(entry.key, entry.value); + } + try { + yield session.send_async(put_message, GLib.Priority.LOW, file_transfer.cancellable); + + if (put_message.status_code < 200 || put_message.status_code >= 300) { + throw new FileSendError.UPLOAD_FAILED("HTTP status code %s".printf(put_message.status_code.to_string())); + } + } catch (Error e) { + throw new FileSendError.UPLOAD_FAILED("HTTP upload error: %s".printf(e.message)); + } + } + + /** @param use_conversation_encryption With OpenPGP, the file on the server is encrypted and the file transfer is safe even if the message is not encrypted. + * With OMEMO, the decryption key is part of the message, thus it has to be encrypted. + */ + public void send_http_file_message(Conversation conversation, FileTransfer file_transfer, string download_url, bool use_conversation_encryption = true) { + Entities.Message message = stream_interactor.get_module(MessageProcessor.IDENTITY).create_out_message(download_url, conversation); + file_transfer.info = message.id.to_string(); + + message.encryption = use_conversation_encryption ? conversation.encryption : Encryption.NONE; + stream_interactor.get_module(MessageProcessor.IDENTITY).send_xmpp_message(message, conversation); + } + + public async FileMeta request_meta_data(FileTransfer file_transfer, FileReceiveData receive_data, FileMeta file_meta) throws FileReceiveError { + HttpFileReceiveData? http_receive_data = receive_data as HttpFileReceiveData; + if (http_receive_data == null) return file_meta; + + var head_message = new Soup.Message("HEAD", http_receive_data.url); + head_message.request_headers.append("Accept-Encoding", "identity"); + string transfer_host = Uri.parse(http_receive_data.url, UriFlags.NONE).get_host(); + head_message.accept_certificate.connect((peer_cert, errors) => { return ConnectionManager.on_invalid_certificate(transfer_host, peer_cert, errors); }); + + try { + yield session.send_async(head_message, GLib.Priority.LOW, null); + } catch (Error e) { + throw new FileReceiveError.GET_METADATA_FAILED("HEAD request failed"); + } + + string? content_type = null, content_length = null; + head_message.response_headers.foreach((name, val) => { + if (name.down() == "content-type") content_type = val; + if (name.down() == "content-length") content_length = val; + }); + file_meta.content_type = new FileContentType.from_mime_type(content_type); + if (content_length != null) { + file_meta.size = int64.parse(content_length); + } + + return file_meta; + } + + private void on_stream_negotiated(Account account, XmppStream stream) { + stream_interactor.module_manager.get_module(account, Xmpp.Xep.HttpFileUpload.Module.IDENTITY).feature_available.connect((stream, max_file_size) => { + max_file_sizes[account] = max_file_size; + http_file_sender.upload_available(account); + }); + } + + public string extract_file_name_from_url(string url) { + string ret = url; + if (ret.contains("#")) { + ret = ret.substring(0, ret.last_index_of("#")); + } + ret = Uri.unescape_string(ret.substring(ret.last_index_of("/") + 1)); + return ret; + } + } + + public class HttpFileProvider : Dino.FileProvider, Object { + + private HttpFileTransfers http_ft; + private StreamInteractor stream_interactor; + + public HttpFileProvider(HttpFileTransfers http_ft, StreamInteractor stream_interactor) { + this.http_ft = http_ft; + this.stream_interactor = stream_interactor; + } + + public async FileMeta get_meta_info(FileTransfer file_transfer, FileReceiveData receive_data, FileMeta file_meta) throws FileReceiveError { + return yield http_ft.request_meta_data(file_transfer, receive_data, file_meta); + } + + public Encryption get_encryption(FileTransfer file_transfer, FileReceiveData receive_data, FileMeta file_meta) { + return Encryption.NONE; + } + + public async InputStream download(FileTransfer file_transfer, FileReceiveData receive_data, FileMeta file_meta) throws IOError { + return yield http_ft.download(file_transfer, (HttpFileReceiveData)receive_data, file_transfer.size); + } + + public FileMeta get_file_meta(FileTransfer file_transfer) throws FileReceiveError { + if (file_transfer.provider == FileManager.SFS_PROVIDER_ID) { + var file_meta = new HttpFileMeta(); + file_meta.size = file_transfer.size; + file_meta.content_type = file_transfer.content_type; + file_meta.file_name = file_transfer.file_name; + file_meta.message = null; + return file_meta; + } + + Conversation? conversation = stream_interactor.get_module(ConversationManager.IDENTITY).get_conversation(file_transfer.counterpart.bare_jid, file_transfer.account); + if (conversation == null) throw new FileReceiveError.GET_METADATA_FAILED("No conversation"); + + Message? message = stream_interactor.get_module(MessageStorage.IDENTITY).get_message_by_id(int.parse(file_transfer.info), conversation); + if (message == null) throw new FileReceiveError.GET_METADATA_FAILED("No message"); + + var file_meta = new HttpFileMeta(); + file_meta.size = file_transfer.size; + file_meta.content_type = file_transfer.content_type; + + file_meta.file_name = http_ft.extract_file_name_from_url(message.body); + + file_meta.message = message; + + return file_meta; + } + + public FileReceiveData? get_file_receive_data(FileTransfer file_transfer) { + return http_ft.get_file_receive_data(file_transfer); + } + + public int get_id() { return 0; } + + public void on_file_message(Entities.Message message, Conversation conversation) { + var additional_info = message.id.to_string(); + + var receive_data = new HttpFileReceiveData(); + receive_data.url = message.body; + + var file_meta = new HttpFileMeta(); + file_meta.file_name = http_ft.extract_file_name_from_url(message.body); + file_meta.message = message; + + file_incoming(additional_info, message.from, message.time, message.local_time, conversation, receive_data, file_meta); + } + } + + public class HttpFileSender : FileSender, Object { + private StreamInteractor stream_interactor; + private HttpFileTransfers http_ft; + + public HttpFileSender(StreamInteractor stream_interactor, HttpFileTransfers http_ft) { + this.http_ft = http_ft; + } + + public async FileSendData? prepare_send_file(Conversation conversation, FileTransfer file_transfer, FileMeta file_meta) throws FileSendError { + HttpFileSendData send_data = new HttpFileSendData(); + if (send_data == null) return null; + + try { + var slot_result = yield http_ft.request_upload_slot(conversation.account, file_transfer.server_file_name, file_meta.size, file_transfer.content_type); + send_data.url_down = slot_result.url_down; + send_data.url_up = slot_result.url_up; + send_data.headers = slot_result.headers; + } catch (Xep.HttpFileUpload.HttpFileTransferError e) { + throw new FileSendError.UPLOAD_FAILED("Http file upload XMPP error: %s".printf(e.message)); + } + + return send_data; + } + + public async void send_file(Conversation conversation, FileTransfer file_transfer, FileSendData file_send_data, FileMeta file_meta) throws FileSendError { + HttpFileSendData http_file_send_data = file_send_data as HttpFileSendData; + yield http_ft.upload(file_transfer, http_file_send_data.url_up, http_file_send_data.headers, file_meta.size); + + http_ft.send_http_file_message(conversation, file_transfer, http_file_send_data.url_down, true); + } + + public async bool can_send(Conversation conversation, FileTransfer file_transfer) { + if (!http_ft.max_file_sizes.has_key(conversation.account)) return false; + + return file_transfer.size < http_ft.max_file_sizes[conversation.account]; + } + + public async long get_file_size_limit(Conversation conversation) { + long? max_size = http_ft.max_file_sizes[conversation.account]; + if (max_size != null) { + return max_size; + } + return -1; + } + + public async bool can_encrypt(Conversation conversation, FileTransfer file_transfer) { + return false; + } + + public async bool is_upload_available(Conversation conversation) { + return http_ft.max_file_sizes.has_key(conversation.account); + } + + public int get_id() { return 0; } + + public float get_priority() { return 100; } + } +} diff --git a/libdino/src/service/content_item_store.vala b/libdino/src/service/content_item_store.vala index d74f9b05..cf4c6975 100644 --- a/libdino/src/service/content_item_store.vala +++ b/libdino/src/service/content_item_store.vala @@ -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/database.vala b/libdino/src/service/database.vala index b38ccfca..8432ff40 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 = 33; + private const int VERSION = 34; public class AccountTable : Table { public Column<int> id = new Column.Integer("id") { primary_key = true, auto_increment = true }; @@ -225,6 +225,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 }; @@ -489,6 +511,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 +549,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 +568,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"); 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..b4a4db3e 100644 --- a/libdino/src/service/file_transfer_storage.vala +++ b/libdino/src/service/file_transfer_storage.vala @@ -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/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 |
