diff --git a/CMakeLists.txt b/CMakeLists.txt index ebf13e1d..15a66f99 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -2,7 +2,7 @@ cmake_minimum_required(VERSION 3.0) list(APPEND CMAKE_MODULE_PATH ${CMAKE_SOURCE_DIR}/cmake) # Prepare Plugins -set(PLUGINS omemo;openpgp) +set(PLUGINS omemo;openpgp;http-files) if(DISABLED_PLUGINS) list(REMOVE_ITEM PLUGINS ${DISABLED_PLUGINS}) endif(DISABLED_PLUGINS) diff --git a/cmake/FindSoup.cmake b/cmake/FindSoup.cmake new file mode 100644 index 00000000..d5afab48 --- /dev/null +++ b/cmake/FindSoup.cmake @@ -0,0 +1,31 @@ +include(PkgConfigWithFallback) +find_pkg_config_with_fallback(Soup + PKG_CONFIG_NAME libsoup-2.4 + LIB_NAMES soup-2.4 + INCLUDE_NAMES libsoup/soup.h + INCLUDE_DIR_SUFFIXES libsoup-2.4 libsoup-2.4/include libsoup libsoup/include + DEPENDS GIO +) + +if(Soup_FOUND AND NOT Soup_VERSION) + find_file(Soup_VERSION_HEADER "libsoup/soup-version.h" HINTS ${Soup_INCLUDE_DIRS}) + mark_as_advanced(Soup_VERSION_HEADER) + + if(Soup_VERSION_HEADER) + file(STRINGS "${Soup_VERSION_HEADER}" Soup_MAJOR_VERSION REGEX "^#define SOUP_MAJOR_VERSION +\\(?([0-9]+)\\)?$") + string(REGEX REPLACE "^#define SOUP_MAJOR_VERSION \\(?([0-9]+)\\)?$" "\\1" Soup_MAJOR_VERSION "${Soup_MAJOR_VERSION}") + file(STRINGS "${Soup_VERSION_HEADER}" Soup_MINOR_VERSION REGEX "^#define SOUP_MINOR_VERSION +\\(?([0-9]+)\\)?$") + string(REGEX REPLACE "^#define SOUP_MINOR_VERSION \\(?([0-9]+)\\)?$" "\\1" Soup_MINOR_VERSION "${Soup_MINOR_VERSION}") + file(STRINGS "${Soup_VERSION_HEADER}" Soup_MICRO_VERSION REGEX "^#define SOUP_MICRO_VERSION +\\(?([0-9]+)\\)?$") + string(REGEX REPLACE "^#define SOUP_MICRO_VERSION \\(?([0-9]+)\\)?$" "\\1" Soup_MICRO_VERSION "${Soup_MICRO_VERSION}") + set(Soup_VERSION "${Soup_MAJOR_VERSION}.${Soup_MINOR_VERSION}.${Soup_MICRO_VERSION}") + unset(Soup_MAJOR_VERSION) + unset(Soup_MINOR_VERSION) + unset(Soup_MICRO_VERSION) + endif() +endif() + +include(FindPackageHandleStandardArgs) +find_package_handle_standard_args(Soup + REQUIRED_VARS Soup_LIBRARY + VERSION_VAR Soup_VERSION) diff --git a/libdino/src/plugin/interfaces.vala b/libdino/src/plugin/interfaces.vala index 705cdfe1..178ca1ab 100644 --- a/libdino/src/plugin/interfaces.vala +++ b/libdino/src/plugin/interfaces.vala @@ -53,4 +53,14 @@ public class ContactDetails : Object { public signal void add(string category, string label, string desc, Widget widget); } -} \ No newline at end of file +public abstract class ConversationTitlebarEntry : Object { + public abstract string id { get; } + public abstract double order { get; } + public abstract ConversationTitlebarWidget get_widget(); +} + +public interface ConversationTitlebarWidget : Gtk.Widget { + public abstract void set_conversation(Conversation conversation); +} + +} diff --git a/libdino/src/plugin/registry.vala b/libdino/src/plugin/registry.vala index e1ba605d..01d18d5f 100644 --- a/libdino/src/plugin/registry.vala +++ b/libdino/src/plugin/registry.vala @@ -6,6 +6,15 @@ public class Registry { internal ArrayList encryption_list_entries = new ArrayList(); internal ArrayList account_settings_entries = new ArrayList(); internal ArrayList contact_details_entries = new ArrayList(); + internal Gee.Collection conversation_titlebar_entries = new Gee.TreeSet((a, b) => { + if (a.order < b.order) { + return -1; + } else if (a.order > b.order) { + return 1; + } else { + return 0; + } + }); public bool register_encryption_list_entry(EncryptionListEntry entry) { lock(encryption_list_entries) { @@ -39,6 +48,16 @@ public class Registry { return true; } } + + public bool register_contact_titlebar_entry(ConversationTitlebarEntry entry) { + lock(conversation_titlebar_entries) { + foreach(ConversationTitlebarEntry e in conversation_titlebar_entries) { + if (e.id == entry.id) return false; + } + conversation_titlebar_entries.add(entry); + return true; + } + } } -} \ No newline at end of file +} diff --git a/libdino/src/service/database.vala b/libdino/src/service/database.vala index 333c599b..b414b943 100644 --- a/libdino/src/service/database.vala +++ b/libdino/src/service/database.vala @@ -276,7 +276,7 @@ public class Database : Qlite.Database { return ret; } - public void add_entity_features(string entity, ArrayList features) { + public void add_entity_features(string entity, Gee.List features) { foreach (string feature in features) { entity_feature.insert() .value(entity_feature.entity, entity) @@ -285,7 +285,7 @@ public class Database : Qlite.Database { } } - public ArrayList get_entity_features(string entity) { + public Gee.List get_entity_features(string entity) { ArrayList ret = new ArrayList(); foreach (Row row in entity_feature.select({entity_feature.feature}).with(entity_feature.entity, "=", entity)) { ret.add(row[entity_feature.feature]); diff --git a/libdino/src/service/entity_capabilities_storage.vala b/libdino/src/service/entity_capabilities_storage.vala index 9774739a..910ce48d 100644 --- a/libdino/src/service/entity_capabilities_storage.vala +++ b/libdino/src/service/entity_capabilities_storage.vala @@ -12,12 +12,12 @@ public class EntityCapabilitiesStorage : Xep.EntityCapabilities.Storage, Object this.db = db; } - public void store_features(string entity, ArrayList features) { + public void store_features(string entity, Gee.List features) { db.add_entity_features(entity, features); } - public ArrayList get_features(string entitiy) { + public Gee.List get_features(string entitiy) { return db.get_entity_features(entitiy); } } -} \ No newline at end of file +} diff --git a/main/CMakeLists.txt b/main/CMakeLists.txt index 60bfd517..e024d3de 100644 --- a/main/CMakeLists.txt +++ b/main/CMakeLists.txt @@ -102,7 +102,10 @@ SOURCES src/ui/conversation_summary/slashme_item.vala src/ui/conversation_summary/status_item.vala src/ui/conversation_summary/view.vala - src/ui/conversation_titlebar.vala + src/ui/conversation_titlebar/encryption_entry.vala + src/ui/conversation_titlebar/menu_entry.vala + src/ui/conversation_titlebar/occupants_entry.vala + src/ui/conversation_titlebar/view.vala src/ui/manage_accounts/account_row.vala src/ui/manage_accounts/add_account_dialog.vala src/ui/manage_accounts/dialog.vala diff --git a/main/src/main.vala b/main/src/main.vala index ce633e30..c4c441a1 100644 --- a/main/src/main.vala +++ b/main/src/main.vala @@ -34,7 +34,7 @@ void main(string[] args) { return -1; }); - foreach (string plugin in new string[]{"omemo", "openpgp"}) { + foreach (string plugin in new string[]{"omemo", "openpgp", "http-files"}) { try { loader.load(plugin, app); } catch (Error e) { diff --git a/main/src/ui/conversation_titlebar.vala b/main/src/ui/conversation_titlebar.vala deleted file mode 100644 index be532392..00000000 --- a/main/src/ui/conversation_titlebar.vala +++ /dev/null @@ -1,139 +0,0 @@ -using Gtk; -using Gee; - -using Dino.Entities; - -namespace Dino.Ui { - -[GtkTemplate (ui = "/org/dino-im/conversation_titlebar.ui")] -public class ConversationTitlebar : Gtk.HeaderBar { - - [GtkChild] private MenuButton menu_button; - [GtkChild] private MenuButton encryption_button; - [GtkChild] private MenuButton groupchat_button; - - private RadioButton? button_unencrypted; - private Map encryption_radios = new HashMap(); - - private StreamInteractor stream_interactor; - private Window window; - private Conversation? conversation; - - public ConversationTitlebar(StreamInteractor stream_interactor, Window window) { - this.stream_interactor = stream_interactor; - this.window = window; - create_conversation_menu(); - create_encryption_menu(); - - stream_interactor.get_module(MucManager.IDENTITY).subject_set.connect((account, jid, subject) => { - Idle.add(() => { - if (conversation != null && conversation.counterpart.equals_bare(jid) && conversation.account.equals(account)) { - update_subtitle(subject); - } - return false; - }); - }); - } - - public void initialize_for_conversation(Conversation conversation) { - this.conversation = conversation; - update_encryption_menu_state(); - update_encryption_menu_icon(); - update_groupchat_menu(); - update_title(); - update_subtitle(); - } - - private void update_encryption_menu_state() { - foreach (RadioButton e in encryption_radios.keys) { - e.set_sensitive(encryption_radios[e].can_encrypt(conversation)); - if (conversation.encryption == encryption_radios[e].encryption) e.set_active(true); - } - if (conversation.encryption == Encryption.NONE) { - button_unencrypted.set_active(true); - } - } - - private void update_encryption_menu_icon() { - encryption_button.visible = (conversation.type_ == Conversation.Type.CHAT); - if (conversation.type_ == Conversation.Type.CHAT) { - if (conversation.encryption == Encryption.NONE) { - encryption_button.set_image(new Image.from_icon_name("changes-allow-symbolic", IconSize.BUTTON)); - } else { - encryption_button.set_image(new Image.from_icon_name("changes-prevent-symbolic", IconSize.BUTTON)); - } - } - } - - private void update_groupchat_menu() { - groupchat_button.visible = conversation.type_ == Conversation.Type.GROUPCHAT; - if (conversation.type_ == Conversation.Type.GROUPCHAT) { - groupchat_button.set_use_popover(true); - OccupantMenu.View menu = new OccupantMenu.View(stream_interactor, window, conversation); - groupchat_button.set_popover(menu); - } - } - - private void update_title() { - set_title(Util.get_conversation_display_name(stream_interactor, conversation)); - } - - private void update_subtitle(string? subtitle = null) { - if (subtitle != null) { - set_subtitle(subtitle); - } else if (conversation.type_ == Conversation.Type.GROUPCHAT) { - string subject = stream_interactor.get_module(MucManager.IDENTITY).get_groupchat_subject(conversation.counterpart, conversation.account); - set_subtitle(subject != "" ? subject : null); - } else { - set_subtitle(null); - } - } - - private void create_conversation_menu() { - Builder builder = new Builder.from_resource("/org/dino-im/menu_conversation.ui"); - MenuModel menu = builder.get_object("menu_conversation") as MenuModel; - menu_button.set_menu_model(menu); - - SimpleAction contact_details_action = new SimpleAction("contact_details", null); - contact_details_action.activate.connect(() => { - ContactDetails.Dialog contact_details_dialog = new ContactDetails.Dialog(stream_interactor, conversation); - contact_details_dialog.set_transient_for((Window) get_toplevel()); - contact_details_dialog.present(); - }); - GLib.Application.get_default().add_action(contact_details_action); - } - - private void encryption_changed() { - foreach (RadioButton e in encryption_radios.keys) { - if (e.get_active()) { - conversation.encryption = encryption_radios[e].encryption; - update_encryption_menu_icon(); - return; - } - } - conversation.encryption = Encryption.NONE; - update_encryption_menu_icon(); - } - - private void create_encryption_menu() { - Builder builder = new Builder.from_resource("/org/dino-im/menu_encryption.ui"); - PopoverMenu menu = builder.get_object("menu_encryption") as PopoverMenu; - Box encryption_box = builder.get_object("encryption_box") as Box; - button_unencrypted = builder.get_object("button_unencrypted") as RadioButton; - button_unencrypted.toggled.connect(encryption_changed); - Application app = GLib.Application.get_default() as Application; - foreach(var e in app.plugin_registry.encryption_list_entries) { - RadioButton btn = new RadioButton.with_label(button_unencrypted.get_group(), e.name); - encryption_radios[btn] = e; - btn.toggled.connect(encryption_changed); - btn.visible = true; - encryption_box.pack_end(btn, false); - } - encryption_button.clicked.connect(update_encryption_menu_state); - encryption_button.set_use_popover(true); - encryption_button.set_popover(menu); - encryption_button.set_image(new Image.from_icon_name("changes-allow-symbolic", IconSize.BUTTON)); - } -} - -} \ No newline at end of file diff --git a/main/src/ui/conversation_titlebar/encryption_entry.vala b/main/src/ui/conversation_titlebar/encryption_entry.vala new file mode 100644 index 00000000..18c09773 --- /dev/null +++ b/main/src/ui/conversation_titlebar/encryption_entry.vala @@ -0,0 +1,83 @@ +using Gtk; +using Gee; + +using Dino.Entities; + +namespace Dino.Ui { + +class EncryptionEntry : Plugins.ConversationTitlebarEntry { + public override string id { get { return "encryption"; } } + + public override double order { get { return 2; } } + public override Plugins.ConversationTitlebarWidget get_widget() { + return new EncryptionWidget() { visible=true }; + } +} + +class EncryptionWidget : MenuButton, Plugins.ConversationTitlebarWidget { + + private Conversation? conversation; + private RadioButton? button_unencrypted; + private Map encryption_radios = new HashMap(); + + public EncryptionWidget() { + Builder builder = new Builder.from_resource("/org/dino-im/menu_encryption.ui"); + PopoverMenu menu = builder.get_object("menu_encryption") as PopoverMenu; + Box encryption_box = builder.get_object("encryption_box") as Box; + button_unencrypted = builder.get_object("button_unencrypted") as RadioButton; + button_unencrypted.toggled.connect(encryption_changed); + Application app = GLib.Application.get_default() as Application; + foreach(var e in app.plugin_registry.encryption_list_entries) { + RadioButton btn = new RadioButton.with_label(button_unencrypted.get_group(), e.name); + encryption_radios[btn] = e; + btn.toggled.connect(encryption_changed); + btn.visible = true; + encryption_box.pack_end(btn, false); + } + clicked.connect(update_encryption_menu_state); + set_use_popover(true); + set_popover(menu); + set_image(new Image.from_icon_name("changes-allow-symbolic", IconSize.BUTTON)); + } + + private void encryption_changed() { + foreach (RadioButton e in encryption_radios.keys) { + if (e.get_active()) { + conversation.encryption = encryption_radios[e].encryption; + update_encryption_menu_icon(); + return; + } + } + conversation.encryption = Encryption.NONE; + update_encryption_menu_icon(); + } + + private void update_encryption_menu_state() { + foreach (RadioButton e in encryption_radios.keys) { + e.set_sensitive(encryption_radios[e].can_encrypt(conversation)); + if (conversation.encryption == encryption_radios[e].encryption) e.set_active(true); + } + if (conversation.encryption == Encryption.NONE) { + button_unencrypted.set_active(true); + } + } + + private void update_encryption_menu_icon() { + visible = (conversation.type_ == Conversation.Type.CHAT); + if (conversation.type_ == Conversation.Type.CHAT) { + if (conversation.encryption == Encryption.NONE) { + set_image(new Image.from_icon_name("changes-allow-symbolic", IconSize.BUTTON)); + } else { + set_image(new Image.from_icon_name("changes-prevent-symbolic", IconSize.BUTTON)); + } + } + } + + public new void set_conversation(Conversation conversation) { + this.conversation = conversation; + update_encryption_menu_state(); + update_encryption_menu_icon(); + } +} + +} diff --git a/main/src/ui/conversation_titlebar/menu_entry.vala b/main/src/ui/conversation_titlebar/menu_entry.vala new file mode 100644 index 00000000..6cead69d --- /dev/null +++ b/main/src/ui/conversation_titlebar/menu_entry.vala @@ -0,0 +1,47 @@ +using Gtk; + +using Dino.Entities; + +namespace Dino.Ui { + +class MenuEntry : Plugins.ConversationTitlebarEntry { + public override string id { get { return "menu"; } } + + StreamInteractor stream_interactor; + + public MenuEntry(StreamInteractor stream_interactor) { + this.stream_interactor = stream_interactor; + } + + public override double order { get { return 0; } } + public override Plugins.ConversationTitlebarWidget get_widget() { + return new MenuWidget(stream_interactor) { visible=true }; + } +} + +class MenuWidget : MenuButton, Plugins.ConversationTitlebarWidget { + + private Conversation? conversation; + + public MenuWidget(StreamInteractor stream_interactor) { + image = new Image.from_icon_name("open-menu-symbolic", IconSize.MENU); + + Builder builder = new Builder.from_resource("/org/dino-im/menu_conversation.ui"); + MenuModel menu = builder.get_object("menu_conversation") as MenuModel; + set_menu_model(menu); + + SimpleAction contact_details_action = new SimpleAction("contact_details", null); + contact_details_action.activate.connect(() => { + ContactDetails.Dialog contact_details_dialog = new ContactDetails.Dialog(stream_interactor, conversation); + contact_details_dialog.set_transient_for((Window) get_toplevel()); + contact_details_dialog.present(); + }); + GLib.Application.get_default().add_action(contact_details_action); + } + + public new void set_conversation(Conversation conversation) { + this.conversation = conversation; + } +} + +} diff --git a/main/src/ui/conversation_titlebar/occupants_entry.vala b/main/src/ui/conversation_titlebar/occupants_entry.vala new file mode 100644 index 00000000..c305bed7 --- /dev/null +++ b/main/src/ui/conversation_titlebar/occupants_entry.vala @@ -0,0 +1,50 @@ +using Gtk; + +using Dino.Entities; + +namespace Dino.Ui { + +class OccupantsEntry : Plugins.ConversationTitlebarEntry { + public override string id { get { return "occupants"; } } + + StreamInteractor stream_interactor; + Window window; + + public OccupantsEntry(StreamInteractor stream_interactor, Window window) { + this.stream_interactor = stream_interactor; + this.window = window; + } + + public override double order { get { return 3; } } + public override Plugins.ConversationTitlebarWidget get_widget() { + return new OccupantsWidget(stream_interactor, window) { visible=true }; + } +} + +class OccupantsWidget : MenuButton, Plugins.ConversationTitlebarWidget { + + private Conversation? conversation; + private StreamInteractor stream_interactor; + private Window window; + + public OccupantsWidget(StreamInteractor stream_interactor, Window window) { + + image = new Image.from_icon_name("system-users-symbolic", IconSize.MENU); + + this.stream_interactor = stream_interactor; + this.window = window; + set_use_popover(true); + } + + public new void set_conversation(Conversation conversation) { + this.conversation = conversation; + + visible = conversation.type_ == Conversation.Type.GROUPCHAT; + if (conversation.type_ == Conversation.Type.GROUPCHAT) { + OccupantMenu.View menu = new OccupantMenu.View(stream_interactor, window, conversation); + set_popover(menu); + } + } +} + +} diff --git a/main/src/ui/conversation_titlebar/view.vala b/main/src/ui/conversation_titlebar/view.vala new file mode 100644 index 00000000..7debddd6 --- /dev/null +++ b/main/src/ui/conversation_titlebar/view.vala @@ -0,0 +1,69 @@ +using Gtk; +using Gee; + +using Dino.Entities; + +namespace Dino.Ui { + +public class ConversationTitlebar : Gtk.HeaderBar { + + private StreamInteractor stream_interactor; + private Window window; + private Conversation? conversation; + private Gee.List widgets = new ArrayList(); + + public ConversationTitlebar(StreamInteractor stream_interactor, Window window) { + this.stream_interactor = stream_interactor; + this.window = window; + + show_close_button = true; + hexpand = true; + + Application app = GLib.Application.get_default() as Application; + app.plugin_registry.register_contact_titlebar_entry(new MenuEntry(stream_interactor)); + app.plugin_registry.register_contact_titlebar_entry(new EncryptionEntry()); + app.plugin_registry.register_contact_titlebar_entry(new OccupantsEntry(stream_interactor, window)); + + foreach(var e in app.plugin_registry.conversation_titlebar_entries) { + Plugins.ConversationTitlebarWidget widget = e.get_widget(); + widgets.add(widget); + pack_end(widget); + } + + stream_interactor.get_module(MucManager.IDENTITY).subject_set.connect((account, jid, subject) => { + Idle.add(() => { + if (conversation != null && conversation.counterpart.equals_bare(jid) && conversation.account.equals(account)) { + update_subtitle(subject); + } + return false; + }); + }); + } + + public void initialize_for_conversation(Conversation conversation) { + this.conversation = conversation; + update_title(); + update_subtitle(); + + foreach (Plugins.ConversationTitlebarWidget widget in widgets) { + widget.set_conversation(conversation); + } + } + + private void update_title() { + set_title(Util.get_conversation_display_name(stream_interactor, conversation)); + } + + private void update_subtitle(string? subtitle = null) { + if (subtitle != null) { + set_subtitle(subtitle); + } else if (conversation.type_ == Conversation.Type.GROUPCHAT) { + string subject = stream_interactor.get_module(MucManager.IDENTITY).get_groupchat_subject(conversation.counterpart, conversation.account); + set_subtitle(subject != "" ? subject : null); + } else { + set_subtitle(null); + } + } +} + +} diff --git a/main/src/ui/occupant_menu/list.vala b/main/src/ui/occupant_menu/list.vala index 50e6b300..42ff3da6 100644 --- a/main/src/ui/occupant_menu/list.vala +++ b/main/src/ui/occupant_menu/list.vala @@ -14,7 +14,7 @@ public class List : Box { [GtkChild] public ListBox list_box; [GtkChild] private SearchEntry search_entry; - private Conversation? conversation; + private Conversation conversation; private string[]? filter_values; private HashMap rows = new HashMap(Jid.hash_func, Jid.equals_func); @@ -167,4 +167,4 @@ public class List : Box { } } -} \ No newline at end of file +} diff --git a/plugins/CMakeLists.txt b/plugins/CMakeLists.txt index 1eb3dbb1..10506a86 100644 --- a/plugins/CMakeLists.txt +++ b/plugins/CMakeLists.txt @@ -7,3 +7,7 @@ if(PLUGIN_ENABLED_omemo) add_subdirectory(omemo) add_subdirectory(signal-protocol) endif(PLUGIN_ENABLED_omemo) + +if(PLUGIN_ENABLED_http-files) + add_subdirectory(http-files) +endif(PLUGIN_ENABLED_http-files) diff --git a/plugins/http-files/CMakeLists.txt b/plugins/http-files/CMakeLists.txt new file mode 100644 index 00000000..565cfef0 --- /dev/null +++ b/plugins/http-files/CMakeLists.txt @@ -0,0 +1,31 @@ +find_packages(HTTP_FILES_PACKAGES REQUIRED + Gee + GLib + GModule + GObject + GTK3 + Soup +) + +vala_precompile(HTTP_FILES_VALA_C +SOURCES + src/contact_titlebar_entry.vala + src/manager.vala + src/plugin.vala + src/register_plugin.vala + src/upload_stream_module.vala +CUSTOM_VAPIS + ${CMAKE_BINARY_DIR}/exports/xmpp-vala.vapi + ${CMAKE_BINARY_DIR}/exports/dino.vapi + ${CMAKE_BINARY_DIR}/exports/qlite.vapi +PACKAGES + ${HTTP_FILES_PACKAGES} +) + +add_definitions(${VALA_CFLAGS}) +add_library(http-files SHARED ${HTTP_FILES_VALA_C}) +target_link_libraries(http-files libdino ${HTTP_FILES_PACKAGES}) +set_target_properties(http-files PROPERTIES PREFIX "") +set_target_properties(http-files PROPERTIES LIBRARY_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/plugins/) + +install(TARGETS http-files ${PLUGIN_INSTALL}) diff --git a/plugins/http-files/src/contact_titlebar_entry.vala b/plugins/http-files/src/contact_titlebar_entry.vala new file mode 100644 index 00000000..a87c7ddf --- /dev/null +++ b/plugins/http-files/src/contact_titlebar_entry.vala @@ -0,0 +1,68 @@ +using Gtk; + +using Dino.Entities; + +namespace Dino.Plugins.HttpFiles { + +public class ConversationsTitlebarEntry : Plugins.ConversationTitlebarEntry { + public override string id { get { return "send_files"; } } + + StreamInteractor stream_interactor; + + public ConversationsTitlebarEntry(StreamInteractor stream_interactor) { + this.stream_interactor = stream_interactor; + } + + public override double order { get { return 4; } } + public override Plugins.ConversationTitlebarWidget get_widget() { + return new ConversationTitlebarWidget(stream_interactor) { visible=true }; + } +} + +public class ConversationTitlebarWidget : Button, Plugins.ConversationTitlebarWidget { + + private Conversation? conversation; + private StreamInteractor stream_interactor; + + public ConversationTitlebarWidget(StreamInteractor stream_interactor) { + this.stream_interactor = stream_interactor; + image = new Image.from_icon_name("mail-attachment-symbolic", IconSize.MENU); + clicked.connect(on_clicked); + stream_interactor.get_module(Manager.IDENTITY).upload_available.connect(on_upload_available); + } + + public void on_clicked() { + FileChooserDialog chooser = new FileChooserDialog ( + "Select file", null, FileChooserAction.OPEN, + "Cancel", ResponseType.CANCEL, + "Select", ResponseType.ACCEPT); + int? max_file_size = stream_interactor.get_module(Manager.IDENTITY).get_max_file_size(conversation.account); + if (max_file_size != null) { + FileFilter filter = new FileFilter(); + filter.add_custom(FileFilterFlags.URI, (filter_info) => { + File file = File.new_for_uri(filter_info.uri); + FileInfo file_info = file.query_info("*", FileQueryInfoFlags.NONE); + return file_info.get_size() <= max_file_size; + }); + chooser.set_filter(filter); + } + if (chooser.run() == Gtk.ResponseType.ACCEPT) { + string uri = chooser.get_filename(); + stream_interactor.get_module(Manager.IDENTITY).send(conversation, uri); + } + chooser.close(); + } + + public void on_upload_available(Account account) { + if (conversation.account.equals(account)) { + visible = true; + } + } + + public new void set_conversation(Conversation conversation) { + this.conversation = conversation; + visible = stream_interactor.get_module(Manager.IDENTITY).is_upload_available(conversation.account); + } +} + +} diff --git a/plugins/http-files/src/manager.vala b/plugins/http-files/src/manager.vala new file mode 100644 index 00000000..5b804a00 --- /dev/null +++ b/plugins/http-files/src/manager.vala @@ -0,0 +1,56 @@ +using Dino.Entities; +using Xmpp; +using Gee; + +namespace Dino.Plugins.HttpFiles { + +public class Manager : StreamInteractionModule, Object { + public static ModuleIdentity IDENTITY = new ModuleIdentity("http_files"); + public string id { get { return IDENTITY.id; } } + + public signal void upload_available(Account account); + + private StreamInteractor stream_interactor; + private HashMap max_file_sizes = new HashMap(Account.hash_func, Account.equals_func); + + private Manager(StreamInteractor stream_interactor) { + this.stream_interactor = stream_interactor; + + stream_interactor.stream_negotiated.connect(on_stream_negotiated); + } + + public void send(Conversation conversation, string file_uri) { + Xmpp.Core.XmppStream? stream = stream_interactor.get_stream(conversation.account); + if (stream != null) { + stream_interactor.module_manager.get_module(conversation.account, UploadStreamModule.IDENTITY).upload(stream, file_uri, + (stream, url_down) => { + stream_interactor.get_module(MessageProcessor.IDENTITY).send_message(url_down, conversation); + }, + () => {} + ); + + } + } + + public bool is_upload_available(Account account) { + return max_file_sizes.has_key(account); + } + + public int? get_max_file_size(Account account) { + return max_file_sizes[account]; + } + + private void on_stream_negotiated(Account account, Core.XmppStream stream) { + stream_interactor.module_manager.get_module(account, UploadStreamModule.IDENTITY).feature_available.connect((stream, max_file_size) => { + max_file_sizes[account] = max_file_size; + upload_available(account); + }); + } + + public static void start(StreamInteractor stream_interactor) { + Manager m = new Manager(stream_interactor); + stream_interactor.add_module(m); + } +} + +} diff --git a/plugins/http-files/src/plugin.vala b/plugins/http-files/src/plugin.vala new file mode 100644 index 00000000..572efd93 --- /dev/null +++ b/plugins/http-files/src/plugin.vala @@ -0,0 +1,31 @@ +extern const string GETTEXT_PACKAGE; +extern const string LOCALE_INSTALL_DIR; + +namespace Dino.Plugins.HttpFiles { + +public class Plugin : RootInterface, Object { + + public Dino.Application app; + public ConversationsTitlebarEntry conversations_titlebar_entry; + + public void registered(Dino.Application app) { + try { + this.app = app; + this.conversations_titlebar_entry = new ConversationsTitlebarEntry(app.stream_interaction); + + this.app.plugin_registry.register_contact_titlebar_entry(conversations_titlebar_entry); + this.app.stream_interaction.module_manager.initialize_account_modules.connect((account, list) => { + list.add(new UploadStreamModule()); + }); + Manager.start(this.app.stream_interaction); + } catch (Error e) { + print(@"Error initializing http-files: $(e.message)\n"); + } + } + + public void shutdown() { + // Nothing to do + } +} + +} diff --git a/plugins/http-files/src/register_plugin.vala b/plugins/http-files/src/register_plugin.vala new file mode 100644 index 00000000..21fac531 --- /dev/null +++ b/plugins/http-files/src/register_plugin.vala @@ -0,0 +1,3 @@ +public Type register_plugin(Module module) { + return typeof (Dino.Plugins.HttpFiles.Plugin); +} diff --git a/plugins/http-files/src/upload_stream_module.vala b/plugins/http-files/src/upload_stream_module.vala new file mode 100644 index 00000000..765d212e --- /dev/null +++ b/plugins/http-files/src/upload_stream_module.vala @@ -0,0 +1,159 @@ +using Xmpp; +using Xmpp.Core; +using Xmpp.Xep; + +namespace Dino.Plugins.HttpFiles { + +private const string NS_URI = "urn:xmpp:http:upload"; +private const string NS_URI_0 = "urn:xmpp:http:upload:0"; + +public class UploadStreamModule : XmppStreamModule { + public static Core.ModuleIdentity IDENTITY = new Core.ModuleIdentity(NS_URI, "0363_http_file_upload"); + + public signal void feature_available(XmppStream stream, int? max_file_size); + + public delegate void OnUploadOk(XmppStream stream, string url_down); + public delegate void OnError(XmppStream stream, string error); + public void upload(XmppStream stream, string file_uri, owned OnUploadOk listener, owned OnError error_listener) { + File file = File.new_for_path(file_uri); + FileInfo file_info = file.query_info("*", FileQueryInfoFlags.NONE); + request_slot(stream, file.get_basename(), (int)file_info.get_size(), file_info.get_content_type(), + (stream, url_down, url_up) => { + uint8[] data; + FileUtils.get_data(file_uri, out data); + + Soup.Message message = new Soup.Message("PUT", url_up); + message.set_request(file_info.get_content_type(), Soup.MemoryUse.COPY, data); + Soup.Session session = new Soup.Session(); + session.send_async(message); + + listener(stream, url_up); + }, + error_listener); + } + + private delegate void OnSlotOk(XmppStream stream, string url_get, string url_put); + private void request_slot(XmppStream stream, string filename, int file_size, string? content_type, owned OnSlotOk listener, owned OnError error_listener) { + Flag? flag = stream.get_flag(Flag.IDENTITY); + if (flag != null) return; + + StanzaNode request_node; + if (flag.ns_ver == NS_URI_0) { + request_node = new StanzaNode.build("request", NS_URI_0).add_self_xmlns(); + request_node.put_attribute("filename", filename).put_attribute("size", file_size.to_string()); + if (content_type != null) request_node.put_attribute("content-type", content_type); + } else{ + request_node = new StanzaNode.build("request", NS_URI).add_self_xmlns() + .put_node(new StanzaNode.build("filename", NS_URI).put_node(new StanzaNode.text(filename))) + .put_node(new StanzaNode.build("size", NS_URI).put_node(new StanzaNode.text(file_size.to_string()))); + if (content_type != null) { + request_node.put_node(new StanzaNode.build("content-type", NS_URI).put_node(new StanzaNode.text(content_type))); + } + } + Iq.Stanza iq = new Iq.Stanza.get(request_node) { to=flag.file_store_jid }; + stream.get_module(Iq.Module.IDENTITY).send_iq(stream, iq, (stream, iq) => { + if (iq.is_error()) { + error_listener(stream, ""); + } else { + string? url_get = iq.stanza.get_deep_string_content(flag.ns_ver + ":slot", flag.ns_ver + ":get"); + string? url_put = iq.stanza.get_deep_string_content(flag.ns_ver + ":slot", flag.ns_ver + ":put"); + listener(stream, url_get, url_put); + } + }); + } + + public override void attach(XmppStream stream) { + Iq.Module.require(stream); + ServiceDiscovery.Module.require(stream); + + query_availability(stream); + } + + public override void detach(XmppStream stream) { + stream.get_module(Bind.Module.IDENTITY).bound_to_resource.disconnect(query_availability); + } + + public static void require(XmppStream stream) { + if (stream.get_module(IDENTITY) == null) stream.add_module(new ChatMarkers.Module()); + } + + public override string get_ns() { return NS_URI; } + public override string get_id() { return IDENTITY.id; } + + private void query_availability(XmppStream stream) { + stream.get_module(ServiceDiscovery.Module.IDENTITY).request_info(stream, stream.remote_name, (stream, info_result) => { + bool available = check_ns_in_info(stream, stream.remote_name, info_result); + if (!available) { + stream.get_module(ServiceDiscovery.Module.IDENTITY).request_items(stream, stream.remote_name, (stream, items_result) => { + foreach (Xep.ServiceDiscovery.Item item in items_result.items) { + if (item.name == "HTTP File Upload") { + stream.get_module(ServiceDiscovery.Module.IDENTITY).request_info(stream, item.jid, (stream, info_result) => { + check_ns_in_info(stream, item.jid, info_result); + }); + break; + } + } + }); + } + }); + } + + private bool check_ns_in_info(XmppStream stream, string jid, Xep.ServiceDiscovery.InfoResult info_result) { + bool ver_available = false; + bool ver_0_available = false; + foreach (string feature in info_result.features) { + if (feature == NS_URI_0) { + ver_0_available = true; + break; + } else if (feature == NS_URI) { + ver_available = true; + } + } + + if (ver_available || ver_0_available) { + int? max_file_size = extract_max_file_size(info_result); + if (ver_0_available) { + stream.add_flag(new Flag(jid, NS_URI_0)); + } else if (ver_available) { + stream.add_flag(new Flag(jid, NS_URI)); + } + + feature_available(stream, max_file_size); + return true; + } + return false; + } + + private int? extract_max_file_size(Xep.ServiceDiscovery.InfoResult info_result) { + string? max_file_size_str = null; + StanzaNode x_node = info_result.iq.stanza.get_deep_subnode("http://jabber.org/protocol/disco#info:query", "jabber:x:data:x"); + Gee.List field_nodes = x_node.get_subnodes("field", "jabber:x:data"); + foreach (StanzaNode node in field_nodes) { + string? var_attr = node.get_attribute("var"); + if (var_attr == "max-file-size") { + StanzaNode value_node = node.get_subnode("value", "jabber:x:data"); + max_file_size_str = value_node.get_string_content(); + break; + } + } + return max_file_size_str != null ? int.parse(max_file_size_str) : (int?) null; + } +} + +public class Flag : XmppStreamFlag { + public static FlagIdentity IDENTITY = new FlagIdentity(NS_URI, "service_discovery"); + + public string file_store_jid; + public string ns_ver; + public int? max_file_size; + + public Flag(string file_store_jid, string ns_ver) { + this.file_store_jid = file_store_jid; + this.ns_ver = ns_ver; + } + + public override string get_ns() { return NS_URI; } + public override string get_id() { return IDENTITY.id; } +} + +} diff --git a/xmpp-vala/src/module/xep/0030_service_discovery/flag.vala b/xmpp-vala/src/module/xep/0030_service_discovery/flag.vala index 7c49fc30..cd847974 100644 --- a/xmpp-vala/src/module/xep/0030_service_discovery/flag.vala +++ b/xmpp-vala/src/module/xep/0030_service_discovery/flag.vala @@ -7,11 +7,12 @@ namespace Xmpp.Xep.ServiceDiscovery { public class Flag : XmppStreamFlag { public static FlagIdentity IDENTITY = new FlagIdentity(NS_URI, "service_discovery"); - private HashMap?> entity_features = new HashMap?>(); - private HashMap?> entity_identities = new HashMap?>(); - public ArrayList features = new ArrayList(); + private HashMap?> entity_features = new HashMap?>(); + private HashMap?> entity_identities = new HashMap?>(); + private HashMap?> entity_items = new HashMap?>(); + public Gee.List features = new ArrayList(); - public ArrayList? get_entity_categories(string jid) { + public Gee.List? get_entity_categories(string jid) { return entity_identities.has_key(jid) ? entity_identities[jid] : null; // TODO isnt this default for hashmap } @@ -30,14 +31,18 @@ public class Flag : XmppStreamFlag { return entity_features[jid].contains(feature); } - public void set_entity_identities(string jid, ArrayList? identities) { + public void set_entity_identities(string jid, Gee.List? identities) { entity_identities[jid] = identities; } - public void set_entity_features(string jid, ArrayList? features) { + public void set_entity_features(string jid, Gee.List? features) { entity_features[jid] = features; } + public void set_entity_items(string jid, Gee.List? features) { + entity_items[jid] = features; + } + public void add_own_feature(string feature) { features.add(feature); } public override string get_ns() { return NS_URI; } @@ -45,4 +50,4 @@ public class Flag : XmppStreamFlag { public override string get_id() { return IDENTITY.id; } } -} \ No newline at end of file +} diff --git a/xmpp-vala/src/module/xep/0030_service_discovery/info_result.vala b/xmpp-vala/src/module/xep/0030_service_discovery/info_result.vala index ae6b9caf..4ae917dc 100644 --- a/xmpp-vala/src/module/xep/0030_service_discovery/info_result.vala +++ b/xmpp-vala/src/module/xep/0030_service_discovery/info_result.vala @@ -7,7 +7,7 @@ namespace Xmpp.Xep.ServiceDiscovery { public class InfoResult { public Iq.Stanza iq { get; private set; } - public ArrayList features { + public Gee.List features { owned get { ArrayList ret = new ArrayList(); foreach (StanzaNode feature_node in iq.stanza.get_subnode("query", NS_URI_INFO).get_subnodes("feature", NS_URI_INFO)) { @@ -22,7 +22,7 @@ public class InfoResult { } } - public ArrayList identities { + public Gee.List identities { owned get { ArrayList ret = new ArrayList(); foreach (StanzaNode feature_node in iq.stanza.get_subnode("query", NS_URI_INFO).get_subnodes("identity", NS_URI_INFO)) { @@ -75,4 +75,4 @@ public class InfoResult { } } -} \ No newline at end of file +} diff --git a/xmpp-vala/src/module/xep/0030_service_discovery/items_result.vala b/xmpp-vala/src/module/xep/0030_service_discovery/items_result.vala index 2c29c320..2979f108 100644 --- a/xmpp-vala/src/module/xep/0030_service_discovery/items_result.vala +++ b/xmpp-vala/src/module/xep/0030_service_discovery/items_result.vala @@ -19,9 +19,13 @@ public class ItemsResult { } } - public ItemsResult.from_iq(Iq.Stanza iq) { + private ItemsResult.from_iq(Iq.Stanza iq) { this.iq = iq; } + + public static ItemsResult? create_from_iq(Iq.Stanza iq) { + return new ItemsResult.from_iq(iq); + } } -} \ No newline at end of file +} diff --git a/xmpp-vala/src/module/xep/0030_service_discovery/module.vala b/xmpp-vala/src/module/xep/0030_service_discovery/module.vala index d8852735..e09a3b8d 100644 --- a/xmpp-vala/src/module/xep/0030_service_discovery/module.vala +++ b/xmpp-vala/src/module/xep/0030_service_discovery/module.vala @@ -29,9 +29,9 @@ public class Module : XmppStreamModule, Iq.Handler { identities.add(new Identity(category, type, name)); } - public delegate void HasEntryCategoryRes(XmppStream stream, ArrayList? identities); + public delegate void HasEntryCategoryRes(XmppStream stream, Gee.List? identities); public void get_entity_categories(XmppStream stream, string jid, owned HasEntryCategoryRes listener) { - ArrayList? res = stream.get_flag(Flag.IDENTITY).get_entity_categories(jid); + Gee.List? res = stream.get_flag(Flag.IDENTITY).get_entity_categories(jid); if (res != null) listener(stream, res); request_info(stream, jid, (stream, query_result) => { listener(stream, query_result != null ? query_result.identities : null); @@ -54,7 +54,12 @@ public class Module : XmppStreamModule, Iq.Handler { public void request_items(XmppStream stream, string jid, owned OnItemsResult listener) { Iq.Stanza iq = new Iq.Stanza.get(new StanzaNode.build("query", NS_URI_ITEMS).add_self_xmlns()); iq.to = jid; - stream.get_module(Iq.Module.IDENTITY).send_iq(stream, iq); + print(iq.stanza.to_string() + "\n"); + stream.get_module(Iq.Module.IDENTITY).send_iq(stream, iq, (stream, iq) => { + ItemsResult? result = ItemsResult.create_from_iq(iq); + stream.get_flag(Flag.IDENTITY).set_entity_items(iq.from, result != null ? result.items : null); + listener(stream, result); + }); } public void on_iq_get(XmppStream stream, Iq.Stanza iq) { diff --git a/xmpp-vala/src/module/xep/0115_entitiy_capabilities.vala b/xmpp-vala/src/module/xep/0115_entitiy_capabilities.vala index 6f19fb12..689c3605 100644 --- a/xmpp-vala/src/module/xep/0115_entitiy_capabilities.vala +++ b/xmpp-vala/src/module/xep/0115_entitiy_capabilities.vala @@ -55,7 +55,7 @@ namespace Xmpp.Xep.EntityCapabilities { StanzaNode? c_node = presence.stanza.get_subnode("c", NS_URI); if (c_node != null) { string ver_attribute = c_node.get_attribute("ver", NS_URI); - ArrayList capabilities = storage.get_features(ver_attribute); + Gee.List capabilities = storage.get_features(ver_attribute); if (capabilities.size == 0) { stream.get_module(ServiceDiscovery.Module.IDENTITY).request_info(stream, presence.from, (stream, query_result) => { store_entity_result(stream, ver_attribute, query_result); @@ -74,7 +74,7 @@ namespace Xmpp.Xep.EntityCapabilities { } } - private static string compute_hash(ArrayList identities, ArrayList features) { + private static string compute_hash(Gee.List identities, Gee.List features) { identities.sort(compare_identities); features.sort(); @@ -109,7 +109,7 @@ namespace Xmpp.Xep.EntityCapabilities { } public interface Storage : Object { - public abstract void store_features(string entitiy, ArrayList capabilities); - public abstract ArrayList get_features(string entitiy); + public abstract void store_features(string entitiy, Gee.List capabilities); + public abstract Gee.List get_features(string entitiy); } }