aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorfiaxh <git@lightrise.org>2024-11-02 22:24:59 +0100
committerfiaxh <git@lightrise.org>2024-11-15 14:40:08 -0600
commit79f792e090330a05753f9edb27332a946eb0840d (patch)
tree5a6f1ad3ac0af0beea44ca9e83e7a9b052263025
parentaaf4542e6208460c305db4be36b15dc832ddc95a (diff)
downloaddino-79f792e090330a05753f9edb27332a946eb0840d.tar.gz
dino-79f792e090330a05753f9edb27332a946eb0840d.zip
Fix and improve stateless file-sharing
-rw-r--r--crypto-vala/src/cipher_converter.vala2
-rw-r--r--libdino/CMakeLists.txt3
-rw-r--r--libdino/meson.build3
-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
-rw-r--r--main/CMakeLists.txt3
-rw-r--r--main/data/gresource.xml1
-rw-r--r--main/data/icons/scalable/actions/small-x-symbolic.svg2
-rw-r--r--main/data/style.css22
-rw-r--r--main/meson.build2
-rw-r--r--main/src/ui/conversation_content_view/file_default_widget.vala4
-rw-r--r--main/src/ui/conversation_content_view/file_image_widget.vala253
-rw-r--r--main/src/ui/conversation_content_view/file_preview_widget.vala90
-rw-r--r--main/src/ui/conversation_content_view/file_transmission_progress.vala120
-rw-r--r--main/src/ui/conversation_content_view/file_widget.vala55
-rw-r--r--main/src/ui/widgets/avatar_picture.vala2
-rw-r--r--plugins/http-files/src/file_provider.vala104
-rw-r--r--plugins/http-files/src/file_sender.vala87
-rw-r--r--xmpp-vala/CMakeLists.txt2
-rw-r--r--xmpp-vala/meson.build6
-rw-r--r--xmpp-vala/src/module/message/stanza.vala6
-rw-r--r--xmpp-vala/src/module/xep/0104_http_scheme_url_data.vala19
-rw-r--r--xmpp-vala/src/module/xep/0264_jingle_content_thumbnails.vala41
-rw-r--r--xmpp-vala/src/module/xep/0300_cryptographic_hashes.vala152
-rw-r--r--xmpp-vala/src/module/xep/0367_message_attaching.vala15
-rw-r--r--xmpp-vala/src/module/xep/0446_file_metadata_element.vala128
-rw-r--r--xmpp-vala/src/module/xep/0447_stateless_file_sharing.vala267
40 files changed, 1289 insertions, 1022 deletions
diff --git a/crypto-vala/src/cipher_converter.vala b/crypto-vala/src/cipher_converter.vala
index b2b52c5a..5fb1f548 100644
--- a/crypto-vala/src/cipher_converter.vala
+++ b/crypto-vala/src/cipher_converter.vala
@@ -55,7 +55,7 @@ public class SymmetricCipherEncrypter : SymmetricCipherConverter {
}
return ConverterResult.CONVERTED;
} catch (Crypto.Error e) {
- throw new IOError.FAILED(@"$(e.domain) error while decrypting: $(e.message)");
+ throw new IOError.FAILED(@"$(e.domain) error while encrypting: $(e.message)");
}
}
}
diff --git a/libdino/CMakeLists.txt b/libdino/CMakeLists.txt
index 34cf9575..c7c4a521 100644
--- a/libdino/CMakeLists.txt
+++ b/libdino/CMakeLists.txt
@@ -64,10 +64,13 @@ SOURCES
src/service/registration.vala
src/service/roster_manager.vala
src/service/search_processor.vala
+ src/service/sfs_metadata.vala
+ src/service/stateless_file_sharing.vala
src/service/stream_interactor.vala
src/service/util.vala
src/util/display_name.vala
+ src/util/limit_input_stream.vala
src/util/send_message.vala
src/util/util.vala
src/util/weak_map.vala
diff --git a/libdino/meson.build b/libdino/meson.build
index 559a81b5..85487d48 100644
--- a/libdino/meson.build
+++ b/libdino/meson.build
@@ -71,9 +71,12 @@ sources = files(
'src/service/registration.vala',
'src/service/roster_manager.vala',
'src/service/search_processor.vala',
+ 'src/service/sfs_metadata.vala',
+ 'src/service/stateless_file_sharing.vala',
'src/service/stream_interactor.vala',
'src/service/util.vala',
'src/util/display_name.vala',
+ 'src/util/limit_input_stream.vala',
'src/util/send_message.vala',
'src/util/util.vala',
'src/util/weak_map.vala',
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;
+}
+
}
diff --git a/main/CMakeLists.txt b/main/CMakeLists.txt
index fa5f56e8..70665903 100644
--- a/main/CMakeLists.txt
+++ b/main/CMakeLists.txt
@@ -19,6 +19,7 @@ set(RESOURCE_LIST
icons/scalable/actions/dino-emoticon-add-symbolic.svg
icons/scalable/actions/dino-qr-code-symbolic.svg
+ icons/scalable/actions/small-x-symbolic.svg
icons/scalable/apps/im.dino.Dino.svg
icons/scalable/apps/im.dino.Dino-symbolic.svg
@@ -185,7 +186,7 @@ SOURCES
src/ui/conversation_content_view/date_separator_populator.vala
src/ui/conversation_content_view/file_default_widget.vala
src/ui/conversation_content_view/file_image_widget.vala
- src/ui/conversation_content_view/file_preview_widget.vala
+ src/ui/conversation_content_view/file_transmission_progress.vala
src/ui/conversation_content_view/file_widget.vala
src/ui/conversation_content_view/item_actions.vala
src/ui/conversation_content_view/message_widget.vala
diff --git a/main/data/gresource.xml b/main/data/gresource.xml
index edceea3e..661185ab 100644
--- a/main/data/gresource.xml
+++ b/main/data/gresource.xml
@@ -24,6 +24,7 @@
<file>gtk/help-overlay.ui</file>
<file>icons/scalable/actions/dino-emoticon-add-symbolic.svg</file>
<file>icons/scalable/actions/dino-qr-code-symbolic.svg</file>
+ <file>icons/scalable/actions/small-x-symbolic.svg</file>
<file>icons/scalable/apps/im.dino.Dino-symbolic.svg</file>
<file>icons/scalable/apps/im.dino.Dino.svg</file>
<file>icons/scalable/devices/dino-device-desktop-symbolic.svg</file>
diff --git a/main/data/icons/scalable/actions/small-x-symbolic.svg b/main/data/icons/scalable/actions/small-x-symbolic.svg
new file mode 100644
index 00000000..aadc5d1e
--- /dev/null
+++ b/main/data/icons/scalable/actions/small-x-symbolic.svg
@@ -0,0 +1,2 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<svg xmlns="http://www.w3.org/2000/svg" height="16px" viewBox="0 0 16 16" width="16px"><path d="m 4 4 h 1 h 0.03125 c 0.253906 0.011719 0.511719 0.128906 0.6875 0.3125 l 2.28125 2.28125 l 2.3125 -2.28125 c 0.265625 -0.230469 0.445312 -0.304688 0.6875 -0.3125 h 1 v 1 c 0 0.285156 -0.035156 0.550781 -0.25 0.75 l -2.28125 2.28125 l 2.25 2.25 c 0.1875 0.1875 0.28125 0.453125 0.28125 0.71875 v 1 h -1 c -0.265625 0 -0.53125 -0.09375 -0.71875 -0.28125 l -2.28125 -2.28125 l -2.28125 2.28125 c -0.1875 0.1875 -0.453125 0.28125 -0.71875 0.28125 h -1 v -1 c 0 -0.265625 0.09375 -0.53125 0.28125 -0.71875 l 2.28125 -2.25 l -2.28125 -2.28125 c -0.210938 -0.195312 -0.304688 -0.46875 -0.28125 -0.75 z m 0 0" fill="#222222"/></svg>
diff --git a/main/data/style.css b/main/data/style.css
index 5b9da21d..0909f8a0 100644
--- a/main/data/style.css
+++ b/main/data/style.css
@@ -181,6 +181,28 @@ window.dino-main .file-image-widget .file-box-outer button:hover {
border-radius: 6px;
}
+.dino-main .file-details {
+ color: white;
+ background: alpha(black, 0.3);
+ border-radius: 5px;
+ padding: 5px 10px;
+}
+
+.dino-main .circular-loading-indicator {
+ border-radius: 50%;
+ padding: 5px;
+ transition: background-image 0.5s linear;
+}
+
+.dino-main .circular-loading-indicator > * {
+ border-radius: 50%;
+ background: @theme_bg_color;
+}
+
+.dino-main .circular-loading-indicator button {
+ padding: 2px;
+}
+
/* Call widget */
window.dino-main .call-box-outer.incoming {
diff --git a/main/meson.build b/main/meson.build
index 30a28032..2dcf751b 100644
--- a/main/meson.build
+++ b/main/meson.build
@@ -47,6 +47,7 @@ sources = files(
'src/ui/conversation_content_view/date_separator_populator.vala',
'src/ui/conversation_content_view/file_default_widget.vala',
'src/ui/conversation_content_view/file_image_widget.vala',
+ 'src/ui/conversation_content_view/file_transmission_progress.vala',
'src/ui/conversation_content_view/file_widget.vala',
'src/ui/conversation_content_view/item_actions.vala',
'src/ui/conversation_content_view/message_widget.vala',
@@ -77,6 +78,7 @@ sources = files(
'src/ui/util/accounts_combo_box.vala',
'src/ui/util/config.vala',
'src/ui/util/data_forms.vala',
+ 'src/ui/util/file_metadata_providers.vala',
'src/ui/util/helper.vala',
'src/ui/util/label_hybrid.vala',
'src/ui/util/preference_group.vala',
diff --git a/main/src/ui/conversation_content_view/file_default_widget.vala b/main/src/ui/conversation_content_view/file_default_widget.vala
index 352c8d7a..02249b3f 100644
--- a/main/src/ui/conversation_content_view/file_default_widget.vala
+++ b/main/src/ui/conversation_content_view/file_default_widget.vala
@@ -39,7 +39,7 @@ public class FileDefaultWidget : Box {
});
}
- public void update_file_info(string? mime_type, FileTransfer.State state, long size) {
+ public void update_file_info(string? mime_type, FileTransfer.State state, int64 size) {
this.state = state;
spinner.stop(); // A hidden spinning spinner still uses CPU. Deactivate asap
@@ -132,7 +132,7 @@ public class FileDefaultWidget : Box {
}
}
- private static string get_size_string(long size) {
+ public static string get_size_string(int64 size) {
if (size < 1024) {
return @"$(size) B";
} else if (size < 1000 * 1000) {
diff --git a/main/src/ui/conversation_content_view/file_image_widget.vala b/main/src/ui/conversation_content_view/file_image_widget.vala
index a3579185..d5415f31 100644
--- a/main/src/ui/conversation_content_view/file_image_widget.vala
+++ b/main/src/ui/conversation_content_view/file_image_widget.vala
@@ -1,51 +1,37 @@
using Gee;
using Gdk;
using Gtk;
+using Xmpp;
using Dino.Entities;
namespace Dino.Ui {
public class FileImageWidget : Box {
+ enum State {
+ EMPTY,
+ PREVIEW,
+ IMAGE
+ }
+ private State state = State.EMPTY;
- public FileImageWidget() {
- this.halign = Align.START;
+ private Stack stack = new Stack() { transition_duration=600, transition_type=StackTransitionType.CROSSFADE };
+ private Overlay overlay = new Overlay();
- this.add_css_class("file-image-widget");
- this.set_cursor_from_name("zoom-in");
- }
+ private bool show_image_overlay_toolbar = false;
+ private Gtk.Box image_overlay_toolbar = new Gtk.Box(Orientation.VERTICAL, 0) { halign=Align.END, valign=Align.START, margin_top=10, margin_start=10, margin_end=10, margin_bottom=10, vexpand=false, visible=false };
+ private Label file_size_label = new Label(null) { halign=Align.START, valign=Align.END, margin_bottom=4, margin_start=4, visible=false };
- public async void load_from_file(File file, string file_name, int MAX_WIDTH=600, int MAX_HEIGHT=300) throws GLib.Error {
- Gtk.Box image_overlay_toolbar = new Gtk.Box(Orientation.HORIZONTAL, 0) { halign=Gtk.Align.END, valign=Gtk.Align.START, margin_top=10, margin_start=10, margin_end=10, margin_bottom=10, vexpand=false, visible=false };
- image_overlay_toolbar.add_css_class("card");
- image_overlay_toolbar.add_css_class("toolbar");
- image_overlay_toolbar.add_css_class("overlay-toolbar");
- image_overlay_toolbar.set_cursor_from_name("default");
+ private FileTransfer file_transfer;
- FixedRatioPicture image = new FixedRatioPicture() { min_width=100, min_height=100, max_width=MAX_WIDTH, max_height=MAX_HEIGHT, file=file };
- GestureClick gesture_click_controller = new GestureClick();
- gesture_click_controller.button = 1; // listen for left clicks
- gesture_click_controller.released.connect((n_press, x, y) => {
- switch (gesture_click_controller.get_device().source) {
- case Gdk.InputSource.TOUCHSCREEN:
- case Gdk.InputSource.PEN:
- if (n_press == 1) {
- image_overlay_toolbar.visible = !image_overlay_toolbar.visible;
- } else if (n_press == 2) {
- this.activate_action("file.open", null);
- image_overlay_toolbar.visible = false;
- }
- break;
- default:
- this.activate_action("file.open", null);
- image_overlay_toolbar.visible = false;
- break;
- }
- });
- image.add_controller(gesture_click_controller);
+ private FileTransmissionProgress transmission_progress = new FileTransmissionProgress() { halign=Align.CENTER, valign=Align.CENTER, visible=false };
+
+ public FileImageWidget(int MAX_WIDTH=600, int MAX_HEIGHT=300) {
+ this.halign = Align.START;
- FileInfo file_info = file.query_info("*", FileQueryInfoFlags.NONE);
+ this.add_css_class("file-image-widget");
+ // Setup menu button overlay
MenuButton button = new MenuButton();
button.icon_name = "view-more";
Menu menu_model = new Menu();
@@ -53,27 +39,216 @@ public class FileImageWidget : Box {
menu_model.append(_("Save as…"), "file.save_as");
Gtk.PopoverMenu popover_menu = new Gtk.PopoverMenu.from_model(menu_model);
button.popover = popover_menu;
-
image_overlay_toolbar.append(button);
+ image_overlay_toolbar.add_css_class("card");
+ image_overlay_toolbar.add_css_class("toolbar");
+ image_overlay_toolbar.add_css_class("overlay-toolbar");
+ image_overlay_toolbar.set_cursor_from_name("default");
- Overlay overlay = new Overlay();
- overlay.set_child(image);
+ file_size_label.add_css_class("file-details");
+
+ overlay.set_child(stack);
+ overlay.set_measure_overlay(stack, true);
+ overlay.add_overlay(file_size_label);
+ overlay.add_overlay(transmission_progress);
overlay.add_overlay(image_overlay_toolbar);
- overlay.set_measure_overlay(image, true);
overlay.set_clip_overlay(image_overlay_toolbar, true);
+ this.append(overlay);
+
+ GestureClick gesture_click_controller = new GestureClick();
+ gesture_click_controller.button = 1; // listen for left clicks
+ gesture_click_controller.released.connect(on_image_clicked);
+ stack.add_controller(gesture_click_controller);
+
EventControllerMotion this_motion_events = new EventControllerMotion();
this.add_controller(this_motion_events);
this_motion_events.enter.connect(() => {
- image_overlay_toolbar.visible = true;
+ image_overlay_toolbar.visible = show_image_overlay_toolbar;
+ file_size_label.visible = file_transfer != null && file_transfer.direction == FileTransfer.DIRECTION_RECEIVED && file_transfer.state == FileTransfer.State.NOT_STARTED && !file_transfer.sfs_sources.is_empty;
});
this_motion_events.leave.connect(() => {
if (button.popover != null && button.popover.visible) return;
image_overlay_toolbar.visible = false;
+ file_size_label.visible = false;
});
+ }
- this.append(overlay);
+ public async void set_file_transfer(FileTransfer file_transfer) {
+ this.file_transfer = file_transfer;
+
+ this.file_transfer.bind_property("size", file_size_label, "label", BindingFlags.SYNC_CREATE, (_, from_value, ref to_value) => {
+ to_value = FileDefaultWidget.get_size_string((int64) from_value);
+ return true;
+ });
+ this.file_transfer.bind_property("size", transmission_progress, "file-size", BindingFlags.SYNC_CREATE);
+ this.file_transfer.bind_property("transferred-bytes", transmission_progress, "transferred-size");
+
+ file_transfer.notify["state"].connect(refresh_state);
+ file_transfer.sources_changed.connect(refresh_state);
+ refresh_state();
+ }
+
+ private void refresh_state() {
+ if ((state == EMPTY || state == PREVIEW) && file_transfer.path != null) {
+ if (state == EMPTY) {
+ load_from_file.begin(file_transfer.get_file(), file_transfer.file_name);
+ show_image_overlay_toolbar = true;
+ } if (state == PREVIEW) {
+ Timeout.add(500, () => {
+ load_from_file.begin(file_transfer.get_file(), file_transfer.file_name);
+ show_image_overlay_toolbar = true;
+ return false;
+ });
+ }
+ this.set_cursor_from_name("zoom-in");
+
+ state = IMAGE;
+ } else if (state == EMPTY && file_transfer.thumbnails.size > 0) {
+ load_from_thumbnail.begin(file_transfer);
+
+ transmission_progress.visible = true;
+ show_image_overlay_toolbar = false;
+
+ state = PREVIEW;
+ }
+
+ if (file_transfer.state == IN_PROGRESS || file_transfer.state == NOT_STARTED || file_transfer.state == FAILED) {
+ transmission_progress.visible = true;
+ show_image_overlay_toolbar = false;
+ } else if (transmission_progress.visible) {
+ Timeout.add(500, () => {
+ transmission_progress.transferred_size = transmission_progress.file_size;
+ transmission_progress.visible = false;
+ show_image_overlay_toolbar = true;
+ return false;
+ });
+ }
+
+ if (file_transfer.state == FileTransfer.State.IN_PROGRESS) {
+ if (file_transfer.direction == FileTransfer.DIRECTION_RECEIVED) {
+ transmission_progress.state = FileTransmissionProgress.State.DOWNLOADING;
+ } else {
+ transmission_progress.state = FileTransmissionProgress.State.UPLOADING;
+ }
+ } else if (file_transfer.sfs_sources.is_empty) {
+ transmission_progress.state = UNKNOWN_SOURCE;
+ } else if (file_transfer.state == NOT_STARTED && file_transfer.direction == FileTransfer.DIRECTION_RECEIVED) {
+ transmission_progress.state = DOWNLOAD_NOT_STARTED;
+ } else if (file_transfer.state == FileTransfer.State.FAILED) {
+ transmission_progress.state = DOWNLOAD_NOT_STARTED_FAILED_BEFORE;
+ }
+ }
+
+ public async void load_from_file(File file, string file_name) throws GLib.Error {
+ FixedRatioPicture image = new FixedRatioPicture() { min_width=100, min_height=100, max_width=600, max_height=300 };
+ image.file = file;
+ stack.add_child(image);
+ stack.set_visible_child(image);
+ }
+
+ public async void load_from_thumbnail(FileTransfer file_transfer) throws GLib.Error {
+ this.file_transfer = file_transfer;
+
+ Gdk.Pixbuf? pixbuf = null;
+ foreach (Xep.JingleContentThumbnails.Thumbnail thumbnail in file_transfer.thumbnails) {
+ pixbuf = parse_thumbnail(thumbnail);
+ if (pixbuf != null) {
+ break;
+ }
+ }
+ if (pixbuf == null) {
+ warning("Can't load thumbnails of file %s", file_transfer.file_name);
+ throw new Error(-1, 0, "Error loading preview image");
+ }
+ // TODO: should this be executed? If yes, before or after scaling
+ pixbuf = pixbuf.apply_embedded_orientation();
+
+ if (file_transfer.width > 0 && file_transfer.height > 0) {
+ pixbuf = pixbuf.scale_simple(file_transfer.width, file_transfer.height, InterpType.BILINEAR);
+ } else {
+ warning("Preview: Not scaling image, width: %d, height: %d\n", file_transfer.width, file_transfer.height);
+ }
+ if (pixbuf == null) {
+ warning("Can't scale thumbnail %s", file_transfer.file_name);
+ throw new Error(-1, 0, "Error scaling preview image");
+ }
+
+ FixedRatioPicture image = new FixedRatioPicture() { min_width=100, min_height=100, max_width=600, max_height=300 };
+ image.paintable = Texture.for_pixbuf(pixbuf);
+ stack.add_child(image);
+ stack.set_visible_child(image);
+ }
+
+ public void on_image_clicked(GestureClick gesture_click_controller, int n_press, double x, double y) {
+ if (this.file_transfer.state != COMPLETE) return;
+
+ switch (gesture_click_controller.get_device().source) {
+ case Gdk.InputSource.TOUCHSCREEN:
+ case Gdk.InputSource.PEN:
+ if (n_press == 1) {
+ image_overlay_toolbar.visible = !image_overlay_toolbar.visible;
+ } else if (n_press == 2) {
+ this.activate_action("file.open", null);
+ image_overlay_toolbar.visible = false;
+ }
+ break;
+ default:
+ this.activate_action("file.open", null);
+ image_overlay_toolbar.visible = false;
+ break;
+ }
+ }
+
+ public static Pixbuf? parse_thumbnail(Xep.JingleContentThumbnails.Thumbnail thumbnail) {
+ string[] splits = thumbnail.uri.split(":", 2);
+ if (splits.length != 2) {
+ warning("Thumbnail parsing error: ':' not found");
+ return null;
+ }
+ if (splits[0] != "data") {
+ warning("Unsupported thumbnail: unimplemented uri type\n");
+ return null;
+ }
+ splits = splits[1].split(";", 2);
+ if (splits.length != 2) {
+ warning("Thumbnail parsing error: ';' not found");
+ return null;
+ }
+ if (splits[0] != "image/png") {
+ warning("Unsupported thumbnail: unsupported mime-type\n");
+ return null;
+ }
+ splits = splits[1].split(",", 2);
+ if (splits.length != 2) {
+ warning("Thumbnail parsing error: ',' not found");
+ return null;
+ }
+ if (splits[0] != "base64") {
+ warning("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;
+ }
+
+ public static bool can_display(FileTransfer file_transfer) {
+ return file_transfer.mime_type != null && is_pixbuf_supported_mime_type(file_transfer.mime_type) &&
+ (file_transfer.state == FileTransfer.State.COMPLETE || file_transfer.thumbnails.size > 0);
+ }
+
+ public static bool is_pixbuf_supported_mime_type(string mime_type) {
+ if (mime_type == null) return false;
+
+ foreach (PixbufFormat pixbuf_format in Pixbuf.get_formats()) {
+ foreach (string pixbuf_mime in pixbuf_format.get_mime_types()) {
+ if (pixbuf_mime == mime_type) return true;
+ }
+ }
+ return false;
}
}
diff --git a/main/src/ui/conversation_content_view/file_preview_widget.vala b/main/src/ui/conversation_content_view/file_preview_widget.vala
deleted file mode 100644
index e7cee93a..00000000
--- a/main/src/ui/conversation_content_view/file_preview_widget.vala
+++ /dev/null
@@ -1,90 +0,0 @@
-using Gee;
-using Gdk;
-using Gtk;
-using Xmpp;
-
-using Dino.Entities;
-
-namespace Dino.Ui {
-
- public class FilePreviewWidget : Box {
-
- private ScalingImage image;
- FileDefaultWidget file_default_widget;
- FileDefaultWidgetController file_default_widget_controller;
-
- public FilePreviewWidget() {
- this.halign = Align.START;
-
- this.add_css_class("file-preview-widget");
- }
-
- public async void load_from_thumbnail(FileTransfer file_transfer, StreamInteractor stream_interactor, int MAX_WIDTH=600, int MAX_HEIGHT=300) throws GLib.Error {
- Thread<ScalingImage?> thread = new Thread<ScalingImage?> (null, () => {
- ScalingImage image = new ScalingImage() { halign=Align.START, visible = true, max_width = MAX_WIDTH, max_height = MAX_HEIGHT };
- Gdk.Pixbuf? pixbuf = null;
- foreach (Xep.JingleContentThumbnails.Thumbnail thumbnail in file_transfer.thumbnails) {
- pixbuf = ImageFileMetadataProvider.parse_thumbnail(thumbnail);
- if (pixbuf != null) {
- break;
- }
- }
- if (pixbuf == null) {
- warning("Can't load thumbnails of file %s", file_transfer.file_name);
- Idle.add(load_from_thumbnail.callback);
- throw new Error(-1, 0, "Error loading preview image");
- }
- // TODO: should this be executed? If yes, before or after scaling
- pixbuf = pixbuf.apply_embedded_orientation();
-
- if (file_transfer.width > 0 && file_transfer.height > 0) {
- pixbuf = pixbuf.scale_simple(file_transfer.width, file_transfer.height, InterpType.BILINEAR);
- } else {
- warning("Preview: Not scaling image, width: %d, height: %d\n", file_transfer.width, file_transfer.height);
- }
- if (pixbuf == null) {
- warning("Can't scale thumbnail %s", file_transfer.file_name);
- throw new Error(-1, 0, "Error scaling preview image");
- }
-
- image.load(pixbuf);
- Idle.add(load_from_thumbnail.callback);
- return image;
- });
- yield;
- this.image = thread.join();
-
- file_default_widget = new FileDefaultWidget() { valign=Align.END, vexpand=false, visible=false };
- file_default_widget.image_stack.visible = false;
- file_default_widget_controller = new FileDefaultWidgetController(file_default_widget);
- file_default_widget_controller.set_file_transfer(file_transfer, stream_interactor);
-
- Overlay overlay = new Overlay();
- overlay.set_child(image);
- overlay.add_overlay(file_default_widget);
- overlay.set_measure_overlay(image, true);
- overlay.set_clip_overlay(file_default_widget, true);
-
- EventControllerMotion this_motion_events = new EventControllerMotion();
- this.add_controller(this_motion_events);
- this_motion_events.enter.connect(() => {
- file_default_widget.visible = true;
- });
- this_motion_events.leave.connect(() => {
- if (file_default_widget.file_menu.popover != null && file_default_widget.file_menu.popover.visible) return;
-
- file_default_widget.visible = false;
- });
- GestureClick gesture_click_controller = new GestureClick();
- gesture_click_controller.set_button(1); // listen for left clicks
- this.add_controller(gesture_click_controller);
- gesture_click_controller.pressed.connect((n_press, x, y) => {
- // Check whether the click was inside the file menu. Otherwise, open the file.
- this.file_default_widget.clicked();
- });
-
- this.append(overlay);
- }
- }
-
-}
diff --git a/main/src/ui/conversation_content_view/file_transmission_progress.vala b/main/src/ui/conversation_content_view/file_transmission_progress.vala
new file mode 100644
index 00000000..bf3f377b
--- /dev/null
+++ b/main/src/ui/conversation_content_view/file_transmission_progress.vala
@@ -0,0 +1,120 @@
+using Gee;
+using Gdk;
+using Gtk;
+using Xmpp;
+
+using Dino.Entities;
+
+namespace Dino.Ui {
+
+ public class FileTransmissionProgress : Adw.Bin {
+
+ public enum State {
+ UNKNOWN_SOURCE,
+ DOWNLOAD_NOT_STARTED,
+ DOWNLOAD_NOT_STARTED_FAILED_BEFORE,
+ DOWNLOADING,
+ UPLOADING
+ }
+
+ public int64 file_size { get; set; }
+ public int64 transferred_size { get; set; }
+ public State state { get; set; }
+
+ private CssProvider css_provider = new CssProvider();
+ private Button button = new Button();
+
+ private uint64 next_update_time = 0;
+ private int64 last_progress_percent = 0;
+ private uint update_progress_timeout_id = -1;
+
+ construct {
+ add_css_class("circular-loading-indicator");
+
+ button.add_css_class("circular");
+ Adw.Bin holder = new Adw.Bin();
+ holder.set_child(button);
+ this.set_child(holder);
+
+ this.button.clicked.connect(on_button_clicked);
+
+ this.notify["transferred-size"].connect(on_transferred_size_update);
+ this.notify["state"].connect(on_state_changed);
+ on_state_changed();
+ }
+
+ private void on_transferred_size_update() {
+ if (update_progress_timeout_id == -1) {
+ int64 progress_percent = transferred_size * 100 / file_size;
+ if (progress_percent != last_progress_percent) {
+ uint64 time_now = get_monotonic_time() / 1000;
+ if (next_update_time > time_now) {
+ update_progress_timeout_id = Timeout.add((uint) (next_update_time - time_now), () => {
+ update_progress();
+ update_progress_timeout_id = -1;
+ return Source.REMOVE;
+ });
+ } else {
+ update_progress();
+ }
+ }
+ }
+ }
+
+ private void on_state_changed() {
+ sensitive = state != UNKNOWN_SOURCE;
+
+ switch (this.state) {
+ case UNKNOWN_SOURCE:
+ case DOWNLOAD_NOT_STARTED:
+ button.icon_name = "document-save-symbolic";
+ break;
+ case DOWNLOADING:
+ case UPLOADING:
+ button.icon_name = "small-x-symbolic";
+ break;
+ case DOWNLOAD_NOT_STARTED_FAILED_BEFORE:
+ button.icon_name = "dialog-warning-symbolic";
+ break;
+ }
+ }
+
+ private void update_progress() {
+ this.get_style_context().remove_provider(css_provider);
+ css_provider = new CssProvider();
+ int64 progress_percent = transferred_size * 100 / file_size;
+
+ css_provider.load_from_string(@"
+ .circular-loading-indicator {
+ background-image: conic-gradient(@accent_color $(progress_percent)%, transparent $(progress_percent)%);
+ }
+ ");
+
+ this.get_style_context().add_provider(css_provider, Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION);
+ next_update_time = get_monotonic_time() / 1000 + 500;
+ last_progress_percent = progress_percent;
+ }
+
+ private void on_button_clicked() {
+ switch (this.state) {
+ case UNKNOWN_SOURCE:
+ break;
+ case DOWNLOAD_NOT_STARTED_FAILED_BEFORE:
+ case DOWNLOAD_NOT_STARTED:
+ this.activate_action("file.download", null);
+ break;
+ case DOWNLOADING:
+ case UPLOADING:
+ this.activate_action("file.cancel", null);
+ break;
+ }
+ }
+
+ public override void dispose() {
+ if (update_progress_timeout_id != -1) {
+ Source.remove(update_progress_timeout_id);
+ update_progress_timeout_id = -1;
+ }
+ }
+ }
+} \ No newline at end of file
diff --git a/main/src/ui/conversation_content_view/file_widget.vala b/main/src/ui/conversation_content_view/file_widget.vala
index 69781f30..583609d2 100644
--- a/main/src/ui/conversation_content_view/file_widget.vala
+++ b/main/src/ui/conversation_content_view/file_widget.vala
@@ -27,7 +27,7 @@ public class FileMetaItem : ConversationSummary.ContentMetaItem {
}
public override Gee.List<Plugins.MessageAction>? get_item_actions(Plugins.WidgetType type) {
- if (file_transfer.provider != 0 || file_transfer.info == null) return null;
+ if ((file_transfer.provider != FileManager.HTTP_PROVIDER_ID && file_transfer.provider != FileManager.SFS_PROVIDER_ID) || file_transfer.info == null) return null;
Gee.List<Plugins.MessageAction> actions = new ArrayList<Plugins.MessageAction>();
@@ -43,7 +43,6 @@ public class FileWidget : SizeRequestBox {
enum State {
IMAGE,
- IMAGE_PREVIEW,
DEFAULT
}
@@ -90,13 +89,15 @@ public class FileWidget : SizeRequestBox {
}
private async void update_widget() {
- if (show_image() && state != State.IMAGE) {
+ bool show_image = FileImageWidget.can_display(file_transfer);
+
+ if (show_image && state != State.IMAGE) {
var content_bak = content;
FileImageWidget file_image_widget = null;
try {
file_image_widget = new FileImageWidget();
- yield file_image_widget.load_from_file(file_transfer.get_file(), file_transfer.file_name);
+ yield file_image_widget.set_file_transfer(file_transfer);
// If the widget changed in the meanwhile, stop
if (content != content_bak) return;
@@ -109,26 +110,7 @@ public class FileWidget : SizeRequestBox {
} catch (Error e) { }
}
- if (show_preview() && state != State.IMAGE_PREVIEW) {
- var content_bak = content;
-
- FilePreviewWidget file_preview_widget = null;
- try {
- file_preview_widget = new FilePreviewWidget() { visible=true };
- yield file_preview_widget.load_from_thumbnail(file_transfer, stream_interactor);
-
- // If the widget changed in the meanwhile, stop
- if (content != content_bak) return;
-
- if (content != null) this.remove(content);
- content = file_preview_widget;
- state = State.IMAGE_PREVIEW;
- this.append(content);
- return;
- } catch (Error e) { }
- }
-
- if (!show_image() && state != State.DEFAULT && state != State.IMAGE_PREVIEW) {
+ if (!show_image && state != State.DEFAULT) {
if (content != null) this.remove(content);
FileDefaultWidget default_file_widget = new FileDefaultWidget();
default_widget_controller = new FileDefaultWidgetController(default_file_widget);
@@ -139,31 +121,6 @@ public class FileWidget : SizeRequestBox {
}
}
- private bool show_image() {
- if (file_transfer.mime_type == null) return false;
-
- // If the image is being sent by this client, we already have the file
- bool in_progress_from_us = file_transfer.direction == FileTransfer.DIRECTION_SENT &&
- file_transfer.ourpart.equals(file_transfer.account.full_jid) &&
- file_transfer.state == FileTransfer.State.IN_PROGRESS;
- if (file_transfer.state != FileTransfer.State.COMPLETE && !in_progress_from_us) {
- return false;
- }
-
- foreach (PixbufFormat pixbuf_format in Pixbuf.get_formats()) {
- foreach (string mime_type in pixbuf_format.get_mime_types()) {
- if (mime_type == file_transfer.mime_type) {
- return true;
- }
- }
- }
- return false;
- }
-
- private bool show_preview() {
- return !this.file_transfer.thumbnails.is_empty;
- }
-
public override void dispose() {
if (default_widget_controller != null) default_widget_controller.dispose();
default_widget_controller = null;
diff --git a/main/src/ui/widgets/avatar_picture.vala b/main/src/ui/widgets/avatar_picture.vala
index fb254915..ef1462ae 100644
--- a/main/src/ui/widgets/avatar_picture.vala
+++ b/main/src/ui/widgets/avatar_picture.vala
@@ -285,7 +285,7 @@ public class Dino.Ui.CompatAvatarDrawer {
private Cairo.Surface sub_surface_idx(Cairo.Context ctx, int idx, int width, int height, int font_factor = 1) {
ViewModel.AvatarPictureTileModel tile = (ViewModel.AvatarPictureTileModel) this.model.tiles.get_item(idx);
- Gdk.Pixbuf? avatar = new Gdk.Pixbuf.from_file(tile.image_file.get_path());
+ Gdk.Pixbuf? avatar = tile.image_file != null ? new Gdk.Pixbuf.from_file(tile.image_file.get_path()) : null;
string? name = idx >= 0 ? tile.display_text : "";
Gdk.RGBA hex_color = tile.background_color;
return sub_surface(ctx, font_family, avatar, name, hex_color, width, height, font_factor);
diff --git a/plugins/http-files/src/file_provider.vala b/plugins/http-files/src/file_provider.vala
index bbee8e50..420e041d 100644
--- a/plugins/http-files/src/file_provider.vala
+++ b/plugins/http-files/src/file_provider.vala
@@ -38,9 +38,10 @@ public class FileProvider : Dino.FileProvider, Object {
}
public override async bool run(Entities.Message message, Xmpp.MessageStanza stanza, Conversation conversation) {
- if (Xep.StatelessFileSharing.MessageFlag.get_flag(stanza) != null) {
- return true;
+ if (Xep.StatelessFileSharing.get_file_shares(stanza) != null || Xep.StatelessFileSharing.get_source_attachments(stanza) != null) {
+ return false;
}
+
string? oob_url = Xmpp.Xep.OutOfBandData.get_url_from_message(stanza);
bool normal_file = oob_url != null && oob_url == message.body && FileProvider.http_url_regex.match(message.body);
bool omemo_file = FileProvider.omemo_url_regex.match(message.body);
@@ -52,57 +53,6 @@ public class FileProvider : Dino.FileProvider, Object {
}
}
- private class LimitInputStream : InputStream, PollableInputStream {
- InputStream inner;
- int64 remaining_size;
-
- public LimitInputStream(InputStream inner, int64 max_size) {
- this.inner = inner;
- this.remaining_size = max_size;
- }
-
- 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_size <= 0 || ((PollableInputStream)inner).is_readable();
- }
-
- private ssize_t check_limit(ssize_t read) throws IOError {
- this.remaining_size -= read;
- if (remaining_size < 0) throw new IOError.FAILED("Stream length exceeded limit");
- return read;
- }
-
- public override ssize_t read(uint8[] buffer, Cancellable? cancellable = null) throws IOError {
- return check_limit(inner.read(buffer, cancellable));
- }
-
- public override async ssize_t read_async(uint8[]? buffer, int io_priority = GLib.Priority.DEFAULT, Cancellable? cancellable = null) throws IOError {
- return check_limit(yield inner.read_async(buffer, io_priority, cancellable));
- }
-
- 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);
- }
- }
-
private void on_file_message(Entities.Message message, Conversation conversation) {
var additional_info = message.id.to_string();
@@ -155,7 +105,7 @@ public class FileProvider : Dino.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 {
HttpFileReceiveData? http_receive_data = receive_data as HttpFileReceiveData;
if (http_receive_data == null) assert(false);
@@ -166,25 +116,20 @@ public class FileProvider : Dino.FileProvider, Object {
get_message.accept_certificate.connect((peer_cert, errors) => { return ConnectionManager.on_invalid_certificate(transfer_host, peer_cert, errors); });
#endif
- try {
#if SOUP_3_0
- InputStream stream = yield session.send_async(get_message, GLib.Priority.LOW, file_transfer.cancellable);
+ InputStream stream = yield session.send_async(get_message, GLib.Priority.LOW, file_transfer.cancellable);
#else
- InputStream stream = yield session.send_async(get_message, file_transfer.cancellable);
+ InputStream stream = yield session.send_async(get_message, file_transfer.cancellable);
#endif
- if (file_meta.size != -1) {
- return new LimitInputStream(stream, file_meta.size);
- } else {
- return stream;
- }
- } catch (Error e) {
- throw new FileReceiveError.DOWNLOAD_FAILED("Downloading file error: %s".printf(e.message));
+ if (file_meta.size != -1) {
+ return new LimitInputStream(stream, file_meta.size);
+ } else {
+ return stream;
}
}
public FileMeta get_file_meta(FileTransfer file_transfer) throws FileReceiveError {
- // TODO: replace '2' with constant?
- if (file_transfer.provider == 2) {
+ if (file_transfer.provider == FileManager.SFS_PROVIDER_ID) {
var file_meta = new HttpFileMeta();
file_meta.size = file_transfer.size;
file_meta.mime_type = file_transfer.mime_type;
@@ -210,26 +155,19 @@ public class FileProvider : Dino.FileProvider, Object {
return file_meta;
}
- public async FileReceiveData? get_file_receive_data(FileTransfer file_transfer) {
- // TODO: replace '2' with constant?
- if (file_transfer.provider == 2) {
- Xep.StatelessFileSharing.HttpSource http_source = null;
- for(int i = 0; i < file_transfer.sfs_sources.get_n_items(); i++) {
- Object source_object = file_transfer.sfs_sources.get_item(i);
- FileTransfer.SerializedSfsSource source = source_object as FileTransfer.SerializedSfsSource;
- if (source.type == Xep.StatelessFileSharing.HttpSource.SOURCE_TYPE) {
- http_source = yield Xep.StatelessFileSharing.HttpSource.deserialize(source.data);
- assert(source != null);
+ public FileReceiveData? 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;
}
}
- if (http_source == null) {
- printerr("Sfs file transfer has no http sources attached!");
- return 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;
diff --git a/plugins/http-files/src/file_sender.vala b/plugins/http-files/src/file_sender.vala
index 91b50944..02336b43 100644
--- a/plugins/http-files/src/file_sender.vala
+++ b/plugins/http-files/src/file_sender.vala
@@ -17,7 +17,7 @@ public class HttpFileSender : FileSender, Object {
session.user_agent = @"Dino/$(Dino.get_short_version()) ";
stream_interactor.stream_negotiated.connect(on_stream_negotiated);
- stream_interactor.get_module(MessageProcessor.IDENTITY).build_message_stanza.connect(check_add_oob);
+ stream_interactor.get_module(MessageProcessor.IDENTITY).build_message_stanza.connect(check_add_sfs_element);
}
public async FileSendData? prepare_send_file(Conversation conversation, FileTransfer file_transfer, FileMeta file_meta) throws FileSendError {
@@ -43,39 +43,55 @@ public class HttpFileSender : FileSender, Object {
HttpFileSendData? send_data = file_send_data as HttpFileSendData;
if (send_data == null) return;
- yield upload(file_transfer, send_data, file_meta);
+ bool can_reference_element = !conversation.type_.is_muc_semantic() || stream_interactor.get_module(EntityInfo.IDENTITY).has_feature_cached(conversation.account, conversation.counterpart, Xep.UniqueStableStanzaIDs.NS_URI);
+
+ // Share unencrypted files via SFS (only if we'll be able to reference messages)
+ if (conversation.encryption == Encryption.NONE && can_reference_element) {
+ // Announce the file share
+ Entities.Message file_share_message = stream_interactor.get_module(MessageProcessor.IDENTITY).create_out_message(null, conversation);
+ 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);
+
+ // Upload file
+ yield upload(file_transfer, send_data, file_meta);
+
+ // 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 (file_share_message.server_id == null) {
+ ulong server_id_notify_id = file_share_message.notify["server-id"].connect(() => {
+ Idle.add(send_file.callback);
+ });
+ yield;
+ file_share_message.disconnect(server_id_notify_id);
+ }
+ }
+
+ file_transfer.sfs_sources.add(new Xep.StatelessFileSharing.HttpSource() { url=send_data.url_down } );
+
+ // Send source attachment
+ MessageStanza stanza = new MessageStanza() { to = conversation.counterpart, type_ = conversation.type_ == GROUPCHAT ? MessageStanza.TYPE_GROUPCHAT : MessageStanza.TYPE_CHAT };
+ stanza.body = send_data.url_down;
+ Xep.OutOfBandData.add_url_to_message(stanza, send_data.url_down);
+ var sources = new ArrayList<Xep.StatelessFileSharing.Source>();
+ sources.add(new Xep.StatelessFileSharing.HttpSource() { url = send_data.url_down });
+ string attach_to_id = MessageStorage.get_reference_id(file_share_message);
+ Xep.StatelessFileSharing.set_sfs_attachment(stanza, attach_to_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);
+ }
+ // Share encrypted files without SFS
+ else {
+ yield upload(file_transfer, send_data, file_meta);
- if (send_data.encrypt_message && conversation.encryption != Encryption.NONE) {
Entities.Message message = stream_interactor.get_module(MessageProcessor.IDENTITY).create_out_message(send_data.url_down, conversation);
file_transfer.info = message.id.to_string();
- message.encryption = conversation.encryption;
+ message.encryption = send_data.encrypt_message ? conversation.encryption : Encryption.NONE;
stream_interactor.get_module(MessageProcessor.IDENTITY).send_xmpp_message(message, conversation);
- } else {
- // Use stateless file sharing for unencrypted file sharing
- Xep.StatelessFileSharing.HttpSource source = new Xep.StatelessFileSharing.HttpSource();
- source.url = send_data.url_down;
- file_transfer.sfs_sources.append(new FileTransfer.SerializedSfsSource.from_sfs_source(source) as Object);
- this.db.sfs_sources.insert()
- .value(db.sfs_sources.id, file_transfer.id)
- .value(db.sfs_sources.type, source.type())
- .value(db.sfs_sources.data, source.serialize())
- .perform();
- XmppStream stream = stream_interactor.get_stream(conversation.account);
- Xep.StatelessFileSharing.Module sfs_module = stream.get_module(Xep.StatelessFileSharing.Module.IDENTITY);
- string message_type;
- if (conversation.type_ == Conversation.Type.GROUPCHAT) {
- message_type = MessageStanza.TYPE_GROUPCHAT;
- } else {
- message_type = MessageStanza.TYPE_CHAT;
- }
- MessageStanza sfs_message = new MessageStanza() { to=conversation.counterpart, type_=message_type };
- // TODO: is this the correct way of adding out-of-band-data?
- sfs_message.body = source.url;
- Xep.OutOfBandData.add_url_to_message(sfs_message, source.url);
- // TODO: message hint correct?
- Xep.MessageProcessingHints.set_message_hint(sfs_message, Xep.MessageProcessingHints.HINT_STORE);
- sfs_module.send_stateless_file_transfer(stream, sfs_message, yield file_transfer.to_sfs_element());
}
}
@@ -159,10 +175,15 @@ public class HttpFileSender : FileSender, Object {
});
}
- private void check_add_oob(Entities.Message message, Xmpp.MessageStanza message_stanza, Conversation conversation) {
- if (message.encryption == Encryption.NONE && message.body.has_prefix("http") && message_is_file(db, message)) {
- Xep.OutOfBandData.add_url_to_message(message_stanza, message_stanza.body);
- }
+ private void check_add_sfs_element(Entities.Message message, Xmpp.MessageStanza message_stanza, Conversation conversation) {
+ if (message.encryption != Encryption.NONE) return;
+
+ FileTransfer? file_transfer = stream_interactor.get_module(FileTransferStorage.IDENTITY).get_file_by_message_id(message.id, conversation);
+ if (file_transfer == null) return;
+
+ Xep.StatelessFileSharing.set_sfs_element(message_stanza, file_transfer.file_sharing_id, file_transfer.file_metadata, file_transfer.sfs_sources);
+
+ Xep.MessageProcessingHints.set_message_hint(message_stanza, Xep.MessageProcessingHints.HINT_STORE);
}
public int get_id() { return 0; }
diff --git a/xmpp-vala/CMakeLists.txt b/xmpp-vala/CMakeLists.txt
index 824b667e..1fc5e436 100644
--- a/xmpp-vala/CMakeLists.txt
+++ b/xmpp-vala/CMakeLists.txt
@@ -81,6 +81,7 @@ SOURCES
"src/module/xep/0082_date_time_profiles.vala"
"src/module/xep/0084_user_avatars.vala"
"src/module/xep/0085_chat_state_notifications.vala"
+ "src/module/xep/0104_http_scheme_url_data.vala"
"src/module/xep/0115_entity_capabilities.vala"
"src/module/xep/0166_jingle/content.vala"
@@ -139,6 +140,7 @@ SOURCES
"src/module/xep/0353_jingle_message_initiation.vala"
"src/module/xep/0359_unique_stable_stanza_ids.vala"
"src/module/xep/0363_http_file_upload.vala"
+ "src/module/xep/0367_message_attaching.vala"
"src/module/xep/0380_explicit_encryption.vala"
"src/module/xep/0391_jingle_encrypted_transports.vala"
"src/module/xep/0410_muc_self_ping.vala"
diff --git a/xmpp-vala/meson.build b/xmpp-vala/meson.build
index 4d8c0493..fdcb54dd 100644
--- a/xmpp-vala/meson.build
+++ b/xmpp-vala/meson.build
@@ -66,6 +66,7 @@ sources = files(
'src/module/xep/0082_date_time_profiles.vala',
'src/module/xep/0084_user_avatars.vala',
'src/module/xep/0085_chat_state_notifications.vala',
+ 'src/module/xep/0104_http_scheme_url_data.vala',
'src/module/xep/0115_entity_capabilities.vala',
'src/module/xep/0166_jingle/component.vala',
'src/module/xep/0166_jingle/content.vala',
@@ -99,10 +100,12 @@ sources = files(
'src/module/xep/0249_direct_muc_invitations.vala',
'src/module/xep/0260_jingle_socks5_bytestreams.vala',
'src/module/xep/0261_jingle_in_band_bytestreams.vala',
+ 'src/module/xep/0264_jingle_content_thumbnails.vala',
'src/module/xep/0272_muji.vala',
'src/module/xep/0280_message_carbons.vala',
'src/module/xep/0297_stanza_forwarding.vala',
'src/module/xep/0298_coin.vala',
+ 'src/module/xep/0300_cryptographic_hashes.vala',
'src/module/xep/0308_last_message_correction.vala',
'src/module/xep/0313_2_message_archive_management.vala',
'src/module/xep/0313_message_archive_management.vala',
@@ -111,6 +114,7 @@ sources = files(
'src/module/xep/0353_jingle_message_initiation.vala',
'src/module/xep/0359_unique_stable_stanza_ids.vala',
'src/module/xep/0363_http_file_upload.vala',
+ 'src/module/xep/0367_message_attaching.vala',
'src/module/xep/0380_explicit_encryption.vala',
'src/module/xep/0384_omemo/omemo_decryptor.vala',
'src/module/xep/0384_omemo/omemo_encryptor.vala',
@@ -122,6 +126,8 @@ sources = files(
'src/module/xep/0421_occupant_ids.vala',
'src/module/xep/0428_fallback_indication.vala',
'src/module/xep/0444_reactions.vala',
+ 'src/module/xep/0446_file_metadata_element.vala',
+ 'src/module/xep/0447_stateless_file_sharing.vala',
'src/module/xep/0461_replies.vala',
'src/module/xep/0482_call_invites.vala',
'src/module/xep/pixbuf_storage.vala',
diff --git a/xmpp-vala/src/module/message/stanza.vala b/xmpp-vala/src/module/message/stanza.vala
index 053c44dd..cb07ab2a 100644
--- a/xmpp-vala/src/module/message/stanza.vala
+++ b/xmpp-vala/src/module/message/stanza.vala
@@ -15,13 +15,17 @@ public class MessageStanza : Xmpp.Stanza {
public bool rerun_parsing = false;
private ArrayList<MessageFlag> flags = new ArrayList<MessageFlag>();
- public string body {
+ public string? body {
get {
StanzaNode? body_node = stanza.get_subnode(NODE_BODY);
return body_node == null ? null : body_node.get_string_content();
}
set {
StanzaNode? body_node = stanza.get_subnode(NODE_BODY);
+ if (value == null) {
+ if (body_node != null) stanza.sub_nodes.remove(body_node);
+ return;
+ }
if (body_node == null) {
body_node = new StanzaNode.build(NODE_BODY);
stanza.put_node(body_node);
diff --git a/xmpp-vala/src/module/xep/0104_http_scheme_url_data.vala b/xmpp-vala/src/module/xep/0104_http_scheme_url_data.vala
new file mode 100644
index 00000000..b177a1ef
--- /dev/null
+++ b/xmpp-vala/src/module/xep/0104_http_scheme_url_data.vala
@@ -0,0 +1,19 @@
+using Xmpp;
+
+namespace Xmpp.Xep.HttpSchemeForUrlData {
+ public const string NS_URI = "http://jabber.org/protocol/url-data";
+
+ // If there are multiple URLs, this will only return the first one
+ public static string? get_url(StanzaNode node) {
+ StanzaNode? url_data_node = node.get_subnode("url-data", NS_URI);
+ if (url_data_node == null) return null;
+
+ return url_data_node.get_attribute("target");
+ }
+
+ public static StanzaNode to_stanza_node(string url) {
+ return new StanzaNode.build("url-data", NS_URI).add_self_xmlns()
+ .put_attribute("target", url, NS_URI);
+
+ }
+} \ No newline at end of file
diff --git a/xmpp-vala/src/module/xep/0264_jingle_content_thumbnails.vala b/xmpp-vala/src/module/xep/0264_jingle_content_thumbnails.vala
index cb281f80..053fb7d5 100644
--- a/xmpp-vala/src/module/xep/0264_jingle_content_thumbnails.vala
+++ b/xmpp-vala/src/module/xep/0264_jingle_content_thumbnails.vala
@@ -1,6 +1,16 @@
namespace Xmpp.Xep.JingleContentThumbnails {
public const string NS_URI = "urn:xmpp:thumbs:1";
- public const string STANZA_NAME = "thumbnail";
+
+ public Gee.List<Thumbnail> get_thumbnails(StanzaNode node) {
+ var thumbnails = new Gee.ArrayList<Thumbnail>();
+ foreach (StanzaNode thumbnail_node in node.get_subnodes("thumbnail", Xep.JingleContentThumbnails.NS_URI)) {
+ var thumbnail = Thumbnail.from_stanza_node(thumbnail_node);
+ if (thumbnail != null) {
+ thumbnails.add(thumbnail);
+ }
+ }
+ return thumbnails;
+ }
public class Thumbnail {
public string uri;
@@ -8,41 +18,30 @@ namespace Xmpp.Xep.JingleContentThumbnails {
public int width;
public int height;
- const string URI_ATTRIBUTE = "uri";
- const string MIME_ATTRIBUTE = "media-type";
- const string WIDTH_ATTRIBUTE = "width";
- const string HEIGHT_ATTRIBUTE = "height";
-
public StanzaNode to_stanza_node() {
- StanzaNode node = new StanzaNode.build(STANZA_NAME, NS_URI).add_self_xmlns()
- .put_attribute(URI_ATTRIBUTE, this.uri);
+ StanzaNode node = new StanzaNode.build("thumbnail", NS_URI).add_self_xmlns()
+ .put_attribute("uri", this.uri);
if (this.media_type != null) {
- node.put_attribute(MIME_ATTRIBUTE, this.media_type);
+ node.put_attribute("media-type", this.media_type);
}
if (this.width != -1) {
- node.put_attribute(WIDTH_ATTRIBUTE, this.width.to_string());
+ node.put_attribute("width", this.width.to_string());
}
if (this.height != -1) {
- node.put_attribute(HEIGHT_ATTRIBUTE, this.height.to_string());
+ node.put_attribute("height", this.height.to_string());
}
return node;
}
public static Thumbnail? from_stanza_node(StanzaNode node) {
Thumbnail thumbnail = new Thumbnail();
- thumbnail.uri = node.get_attribute(URI_ATTRIBUTE);
+ thumbnail.uri = node.get_attribute("uri");
if (thumbnail.uri == null) {
return null;
}
- thumbnail.media_type = node.get_attribute(MIME_ATTRIBUTE);
- string? width = node.get_attribute(WIDTH_ATTRIBUTE);
- if (width != null) {
- thumbnail.width = int.parse(width);
- }
- string? height = node.get_attribute(HEIGHT_ATTRIBUTE);
- if (height != null) {
- thumbnail.height = int.parse(height);
- }
+ thumbnail.media_type = node.get_attribute("media-type");
+ thumbnail.width = node.get_attribute_int("width");
+ thumbnail.height = node.get_attribute_int("height");
return thumbnail;
}
}
diff --git a/xmpp-vala/src/module/xep/0300_cryptographic_hashes.vala b/xmpp-vala/src/module/xep/0300_cryptographic_hashes.vala
index 00f9e2ee..b73e63a5 100644
--- a/xmpp-vala/src/module/xep/0300_cryptographic_hashes.vala
+++ b/xmpp-vala/src/module/xep/0300_cryptographic_hashes.vala
@@ -4,69 +4,54 @@ using Gee;
namespace Xmpp.Xep.CryptographicHashes {
public const string NS_URI = "urn:xmpp:hashes:2";
- public enum HashCmp {
- Match,
- Mismatch,
- None,
+ public Gee.List<Hash> get_hashes(StanzaNode node) {
+ var hashes = new ArrayList<Hash>();
+ foreach (StanzaNode hash_node in node.get_subnodes("hash", NS_URI)) {
+ hashes.add(new Hash.from_stanza_node(hash_node));
+ }
+ return hashes;
+ }
+
+ public Gee.List<Hash> get_supported_hashes(Gee.List<Hash> hashes) {
+ var ret = new ArrayList<Hash>();
+ foreach (Hash hash in hashes) {
+ ChecksumType? hash_type = hash_string_to_type(hash.algo);
+ if (hash_type != null) {
+ ret.add(hash);
+ }
+ }
+ return ret;
+ }
+
+ public bool has_supported_hashes(Gee.List<Hash> hashes) {
+ foreach (Hash hash in hashes) {
+ ChecksumType? hash_type = hash_string_to_type(hash.algo);
+ if (hash_type != null) return true;
+ }
+ return false;
}
- public class Hash {
+ public class Hash : Object {
public string algo;
// hash encoded in Base64
public string val;
- public static string hash_name(ChecksumType type) {
- switch(type) {
- case ChecksumType.MD5:
- return "md5";
- case ChecksumType.SHA1:
- return "sha-1";
- case ChecksumType.SHA256:
- return "sha-256";
- case ChecksumType.SHA384:
- return "sha-384";
- case ChecksumType.SHA512:
- return "sha-512";
- }
- return "(null)";
- }
-
- public static ChecksumType? supported_hash(string hash) {
- switch (hash) {
- case "sha-1":
- return ChecksumType.SHA1;
- case "sha-256":
- return ChecksumType.SHA256;
- case "sha-384":
- return ChecksumType.SHA384;
- case "sha-512":
- return ChecksumType.SHA512;
- }
- return null;
+ public Hash.with_checksum(ChecksumType checksum_type, string hash) {
+ algo = hash_type_to_string(checksum_type);
+ val = hash;
}
- public Hash.from_data(GLib.ChecksumType type, uint8[] data) {
+ public Hash.compute(GLib.ChecksumType type, uint8[] data) {
GLib.Checksum checksum = new GLib.Checksum(type);
checksum.update(data, data.length);
// 64 * 8 = 512 (sha-512 is the longest hash variant)
uint8[] digest = new uint8[64];
size_t length = digest.length;
checksum.get_digest(digest, ref length);
- this.algo = hash_name(type);
+ this.algo = hash_type_to_string(type);
this.val = GLib.Base64.encode(digest[0:length]);
}
- public HashCmp compare(Hash other) {
- if (this.algo != other.algo) {
- return HashCmp.None;
- }
- if (this.val == other.val) {
- return HashCmp.Match;
- } else {
- return HashCmp.Mismatch;
- }
- }
-
public StanzaNode to_stanza_node() {
return new StanzaNode.build("hash", NS_URI).add_self_xmlns()
.put_attribute("algo", this.algo)
@@ -79,58 +64,33 @@ namespace Xmpp.Xep.CryptographicHashes {
}
}
- public class Hashes {
- public Gee.List<Hash> hashes = new ArrayList<Hash>();
-
- public Gee.List<ChecksumType> supported_hashes() {
- Gee.List<ChecksumType> supported = new ArrayList<ChecksumType>();
- foreach (Hash hash in this.hashes) {
- ChecksumType? hash_type = Hash.supported_hash(hash.algo);
- if (hash_type != null) {
- supported.add(hash_type);
- }
- }
- return supported;
- }
-
- public Hashes.from_data(Gee.List<ChecksumType> types, uint8[] data) {
- foreach (ChecksumType type in types) {
- this.hashes.add(new Hash.from_data(type, data));
- }
- }
-
- public HashCmp compare(Hashes other) {
- HashCmp cmp = HashCmp.None;
- foreach (Hash this_hash in this.hashes) {
- foreach (Hash other_hash in other.hashes) {
- switch (this_hash.compare(other_hash)) {
- case HashCmp.Mismatch:
- return HashCmp.Mismatch;
- case HashCmp.Match:
- cmp = HashCmp.Match;
- break;
- case HashCmp.None:
- continue;
- }
- }
- }
- return cmp;
- }
-
- public Gee.List<StanzaNode> to_stanza_nodes() {
- Gee.List<StanzaNode> nodes = new ArrayList<StanzaNode>();
- foreach (Hash hash in this.hashes) {
- nodes.add(hash.to_stanza_node());
- }
- return nodes;
+ public static string hash_type_to_string(ChecksumType type) {
+ switch(type) {
+ case ChecksumType.MD5:
+ return "md5";
+ case ChecksumType.SHA1:
+ return "sha-1";
+ case ChecksumType.SHA256:
+ return "sha-256";
+ case ChecksumType.SHA384:
+ return "sha-384";
+ case ChecksumType.SHA512:
+ return "sha-512";
}
+ return "(null)";
+ }
- public Hashes.from_stanza_subnodes(StanzaNode node) {
- Gee.List<StanzaNode> subnodes = node.get_subnodes("hash", NS_URI);
- this.hashes = new ArrayList<Hash>();
- foreach (StanzaNode subnode in subnodes) {
- this.hashes.add(new Hash.from_stanza_node(subnode));
- }
+ public static ChecksumType? hash_string_to_type(string hash) {
+ switch (hash) {
+ case "sha-1":
+ return ChecksumType.SHA1;
+ case "sha-256":
+ return ChecksumType.SHA256;
+ case "sha-384":
+ return ChecksumType.SHA384;
+ case "sha-512":
+ return ChecksumType.SHA512;
}
+ return null;
}
}
diff --git a/xmpp-vala/src/module/xep/0367_message_attaching.vala b/xmpp-vala/src/module/xep/0367_message_attaching.vala
new file mode 100644
index 00000000..7441cd19
--- /dev/null
+++ b/xmpp-vala/src/module/xep/0367_message_attaching.vala
@@ -0,0 +1,15 @@
+namespace Xmpp.Xep.MessageAttaching {
+ public const string NS_URI = "urn:xmpp:message-attaching:1";
+
+ public static string? get_attach_to(StanzaNode node) {
+ StanzaNode? attach_to = node.get_subnode("attach-to", NS_URI);
+ if (attach_to == null) return null;
+
+ return attach_to.get_attribute("id", NS_URI);
+ }
+
+ public static StanzaNode to_stanza_node(string id) {
+ return new StanzaNode.build("attach-to", NS_URI).add_self_xmlns()
+ .put_attribute("id", id, NS_URI);
+ }
+} \ No newline at end of file
diff --git a/xmpp-vala/src/module/xep/0446_file_metadata_element.vala b/xmpp-vala/src/module/xep/0446_file_metadata_element.vala
index d7fbb06f..56b3d379 100644
--- a/xmpp-vala/src/module/xep/0446_file_metadata_element.vala
+++ b/xmpp-vala/src/module/xep/0446_file_metadata_element.vala
@@ -1,23 +1,24 @@
-using Xmpp.Xep.CryptographicHashes;
-
namespace Xmpp.Xep.FileMetadataElement {
public const string NS_URI = "urn:xmpp:file:metadata:0";
public class FileMetadata {
- public string name { get; set; }
+ public string? name { get; set; }
public string? mime_type { get; set; }
public int64 size { get; set; default=-1; }
public string? desc { get; set; }
public DateTime? date { get; set; }
public int width { get; set; default=-1; } // Width of image in pixels
- public int height { get; set; default=-1; } // Height of image in pixels
- public CryptographicHashes.Hashes hashes = new CryptographicHashes.Hashes();
- public int64 length { get; set; default=-1; } // Length of audio/video in milliseconds
- public Gee.List<Xep.JingleContentThumbnails.Thumbnail> thumbnails = new Gee.ArrayList<Xep.JingleContentThumbnails.Thumbnail>();
+ public int height { get; set; default=-1; } // Height of image in pixels
+ public Gee.List<CryptographicHashes.Hash> hashes = new Gee.ArrayList<CryptographicHashes.Hash>();
+ public int64 length { get; set; default=-1; } // Length of audio/video in milliseconds
+ public Gee.List<Xep.JingleContentThumbnails.Thumbnail> thumbnails = new Gee.ArrayList<Xep.JingleContentThumbnails.Thumbnail>();
public StanzaNode to_stanza_node() {
- StanzaNode node = new StanzaNode.build("file", NS_URI).add_self_xmlns()
- .put_node(new StanzaNode.build("name", NS_URI).put_node(new StanzaNode.text(this.name)));
+ StanzaNode node = new StanzaNode.build("file", NS_URI).add_self_xmlns();
+
+ if (this.name != null) {
+ node.put_node(new StanzaNode.build("name", NS_URI).put_node(new StanzaNode.text(this.name)));
+ }
if (this.mime_type != null) {
node.put_node(new StanzaNode.build("media_type", NS_URI).put_node(new StanzaNode.text(this.mime_type)));
}
@@ -39,82 +40,57 @@ namespace Xmpp.Xep.FileMetadataElement {
if (this.length != -1) {
node.put_node(new StanzaNode.build("length", NS_URI).put_node(new StanzaNode.text(this.length.to_string())));
}
- node.sub_nodes.add_all(this.hashes.to_stanza_nodes());
+ foreach (var hash in hashes) {
+ node.put_node(hash.to_stanza_node());
+ }
foreach (Xep.JingleContentThumbnails.Thumbnail thumbnail in this.thumbnails) {
node.put_node(thumbnail.to_stanza_node());
}
return node;
}
+ }
- public void add_to_message(MessageStanza message) {
- StanzaNode node = this.to_stanza_node();
- printerr("Attaching file metadata:\n");
- printerr("%s\n", node.to_ansi_string(true));
- message.stanza.put_node(node);
- }
+ public static FileMetadata? get_file_metadata(StanzaNode node) {
+ StanzaNode? file_node = node.get_subnode("file", Xep.FileMetadataElement.NS_URI);
+ if (file_node == null) return null;
- public static FileMetadata? from_stanza_node(StanzaNode node) {
- FileMetadata metadata = new FileMetadata();
- // TODO: null checks on final values
- StanzaNode? name_node = node.get_subnode("name");
- if (name_node == null || name_node.get_string_content() == null) {
- return null;
- } else {
- metadata.name = name_node.get_string_content();
- }
- StanzaNode? desc_node = node.get_subnode("desc");
- if (desc_node != null && desc_node.get_string_content() != null) {
- metadata.desc = desc_node.get_string_content();
- }
- StanzaNode? mime_node = node.get_subnode("media_type");
- if (mime_node != null && mime_node.get_string_content() != null) {
- metadata.mime_type = mime_node.get_string_content();
- }
- StanzaNode? size_node = node.get_subnode("size");
- if (size_node != null && size_node.get_string_content() != null) {
- metadata.size = int64.parse(size_node.get_string_content());
- }
- StanzaNode? date_node = node.get_subnode("date");
- if (date_node != null && date_node.get_string_content() != null) {
- metadata.date = new DateTime.from_iso8601(date_node.get_string_content(), null);
- }
- StanzaNode? width_node = node.get_subnode("width");
- if (width_node != null && width_node.get_string_content() != null) {
- metadata.width = int.parse(width_node.get_string_content());
- }
- StanzaNode? height_node = node.get_subnode("height");
- if (height_node != null && height_node.get_string_content() != null) {
- metadata.height = int.parse(height_node.get_string_content());
- }
- StanzaNode? length_node = node.get_subnode("length");
- if (length_node != null && length_node.get_string_content() != null) {
- metadata.length = int.parse(length_node.get_string_content());
- }
- foreach (StanzaNode thumbnail_node in node.get_subnodes(Xep.JingleContentThumbnails.STANZA_NAME, Xep.JingleContentThumbnails.NS_URI)) {
- Xep.JingleContentThumbnails.Thumbnail? thumbnail = Xep.JingleContentThumbnails.Thumbnail.from_stanza_node(thumbnail_node);
- if (thumbnail != null) {
- metadata.thumbnails.add(thumbnail);
- }
- }
- metadata.hashes = new CryptographicHashes.Hashes.from_stanza_subnodes(node);
- return metadata;
+ FileMetadata metadata = new FileMetadata();
+
+ StanzaNode? name_node = file_node.get_subnode("name");
+ if (name_node != null && name_node.get_string_content() != null) {
+ metadata.name = name_node.get_string_content();
}
- public static FileMetadata? from_message(MessageStanza message) {
- StanzaNode? node = message.stanza.get_subnode("file", NS_URI);
- if (node == null) {
- return null;
- }
- printerr("Parsing metadata from message:\n");
- printerr("%s\n", node.to_xml());
- FileMetadata metadata = FileMetadata.from_stanza_node(node);
- if (metadata != null) {
- printerr("Parsed metadata:\n");
- printerr("%s\n", metadata.to_stanza_node().to_ansi_string(true));
- } else {
- printerr("Failed to parse metadata!\n");
- }
- return FileMetadata.from_stanza_node(node);
+ StanzaNode? desc_node = file_node.get_subnode("desc");
+ if (desc_node != null && desc_node.get_string_content() != null) {
+ metadata.desc = desc_node.get_string_content();
+ }
+ StanzaNode? mime_node = file_node.get_subnode("media_type");
+ if (mime_node != null && mime_node.get_string_content() != null) {
+ metadata.mime_type = mime_node.get_string_content();
+ }
+ StanzaNode? size_node = file_node.get_subnode("size");
+ if (size_node != null && size_node.get_string_content() != null) {
+ metadata.size = int64.parse(size_node.get_string_content());
+ }
+ StanzaNode? date_node = file_node.get_subnode("date");
+ if (date_node != null && date_node.get_string_content() != null) {
+ metadata.date = new DateTime.from_iso8601(date_node.get_string_content(), null);
+ }
+ StanzaNode? width_node = file_node.get_subnode("width");
+ if (width_node != null && width_node.get_string_content() != null) {
+ metadata.width = int.parse(width_node.get_string_content());
+ }
+ StanzaNode? height_node = file_node.get_subnode("height");
+ if (height_node != null && height_node.get_string_content() != null) {
+ metadata.height = int.parse(height_node.get_string_content());
+ }
+ StanzaNode? length_node = file_node.get_subnode("length");
+ if (length_node != null && length_node.get_string_content() != null) {
+ metadata.length = int.parse(length_node.get_string_content());
}
+ metadata.thumbnails = Xep.JingleContentThumbnails.get_thumbnails(file_node);
+ metadata.hashes = CryptographicHashes.get_hashes(file_node);
+ return metadata;
}
}
diff --git a/xmpp-vala/src/module/xep/0447_stateless_file_sharing.vala b/xmpp-vala/src/module/xep/0447_stateless_file_sharing.vala
index 285bfe78..c63f7ea6 100644
--- a/xmpp-vala/src/module/xep/0447_stateless_file_sharing.vala
+++ b/xmpp-vala/src/module/xep/0447_stateless_file_sharing.vala
@@ -1,225 +1,122 @@
+using Gee;
using Xmpp;
namespace Xmpp.Xep.StatelessFileSharing {
+ public const string NS_URI = "urn:xmpp:sfs:0";
- public const string STANZA_NAME = "file-transfer";
+ public static Gee.List<FileShare> get_file_shares(MessageStanza message) {
+ var ret = new ArrayList<FileShare>();
+ foreach (StanzaNode file_sharing_node in message.stanza.get_subnodes("file-sharing", NS_URI)) {
+ var metadata = Xep.FileMetadataElement.get_file_metadata(file_sharing_node);
+ if (metadata == null) continue;
- public interface SfsSource: Object {
- public abstract string type();
- public abstract string serialize();
-
- public abstract StanzaNode to_stanza_node();
- }
-
- public class HttpSource: Object, SfsSource {
- public string url;
+ var sources_node = message.stanza.get_subnode("sources", NS_URI);
- public const string HTTP_NS_URI = "http://jabber.org/protocol/url-data";
- public const string HTTP_STANZA_NAME = "url-data";
- public const string HTTP_URL_ATTRIBUTE = "target";
- public const string SOURCE_TYPE = "http";
-
- public string type() {
- return SOURCE_TYPE;
+ ret.add(new FileShare() {
+ id = file_sharing_node.get_attribute("id", NS_URI),
+ metadata = Xep.FileMetadataElement.get_file_metadata(file_sharing_node),
+ sources = sources_node != null ? get_sources(sources_node) : null
+ });
}
- public string serialize() {
- return this.to_stanza_node().to_xml();
- }
+ if (ret.size == 0) return null;
- public StanzaNode to_stanza_node() {
- StanzaNode node = new StanzaNode.build(HTTP_STANZA_NAME, HTTP_NS_URI).add_self_xmlns();
- node.put_attribute(HTTP_URL_ATTRIBUTE, this.url);
- return node;
- }
+ return ret;
+ }
- public static async HttpSource deserialize(string data) {
- StanzaNode node = yield new StanzaReader.for_string(data).read_stanza_node();
- HttpSource source = HttpSource.from_stanza_node(node);
- assert(source != null);
- return source;
- }
+ public static Gee.List<SourceAttachment>? get_source_attachments(MessageStanza message) {
+ Gee.List<StanzaNode> sources_nodes = message.stanza.get_subnodes("sources", NS_URI);
+ if (sources_nodes.is_empty) return null;
- public static HttpSource? from_stanza_node(StanzaNode node) {
- string? url = node.get_attribute(HTTP_URL_ATTRIBUTE);
- if (url == null) {
- return null;
- }
- HttpSource source = new HttpSource();
- source.url = url;
- return source;
- }
+ string? attach_to_id = MessageAttaching.get_attach_to(message.stanza);
+ if (attach_to_id == null) return null;
- public static Gee.List<HttpSource> extract_sources(StanzaNode node) {
- Gee.List<HttpSource> sources = new Gee.ArrayList<HttpSource>();
- foreach (StanzaNode http_node in node.get_subnodes(HTTP_STANZA_NAME, HTTP_NS_URI)) {
- HttpSource? source = HttpSource.from_stanza_node(http_node);
- if (source != null) {
- sources.add(source);
- }
- }
- return sources;
- }
- }
+ var ret = new ArrayList<SourceAttachment>();
- public class SfsElement {
- public Xep.FileMetadataElement.FileMetadata metadata = new Xep.FileMetadataElement.FileMetadata();
- public Gee.List<SfsSource> sources = new Gee.ArrayList<SfsSource>();
-
- public static SfsElement? from_stanza_node(StanzaNode node) {
- SfsElement element = new SfsElement();
- StanzaNode? metadata_node = node.get_subnode("file", Xep.FileMetadataElement.NS_URI);
- if (metadata_node == null) {
- return null;
- }
- Xep.FileMetadataElement.FileMetadata metadata = Xep.FileMetadataElement.FileMetadata.from_stanza_node(metadata_node);
- if (metadata == null) {
- return null;
- }
- element.metadata = metadata;
- StanzaNode? sources_node = node.get_subnode("sources");
- if (sources_node == null) {
- return null;
- }
- Gee.List<HttpSource> sources = HttpSource.extract_sources(sources_node);
- if (sources.is_empty) {
- return null;
- }
- element.sources = sources;
- return element;
- }
-
- public StanzaNode to_stanza_node() {
- StanzaNode node = new StanzaNode.build(STANZA_NAME, NS_URI).add_self_xmlns();
- node.put_node(this.metadata.to_stanza_node());
- StanzaNode sources_node = new StanzaNode.build("sources", NS_URI);
- Gee.List<StanzaNode> sources = new Gee.ArrayList<StanzaNode>();
- foreach (SfsSource source in this.sources) {
- sources.add(source.to_stanza_node());
- }
- sources_node.sub_nodes = sources;
- node.put_node(sources_node);
- return node;
+ foreach (StanzaNode sources_node in sources_nodes) {
+ ret.add(new SourceAttachment() {
+ to_message_id = attach_to_id,
+ to_file_transfer_id = sources_node.get_attribute("id", NS_URI),
+ sources = get_sources(sources_node)
+ });
}
+ return ret;
}
- public class SfsSourceAttachment {
- public string sfs_id;
- public Gee.List<SfsSource> sources = new Gee.ArrayList<SfsSource>();
-
- public const string ATTACHMENT_NS_URI = "urn:xmpp:message-attaching:1";
- public const string ATTACH_TO_STANZA_NAME = "attach-to";
- public const string SOURCES_STANZA_NAME = "sources";
- public const string ID_ATTRIBUTE_NAME = "id";
-
-
- public static SfsSourceAttachment? from_message_stanza(MessageStanza stanza) {
- StanzaNode? attach_to = stanza.stanza.get_subnode(ATTACH_TO_STANZA_NAME, ATTACHMENT_NS_URI);
- StanzaNode? sources = stanza.stanza.get_subnode(SOURCES_STANZA_NAME, NS_URI);
- if (attach_to == null || sources == null) {
- return null;
- }
- string? id = attach_to.get_attribute(ID_ATTRIBUTE_NAME, ATTACHMENT_NS_URI);
- if (id == null) {
- return null;
- }
- SfsSourceAttachment attachment = new SfsSourceAttachment();
- attachment.sfs_id = id;
- Gee.List<HttpSource> http_sources = HttpSource.extract_sources(sources);
- if (http_sources.is_empty) {
- return null;
- }
- attachment.sources = http_sources;
- return attachment;
- }
+ // Currently only returns a single http source
+ private static Gee.List<Source>? get_sources(StanzaNode sources_node) {
+ string? url = HttpSchemeForUrlData.get_url(sources_node);
+ if (url == null) return null;
- public MessageStanza to_message_stanza(Jid to, string message_type) {
- MessageStanza stanza = new MessageStanza() { to=to, type_=message_type };
- Xep.MessageProcessingHints.set_message_hint(stanza, Xep.MessageProcessingHints.HINT_STORE);
+ var http_source = new HttpSource() { url=url };
+ var sources = new Gee.ArrayList<Source>();
+ sources.add(http_source);
- StanzaNode attach_to = new StanzaNode.build(ATTACH_TO_STANZA_NAME, ATTACHMENT_NS_URI);
- attach_to.add_attribute(new StanzaAttribute.build(ATTACHMENT_NS_URI, "id", this.sfs_id));
- stanza.stanza.put_node(attach_to);
-
- StanzaNode sources = new StanzaNode.build(SOURCES_STANZA_NAME, NS_URI);
- Gee.List<StanzaNode> sources_nodes = new Gee.ArrayList<StanzaNode>();
- foreach (SfsSource source in this.sources) {
- sources_nodes.add(source.to_stanza_node());
- }
- sources.sub_nodes = sources_nodes;
- stanza.stanza.put_node(sources);
-
- return stanza;
- }
+ return sources;
}
- public class MessageFlag : Xmpp.MessageFlag {
- public const string ID = "stateless_file_sharing";
-
- public static MessageFlag? get_flag(MessageStanza message) {
- return (MessageFlag) message.get_flag(NS_URI, ID);
+ public static void set_sfs_element(MessageStanza message, string file_sharing_id, FileMetadataElement.FileMetadata metadata, Gee.List<Xep.StatelessFileSharing.Source>? sources) {
+ var file_sharing_node = new StanzaNode.build("file-sharing", NS_URI).add_self_xmlns()
+ .put_attribute("id", file_sharing_id, NS_URI)
+ .put_node(metadata.to_stanza_node());
+ if (sources != null && !sources.is_empty) {
+ file_sharing_node.put_node(create_sources_node(file_sharing_id, sources));
}
+ message.stanza.put_node(file_sharing_node);
+ }
- public override string get_ns() {
- return NS_URI;
- }
+ public static void set_sfs_attachment(MessageStanza message, string attach_to_id, string attach_to_file_id, Gee.List<Xep.StatelessFileSharing.Source> sources) {
+ message.stanza.put_node(MessageAttaching.to_stanza_node(attach_to_id));
+ message.stanza.put_node(create_sources_node(attach_to_file_id, sources).add_self_xmlns());
+ }
- public override string get_id() {
- return ID;
+ private static StanzaNode create_sources_node(string file_sharing_id, Gee.List<Xep.StatelessFileSharing.Source> sources) {
+ StanzaNode sources_node = new StanzaNode.build("sources", NS_URI)
+ .put_attribute("id", file_sharing_id, NS_URI);
+ foreach (var source in sources) {
+ sources_node.put_node(source.to_stanza_node());
}
+ return sources_node;
}
- public class Module : XmppStreamModule {
- public static ModuleIdentity<Module> IDENTITY = new ModuleIdentity<Module>(NS_URI, "stateless_file_sharing");
-
- public signal void received_sfs(Jid from, Jid to, SfsElement sfs_element, MessageStanza message);
- public signal void received_sfs_attachment(Jid from, Jid to, SfsSourceAttachment attachment, MessageStanza message);
-
- public void send_stateless_file_transfer(XmppStream stream, MessageStanza sfs_message, SfsElement sfs_element) {
- StanzaNode sfs_node = sfs_element.to_stanza_node();
- printerr(sfs_node.to_ansi_string(true));
+ public class FileShare : Object {
+ public string? id { get; set; }
+ public Xep.FileMetadataElement.FileMetadata metadata { get; set; }
+ public Gee.List<Source>? sources { get; set; }
+ }
- sfs_message.stanza.put_node(sfs_node);
- printerr("Sending message:\n");
- printerr(sfs_message.stanza.to_ansi_string(true));
- stream.get_module(MessageModule.IDENTITY).send_message.begin(stream, sfs_message);
- }
+ public class SourceAttachment : Object {
+ public string to_message_id { get; set; }
+ public string? to_file_transfer_id { get; set; }
+ public Gee.List<Source>? sources { get; set; }
+ }
- public void send_stateless_file_transfer_attachment(XmppStream stream, Jid to, string message_type, SfsSourceAttachment attachment) {
- MessageStanza message = attachment.to_message_stanza(to, message_type);
+ public interface Source: Object {
+ public abstract string type();
+ public abstract StanzaNode to_stanza_node();
+ public abstract bool equals(Source source);
- printerr("Sending message:\n");
- printerr(message.stanza.to_ansi_string(true));
- stream.get_module(MessageModule.IDENTITY).send_message.begin(stream, message);
+ public static bool equals_func(Source s1, Source s2) {
+ return s1.equals(s2);
}
+ }
- private void on_received_message(XmppStream stream, MessageStanza message) {
- StanzaNode? sfs_node = message.stanza.get_subnode(STANZA_NAME, NS_URI);
- if (sfs_node != null) {
- SfsElement? sfs_element = SfsElement.from_stanza_node(sfs_node);
- if (sfs_element == null) {
- return;
- }
- message.add_flag(new MessageFlag());
- received_sfs(message.from, message.to, sfs_element, message);
- }
- SfsSourceAttachment? attachment = SfsSourceAttachment.from_message_stanza(message);
- if (attachment != null) {
- received_sfs_attachment(message.from, message.to, attachment, message);
- }
+ public class HttpSource : Object, Source {
+ public string url { get; set; }
+ public string type() {
+ return "http";
}
- public override void attach(XmppStream stream) {
- stream.get_module(MessageModule.IDENTITY).received_message.connect(on_received_message);
+ public StanzaNode to_stanza_node() {
+ return HttpSchemeForUrlData.to_stanza_node(url);
}
- public override void detach(XmppStream stream) {
- stream.get_module(MessageModule.IDENTITY).received_message.disconnect(on_received_message);
+ public bool equals(Source source) {
+ HttpSource? http_source = source as HttpSource;
+ if (http_source == null) return false;
+ return http_source.url == this.url;
}
-
- public override string get_ns() { return NS_URI; }
- public override string get_id() { return IDENTITY.id; }
}
}