aboutsummaryrefslogtreecommitdiff
path: root/libdino/src
diff options
context:
space:
mode:
Diffstat (limited to 'libdino/src')
-rw-r--r--libdino/src/application.vala1
-rw-r--r--libdino/src/entity/file_transfer.vala156
-rw-r--r--libdino/src/entity/message.vala1
-rw-r--r--libdino/src/service/content_item_store.vala8
-rw-r--r--libdino/src/service/database.vala23
-rw-r--r--libdino/src/service/file_manager.vala301
-rw-r--r--libdino/src/service/file_transfer_storage.vala47
-rw-r--r--libdino/src/service/jingle_file_transfers.vala12
-rw-r--r--libdino/src/service/message_processor.vala7
-rw-r--r--libdino/src/service/message_storage.vala18
-rw-r--r--libdino/src/service/module_manager.vala1
-rw-r--r--libdino/src/service/sfs_metadata.vala81
-rw-r--r--libdino/src/service/stateless_file_sharing.vala162
-rw-r--r--libdino/src/util/limit_input_stream.vala73
-rw-r--r--libdino/src/util/util.vala31
15 files changed, 578 insertions, 344 deletions
diff --git a/libdino/src/application.vala b/libdino/src/application.vala
index 0fcee731..4aa4b8ad 100644
--- a/libdino/src/application.vala
+++ b/libdino/src/application.vala
@@ -58,6 +58,7 @@ public interface Application : GLib.Application {
Replies.start(stream_interactor, db);
FallbackBody.start(stream_interactor, db);
ContactModels.start(stream_interactor);
+ StatelessFileSharing.start(stream_interactor, db);
create_actions();
diff --git a/libdino/src/entity/file_transfer.vala b/libdino/src/entity/file_transfer.vala
index c9c7916e..49ff2aa3 100644
--- a/libdino/src/entity/file_transfer.vala
+++ b/libdino/src/entity/file_transfer.vala
@@ -4,6 +4,8 @@ namespace Dino.Entities {
public class FileTransfer : Object {
+ public signal void sources_changed();
+
public const bool DIRECTION_SENT = true;
public const bool DIRECTION_RECEIVED = false;
@@ -14,23 +16,8 @@ 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 string? file_sharing_id { get; set; }
public Account account { get; set; }
public Jid counterpart { get; set; }
public Jid ourpart { get; set; }
@@ -85,13 +72,45 @@ public class FileTransfer : Object {
public int provider { get; set; }
public string info { get; set; }
public Cancellable cancellable { get; default=new Cancellable(); }
+
+ // This value is not persisted
+ public int64 transferred_bytes { get; set; }
+
+ public Xep.FileMetadataElement.FileMetadata file_metadata {
+ owned get {
+ return new Xep.FileMetadataElement.FileMetadata() {
+ name = this.file_name,
+ mime_type = this.mime_type,
+ size = this.size,
+ desc = this.desc,
+ date = this.modification_date,
+ width = this.width,
+ height = this.height,
+ length = this.length,
+ hashes = this.hashes,
+ thumbnails = this.thumbnails
+ };
+ }
+ set {
+ this.file_name = value.name;
+ this.mime_type = value.mime_type;
+ this.size = value.size;
+ this.desc = value.desc;
+ this.modification_date = value.date;
+ this.width = value.width;
+ this.height = value.height;
+ this.length = value.length;
+ this.hashes = value.hashes;
+ this.thumbnails = value.thumbnails;
+ }
+ }
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.CryptographicHashes.Hash> hashes = new Gee.ArrayList<Xep.CryptographicHashes.Hash>();
+ public Gee.List<Xep.StatelessFileSharing.Source> sfs_sources = new Gee.ArrayList<Xep.StatelessFileSharing.Source>(Xep.StatelessFileSharing.Source.equals_func);
public Gee.List<Xep.JingleContentThumbnails.Thumbnail> thumbnails = new Gee.ArrayList<Xep.JingleContentThumbnails.Thumbnail>();
private Database? db;
@@ -102,6 +121,7 @@ public class FileTransfer : Object {
this.storage_dir = storage_dir;
id = row[db.file_transfer.id];
+ file_sharing_id = row[db.file_transfer.file_sharing_id];
account = db.get_account_by_id(row[db.file_transfer.account_id]); // TODO don’t have to generate acc new
counterpart = db.get_jid_by_id(row[db.file_transfer.counterpart_id]);
@@ -130,11 +150,12 @@ public class FileTransfer : Object {
height = row[db.file_transfer.height];
length = (int64) row[db.file_transfer.length];
+ // TODO put those into the initial query
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);
+ hashes.add(hash);
}
foreach(var thumbnail_row in db.file_thumbnails.select().with(db.file_thumbnails.id, "=", id)) {
@@ -146,11 +167,10 @@ public class FileTransfer : Object {
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);
+ foreach(Qlite.Row source_row in db.sfs_sources.select().with(db.sfs_sources.file_transfer_id, "=", id)) {
+ if (source_row[db.sfs_sources.type] == "http") {
+ sfs_sources.add(new Xep.StatelessFileSharing.HttpSource() { url=source_row[db.sfs_sources.data] });
+ }
}
notify.connect(on_update);
@@ -175,6 +195,7 @@ public class FileTransfer : Object {
.value(db.file_transfer.provider, provider)
.value(db.file_transfer.info, info);
+ if (file_sharing_id != null) builder.value(db.file_transfer.file_sharing_id, file_sharing_id);
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);
@@ -185,7 +206,7 @@ public class FileTransfer : Object {
id = (int) builder.perform();
- foreach (Xep.CryptographicHashes.Hash hash in hashes.hashes) {
+ foreach (Xep.CryptographicHashes.Hash hash in hashes) {
db.file_hashes.insert()
.value(db.file_hashes.id, id)
.value(db.file_hashes.algo, hash.algo)
@@ -202,29 +223,40 @@ public class FileTransfer : Object {
.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;
+ foreach (Xep.StatelessFileSharing.Source source in sfs_sources) {
+ add_sfs_source(source);
+ }
+
+ notify.connect(on_update);
+ }
+
+ public void add_sfs_source(Xep.StatelessFileSharing.Source source) {
+ if (sfs_sources.contains(source)) return; // Don't add the same source twice. Might happen due to MAM and lacking deduplication.
+
+ sfs_sources.add(source);
+
+ Xep.StatelessFileSharing.HttpSource? http_source = source as Xep.StatelessFileSharing.HttpSource;
+ if (http_source != null) {
db.sfs_sources.insert()
- .value(db.sfs_sources.id, id)
- .value(db.sfs_sources.type, source.type)
- .value(db.sfs_sources.data, source.data)
+ .value(db.sfs_sources.file_transfer_id, id)
+ .value(db.sfs_sources.type, "http")
+ .value(db.sfs_sources.data, http_source.url)
.perform();
}
- notify.connect(on_update);
- sfs_sources.items_changed.connect((position, removed, added) => {
- on_update_sources_items(this, position, removed, added);
- });
+ sources_changed();
}
- public File get_file() {
+ public File? get_file() {
+ if (path == null) return null;
return File.new_for_path(Path.build_filename(Dino.get_storage_dir(), "files", path));
}
private void on_update(Object o, ParamSpec sp) {
Qlite.UpdateBuilder update_builder = db.file_transfer.update().with(db.file_transfer.id, "=", id);
switch (sp.name) {
+ case "file-sharing-id":
+ update_builder.set(db.file_transfer.file_sharing_id, file_sharing_id); break;
case "counterpart":
update_builder.set(db.file_transfer.counterpart_id, db.get_jid_id(counterpart));
update_builder.set(db.file_transfer.counterpart_resource, counterpart.resourcepart); break;
@@ -264,58 +296,6 @@ public class FileTransfer : Object {
}
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 d48f999b..e5aad25f 100644
--- a/libdino/src/entity/message.vala
+++ b/libdino/src/entity/message.vala
@@ -1,6 +1,5 @@
using Gee;
using Xmpp;
-using Xmpp.Xep;
namespace Dino.Entities {
diff --git a/libdino/src/service/content_item_store.vala b/libdino/src/service/content_item_store.vala
index 740dc2a9..7d7ed1fb 100644
--- a/libdino/src/service/content_item_store.vala
+++ b/libdino/src/service/content_item_store.vala
@@ -121,13 +121,7 @@ public class ContentItemStore : StreamInteractionModule, Object {
Message? message = get_message_for_content_item(conversation, content_item);
if (message == null) return null;
- if (message.edit_to != null) return message.edit_to;
-
- if (conversation.type_ == Conversation.Type.CHAT) {
- return message.stanza_id;
- } else {
- return message.server_id;
- }
+ return MessageStorage.get_reference_id(message);
}
public Jid? get_message_sender_for_content_item(Conversation conversation, ContentItem content_item) {
diff --git a/libdino/src/service/database.vala b/libdino/src/service/database.vala
index 30d6d55f..bfa76890 100644
--- a/libdino/src/service/database.vala
+++ b/libdino/src/service/database.vala
@@ -180,6 +180,7 @@ public class Database : Qlite.Database {
public class FileTransferTable : Table {
public Column<int> id = new Column.Integer("id") { primary_key = true, auto_increment = true };
+ public Column<string> file_sharing_id = new Column.Text("file_sharing_id") { min_version=28 };
public Column<int> account_id = new Column.Integer("account_id") { not_null = true };
public Column<int> counterpart_id = new Column.Integer("counterpart_id") { not_null = true };
public Column<string> counterpart_resource = new Column.Text("counterpart_resource");
@@ -191,7 +192,7 @@ 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<long> size = new Column.Long("size") { default = "-1", min_version=28 };
+ public Column<long> size = new Column.Long("size");
public Column<int> state = new Column.Integer("state");
public Column<int> provider = new Column.Integer("provider");
public Column<string> info = new Column.Text("info");
@@ -202,9 +203,9 @@ public class Database : Qlite.Database {
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, modification_date, width, height,
- length});
+ init({id, file_sharing_id, account_id, counterpart_id, counterpart_resource, our_resource, direction,
+ time, local_time, encryption, file_name, path, mime_type, size, state, provider, info, modification_date,
+ width, height, length});
}
}
@@ -233,15 +234,15 @@ public class Database : Qlite.Database {
}
}
- public class SfsSourcesTable : Table {
- public Column<int> id = new Column.Integer("id");
+ public class SourcesTable : Table {
+ public Column<int> file_transfer_id = new Column.Integer("file_transfer_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) {
+ internal SourcesTable(Database db) {
base(db, "sfs_sources");
- init({id, type, data});
- unique({id, type, data}, "REPLACE");
+ init({file_transfer_id, type, data});
+ index("sfs_sources_file_transfer_id_idx", {file_transfer_id});
}
}
@@ -445,7 +446,7 @@ public class Database : Qlite.Database {
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 SourcesTable sfs_sources { get; private set; }
public CallTable call { get; private set; }
public CallCounterpartTable call_counterpart { get; private set; }
public ConversationTable conversation { get; private set; }
@@ -478,7 +479,7 @@ public class Database : Qlite.Database {
file_transfer = new FileTransferTable(this);
file_hashes = new FileHashesTable(this);
file_thumbnails = new FileThumbnailsTable(this);
- sfs_sources = new SfsSourcesTable(this);
+ sfs_sources = new SourcesTable(this);
call = new CallTable(this);
call_counterpart = new CallCounterpartTable(this);
conversation = new ConversationTable(this);
diff --git a/libdino/src/service/file_manager.vala b/libdino/src/service/file_manager.vala
index 32cf23c4..2a665e1e 100644
--- a/libdino/src/service/file_manager.vala
+++ b/libdino/src/service/file_manager.vala
@@ -22,6 +22,11 @@ public class FileManager : StreamInteractionModule, Object {
private Gee.List<FileProvider> file_providers = new ArrayList<FileProvider>();
private Gee.List<FileMetadataProvider> file_metadata_providers = new ArrayList<FileMetadataProvider>();
+ public StatelessFileSharing sfs {
+ owned get { return stream_interactor.get_module(StatelessFileSharing.IDENTITY); }
+ private set { }
+ }
+
public static void start(StreamInteractor stream_interactor, Database db) {
FileManager m = new FileManager(stream_interactor, db);
stream_interactor.add_module(m);
@@ -40,15 +45,12 @@ public class FileManager : StreamInteractionModule, Object {
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) {
+ public 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()) {
@@ -61,98 +63,7 @@ public class FileManager : StreamInteractionModule, Object {
return null;
}
- // 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) {
+ 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);
@@ -180,7 +91,7 @@ public class FileManager : StreamInteractionModule, Object {
yield file_metadata_provider.fill_metadata(file, metadata);
}
}
- file_transfer.with_metadata_element(metadata);
+ file_transfer.file_metadata = metadata;
try {
file_transfer.input_stream = yield file.read_async();
@@ -237,7 +148,16 @@ public class FileManager : StreamInteractionModule, Object {
file_send_data = file_encryptor.preprocess_send_file(conversation, file_transfer, file_send_data, file_meta);
}
+ file_transfer.state = FileTransfer.State.IN_PROGRESS;
+
+ // Update current download progress in the FileTransfer
+ LimitInputStream? limit_stream = file_transfer.input_stream as LimitInputStream;
+ if (limit_stream != null) {
+ limit_stream.bind_property("retrieved-bytes", file_transfer, "transferred-bytes", BindingFlags.SYNC_CREATE);
+ }
+
yield file_sender.send_file(conversation, file_transfer, file_send_data, file_meta);
+ file_transfer.state = FileTransfer.State.COMPLETE;
} catch (Error e) {
warning("Send file error: %s", e.message);
@@ -265,12 +185,7 @@ 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) => {
- 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);
- }
+ handle_incoming_file.begin(file_provider, info, from, time, local_time, conversation, receive_data, file_meta);
});
}
@@ -296,10 +211,12 @@ public class FileManager : StreamInteractionModule, Object {
file_metadata_providers.add(file_metadata_provider);
}
- private bool is_jid_trustworthy(Jid from, Conversation conversation) {
+ 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(from, conversation.account);
+ relevant_jid = stream_interactor.get_module(MucManager.IDENTITY).get_real_jid(file_transfer.from, conversation.account);
}
if (relevant_jid == null) return false;
@@ -307,12 +224,6 @@ 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);
@@ -337,7 +248,11 @@ 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 = yield file_provider.get_file_receive_data(file_transfer);
+ FileReceiveData? receive_data = file_provider.get_file_receive_data(file_transfer);
+ if (receive_data == null) {
+ warning("Don't have download data (yet)");
+ return;
+ }
FileDecryptor? file_decryptor = null;
foreach (FileDecryptor decryptor in file_decryptors) {
if (decryptor.can_decrypt_file(conversation, file_transfer, receive_data)) {
@@ -367,10 +282,17 @@ public class FileManager : StreamInteractionModule, Object {
input_stream = yield file_decryptor.decrypt_file(input_stream, conversation, file_transfer, receive_data);
}
+ // Update current download progress in the FileTransfer
+ LimitInputStream? limit_stream = input_stream as LimitInputStream;
+ if (limit_stream != null) {
+ limit_stream.bind_property("retrieved-bytes", file_transfer, "transferred-bytes", BindingFlags.SYNC_CREATE);
+ }
+
// Save file
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));
+ // libsoup doesn't properly support splicing
OutputStream os = file.create(FileCreateFlags.REPLACE_DESTINATION);
uint8[] buffer = new uint8[1024];
ssize_t read;
@@ -381,20 +303,49 @@ public class FileManager : StreamInteractionModule, Object {
}
yield input_stream.close_async(Priority.LOW, file_transfer.cancellable);
yield os.close_async(Priority.LOW, file_transfer.cancellable);
+
+ // Verify the hash of the downloaded file, if it is known
+ var supported_hashes = Xep.CryptographicHashes.get_supported_hashes(file_transfer.hashes);
+ if (!supported_hashes.is_empty) {
+ var checksum_types = new ArrayList<ChecksumType>();
+ var hashes = new HashMap<ChecksumType, string>();
+ foreach (var hash in supported_hashes) {
+ var checksum_type = Xep.CryptographicHashes.hash_string_to_type(hash.algo);
+ checksum_types.add(checksum_type);
+ hashes[checksum_type] = hash.val;
+ }
+
+ var computed_hashes = yield compute_file_hashes(file, checksum_types);
+ foreach (var checksum_type in hashes.keys) {
+ if (hashes[checksum_type] != computed_hashes[checksum_type]) {
+ warning("Hash of downloaded file does not equal advertised hash, discarding: %s. %s should be %s, was %s",
+ file_transfer.file_name, checksum_type.to_string(), hashes[checksum_type], computed_hashes[checksum_type]);
+ FileUtils.remove(file.get_path());
+ file_transfer.state = FileTransfer.State.FAILED;
+ return;
+ }
+ }
+ }
+
file_transfer.path = file.get_basename();
- file_transfer.input_stream = yield file.read_async();
FileInfo file_info = file_transfer.get_file().query_info("*", FileQueryInfoFlags.NONE);
file_transfer.mime_type = file_info.get_content_type();
file_transfer.state = FileTransfer.State.COMPLETE;
+ } catch (IOError.CANCELLED e) {
+ print("cancelled\n");
} catch (Error e) {
warning("Error downloading file: %s", e.message);
- file_transfer.state = FileTransfer.State.FAILED;
+ if (file_transfer.provider == 0 || file_transfer.provider == FileManager.SFS_PROVIDER_ID) {
+ file_transfer.state = FileTransfer.State.NOT_STARTED;
+ } else {
+ file_transfer.state = FileTransfer.State.FAILED;
+ }
}
}
- private async void handle_incoming_file(FileProvider file_provider, string info, Jid from, DateTime time, DateTime local_time, Conversation conversation, FileReceiveData receive_data, FileMeta file_meta) {
+ public FileTransfer create_file_transfer_from_provider_incoming(FileProvider file_provider, string info, Jid from, DateTime time, DateTime local_time, Conversation conversation, FileReceiveData receive_data, FileMeta file_meta) {
FileTransfer file_transfer = new FileTransfer();
file_transfer.account = conversation.account;
file_transfer.counterpart = file_transfer.direction == FileTransfer.DIRECTION_RECEIVED ? from : conversation.counterpart;
@@ -426,6 +377,11 @@ public class FileManager : StreamInteractionModule, Object {
}
}
+ return file_transfer;
+ }
+
+ private async void handle_incoming_file(FileProvider file_provider, string info, Jid from, DateTime time, DateTime local_time, Conversation conversation, FileReceiveData receive_data, FileMeta file_meta) {
+ 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)) {
@@ -451,10 +407,10 @@ public class FileManager : StreamInteractionModule, Object {
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);
+ 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 = yield file.read_async();
+ 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));
}
@@ -467,10 +423,10 @@ public errordomain FileSendError {
SAVE_FAILED
}
+// Get rid of this Error and pass IoErrors instead - DOWNLOAD_FAILED already removed
public errordomain FileReceiveError {
GET_METADATA_FAILED,
- DECRYPTION_FAILED,
- DOWNLOAD_FAILED
+ DECRYPTION_FAILED
}
public class FileMeta {
@@ -505,10 +461,10 @@ 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 async FileReceiveData? get_file_receive_data(FileTransfer file_transfer);
+ public abstract 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;
+ public abstract async InputStream download(FileTransfer file_transfer, FileReceiveData receive_data, FileMeta file_meta) throws IOError;
public abstract int get_id();
}
@@ -541,107 +497,4 @@ 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/file_transfer_storage.vala b/libdino/src/service/file_transfer_storage.vala
index 1cc62403..64bb6b81 100644
--- a/libdino/src/service/file_transfer_storage.vala
+++ b/libdino/src/service/file_transfer_storage.vala
@@ -14,6 +14,8 @@ namespace Dino {
private Database db;
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<string, FileTransfer> files_by_message_and_file_id = new WeakMap<string, FileTransfer>();
public static void start(StreamInteractor stream_interactor, Database db) {
FileTransferStorage m = new FileTransferStorage(stream_interactor, db);
@@ -41,6 +43,42 @@ namespace Dino {
return create_file_from_row_opt(row_option, conversation);
}
+ // Http file transfers store the corresponding message id in the `info` field
+ public FileTransfer? get_file_by_message_id(int id, Conversation conversation) {
+ FileTransfer? file_transfer = files_by_message_id[id];
+ if (file_transfer != null) {
+ return file_transfer;
+ }
+
+ RowOption row_option = db.file_transfer.select()
+ .with(db.file_transfer.info, "=", id.to_string())
+ .single()
+ .row();
+
+ return create_file_from_row_opt(row_option, conversation);
+ }
+
+ 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];
+
+ if (file_transfer == null) {
+ RowOption row_option = db.file_transfer.select()
+ .with(db.file_transfer.info, "=", message_id.to_string())
+ .with(db.file_transfer.file_sharing_id, "=", file_sharing_id)
+ .single()
+ .row();
+
+ file_transfer = create_file_from_row_opt(row_option, conversation);
+ }
+
+ // There can be collisions in the combined identifier, check it's the correct FileTransfer
+ if (file_transfer != null && file_transfer.info == message_id.to_string() && file_transfer.file_sharing_id == file_sharing_id) {
+ return file_transfer;
+ }
+ return null;
+ }
+
private FileTransfer? create_file_from_row_opt(RowOption row_opt, Conversation conversation) {
if (!row_opt.is_present()) return null;
@@ -61,6 +99,15 @@ namespace Dino {
private void cache_file(FileTransfer file_transfer) {
files_by_db_id[file_transfer.id] = file_transfer;
+
+ if (file_transfer.info != null && file_transfer.info != "") {
+ files_by_message_id[int.parse(file_transfer.info)] = file_transfer;
+
+ if (file_transfer.file_sharing_id != null && file_transfer.info != null) {
+ string combined_identifier = file_transfer.info + file_transfer.file_sharing_id;
+ files_by_message_and_file_id[combined_identifier] = file_transfer;
+ }
+ }
}
}
} \ No newline at end of file
diff --git a/libdino/src/service/jingle_file_transfers.vala b/libdino/src/service/jingle_file_transfers.vala
index daccd309..e0d3fce1 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 async FileReceiveData? get_file_receive_data(FileTransfer file_transfer) {
+ public FileReceiveData? get_file_receive_data(FileTransfer file_transfer) {
return new FileReceiveData();
}
@@ -95,18 +95,14 @@ public class JingleFileProvider : FileProvider, Object {
return Encryption.NONE;
}
- public async InputStream download(FileTransfer file_transfer, FileReceiveData receive_data, FileMeta file_meta) throws FileReceiveError {
+ public async InputStream download(FileTransfer file_transfer, FileReceiveData receive_data, FileMeta file_meta) throws IOError {
// TODO(hrxi) What should happen if `stream == null`?
XmppStream? stream = stream_interactor.get_stream(file_transfer.account);
Xmpp.Xep.JingleFileTransfer.FileTransfer? jingle_file_transfer = file_transfers[file_transfer.info];
if (jingle_file_transfer == null) {
- throw new FileReceiveError.DOWNLOAD_FAILED("Transfer data not available anymore");
- }
- try {
- yield jingle_file_transfer.accept(stream);
- } catch (IOError e) {
- throw new FileReceiveError.DOWNLOAD_FAILED("Establishing connection did not work");
+ throw new IOError.FAILED("Transfer data not available anymore");
}
+ yield jingle_file_transfer.accept(stream);
return jingle_file_transfer.stream;
}
diff --git a/libdino/src/service/message_processor.vala b/libdino/src/service/message_processor.vala
index d8ea3e2d..e8a43b05 100644
--- a/libdino/src/service/message_processor.vala
+++ b/libdino/src/service/message_processor.vala
@@ -307,7 +307,8 @@ public class MessageProcessor : StreamInteractionModule, Object {
public override string[] after_actions { get { return after_actions_const; } }
public override async bool run(Entities.Message message, Xmpp.MessageStanza stanza, Conversation conversation) {
- return (message.body == null);
+ return message.body == null &&
+ Xep.StatelessFileSharing.get_file_shares(stanza) == null;
}
}
@@ -326,8 +327,6 @@ public class MessageProcessor : StreamInteractionModule, Object {
}
public override async bool run(Entities.Message message, Xmpp.MessageStanza stanza, Conversation conversation) {
- if (message.body == null || outer.is_duplicate(message, stanza, conversation)) return true;
-
stream_interactor.get_module(MessageStorage.IDENTITY).add_message(message, conversation);
return false;
}
@@ -371,7 +370,7 @@ public class MessageProcessor : StreamInteractionModule, Object {
}
}
- public Entities.Message create_out_message(string text, Conversation conversation) {
+ public Entities.Message create_out_message(string? text, Conversation conversation) {
Entities.Message message = new Entities.Message(text);
message.type_ = Util.get_message_type_for_conversation(conversation);
message.stanza_id = random_uuid();
diff --git a/libdino/src/service/message_storage.vala b/libdino/src/service/message_storage.vala
index 3dadab7b..81df46d5 100644
--- a/libdino/src/service/message_storage.vala
+++ b/libdino/src/service/message_storage.vala
@@ -99,6 +99,14 @@ public class MessageStorage : StreamInteractionModule, Object {
return create_message_from_row_opt(row_option, conversation);
}
+ 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);
+ } else {
+ return stream_interactor.get_module(MessageStorage.IDENTITY).get_message_by_server_id(id, conversation);
+ }
+ }
+
public Message? get_message_by_stanza_id(string stanza_id, Conversation conversation) {
if (messages_by_stanza_id.has_key(conversation)) {
Message? message = messages_by_stanza_id[conversation][stanza_id];
@@ -191,6 +199,16 @@ public class MessageStorage : StreamInteractionModule, Object {
message_refs.remove_at(message_refs.size - 1);
}
}
+
+ public static string? get_reference_id(Message message) {
+ if (message.edit_to != null) return message.edit_to;
+
+ if (message.type_ == Message.Type.CHAT) {
+ return message.stanza_id;
+ } else {
+ return message.server_id;
+ }
+ }
}
}
diff --git a/libdino/src/service/module_manager.vala b/libdino/src/service/module_manager.vala
index 28ccf8ef..eb9e4fbc 100644
--- a/libdino/src/service/module_manager.vala
+++ b/libdino/src/service/module_manager.vala
@@ -86,7 +86,6 @@ 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]);
}
}
diff --git a/libdino/src/service/sfs_metadata.vala b/libdino/src/service/sfs_metadata.vala
new file mode 100644
index 00000000..ff2a57b4
--- /dev/null
+++ b/libdino/src/service/sfs_metadata.vala
@@ -0,0 +1,81 @@
+using Gdk;
+using GLib;
+using Gee;
+
+using Xmpp;
+using Xmpp.Xep;
+using Dino.Entities;
+
+
+namespace Dino {
+ 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();
+
+ var checksum_types = new ArrayList<ChecksumType>.wrap(new ChecksumType[] { ChecksumType.SHA256, ChecksumType.SHA512 });
+ var file_hashes = yield compute_file_hashes(file, checksum_types);
+
+ metadata.hashes.add(new CryptographicHashes.Hash.with_checksum(ChecksumType.SHA256, file_hashes[ChecksumType.SHA256]));
+ metadata.hashes.add(new CryptographicHashes.Hash.with_checksum(ChecksumType.SHA512, file_hashes[ChecksumType.SHA512]));
+ }
+ }
+
+ 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);
+ }
+ }
+}
+
diff --git a/libdino/src/service/stateless_file_sharing.vala b/libdino/src/service/stateless_file_sharing.vala
new file mode 100644
index 00000000..9d3c53ab
--- /dev/null
+++ b/libdino/src/service/stateless_file_sharing.vala
@@ -0,0 +1,162 @@
+using Gdk;
+using Gee;
+
+using Xmpp;
+using Xmpp.Xep;
+using Dino.Entities;
+
+public class Dino.StatelessFileSharing : StreamInteractionModule, Object {
+ public static ModuleIdentity<StatelessFileSharing> IDENTITY = new ModuleIdentity<StatelessFileSharing>("sfs");
+ public string id { get { return IDENTITY.id; } }
+
+ public const int SFS_PROVIDER_ID = 2;
+
+ public StreamInteractor stream_interactor {
+ owned get { return Application.get_default().stream_interactor; }
+ private set { }
+ }
+
+ public FileManager file_manager {
+ owned get { return stream_interactor.get_module(FileManager.IDENTITY); }
+ private set { }
+ }
+
+ public Database db {
+ owned get { return Application.get_default().db; }
+ private set { }
+ }
+
+ private StatelessFileSharing(StreamInteractor stream_interactor, Database db) {
+ this.stream_interactor = stream_interactor;
+ this.db = db;
+
+ stream_interactor.get_module(MessageProcessor.IDENTITY).received_pipeline.connect(new ReceivedMessageListener(this));
+ }
+
+ public static void start(StreamInteractor stream_interactor, Database db) {
+ StatelessFileSharing m = new StatelessFileSharing(stream_interactor, db);
+ 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) {
+ FileTransfer file_transfer = new FileTransfer();
+ file_transfer.file_sharing_id = file_sharing_id;
+ file_transfer.account = message.account;
+ file_transfer.counterpart = message.counterpart;
+ file_transfer.ourpart = message.ourpart;
+ file_transfer.direction = message.direction;
+ file_transfer.time = message.time;
+ file_transfer.local_time = message.local_time;
+ file_transfer.provider = SFS_PROVIDER_ID;
+ file_transfer.file_metadata = metadata;
+ file_transfer.info = message.id.to_string();
+ if (sources != null) {
+ file_transfer.sfs_sources = sources;
+ }
+
+ stream_interactor.get_module(FileTransferStorage.IDENTITY).add_file(file_transfer);
+
+ conversation.last_active = file_transfer.time;
+ file_manager.received_file(file_transfer, conversation);
+ }
+
+ 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) {
+ Message? message = stream_interactor.get_module(MessageStorage.IDENTITY).get_message_by_referencing_id(attach_to_message_id, conversation);
+ if (message == null) return;
+
+ FileTransfer? file_transfer = null;
+ if (attach_to_file_id != null) {
+ file_transfer = stream_interactor.get_module(FileTransferStorage.IDENTITY).get_files_by_message_and_file_id(message.id, attach_to_file_id, conversation);
+ } else {
+ file_transfer = stream_interactor.get_module(FileTransferStorage.IDENTITY).get_file_by_message_id(message.id, conversation);
+ }
+ if (file_transfer == null) return;
+
+ // "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");
+ return;
+ }
+
+ foreach (var source in sources) {
+ 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);
+ }
+ }
+
+ /*
+ 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;
+
+ 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);
+ }
+ }
+
+ var metadata = new Xep.FileMetadataElement.FileMetadata();
+ metadata.size = file_meta.size;
+ metadata.name = file_meta.file_name;
+ metadata.mime_type = file_meta.mime_type;
+
+ 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;
+ }
+
+ 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;
+ }
+ }
+ return false;
+ }
+ }
+} \ No newline at end of file
diff --git a/libdino/src/util/limit_input_stream.vala b/libdino/src/util/limit_input_stream.vala
new file mode 100644
index 00000000..5569d778
--- /dev/null
+++ b/libdino/src/util/limit_input_stream.vala
@@ -0,0 +1,73 @@
+public class Dino.LimitInputStream : InputStream, PollableInputStream {
+ private InputStream inner;
+ public int64 max_bytes { public get; private set; }
+ public int64 retrieved_bytes { public get; private set; }
+
+ public int64 remaining_bytes { get {
+ return max_bytes < 0 ? -1 : max_bytes - retrieved_bytes;
+ }}
+
+ public LimitInputStream(InputStream inner, int64 max_bytes) {
+ this.inner = inner;
+ this.max_bytes = max_bytes;
+ }
+
+ public bool can_poll() {
+ return inner is PollableInputStream && ((PollableInputStream)inner).can_poll();
+ }
+
+ public PollableSource create_source(Cancellable? cancellable = null) {
+ if (!can_poll()) throw new IOError.NOT_SUPPORTED("Stream is not pollable");
+ return ((PollableInputStream)inner).create_source(cancellable);
+ }
+
+ public bool is_readable() {
+ if (!can_poll()) throw new IOError.NOT_SUPPORTED("Stream is not pollable");
+ return remaining_bytes == 0 || ((PollableInputStream)inner).is_readable();
+ }
+
+ private ssize_t check_limit(ssize_t read) throws IOError {
+ if (remaining_bytes - (int64) read < 0) throw new IOError.FAILED("Stream length exceeded limit");
+ this.retrieved_bytes += read;
+ return read;
+ }
+
+ public override ssize_t read(uint8[] buffer, Cancellable? cancellable = null) throws IOError {
+ if (remaining_bytes == 0) return 0;
+ int original_buffer_length = buffer.length;
+ if (remaining_bytes != -1 && (int64) buffer.length > remaining_bytes) {
+ // Never read more than remaining_bytes by limiting the buffer length
+ buffer.length = (int) remaining_bytes;
+ }
+ ssize_t read_bytes = inner.read(buffer, cancellable);
+ this.retrieved_bytes += read_bytes;
+ buffer.length = original_buffer_length;
+ return read_bytes;
+ }
+
+ public override async ssize_t read_async(uint8[]? buffer, int io_priority = GLib.Priority.DEFAULT, Cancellable? cancellable = null) throws IOError {
+ if (remaining_bytes == 0) return 0;
+ int original_buffer_length = buffer.length;
+ if (remaining_bytes != -1 && (int64) buffer.length > remaining_bytes) {
+ // Never read more than remaining_bytes by limiting the buffer length
+ buffer.length = (int) remaining_bytes;
+ }
+ ssize_t read_bytes = yield inner.read_async(buffer, io_priority, cancellable);
+ this.retrieved_bytes += read_bytes;
+ buffer.length = original_buffer_length;
+ return read_bytes;
+ }
+
+ public ssize_t read_nonblocking_fn(uint8[] buffer) throws Error {
+ if (!is_readable()) throw new IOError.WOULD_BLOCK("Stream is not readable");
+ return read(buffer);
+ }
+
+ public override bool close(Cancellable? cancellable = null) throws IOError {
+ return inner.close(cancellable);
+ }
+
+ public override async bool close_async(int io_priority = GLib.Priority.DEFAULT, Cancellable? cancellable = null) throws IOError {
+ return yield inner.close_async(io_priority, cancellable);
+ }
+} \ No newline at end of file
diff --git a/libdino/src/util/util.vala b/libdino/src/util/util.vala
index 9f7ae45f..31f4c105 100644
--- a/libdino/src/util/util.vala
+++ b/libdino/src/util/util.vala
@@ -1,3 +1,6 @@
+using Gee;
+using Xmpp;
+
namespace Dino {
private extern const string SYSTEM_LIBDIR_NAME;
@@ -90,4 +93,32 @@ public static void internationalize(string gettext_package, string locales_dir)
Intl.bindtextdomain(gettext_package, locales_dir);
}
+public static async HashMap<ChecksumType, string> compute_file_hashes(File file, Gee.List<ChecksumType> checksum_types) {
+ var checksums = new Checksum[checksum_types.size];
+
+ for (int i = 0; i < checksum_types.size; i++) {
+ checksums[i] = new Checksum(checksum_types.get(i));
+ }
+
+ FileInputStream stream = yield file.read_async();
+ uint8 fbuf[1024];
+ size_t size;
+ while ((size = yield stream.read_async(fbuf)) > 0) {
+ for (int i = 0; i < checksum_types.size; i++) {
+ checksums[i].update(fbuf, size);
+ }
+ }
+
+ var ret = new HashMap<ChecksumType, string>();
+ for (int i = 0; i < checksum_types.size; i++) {
+ var checksum_type = checksum_types.get(i);
+ uint8[] digest = new uint8[64];
+ size_t length = digest.length;
+ checksums[i].get_digest(digest, ref length);
+ string computed_hash = GLib.Base64.encode(digest[0:length]);
+ ret[checksum_type] = computed_hash;
+ }
+ return ret;
+}
+
}