diff --git a/libdino/CMakeLists.txt b/libdino/CMakeLists.txt index 62c73eca..9b3e991a 100644 --- a/libdino/CMakeLists.txt +++ b/libdino/CMakeLists.txt @@ -40,6 +40,7 @@ SOURCES src/service/muc_manager.vala src/service/notification_events.vala src/service/presence_manager.vala + src/service/registration.vala src/service/roster_manager.vala src/service/stream_interactor.vala src/service/util.vala diff --git a/libdino/src/service/module_manager.vala b/libdino/src/service/module_manager.vala index 78819bb3..dac5aef2 100644 --- a/libdino/src/service/module_manager.vala +++ b/libdino/src/service/module_manager.vala @@ -76,6 +76,7 @@ public class ModuleManager { module_map[account].add(new Xep.Ping.Module()); module_map[account].add(new Xep.DelayedDelivery.Module()); module_map[account].add(new StreamError.Module()); + module_map[account].add(new Xep.InBandRegistration.Module()); initialize_account_modules(account, module_map[account]); } } diff --git a/libdino/src/service/registration.vala b/libdino/src/service/registration.vala new file mode 100644 index 00000000..32d8b04b --- /dev/null +++ b/libdino/src/service/registration.vala @@ -0,0 +1,42 @@ +using Gee; + +using Xmpp; +using Dino.Entities; + +namespace Dino { + +public class Register { + + public static async Xep.InBandRegistration.Form get_registration_form(Jid jid) { + XmppStream stream = new XmppStream(); + stream.add_module(new Tls.Module()); + stream.add_module(new Iq.Module()); + stream.add_module(new Xep.InBandRegistration.Module()); + stream.connect.begin(jid.bare_jid.to_string()); + + Xep.InBandRegistration.Form? form = null; + SourceFunc callback = get_registration_form.callback; + stream.stream_negotiated.connect(() => { + if (callback != null) { + Idle.add((owned)callback); + } + }); + Timeout.add_seconds(5, () => { + if (callback != null) { + Idle.add((owned)callback); + } + return false; + }); + yield; + if (stream.negotiation_complete) { + form = yield stream.get_module(Xep.InBandRegistration.Module.IDENTITY).get_from_server(stream, jid); + } + return form; + } + + public static async string submit_form(Jid jid, Xep.InBandRegistration.Form form) { + return yield form.stream.get_module(Xep.InBandRegistration.Module.IDENTITY).submit_to_server(form.stream, jid, form); + } +} + +} diff --git a/main/CMakeLists.txt b/main/CMakeLists.txt index 9c5b06ff..00ca692a 100644 --- a/main/CMakeLists.txt +++ b/main/CMakeLists.txt @@ -124,6 +124,7 @@ SOURCES src/ui/settings_dialog.vala src/ui/unified_window.vala src/ui/util/accounts_combo_box.vala + src/ui/util/data_forms.vala src/ui/util/helper.vala src/ui/util/label_hybrid.vala src/ui/util/preview_file_chooser_native.vala diff --git a/main/data/manage_accounts/add_account_dialog.ui b/main/data/manage_accounts/add_account_dialog.ui index 44c131c3..6698b337 100644 --- a/main/data/manage_accounts/add_account_dialog.ui +++ b/main/data/manage_accounts/add_account_dialog.ui @@ -1,136 +1,390 @@ diff --git a/main/src/ui/contact_details/muc_config_form_provider.vala b/main/src/ui/contact_details/muc_config_form_provider.vala index 072627bf..a088bd97 100644 --- a/main/src/ui/contact_details/muc_config_form_provider.vala +++ b/main/src/ui/contact_details/muc_config_form_provider.vala @@ -74,57 +74,9 @@ public class MucConfigFormProvider : Plugins.ContactDetailsProvider, Object { } } - Widget? widget = get_widget(field); + Widget? widget = Util.get_data_form_fild_widget(field); if (widget != null) contact_details.add(_("Room Configuration"), label, desc, widget); } - - private static Widget? get_widget(DataForms.DataForm.Field field) { - if (field.type_ == null) return null; - switch (field.type_) { - case DataForms.DataForm.Type.BOOLEAN: - DataForms.DataForm.BooleanField boolean_field = field as DataForms.DataForm.BooleanField; - Switch sw = new Switch() { active=boolean_field.value, valign=Align.CENTER, visible=true }; - sw.state_set.connect((state) => { - boolean_field.value = state; - return false; - }); - return sw; - case DataForms.DataForm.Type.JID_MULTI: - return null; - case DataForms.DataForm.Type.LIST_SINGLE: - DataForms.DataForm.ListSingleField list_single_field = field as DataForms.DataForm.ListSingleField; - ComboBoxText combobox = new ComboBoxText() { valign=Align.CENTER, visible=true }; - for (int i = 0; i < list_single_field.options.size; i++) { - DataForms.DataForm.Option option = list_single_field.options[i]; - combobox.append(option.value, option.label); - if (option.value == list_single_field.value) combobox.active = i; - } - combobox.changed.connect(() => { - list_single_field.value = combobox.get_active_id(); - }); - return combobox; - case DataForms.DataForm.Type.LIST_MULTI: - return null; - case DataForms.DataForm.Type.TEXT_PRIVATE: - DataForms.DataForm.TextPrivateField text_private_field = field as DataForms.DataForm.TextPrivateField; - Entry entry = new Entry() { text=text_private_field.value ?? "", valign=Align.CENTER, visible=true, visibility=false }; - entry.key_release_event.connect(() => { - text_private_field.value = entry.text; - return false; - }); - return entry; - case DataForms.DataForm.Type.TEXT_SINGLE: - DataForms.DataForm.TextSingleField text_single_field = field as DataForms.DataForm.TextSingleField; - Entry entry = new Entry() { text=text_single_field.value ?? "", valign=Align.CENTER, visible=true }; - entry.key_release_event.connect(() => { - text_single_field.value = entry.text; - return false; - }); - return entry; - default: - return null; - } - } } } diff --git a/main/src/ui/manage_accounts/add_account_dialog.vala b/main/src/ui/manage_accounts/add_account_dialog.vala index 5715db51..f9ab794e 100644 --- a/main/src/ui/manage_accounts/add_account_dialog.vala +++ b/main/src/ui/manage_accounts/add_account_dialog.vala @@ -11,29 +11,120 @@ public class AddAccountDialog : Gtk.Dialog { public signal void added(Account account); - [GtkChild] private Button cancel_button; - [GtkChild] private Button ok_button; - [GtkChild] private Entry alias_entry; + [GtkChild] private Stack stack; + + [GtkChild] private Revealer notification_revealer; + [GtkChild] private Label notification_label; + + // Sign in + [GtkChild] private Box sign_in_box; [GtkChild] private Entry jid_entry; + [GtkChild] private Entry alias_entry; [GtkChild] private Entry password_entry; + [GtkChild] private Button sign_in_continue; + [GtkChild] private Button serverlist_button; + + // Select Server + [GtkChild] private Box create_account_box; + [GtkChild] private Button login_button; + [GtkChild] private Stack select_server_continue_stack; + [GtkChild] private Button select_server_continue; + [GtkChild] private Label register_form_continue_label; + [GtkChild] private ListBox server_list_box; + [GtkChild] private Entry server_entry; + + // Register Form + [GtkChild] private Box register_box; + [GtkChild] private Label register_title; + [GtkChild] private Box form_box; + [GtkChild] private Button register_form_back; + [GtkChild] private Stack register_form_continue_stack; + [GtkChild] private Button register_form_continue; + + private static string[] server_list = new string[]{ + "5222.de", + "jabber.fr", + "movim.eu", + "yax.im" + }; + private HashMap list_box_jids = new HashMap(); + private Jid? server_jid = null; + private Xep.InBandRegistration.Form? form = null; public AddAccountDialog(StreamInteractor stream_interactor) { - Object(use_header_bar : 1); this.title = _("Add Account"); - cancel_button.clicked.connect(() => { close(); }); - ok_button.clicked.connect(on_ok_button_clicked); + // Sign in jid_entry.changed.connect(on_jid_entry_changed); jid_entry.focus_out_event.connect(on_jid_entry_focus_out_event); + sign_in_continue.clicked.connect(on_sign_in_continue_clicked); + serverlist_button.clicked.connect(show_select_server); + + // Select Server + server_entry.changed.connect(() => { + Jid? jid = Jid.parse(server_entry.text); + select_server_continue.sensitive = jid != null && jid.localpart == null && jid.resourcepart == null; + }); + select_server_continue.clicked.connect(on_select_server_continue); + login_button.clicked.connect(show_sign_in); + + foreach (string server in server_list) { + ListBoxRow list_box_row = new ListBoxRow() { visible=true }; + list_box_row.add(new Label(server) { xalign=0, margin=3, margin_start=7, margin_end=7, visible=true }); + list_box_jids[list_box_row] = server; + server_list_box.add(list_box_row); + } + + // Register Form + register_form_continue.clicked.connect(on_register_form_continue_clicked); + register_form_back.clicked.connect(show_select_server); + + show_sign_in(); + } + + private void show_sign_in() { + sign_in_box.visible = true; + stack.visible_child_name = "login"; + create_account_box.visible = false; + register_box.visible = false; + set_default(sign_in_continue); + animate_window_resize(sign_in_box); + } + + private void show_select_server() { + server_entry.text = ""; + server_entry.grab_focus(); + set_default(select_server_continue); + + server_list_box.row_selected.disconnect(on_server_list_row_selected); + server_list_box.unselect_all(); + server_list_box.row_selected.connect(on_server_list_row_selected); + + create_account_box.visible = true; + stack.visible_child_name = "server"; + sign_in_box.visible = false; + register_box.visible = false; + + animate_window_resize(create_account_box); + } + + private void show_register_form() { + register_box.visible = true; + stack.visible_child_name = "form"; + sign_in_box.visible = false; + create_account_box.visible = false; + + set_default(register_form_continue); + animate_window_resize(register_box); } private void on_jid_entry_changed() { Jid? jid = Jid.parse(jid_entry.text); if (jid != null && jid.localpart != null && jid.resourcepart == null) { - ok_button.set_sensitive(true); + sign_in_continue.set_sensitive(true); jid_entry.secondary_icon_name = null; } else { - ok_button.set_sensitive(false); + sign_in_continue.set_sensitive(false); } } @@ -41,7 +132,6 @@ public class AddAccountDialog : Gtk.Dialog { Jid? jid = Jid.parse(jid_entry.text); if (jid == null || jid.localpart == null || jid.resourcepart != null) { jid_entry.secondary_icon_name = "dialog-warning-symbolic"; - // TODO why doesn't the tooltip work jid_entry.set_icon_tooltip_text(EntryIconPosition.SECONDARY, _("JID should be of the form “user@example.com”")); } else { jid_entry.secondary_icon_name = null; @@ -49,13 +139,131 @@ public class AddAccountDialog : Gtk.Dialog { return false; } - private void on_ok_button_clicked() { + private void on_sign_in_continue_clicked() { Jid jid = new Jid(jid_entry.get_text()); string password = password_entry.get_text(); string alias = alias_entry.get_text(); + store_account(jid, password, alias); + close(); + } + + private void on_select_server_continue() { + server_jid = new Jid(server_entry.text); + request_show_register_form.begin(); + } + + private void on_server_list_row_selected(ListBox box, ListBoxRow? row) { + server_jid = new Jid(list_box_jids[row]); + request_show_register_form.begin(); + } + + private async void request_show_register_form() { + select_server_continue_stack.visible_child_name = "spinner"; + form = yield Register.get_registration_form(server_jid); + if (select_server_continue_stack == null) { + return; + } + select_server_continue_stack.visible_child_name = "label"; + if (form != null) { + set_register_form(server_jid, form); + show_register_form(); + } else { + display_notification(_("No response from server")); + } + } + + private void set_register_form(Jid server, Xep.InBandRegistration.Form form) { + form_box.foreach((widget) => { widget.destroy(); }); + register_title.label = _("Register on %s").printf(server.to_string()); + + if (form.oob != null) { + form_box.add(new Label(_("The server requires to sign up through a website")){ use_markup=true, visible=true } ); + form_box.add(new Label(@"$(form.oob)") { use_markup=true, visible=true }); + register_form_continue_label.label = _("Open Registration"); + register_form_continue.visible = true; + register_form_continue.grab_focus(); + } else if (form.fields.size > 0) { + int i = 0; + foreach (Xep.DataForms.DataForm.Field field in form.fields) { + if (field.label != null && field.label != "") { + form_box.add(new Label(field.label) { xalign=0, margin_top=7, visible=true }); + } + Widget field_widget = Util.get_data_form_fild_widget(field); + if (field_widget != null) { + form_box.add(field_widget); + } + i++; + } + register_form_continue.visible = true; + register_form_continue_label.label = _("Register"); + } else { + form_box.add(new Label(_("Check %s for information on how to sign up").printf(@"$(server)")) { use_markup=true, visible=true }); + register_form_continue.visible = false; + } + } + + private async void on_register_form_continue_clicked() { + notification_revealer.set_reveal_child(false); + // Button is opening a registration website + if (form.oob != null) { + try { + AppInfo.launch_default_for_uri(form.oob, null); + } catch (Error e) { } + show_sign_in(); + return; + } + + register_form_continue_stack.visible_child_name = "spinner"; + string? error = yield Register.submit_form(server_jid, form); + if (register_form_continue_stack == null) { + return; + } + register_form_continue_stack.visible_child_name = "label"; + if (error == null) { + string? username = null, password = null; + foreach (Xep.DataForms.DataForm.Field field in form.fields) { + switch (field.var) { + case "username": username = field.get_value_string(); break; + case "password": password = field.get_value_string(); break; + } + } + store_account(new Jid(username + "@" + server_jid.domainpart), password, ""); + close(); + } else { + display_notification(error); + } + } + + private void store_account(Jid jid, string password, string? alias) { Account account = new Account(jid, null, password, alias); added(account); - close(); + } + + private void display_notification(string text) { + notification_label.label = text; + notification_revealer.set_reveal_child(true); + Timeout.add_seconds(5, () => { + notification_revealer.set_reveal_child(false); + return false; + }); + } + + private void animate_window_resize(Widget widget) { // TODO code duplication + int def_height, curr_width, curr_height; + get_size(out curr_width, out curr_height); + widget.get_preferred_height(null, out def_height); + def_height += 5; + int difference = def_height - curr_height; + Timer timer = new Timer(); + Timeout.add((int) (stack.transition_duration / 30), + () => { + ulong microsec; + timer.elapsed(out microsec); + ulong millisec = microsec / 1000; + double partial = double.min(1, (double) millisec / stack.transition_duration); + resize(curr_width, (int) (curr_height + difference * partial)); + return millisec < stack.transition_duration; + }); } } diff --git a/main/src/ui/util/data_forms.vala b/main/src/ui/util/data_forms.vala new file mode 100644 index 00000000..11308462 --- /dev/null +++ b/main/src/ui/util/data_forms.vala @@ -0,0 +1,57 @@ +using Gee; +using Gtk; + +using Dino.Entities; +using Xmpp.Xep; + +namespace Dino.Ui.Util { + +public static Widget? get_data_form_fild_widget(DataForms.DataForm.Field field) { + if (field.type_ == null) return null; + switch (field.type_) { + case DataForms.DataForm.Type.BOOLEAN: + DataForms.DataForm.BooleanField boolean_field = field as DataForms.DataForm.BooleanField; + Switch sw = new Switch() { active=boolean_field.value, valign=Align.CENTER, visible=true }; + sw.state_set.connect((state) => { + boolean_field.value = state; + return false; + }); + return sw; + case DataForms.DataForm.Type.JID_MULTI: + return null; + case DataForms.DataForm.Type.LIST_SINGLE: + DataForms.DataForm.ListSingleField list_single_field = field as DataForms.DataForm.ListSingleField; + ComboBoxText combobox = new ComboBoxText() { valign=Align.CENTER, visible=true }; + for (int i = 0; i < list_single_field.options.size; i++) { + DataForms.DataForm.Option option = list_single_field.options[i]; + combobox.append(option.value, option.label); + if (option.value == list_single_field.value) combobox.active = i; + } + combobox.changed.connect(() => { + list_single_field.value = combobox.get_active_id(); + }); + return combobox; + case DataForms.DataForm.Type.LIST_MULTI: + return null; + case DataForms.DataForm.Type.TEXT_PRIVATE: + DataForms.DataForm.TextPrivateField text_private_field = field as DataForms.DataForm.TextPrivateField; + Entry entry = new Entry() { text=text_private_field.value ?? "", valign=Align.CENTER, visible=true, visibility=false }; + entry.key_release_event.connect(() => { + text_private_field.value = entry.text; + return false; + }); + return entry; + case DataForms.DataForm.Type.TEXT_SINGLE: + DataForms.DataForm.TextSingleField text_single_field = field as DataForms.DataForm.TextSingleField; + Entry entry = new Entry() { text=text_single_field.value ?? "", valign=Align.CENTER, visible=true }; + entry.key_release_event.connect(() => { + text_single_field.value = entry.text; + return false; + }); + return entry; + default: + return null; + } +} + +} diff --git a/xmpp-vala/CMakeLists.txt b/xmpp-vala/CMakeLists.txt index 6734f0ee..1649411e 100644 --- a/xmpp-vala/CMakeLists.txt +++ b/xmpp-vala/CMakeLists.txt @@ -55,6 +55,7 @@ SOURCES "src/module/xep/0054_vcard/module.vala" "src/module/xep/0060_pubsub.vala" "src/module/xep/0066_out_of_band_data.vala" + "src/module/xep/0077_in_band_registration.vala" "src/module/xep/0082_date_time_profiles.vala" "src/module/xep/0084_user_avatars.vala" "src/module/xep/0085_chat_state_notifications.vala" diff --git a/xmpp-vala/src/module/stanza_error.vala b/xmpp-vala/src/module/stanza_error.vala index 51aa2629..c45ff4e3 100644 --- a/xmpp-vala/src/module/stanza_error.vala +++ b/xmpp-vala/src/module/stanza_error.vala @@ -36,6 +36,10 @@ namespace Xmpp { get { return error_node.get_attribute("by"); } } + public string? text { + get { return error_node.get_deep_string_content("urn:ietf:params:xml:ns:xmpp-stanzas:text"); } + } + public string condition { get { Gee.List subnodes = error_node.sub_nodes; @@ -57,7 +61,7 @@ namespace Xmpp { } public StanzaNode stanza; - private StanzaNode error_node; + public StanzaNode error_node; public ErrorStanza.from_stanza(StanzaNode stanza) { this.stanza = stanza; diff --git a/xmpp-vala/src/module/xep/0004_data_forms.vala b/xmpp-vala/src/module/xep/0004_data_forms.vala index 69c14b08..cc0a1a28 100644 --- a/xmpp-vala/src/module/xep/0004_data_forms.vala +++ b/xmpp-vala/src/module/xep/0004_data_forms.vala @@ -78,7 +78,7 @@ public class DataForm { return ret; } - internal string get_value_string() { + public string get_value_string() { Gee.List values = get_values(); return values.size > 0 ? values[0] : ""; } @@ -197,7 +197,7 @@ public class DataForm { // TODO text-multi - internal DataForm.from_node(StanzaNode node, XmppStream stream, owned OnResult listener) { + internal DataForm.from_node(StanzaNode node, XmppStream stream, owned OnResult? listener = null) { this.stanza_node = node; this.stream = stream; this.on_result = (owned)listener; diff --git a/xmpp-vala/src/module/xep/0077_in_band_registration.vala b/xmpp-vala/src/module/xep/0077_in_band_registration.vala new file mode 100644 index 00000000..1c544c18 --- /dev/null +++ b/xmpp-vala/src/module/xep/0077_in_band_registration.vala @@ -0,0 +1,64 @@ +using Gee; + +namespace Xmpp.Xep.InBandRegistration { + +public const string NS_URI = "jabber:iq:register"; + +public class Module : XmppStreamNegotiationModule { + public static ModuleIdentity IDENTITY = new ModuleIdentity(NS_URI, "0077_in_band_registration"); + + public async Form? get_from_server(XmppStream stream, Jid jid) { + Iq.Stanza request_form_iq = new Iq.Stanza.get(new StanzaNode.build("query", NS_URI).add_self_xmlns()); + request_form_iq.to = jid; + SourceFunc callback = get_from_server.callback; + Form? form = null; + stream.get_module(Iq.Module.IDENTITY).send_iq(stream, request_form_iq, (stream, response_iq) => { + form = new Form.from_node(stream, response_iq); + Idle.add((owned)callback); + }); + yield; + return form; + } + + public async string submit_to_server(XmppStream stream, Jid jid, Form form) { + StanzaNode query_node = new StanzaNode.build("query", NS_URI).add_self_xmlns(); + query_node.put_node(form.get_submit_node()); + Iq.Stanza iq = new Iq.Stanza.set(query_node); + iq.to = jid; + string? error_message = null; + SourceFunc callback = submit_to_server.callback; + stream.get_module(Iq.Module.IDENTITY).send_iq(stream, iq, (stream, response_iq) => { + if (response_iq.is_error()) { + ErrorStanza? error_stanza = response_iq.get_error(); + error_message = error_stanza.text ?? "Error"; + } + Idle.add((owned)callback); + }); + yield; + return error_message; + } + + public override bool mandatory_outstanding(XmppStream stream) { return false; } + + public override bool negotiation_active(XmppStream stream) { return false; } + + public override void attach(XmppStream stream) { } + + public override void detach(XmppStream stream) { } + + public override string get_ns() { return NS_URI; } + public override string get_id() { return IDENTITY.id; } +} + +public class Form : DataForms.DataForm { + public string? oob = null; + + internal Form.from_node(XmppStream stream, Iq.Stanza iq) { + StanzaNode? x_node = iq.stanza.get_deep_subnode(NS_URI + ":query", DataForms.NS_URI + ":x"); + base.from_node(x_node ?? new StanzaNode.build("x", NS_URI).add_self_xmlns(), stream); + + oob = iq.stanza.get_deep_string_content(NS_URI + ":query", "jabber:x:oob:x", "url"); + } +} + +}