From 2631a9bdbaf9a40f329f05c55c6e2ea38efeb10c Mon Sep 17 00:00:00 2001 From: Anmol Date: Wed, 22 Apr 2020 23:34:03 +0530 Subject: [PATCH] voice handling in moderated groups (#788) --- libdino/src/plugin/interfaces.vala | 4 +- libdino/src/service/muc_manager.vala | 30 +++++++++++ libdino/src/service/notification_events.vala | 2 + main/CMakeLists.txt | 1 + main/src/ui/application.vala | 8 +++ .../ui/chat_input/chat_input_controller.vala | 35 +++++++++++- main/src/ui/contact_details/dialog.vala | 1 + .../contact_details/permissions_provider.vala | 29 ++++++++++ .../ui/contact_details/settings_provider.vala | 1 + main/src/ui/notifications.vala | 23 ++++++++ main/src/ui/occupant_menu/view.vala | 21 ++++++++ xmpp-vala/src/module/xep/0004_data_forms.vala | 1 + xmpp-vala/src/module/xep/0045_muc/module.vala | 53 +++++++++++++++++++ 13 files changed, 207 insertions(+), 2 deletions(-) create mode 100644 main/src/ui/contact_details/permissions_provider.vala diff --git a/libdino/src/plugin/interfaces.vala b/libdino/src/plugin/interfaces.vala index 1b2033d9..20aa6c83 100644 --- a/libdino/src/plugin/interfaces.vala +++ b/libdino/src/plugin/interfaces.vala @@ -149,11 +149,13 @@ public class InputFieldStatus : Object { public string? message; public MessageType message_type; public InputState input_state; + public bool contains_markup; - public InputFieldStatus(string? message, MessageType message_type, InputState input_state) { + public InputFieldStatus(string? message, MessageType message_type, InputState input_state, bool contains_markup = false) { this.message = message; this.message_type = message_type; this.input_state = input_state; + this.contains_markup = contains_markup; } } diff --git a/libdino/src/service/muc_manager.vala b/libdino/src/service/muc_manager.vala index 93814625..15f32c44 100644 --- a/libdino/src/service/muc_manager.vala +++ b/libdino/src/service/muc_manager.vala @@ -14,6 +14,8 @@ public class MucManager : StreamInteractionModule, Object { public signal void room_info_updated(Account account, Jid muc_jid); public signal void private_room_occupant_updated(Account account, Jid room, Jid occupant); public signal void invite_received(Account account, Jid room_jid, Jid from_jid, string? password, string? reason); + public signal void voice_request_received(Account account, Jid room_jid, Jid from_jid, string? nick, string? role, string? label); + public signal void received_occupant_role(Account account, Jid jid, Xep.Muc.Role? role); public signal void bookmarks_updated(Account account, Set conferences); public signal void conference_added(Account account, Conference conference); public signal void conference_removed(Account account, Jid jid); @@ -118,6 +120,16 @@ public class MucManager : StreamInteractionModule, Object { if (stream != null) stream.get_module(Xep.Muc.Module.IDENTITY).change_affiliation(stream, jid.bare_jid, nick, role); } + public void change_role(Account account, Jid jid, string nick, string role) { + XmppStream? stream = stream_interactor.get_stream(account); + if (stream != null) stream.get_module(Xep.Muc.Module.IDENTITY).change_role(stream, jid.bare_jid, nick, role); + } + + public void request_voice(Account account, Jid jid) { + XmppStream? stream = stream_interactor.get_stream(account); + if (stream != null) stream.get_module(Xep.Muc.Module.IDENTITY).request_voice(stream, jid.bare_jid); + } + public bool kick_possible(Account account, Jid occupant) { XmppStream? stream = stream_interactor.get_stream(account); if (stream != null) return stream.get_module(Xep.Muc.Module.IDENTITY).kick_possible(stream, occupant); @@ -137,6 +149,18 @@ public class MucManager : StreamInteractionModule, Object { return flag.has_room_feature(jid, Xep.Muc.Feature.NON_ANONYMOUS) && flag.has_room_feature(jid, Xep.Muc.Feature.MEMBERS_ONLY); } + public bool is_moderated_room(Account account, Jid jid) { + XmppStream? stream = stream_interactor.get_stream(account); + if (stream == null) { + return false; + } + Xep.Muc.Flag? flag = stream.get_flag(Xep.Muc.Flag.IDENTITY); + if (flag == null) { + return false; + } + return flag.has_room_feature(jid, Xep.Muc.Feature.MODERATED); + } + public bool is_public_room(Account account, Jid jid) { return is_groupchat(jid, account) && !is_private_room(account, jid); } @@ -285,6 +309,12 @@ public class MucManager : StreamInteractionModule, Object { stream_interactor.module_manager.get_module(account, Xep.Muc.Module.IDENTITY).invite_received.connect( (stream, room_jid, from_jid, password, reason) => { invite_received(account, room_jid, from_jid, password, reason); }); + stream_interactor.module_manager.get_module(account, Xep.Muc.Module.IDENTITY).voice_request_received.connect( (stream, room_jid, from_jid, nick, role, label) => { + voice_request_received(account, room_jid, from_jid, nick, role, label); + }); + stream_interactor.module_manager.get_module(account, Xep.Muc.Module.IDENTITY).received_occupant_role.connect( (stream, from_jid, role) => { + received_occupant_role(account, from_jid, role); + }); stream_interactor.module_manager.get_module(account, Xep.Muc.Module.IDENTITY).room_info_updated.connect( (stream, muc_jid) => { room_info_updated(account, muc_jid); }); diff --git a/libdino/src/service/notification_events.vala b/libdino/src/service/notification_events.vala index 54206e99..3f4e3c6d 100644 --- a/libdino/src/service/notification_events.vala +++ b/libdino/src/service/notification_events.vala @@ -13,6 +13,7 @@ public class NotificationEvents : StreamInteractionModule, Object { public signal void notify_subscription_request(Conversation conversation); public signal void notify_connection_error(Account account, ConnectionManager.ConnectionError error); public signal void notify_muc_invite(Account account, Jid room_jid, Jid from_jid, string? password, string? reason); + public signal void notify_voice_request(Account account, Jid room_jid, Jid from_jid, string? nick, string? role, string? label); private StreamInteractor stream_interactor; @@ -27,6 +28,7 @@ public class NotificationEvents : StreamInteractionModule, Object { stream_interactor.get_module(ContentItemStore.IDENTITY).new_item.connect(on_content_item_received); stream_interactor.get_module(PresenceManager.IDENTITY).received_subscription_request.connect(on_received_subscription_request); stream_interactor.get_module(MucManager.IDENTITY).invite_received.connect((account, room_jid, from_jid, password, reason) => notify_muc_invite(account, room_jid, from_jid, password, reason)); + stream_interactor.get_module(MucManager.IDENTITY).voice_request_received.connect((account, room_jid, from_jid, nick, role, label) => notify_voice_request(account, room_jid, from_jid, nick, role, label)); stream_interactor.connection_manager.connection_error.connect((account, error) => notify_connection_error(account, error)); } diff --git a/main/CMakeLists.txt b/main/CMakeLists.txt index 2a247ba9..f0fa550a 100644 --- a/main/CMakeLists.txt +++ b/main/CMakeLists.txt @@ -132,6 +132,7 @@ SOURCES src/ui/contact_details/blocking_provider.vala src/ui/contact_details/settings_provider.vala + src/ui/contact_details/permissions_provider.vala src/ui/contact_details/dialog.vala src/ui/contact_details/muc_config_form_provider.vala diff --git a/main/src/ui/application.vala b/main/src/ui/application.vala index 8738cd53..90c83562 100644 --- a/main/src/ui/application.vala +++ b/main/src/ui/application.vala @@ -138,6 +138,14 @@ public class Dino.Ui.Application : Gtk.Application, Dino.Application { }); add_action(accept_muc_invite_action); + SimpleAction accept_voice_request_action = new SimpleAction("accept-voice-request", VariantType.INT32); + accept_voice_request_action.activate.connect((variant) => { + Conversation? conversation = stream_interactor.get_module(ConversationManager.IDENTITY).get_conversation_by_id(variant.get_int32()); + if (conversation == null) return; + stream_interactor.get_module(MucManager.IDENTITY).change_role(conversation.account, conversation.counterpart, conversation.nickname, "participant"); + }); + add_action(accept_voice_request_action); + SimpleAction loop_conversations_action = new SimpleAction("loop_conversations", null); loop_conversations_action.activate.connect(() => { window.loop_conversations(false); }); add_action(loop_conversations_action); diff --git a/main/src/ui/chat_input/chat_input_controller.vala b/main/src/ui/chat_input/chat_input_controller.vala index fb7f88b1..55196ea4 100644 --- a/main/src/ui/chat_input/chat_input_controller.vala +++ b/main/src/ui/chat_input/chat_input_controller.vala @@ -5,6 +5,7 @@ using Gtk; using Dino.Entities; namespace Dino.Ui { +private const string OPEN_CONVERSATION_DETAILS_URI = "x-dino:open-conversation-details"; public class ChatInputController : Object { @@ -38,8 +39,19 @@ public class ChatInputController : Object { chat_text_view_controller.send_text.connect(send_text); chat_input.encryption_widget.encryption_changed.connect(on_encryption_changed); - + chat_input.file_button.clicked.connect(() => file_picker_selected()); + + stream_interactor.get_module(MucManager.IDENTITY).received_occupant_role.connect(update_moderated_input_status); + stream_interactor.get_module(MucManager.IDENTITY).room_info_updated.connect(update_moderated_input_status); + + status_description_label.activate_link.connect((uri) => { + if (uri == OPEN_CONVERSATION_DETAILS_URI){ + ContactDetails.Dialog contact_details_dialog = new ContactDetails.Dialog(stream_interactor, conversation); + contact_details_dialog.present(); + } + return true; + }); } public void set_conversation(Conversation conversation) { @@ -51,6 +63,10 @@ public class ChatInputController : Object { chat_input.initialize_for_conversation(conversation); chat_text_view_controller.initialize_for_conversation(conversation); + + Xmpp.Jid? own_jid = stream_interactor.get_module(MucManager.IDENTITY).get_own_jid(conversation.counterpart, conversation.account); + + update_moderated_input_status(conversation.account, own_jid); } public void set_file_upload_active(bool active) { @@ -69,6 +85,10 @@ public class ChatInputController : Object { input_field_status = status; chat_input.set_input_state(status.message_type); + + if (status.contains_markup) status_description_label.use_markup = true; + else status_description_label.use_markup = false; + status_description_label.label = status.message; chat_input.file_button.sensitive = status.input_state == Plugins.InputFieldStatus.InputState.NORMAL; @@ -147,6 +167,19 @@ public class ChatInputController : Object { stream_interactor.get_module(ChatInteraction.IDENTITY).on_message_cleared(conversation); } } + + private void update_moderated_input_status(Account account, Xmpp.Jid jid) { + Xmpp.Jid? own_jid = stream_interactor.get_module(MucManager.IDENTITY).get_own_jid(conversation.counterpart, conversation.account); + if (conversation.type_ == conversation.Type.GROUPCHAT){ + if (stream_interactor.get_module(MucManager.IDENTITY).is_moderated_room(conversation.account, conversation.counterpart) && + stream_interactor.get_module(MucManager.IDENTITY).get_role(own_jid, conversation.account)==Xmpp.Xep.Muc.Role.VISITOR) { + set_input_field_status(new Plugins.InputFieldStatus(_("This conference does not allow you to send messages. %s").printf("" + _("Request permission") + ""), + Plugins.InputFieldStatus.MessageType.ERROR, Plugins.InputFieldStatus.InputState.NO_SEND, true)); + } else { + reset_input_field_status(); + } + } + } private bool on_text_input_key_press(EventKey event) { if (event.keyval == Gdk.Key.Up && chat_input.chat_text_view.text_view.buffer.text == "") { diff --git a/main/src/ui/contact_details/dialog.vala b/main/src/ui/contact_details/dialog.vala index ba9213a8..cf85e691 100644 --- a/main/src/ui/contact_details/dialog.vala +++ b/main/src/ui/contact_details/dialog.vala @@ -46,6 +46,7 @@ public class Dialog : Gtk.Dialog { app.plugin_registry.register_contact_details_entry(new SettingsProvider(stream_interactor)); app.plugin_registry.register_contact_details_entry(new BlockingProvider(stream_interactor)); app.plugin_registry.register_contact_details_entry(new MucConfigFormProvider(stream_interactor)); + app.plugin_registry.register_contact_details_entry(new PermissionsProvider(stream_interactor)); foreach (Plugins.ContactDetailsProvider provider in app.plugin_registry.contact_details_entries) { provider.populate(conversation, contact_details, Plugins.WidgetType.GTK); diff --git a/main/src/ui/contact_details/permissions_provider.vala b/main/src/ui/contact_details/permissions_provider.vala new file mode 100644 index 00000000..1a8649a8 --- /dev/null +++ b/main/src/ui/contact_details/permissions_provider.vala @@ -0,0 +1,29 @@ +using Gtk; + +using Dino.Entities; + +namespace Dino.Ui.ContactDetails { + +public class PermissionsProvider : Plugins.ContactDetailsProvider, Object { + public string id { get { return "permissions"; } } + + private StreamInteractor stream_interactor; + + public PermissionsProvider(StreamInteractor stream_interactor) { + this.stream_interactor = stream_interactor; + } + + public void populate(Conversation conversation, Plugins.ContactDetails contact_details, Plugins.WidgetType type) { + if (type != Plugins.WidgetType.GTK) return; + + Xmpp.Jid? own_jid = stream_interactor.get_module(MucManager.IDENTITY).get_own_jid(conversation.counterpart, conversation.account); + if (stream_interactor.get_module(MucManager.IDENTITY).get_role(own_jid, conversation.account)==Xmpp.Xep.Muc.Role.VISITOR){ + Button voice_request = new Button() {visible=true, label=_("Request")}; + voice_request.clicked.connect(()=>stream_interactor.get_module(MucManager.IDENTITY).request_voice(conversation.account, conversation.counterpart)); + contact_details.add(_("Permissions"), _("Request permission to send messages"), "", voice_request); + + } + } +} + +} diff --git a/main/src/ui/contact_details/settings_provider.vala b/main/src/ui/contact_details/settings_provider.vala index adc2e371..262029a2 100644 --- a/main/src/ui/contact_details/settings_provider.vala +++ b/main/src/ui/contact_details/settings_provider.vala @@ -45,6 +45,7 @@ public class SettingsProvider : Plugins.ContactDetailsProvider, Object { combobox.append("on", get_notify_setting_string(Conversation.NotifySetting.ON)); combobox.append("off", get_notify_setting_string(Conversation.NotifySetting.OFF)); contact_details.add(DETAILS_HEADLINE_ROOM, _("Notifications"), "", combobox); + combobox.active_id = get_notify_setting_id(conversation.notify_setting); combobox.changed.connect(() => { conversation.notify_setting = get_notify_setting(combobox.active_id); } ); } diff --git a/main/src/ui/notifications.vala b/main/src/ui/notifications.vala index 7b45a46e..dc73bda5 100644 --- a/main/src/ui/notifications.vala +++ b/main/src/ui/notifications.vala @@ -43,6 +43,7 @@ public class Notifications : Object { stream_interactor.get_module(NotificationEvents.IDENTITY).notify_subscription_request.connect(notify_subscription_request); stream_interactor.get_module(NotificationEvents.IDENTITY).notify_connection_error.connect(notify_connection_error); stream_interactor.get_module(NotificationEvents.IDENTITY).notify_muc_invite.connect(on_invite_received); + stream_interactor.get_module(NotificationEvents.IDENTITY).notify_voice_request.connect(on_voice_request_received); } private async void notify_content_item(ContentItem content_item, Conversation conversation) { @@ -140,6 +141,28 @@ public class Notifications : Object { notification.add_button_with_target_value(_("Accept"), "app.open-muc-join", group_conversation.id); GLib.Application.get_default().send_notification(null, notification); } + + private async void on_voice_request_received(Account account, Jid room_jid, Jid from_jid, string? nick, string? role, string? label) { + Conversation? direct_conversation = stream_interactor.get_module(ConversationManager.IDENTITY).get_conversation(room_jid, account, Conversation.Type.GROUPCHAT); + if (direct_conversation == null) return; + string display_name = Util.get_participant_display_name(stream_interactor, direct_conversation, from_jid); + string display_room = room_jid.bare_jid.to_string(); + Notification notification = new Notification(_("Permission request")); + string body = _("%s requests the permission to write in %s").printf(display_name, display_room); + notification.set_body(body); + + try { + Cairo.ImageSurface jid_avatar = (yield Util.get_conversation_avatar_drawer(stream_interactor, direct_conversation)).size(40, 40).draw_image_surface(); + notification.set_icon(get_pixbuf_icon(jid_avatar)); + } catch (Error e) { } + + Conversation group_conversation = stream_interactor.get_module(ConversationManager.IDENTITY).create_conversation(room_jid, account, Conversation.Type.GROUPCHAT); + group_conversation.nickname = nick; + notification.set_default_action_and_target_value("app.accept-voice-request", new Variant.int32(group_conversation.id)); + notification.add_button_with_target_value(_("Deny"), "app.deny-voice-request", group_conversation.id); + notification.add_button_with_target_value(_("Accept"), "app.accept-voice-request", group_conversation.id); + GLib.Application.get_default().send_notification(null, notification); + } private Icon get_pixbuf_icon(Cairo.ImageSurface surface) throws Error { Gdk.Pixbuf avatar = Gdk.pixbuf_get_from_surface(surface, 0, 0, surface.get_width(), surface.get_height()); diff --git a/main/src/ui/occupant_menu/view.vala b/main/src/ui/occupant_menu/view.vala index 958a4aa3..a512ff24 100644 --- a/main/src/ui/occupant_menu/view.vala +++ b/main/src/ui/occupant_menu/view.vala @@ -97,6 +97,21 @@ public class View : Popover { outer_box.add(kick_button); kick_button.clicked.connect(kick_button_clicked); } + if (stream_interactor.get_module(MucManager.IDENTITY).is_moderated_room(conversation.account, conversation.counterpart) && role == Xmpp.Xep.Muc.Role.MODERATOR){ + if (stream_interactor.get_module(MucManager.IDENTITY).get_role(selected_jid, conversation.account) == Xmpp.Xep.Muc.Role.VISITOR) { + ModelButton voice_button = new ModelButton() { active=true, text=_("Grant write permission"), visible=true }; + outer_box.add(voice_button); + voice_button.clicked.connect(() => + voice_button_clicked("participant")); + } + else if (stream_interactor.get_module(MucManager.IDENTITY).get_role(selected_jid, conversation.account) == Xmpp.Xep.Muc.Role.PARTICIPANT){ + ModelButton voice_button = new ModelButton() { active=true, text=_("Revoke write permission"), visible=true }; + outer_box.add(voice_button); + voice_button.clicked.connect(() => + voice_button_clicked("visitor")); + } + + } if (jid_menu != null) jid_menu.destroy(); stack.add_named(outer_box, "menu"); @@ -119,6 +134,12 @@ public class View : Popover { stream_interactor.get_module(MucManager.IDENTITY).kick(conversation.account, conversation.counterpart, selected_jid.resourcepart); } + + private void voice_button_clicked(string role) { + if (selected_jid == null) return; + + stream_interactor.get_module(MucManager.IDENTITY).change_role(conversation.account, conversation.counterpart, selected_jid.resourcepart, role); + } } } diff --git a/xmpp-vala/src/module/xep/0004_data_forms.vala b/xmpp-vala/src/module/xep/0004_data_forms.vala index e042725b..1ed899fc 100644 --- a/xmpp-vala/src/module/xep/0004_data_forms.vala +++ b/xmpp-vala/src/module/xep/0004_data_forms.vala @@ -148,6 +148,7 @@ public class DataForm { public ListSingleField(StanzaNode node) { base.from_node(node); type_ = Type.LIST_SINGLE; + node.set_attribute("type", "list-single"); } } diff --git a/xmpp-vala/src/module/xep/0045_muc/module.vala b/xmpp-vala/src/module/xep/0045_muc/module.vala index 1b16f8a1..3a15a19d 100644 --- a/xmpp-vala/src/module/xep/0045_muc/module.vala +++ b/xmpp-vala/src/module/xep/0045_muc/module.vala @@ -6,6 +6,7 @@ private const string NS_URI = "http://jabber.org/protocol/muc"; private const string NS_URI_ADMIN = NS_URI + "#admin"; private const string NS_URI_OWNER = NS_URI + "#owner"; private const string NS_URI_USER = NS_URI + "#user"; +private const string NS_URI_REQUEST = NS_URI + "#request"; public enum MucEnterError { NONE, @@ -68,6 +69,7 @@ public class Module : XmppStreamModule { public signal void received_occupant_role(XmppStream stream, Jid jid, Role? role); public signal void subject_set(XmppStream stream, string? subject, Jid jid); public signal void invite_received(XmppStream stream, Jid room_jid, Jid from_jid, string? password, string? reason); + public signal void voice_request_received(XmppStream stream, Jid room_jid, Jid from_jid, string? nick, string? role, string? label); public signal void room_info_updated(XmppStream stream, Jid muc_jid); public signal void self_removed_from_room(XmppStream stream, Jid jid, StatusCode code); @@ -154,6 +156,25 @@ public class Module : XmppStreamModule { stream.get_module(MessageModule.IDENTITY).send_message.begin(stream, message); } + public void request_voice(XmppStream stream, Jid to_muc) { + MessageStanza message = new MessageStanza() { to=to_muc }; + + DataForms.DataForm submit_node = new DataForms.DataForm(); + submit_node.get_submit_node(); + + DataForms.DataForm.Field field_node = new DataForms.DataForm.Field() { var="FORM_TYPE" }; + field_node.set_value_string(NS_URI_REQUEST); + + DataForms.DataForm.ListSingleField single_field = new DataForms.DataForm.ListSingleField(new StanzaNode.build("field", DataForms.NS_URI)) { var="muc#role", label="Requested role", value="participant" }; + + submit_node.add_field(field_node); + submit_node.add_field(single_field); + + message.stanza.put_node(submit_node.stanza_node); + + stream.get_module(MessageModule.IDENTITY).send_message.begin(stream, message); + } + public void kick(XmppStream stream, Jid jid, string nick) { change_role(stream, jid, nick, "none"); } @@ -530,6 +551,38 @@ public class ReceivedPipelineListener : StanzaListener { } } } + + StanzaNode? x_field_node = message.stanza.get_subnode("x", DataForms.NS_URI); + if (x_field_node != null){ + Gee.List? fields = x_field_node.get_subnodes("field", DataForms.NS_URI); + Jid? from_jid = null; + string? nick = null; + string? role = null; + string? label = null; + + if (fields.size!=0){ + foreach (var field_node in fields){ + string? var_ = field_node.get_attribute("var"); + if (var_ == "muc#jid"){ + StanzaNode? value_node = field_node.get_subnode("value", DataForms.NS_URI); + if (value_node != null) from_jid = new Jid(value_node.get_string_content()); + } + else if (var_ == "muc#roomnick"){ + StanzaNode? value_node = field_node.get_subnode("value", DataForms.NS_URI); + if (value_node != null) nick = value_node.get_string_content(); + } + else if (var_ == "muc#role"){ + StanzaNode? value_node = field_node.get_subnode("value", DataForms.NS_URI); + if (value_node != null) role = value_node.get_string_content(); + } + else if (var_ == "muc#request_allow"){ + label = field_node.get_attribute("label"); + } + } + outer.voice_request_received(stream, message.from, from_jid, nick, role, label); + return true; + } + } } return false; }