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