aboutsummaryrefslogtreecommitdiff
path: root/libdino
diff options
context:
space:
mode:
authorfiaxh <git@lightrise.org>2025-11-14 14:48:25 -0600
committerfiaxh <git@lightrise.org>2025-11-14 14:48:25 -0600
commit1ede8ade12f26cf39d29d5c616bd28693d677e09 (patch)
treedc0bb08cd95c1b47291e48766dfd3fbd7acbaf40 /libdino
parent9f02aba799e07ad2ab587334fe59c577e5d4605d (diff)
Add support for message moderation and retraction
Diffstat (limited to 'libdino')
-rw-r--r--libdino/meson.build1
-rw-r--r--libdino/src/application.vala1
-rw-r--r--libdino/src/plugin/interfaces.vala1
-rw-r--r--libdino/src/service/message_deletion.vala117
-rw-r--r--libdino/src/service/message_processor.vala2
-rw-r--r--libdino/src/service/muc_manager.vala14
6 files changed, 135 insertions, 1 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);