diff options
| -rw-r--r-- | libdino/meson.build | 1 | ||||
| -rw-r--r-- | libdino/src/application.vala | 1 | ||||
| -rw-r--r-- | libdino/src/plugin/interfaces.vala | 1 | ||||
| -rw-r--r-- | libdino/src/service/message_deletion.vala | 117 | ||||
| -rw-r--r-- | libdino/src/service/message_processor.vala | 2 | ||||
| -rw-r--r-- | libdino/src/service/muc_manager.vala | 14 | ||||
| -rw-r--r-- | main/src/ui/conversation_content_view/conversation_view.vala | 78 | ||||
| -rw-r--r-- | main/src/ui/conversation_content_view/file_widget.vala | 10 | ||||
| -rw-r--r-- | main/src/ui/conversation_content_view/item_actions.vala | 61 | ||||
| -rw-r--r-- | main/src/ui/conversation_content_view/message_widget.vala | 16 | ||||
| -rw-r--r-- | main/src/ui/conversation_selector/conversation_selector_row.vala | 14 | ||||
| -rw-r--r-- | xmpp-vala/meson.build | 1 | ||||
| -rw-r--r-- | xmpp-vala/src/module/xep/0424_message_retraction.vala | 15 | ||||
| -rw-r--r-- | xmpp-vala/src/module/xep/0425_message_moderation.vala | 19 |
14 files changed, 312 insertions, 38 deletions
diff --git a/libdino/meson.build b/libdino/meson.build index c5351228..5b537251 100644 --- a/libdino/meson.build +++ b/libdino/meson.build @@ -59,6 +59,7 @@ sources = files( 'src/service/history_sync.vala', 'src/service/jingle_file_transfers.vala', 'src/service/message_correction.vala', + 'src/service/message_deletion.vala', 'src/service/message_processor.vala', 'src/service/message_storage.vala', 'src/service/module_manager.vala', diff --git a/libdino/src/application.vala b/libdino/src/application.vala index de1325ad..737c76c2 100644 --- a/libdino/src/application.vala +++ b/libdino/src/application.vala @@ -59,6 +59,7 @@ public interface Application : GLib.Application { Replies.start(stream_interactor, db); FallbackBody.start(stream_interactor, db); ContactModels.start(stream_interactor); + MessageDeletion.start(stream_interactor, db); StatelessFileSharing.start(stream_interactor, db); create_actions(); diff --git a/libdino/src/plugin/interfaces.vala b/libdino/src/plugin/interfaces.vala index 86d82bc2..4c5174c6 100644 --- a/libdino/src/plugin/interfaces.vala +++ b/libdino/src/plugin/interfaces.vala @@ -164,6 +164,7 @@ public delegate void MessageActionEvoked(Variant? variant); public class MessageAction : Object { public string name; public bool sensitive = true; + public bool shortcut_action = true; public string icon_name; public string? tooltip; public Object? popover; diff --git a/libdino/src/service/message_deletion.vala b/libdino/src/service/message_deletion.vala new file mode 100644 index 00000000..4467311e --- /dev/null +++ b/libdino/src/service/message_deletion.vala @@ -0,0 +1,117 @@ +using Gee; + +using Xmpp; +using Xmpp.Xep; +using Dino.Entities; +using Qlite; + +namespace Dino { + + public class MessageDeletion : StreamInteractionModule, MessageListener { + public static ModuleIdentity<MessageDeletion> IDENTITY = new ModuleIdentity<MessageDeletion>("message_deletion"); + public string id { get { return IDENTITY.id; } } + + public signal void item_deleted(ContentItem content_item); + + private StreamInteractor stream_interactor; + private Database db; + + public static void start(StreamInteractor stream_interactor, Database db) { + MessageDeletion m = new MessageDeletion(stream_interactor, db); + stream_interactor.add_module(m); + } + + public MessageDeletion(StreamInteractor stream_interactor, Database db) { + this.stream_interactor = stream_interactor; + this.db = db; + + stream_interactor.get_module(MessageProcessor.IDENTITY).received_pipeline.connect(this); + } + + public bool is_deletable(Conversation conversation, ContentItem content_item) { + MessageItem? message_item = content_item as MessageItem; + return message_item != null && message_item.message.body != ""; + } + + public bool can_delete_for_everyone(Conversation conversation, ContentItem content_item) { + if (conversation.type_.is_muc_semantic()) { + bool muc_supports_moderation = stream_interactor.get_module(EntityInfo.IDENTITY) + .has_feature_cached(conversation.account, conversation.counterpart, Xmpp.Xep.MessageModeration.NS_URI); + bool we_are_moderator = stream_interactor.get_module(MucManager.IDENTITY).get_own_role(conversation) == Xmpp.Xep.Muc.Role.MODERATOR; + return muc_supports_moderation && we_are_moderator; + } else { + return content_item.jid.equals_bare(conversation.account.bare_jid); + } + } + + public void delete_globally(Conversation conversation, ContentItem content_item) { + var stream = stream_interactor.get_stream(conversation.account); + if (stream == null) return; + + string message_id_to_delete = stream_interactor.get_module(ContentItemStore.IDENTITY).get_message_id_for_content_item(conversation, content_item); + + if (conversation.type_ == Conversation.Type.CHAT) { + MessageStanza stanza = new MessageStanza() { to = conversation.counterpart }; + Xmpp.Xep.MessageRetraction.set_retract_id(stanza, message_id_to_delete); + stream.get_module(MessageModule.IDENTITY).send_message(stream, stanza); + delete_locally(conversation, content_item, conversation.account.bare_jid); + } else if (conversation.type_.is_muc_semantic()) { + MessageStanza stanza = new MessageStanza() { to = conversation.counterpart }; + Xmpp.Xep.MessageModeration.moderate.begin(stream, conversation.counterpart, message_id_to_delete); + // Message will be deleted locally when the MUC server sends out a moderation message + } + } + + public void delete_locally(Conversation conversation, ContentItem content_item, Jid removed_by) { + // If it's a file transfer, remove the file + if (content_item.type_ == FileItem.TYPE) { + FileItem file_item = (FileItem) content_item; + if (file_item.file_transfer.path != null) { + FileUtils.remove(file_item.file_transfer.path); + } + } + + // Mark the (underlying) message as removed and clear the body + Message? message = stream_interactor.get_module(ContentItemStore.IDENTITY).get_message_for_content_item(conversation, content_item); + if (message != null) { + message.body = ""; + } + + item_deleted(content_item); + } + + public string[] after_actions_const = new string[]{ }; + public override string action_group { get { return "DELETE"; } } + public override string[] after_actions { get { return after_actions_const; } } + + public override async bool run(Entities.Message message, Xmpp.MessageStanza stanza, Conversation conversation) { + string? delete_message_id = Xep.MessageRetraction.get_retract_id(stanza); + if (delete_message_id == null) return false; + + ContentItem? content_item = stream_interactor.get_module(ContentItemStore.IDENTITY).get_content_item_for_referencing_id(conversation, delete_message_id); + if (content_item != null) { + debug("Deletion request: %s wants to remove message %s content item id %i. Allowed: %b", + message.from.to_string(), delete_message_id, content_item.id, + is_removal_allowed(conversation, content_item, stanza.from)); + delete_locally(conversation, content_item, stanza.from); + } + + return false; + } + + private bool is_removal_allowed(Conversation conversation, ContentItem content_item, Jid removed_by) { + if (conversation.type_ == Conversation.Type.CHAT) { + return removed_by.equals_bare(content_item.jid); + } else if (conversation.type_.is_muc_semantic()) { + // Only accept MUC message removals if the MUC server announced support. + // MUC moderations should always come from the MUC bare JID. + bool muc_supports_moderation = stream_interactor.get_module(EntityInfo.IDENTITY) + .has_feature_cached(conversation.account, conversation.counterpart, Xmpp.Xep.MessageModeration.NS_URI); + return muc_supports_moderation && removed_by.equals(conversation.counterpart); + } + + return false; + } + } + +} diff --git a/libdino/src/service/message_processor.vala b/libdino/src/service/message_processor.vala index b9e64923..51fe1808 100644 --- a/libdino/src/service/message_processor.vala +++ b/libdino/src/service/message_processor.vala @@ -311,7 +311,7 @@ public class MessageProcessor : StreamInteractionModule, Object { private class FilterMessageListener : MessageListener { - public string[] after_actions_const = new string[]{ "DECRYPT" }; + public string[] after_actions_const = new string[]{ "DECRYPT", "DELETE" }; public override string action_group { get { return "FILTER_EMPTY"; } } public override string[] after_actions { get { return after_actions_const; } } diff --git a/libdino/src/service/muc_manager.vala b/libdino/src/service/muc_manager.vala index 5b251b07..fd8b4336 100644 --- a/libdino/src/service/muc_manager.vala +++ b/libdino/src/service/muc_manager.vala @@ -353,6 +353,13 @@ public class MucManager : StreamInteractionModule, Object { return null; } + public Xep.Muc.Role? get_own_role(Conversation conversation) { + Xmpp.Jid? own_jid = stream_interactor.get_module(MucManager.IDENTITY).get_own_jid(conversation.counterpart, conversation.account); + if (own_jid == null) return null; + + return stream_interactor.get_module(MucManager.IDENTITY).get_role(own_jid, conversation.account); + } + public Xep.Muc.Affiliation? get_affiliation(Jid muc_jid, Jid jid, Account account) { Xep.Muc.Flag? flag = get_muc_flag(account); if (flag != null) { @@ -698,6 +705,13 @@ public class MucManager : StreamInteractionModule, Object { message.real_jid = real_jid.bare_jid; } } + + string? occupant_id = Xep.OccupantIds.get_occupant_id(stanza.stanza); + if (occupant_id != null) { + message.occupant_db_id = stream_interactor.get_module(OccupantIdStore.IDENTITY) + .cache_occupant_id(conversation.account, occupant_id, message.from); + } + Jid? own_muc_jid = stream_interactor.get_module(MucManager.IDENTITY).get_own_jid(message.counterpart.bare_jid, conversation.account); if (stanza.id != null && own_muc_jid != null && message.from.equals(own_muc_jid)) { Entities.Message? m = stream_interactor.get_module(MessageStorage.IDENTITY).get_message_by_stanza_id(stanza.id, conversation); diff --git a/main/src/ui/conversation_content_view/conversation_view.vala b/main/src/ui/conversation_content_view/conversation_view.vala index 257a659b..233d3312 100644 --- a/main/src/ui/conversation_content_view/conversation_view.vala +++ b/main/src/ui/conversation_content_view/conversation_view.vala @@ -51,26 +51,45 @@ public class ConversationView : Widget, Plugins.ConversationItemCollection, Plug // Setup all message menu buttons var correction_button = new Button() { name="correction" }; correction_button.clicked.connect((button) => { - on_action_button_clicked(button, null); + on_action_button_clicked("correction", null); }); action_buttons["correction"] = correction_button; message_menu_box.append(correction_button); - var reply_button = new Button() { name="reply" }; - reply_button.clicked.connect((button) => { - on_action_button_clicked(button, null); - }); - action_buttons["reply"] = reply_button; - message_menu_box.append(reply_button); - var reaction_button = new MenuButton() { name="reaction" }; EmojiChooser chooser = new EmojiChooser(); chooser.emoji_picked.connect((emoji) => { - on_action_button_clicked(reaction_button, new GLib.Variant.string(emoji)); + on_action_button_clicked("reaction", new GLib.Variant.string(emoji)); }); reaction_button.popover = chooser; action_buttons["reaction"] = reaction_button; message_menu_box.append(reaction_button); + + var reply_button = new Button() { name="reply" }; + reply_button.clicked.connect((button) => { + on_action_button_clicked("reply", null); + }); + action_buttons["reply"] = reply_button; + message_menu_box.append(reply_button); + + var delete_button = new Button() { name="delete" }; + delete_button.clicked.connect((button) => { + on_action_button_clicked("delete", null); + }); + action_buttons["delete"] = delete_button; + message_menu_box.append(delete_button); + + var menu_button = new MenuButton() { name="menu", tooltip_text=_("More actions") }; + action_buttons["menu"] = menu_button; + message_menu_box.append(menu_button); + + SimpleAction action_action = new SimpleAction("action", VariantType.STRING); + action_action.activate.connect((parameter) => { + on_action_button_clicked(parameter.get_string()); + }); + SimpleActionGroup action_group = new SimpleActionGroup(); + action_group.insert(action_action); + this.insert_action_group("action", action_group); } public ConversationView init(StreamInteractor stream_interactor) { @@ -139,7 +158,7 @@ public class ConversationView : Widget, Plugins.ConversationItemCollection, Plug private bool is_highlight_fixed() { foreach (Widget widget in action_buttons.values) { MenuButton? menu_button = widget as MenuButton; - if (menu_button != null && menu_button.popover.visible) return true; + if (menu_button != null && menu_button.popover != null && menu_button.popover.visible) return true; ToggleButton? toggle_button = widget as ToggleButton; if (toggle_button != null && toggle_button.active) return true; @@ -224,24 +243,35 @@ public class ConversationView : Widget, Plugins.ConversationItemCollection, Plug if (message_actions != null) { message_menu_box.visible = true; + Menu menu_model = new Menu(); + ((MenuButton)action_buttons["menu"]).menu_model = menu_model; + foreach (Widget widget in action_buttons.values) { widget.visible = false; } // Configure as many buttons as we need with the actions for the current meta item foreach (var message_action in current_message_actions) { - Widget button_widget = action_buttons[message_action.name]; - button_widget.visible = true; - if (message_action.name == "reaction") { - MenuButton button = (MenuButton) button_widget; - button.sensitive = message_action.sensitive; - button.icon_name = message_action.icon_name; - button.tooltip_text = Util.string_if_tooltips_active(message_action.tooltip); - } else if (message_action.callback != null) { - Button button = (Button) button_widget; - button.sensitive = message_action.sensitive; - button.icon_name = message_action.icon_name; - button.tooltip_text = Util.string_if_tooltips_active(message_action.tooltip); + if (message_action.shortcut_action) { + Widget button_widget = action_buttons[message_action.name]; + button_widget.visible = true; + + if (message_action.name == "reaction") { + MenuButton button = (MenuButton) button_widget; + button.sensitive = message_action.sensitive; + button.icon_name = message_action.icon_name; + button.tooltip_text = Util.string_if_tooltips_active(message_action.tooltip); + } else if (message_action.callback != null) { + Button button = (Button) button_widget; + button.sensitive = message_action.sensitive; + button.icon_name = message_action.icon_name; + button.tooltip_text = Util.string_if_tooltips_active(message_action.tooltip); + } + } else { + MenuItem item = new MenuItem(message_action.tooltip, null); + item.set_action_and_target_value("action.action", new GLib.Variant.string(message_action.name)); + menu_model.append_item(item); + action_buttons["menu"].visible = true; } } } else { @@ -510,9 +540,9 @@ public class ConversationView : Widget, Plugins.ConversationItemCollection, Plug (upper_item.mark == Message.Marked.WONTSEND) == (lower_item.mark == Message.Marked.WONTSEND); } - private void on_action_button_clicked(Widget widget, GLib.Variant? variant = null) { + private void on_action_button_clicked(string action_name, GLib.Variant? variant = null) { foreach (var action in message_actions) { - if (action.name != widget.name) continue; + if (action.name != action_name) continue; action.callback(variant); } } diff --git a/main/src/ui/conversation_content_view/file_widget.vala b/main/src/ui/conversation_content_view/file_widget.vala index 0e3cee03..cb734e5f 100644 --- a/main/src/ui/conversation_content_view/file_widget.vala +++ b/main/src/ui/conversation_content_view/file_widget.vala @@ -31,10 +31,12 @@ public class FileMetaItem : ConversationSummary.ContentMetaItem { Gee.List<Plugins.MessageAction> actions = new ArrayList<Plugins.MessageAction>(); - if (stream_interactor.get_module(ContentItemStore.IDENTITY).get_message_id_for_content_item(file_item.conversation, content_item) != null) { - actions.add(get_reply_action(content_item, file_item.conversation, stream_interactor)); - actions.add(get_reaction_action(content_item, file_item.conversation, stream_interactor)); - } + actions.add(get_reply_action(content_item, file_item.conversation, stream_interactor)); + actions.add(get_reaction_action(content_item, file_item.conversation, stream_interactor)); + + var delete_action = get_delete_action(content_item, file_item.conversation, stream_interactor); + if (delete_action != null) actions.add(delete_action); + return actions; } } diff --git a/main/src/ui/conversation_content_view/item_actions.vala b/main/src/ui/conversation_content_view/item_actions.vala index d4195bdd..6102dc9a 100644 --- a/main/src/ui/conversation_content_view/item_actions.vala +++ b/main/src/ui/conversation_content_view/item_actions.vala @@ -48,4 +48,65 @@ namespace Dino.Ui { } return action; } + + public Plugins.MessageAction? get_delete_action(ContentItem content_item, Conversation conversation, StreamInteractor stream_interactor) { + bool is_deletable = stream_interactor.get_module(MessageDeletion.IDENTITY).is_deletable(conversation, content_item); + if (!is_deletable) return null; + + bool can_delete_for_everyone = stream_interactor.get_module(MessageDeletion.IDENTITY).can_delete_for_everyone(conversation, content_item); + + Plugins.MessageAction action = new Plugins.MessageAction(); + action.name = "delete"; + action.icon_name = "user-trash-symbolic"; + action.tooltip = _("Delete message"); + action.shortcut_action = false; + action.callback = () => { + Adw.AlertDialog dialog = new Adw.AlertDialog(_("Delete message"), null) { content_width = 350, follows_content_size = false }; + if (can_delete_for_everyone) { + CheckButton check_button = new CheckButton() { label="Delete for everyone", active=true }; + + Box extra_child = new Box(Orientation.VERTICAL, 8); + extra_child.append(new Label("Would you like to remove this message for everyone or just for yourself?") { xalign=0, use_markup=true, wrap=true }); + extra_child.append(new Label("The message will only be deleted for others <b>if their app supports it</b>!") { xalign=0, use_markup=true, wrap=true }); + extra_child.append(check_button); + dialog.extra_child = extra_child; + + check_button.toggled.connect(() => { + dialog.remove_response("delete"); + dialog.remove_response("cancel"); + if (check_button.active) { + dialog.add_response("delete", "Delete for everyone"); + } else { + dialog.add_response("delete", "Only delete for yourself"); + } + dialog.set_response_appearance("delete", Adw.ResponseAppearance.DESTRUCTIVE); + dialog.add_response("cancel", "Cancel"); + }); + dialog.response.connect((response) => { + if (response == "delete") { + if (check_button.active) { + stream_interactor.get_module(MessageDeletion.IDENTITY).delete_globally(conversation, content_item); + } else { + stream_interactor.get_module(MessageDeletion.IDENTITY).delete_locally(conversation, content_item, conversation.account.bare_jid); + } + } + }); + dialog.add_response("delete", "Delete for everyone"); + dialog.set_response_appearance("delete", Adw.ResponseAppearance.DESTRUCTIVE); + dialog.add_response("cancel", "Cancel"); + } else { + dialog.extra_child = new Label("This message <b>can't be deleted</b> for everyone. Would you like to remove it only for yourself?") { use_markup=true, wrap=true }; + dialog.add_response("delete", "Only delete for myself"); + dialog.set_response_appearance("delete", Adw.ResponseAppearance.DESTRUCTIVE); + dialog.add_response("cancel", "Cancel"); + dialog.response.connect((response) => { + if (response == "delete") { + stream_interactor.get_module(MessageDeletion.IDENTITY).delete_locally(conversation, content_item, conversation.account.bare_jid); + } + }); + } + dialog.present(((Dino.Ui.Application)Dino.Application.get_default()).window); + }; + return action; + } }
\ No newline at end of file diff --git a/main/src/ui/conversation_content_view/message_widget.vala b/main/src/ui/conversation_content_view/message_widget.vala index d116e7de..5f0c067a 100644 --- a/main/src/ui/conversation_content_view/message_widget.vala +++ b/main/src/ui/conversation_content_view/message_widget.vala @@ -36,7 +36,8 @@ public class MessageMetaItem : ContentMetaItem { message_item = content_item as MessageItem; this.stream_interactor = stream_interactor; - stream_interactor.get_module(MessageCorrection.IDENTITY).received_correction.connect(on_received_correction); + stream_interactor.get_module(MessageCorrection.IDENTITY).received_correction.connect(on_updated_item); + stream_interactor.get_module(MessageDeletion.IDENTITY).item_deleted.connect(on_updated_item); label.activate_link.connect(on_label_activate_link); @@ -157,6 +158,10 @@ public class MessageMetaItem : ContentMetaItem { string dim_color = Util.is_dark_theme(this.label) ? "#BDBDBD" : "#707070"; + if (message.body == "") { + markup_text = @"<i><span size='small' color='$dim_color'>%s</span></i>".printf(_("Message deleted")); + theme_dependent = true; + } if (message.edit_to != null) { markup_text += @" <span size='small' color='$dim_color'>(%s)</span>".printf(_("edited")); theme_dependent = true; @@ -231,6 +236,7 @@ public class MessageMetaItem : ContentMetaItem { action1.name = "correction"; action1.icon_name = "document-edit-symbolic"; action1.tooltip = _("Edit message"); + action1.shortcut_action = false; action1.callback = () => { this.in_edit_mode = true; }; @@ -240,6 +246,9 @@ public class MessageMetaItem : ContentMetaItem { actions.add(get_reply_action(content_item, message_item.conversation, stream_interactor)); actions.add(get_reaction_action(content_item, message_item.conversation, stream_interactor)); + var delete_action = get_delete_action(content_item, message_item.conversation, stream_interactor); + if (delete_action != null) actions.add(delete_action); + return actions; } @@ -277,7 +286,7 @@ public class MessageMetaItem : ContentMetaItem { } } - private void on_received_correction(ContentItem content_item) { + private void on_updated_item(ContentItem content_item) { if (this.content_item.id == content_item.id) { this.content_item = content_item; message_item = content_item as MessageItem; @@ -294,7 +303,8 @@ public class MessageMetaItem : ContentMetaItem { } public override void dispose() { - stream_interactor.get_module(MessageCorrection.IDENTITY).received_correction.disconnect(on_received_correction); + stream_interactor.get_module(MessageCorrection.IDENTITY).received_correction.disconnect(on_updated_item); + stream_interactor.get_module(MessageDeletion.IDENTITY).item_deleted.disconnect(on_updated_item); this.notify["in-edit-mode"].disconnect(on_in_edit_mode_changed); if (marked_notify_handler_id != -1) { this.disconnect(marked_notify_handler_id); diff --git a/main/src/ui/conversation_selector/conversation_selector_row.vala b/main/src/ui/conversation_selector/conversation_selector_row.vala index a0e8b11b..7410937a 100644 --- a/main/src/ui/conversation_selector/conversation_selector_row.vala +++ b/main/src/ui/conversation_selector/conversation_selector_row.vala @@ -77,6 +77,11 @@ public class ConversationSelectorRow : ListBoxRow { content_item_received(item); } }); + stream_interactor.get_module(MessageDeletion.IDENTITY).item_deleted.connect((item) => { + if (last_content_item != null && last_content_item.id == item.id) { + content_item_received(item); + } + }); last_content_item = stream_interactor.get_module(ContentItemStore.IDENTITY).get_latest(conversation); @@ -161,8 +166,13 @@ public class ConversationSelectorRow : ListBoxRow { nick_label.label += ": "; } - change_label_attribute(message_label, attr_style_new(Pango.Style.NORMAL)); - message_label.label = Util.summarize_whitespaces_to_space(body); + if (message_item.message.body == "") { + change_label_attribute(message_label, attr_style_new(Pango.Style.ITALIC)); + message_label.label = _("Message deleted"); + } else { + change_label_attribute(message_label, attr_style_new(Pango.Style.NORMAL)); + message_label.label = Util.summarize_whitespaces_to_space(body); + } break; case FileItem.TYPE: diff --git a/xmpp-vala/meson.build b/xmpp-vala/meson.build index 4aa6e328..d4bfc9c2 100644 --- a/xmpp-vala/meson.build +++ b/xmpp-vala/meson.build @@ -125,6 +125,7 @@ sources = files( 'src/module/xep/0410_muc_self_ping.vala', 'src/module/xep/0421_occupant_ids.vala', 'src/module/xep/0424_message_retraction.vala', + 'src/module/xep/0425_message_moderation.vala', 'src/module/xep/0428_fallback_indication.vala', 'src/module/xep/0444_reactions.vala', 'src/module/xep/0446_file_metadata_element.vala', diff --git a/xmpp-vala/src/module/xep/0424_message_retraction.vala b/xmpp-vala/src/module/xep/0424_message_retraction.vala index 747f4c08..3e60eb2b 100644 --- a/xmpp-vala/src/module/xep/0424_message_retraction.vala +++ b/xmpp-vala/src/module/xep/0424_message_retraction.vala @@ -1,12 +1,19 @@ namespace Xmpp.Xep.MessageRetraction { - private const string NS_URI = "urn:xmpp:message-retract:1"; + public const string NS_URI = "urn:xmpp:message-retract:1"; public static string? get_retract_id(MessageStanza message) { - StanzaNode? node = message.stanza.get_subnode("retract", NS_URI); - if (node == null) return null; + StanzaNode? retract_node = message.stanza.get_subnode("retract", NS_URI); + if (retract_node == null) return null; - return node.get_attribute("id"); + return retract_node.get_attribute("id"); + } + + public static void set_retract_id(MessageStanza message, string message_id) { + StanzaNode retract_node = (new StanzaNode.build("retract", NS_URI)) + .add_self_xmlns() + .put_attribute("id", message_id); + message.stanza.put_node(retract_node); } public class Module : XmppStreamModule { diff --git a/xmpp-vala/src/module/xep/0425_message_moderation.vala b/xmpp-vala/src/module/xep/0425_message_moderation.vala new file mode 100644 index 00000000..80fab905 --- /dev/null +++ b/xmpp-vala/src/module/xep/0425_message_moderation.vala @@ -0,0 +1,19 @@ +namespace Xmpp.Xep.MessageModeration { + + public const string NS_URI = "urn:xmpp:message-moderate:1"; + + + /** + * @return whether the moderation was successful + */ + public static async bool moderate(XmppStream stream, Jid muc_jid, string message_id) { + StanzaNode moderate_node = new StanzaNode.build("moderate", NS_URI) + .add_self_xmlns() + .put_attribute("id", message_id) + .put_node(new StanzaNode.build("retract", Xep.MessageRetraction.NS_URI).add_self_xmlns()); + Iq.Stanza iq = new Iq.Stanza.set(moderate_node) { to = muc_jid }; + + Iq.Stanza result_stanza = yield stream.get_module(Iq.Module.IDENTITY).send_iq_async(stream, iq); + return !result_stanza.is_error(); + } +}
\ No newline at end of file |
