aboutsummaryrefslogtreecommitdiff
path: root/libdino/src
diff options
context:
space:
mode:
authorfiaxh <git@lightrise.org>2023-01-06 13:19:42 +0100
committerfiaxh <git@lightrise.org>2023-01-06 14:03:54 +0100
commitdc52e7595cca06d0a2da7d11b3c88cb2f7ce529c (patch)
tree111f4a86a8541ce51bba7ec56f5b32197fcefc83 /libdino/src
parent4d7809bb12199a598b531ca3ca019a4bb5a867f7 (diff)
downloaddino-dc52e7595cca06d0a2da7d11b3c88cb2f7ce529c.tar.gz
dino-dc52e7595cca06d0a2da7d11b3c88cb2f7ce529c.zip
Add support for XEP-0461 replies (with fallback)
Diffstat (limited to 'libdino/src')
-rw-r--r--libdino/src/application.vala2
-rw-r--r--libdino/src/entity/message.vala37
-rw-r--r--libdino/src/plugin/interfaces.vala2
-rw-r--r--libdino/src/service/content_item_store.vala4
-rw-r--r--libdino/src/service/database.vala36
-rw-r--r--libdino/src/service/fallback_body.vala67
-rw-r--r--libdino/src/service/message_correction.vala1
-rw-r--r--libdino/src/service/message_processor.vala20
-rw-r--r--libdino/src/service/message_storage.vala8
-rw-r--r--libdino/src/service/replies.vala130
10 files changed, 300 insertions, 7 deletions
diff --git a/libdino/src/application.vala b/libdino/src/application.vala
index 229a9de1..ce9ec14a 100644
--- a/libdino/src/application.vala
+++ b/libdino/src/application.vala
@@ -56,6 +56,8 @@ public interface Application : GLib.Application {
MessageCorrection.start(stream_interactor, db);
FileTransferStorage.start(stream_interactor, db);
Reactions.start(stream_interactor, db);
+ Replies.start(stream_interactor, db);
+ FallbackBody.start(stream_interactor, db);
create_actions();
diff --git a/libdino/src/entity/message.vala b/libdino/src/entity/message.vala
index b11e2622..912639b1 100644
--- a/libdino/src/entity/message.vala
+++ b/libdino/src/entity/message.vala
@@ -67,6 +67,9 @@ public class Message : Object {
}
}
public string? edit_to = null;
+ public int quoted_item_id = 0;
+
+ private Gee.List<Xep.FallbackIndication.Fallback> fallbacks = null;
private Database? db;
@@ -105,6 +108,7 @@ public class Message : Object {
if (real_jid_str != null) real_jid = new Jid(real_jid_str);
edit_to = row[db.message_correction.to_stanza_id];
+ quoted_item_id = row[db.reply.quoted_content_item_id];
notify.connect(on_update);
}
@@ -138,6 +142,32 @@ public class Message : Object {
notify.connect(on_update);
}
+ public Gee.List<Xep.FallbackIndication.Fallback> get_fallbacks() {
+ if (fallbacks != null) return fallbacks;
+
+ var fallbacks_by_ns = new HashMap<string, ArrayList<Xep.FallbackIndication.FallbackLocation>>();
+ foreach (Qlite.Row row in db.body_meta.select().with(db.body_meta.message_id, "=", id)) {
+ if (row[db.body_meta.info_type] != Xep.FallbackIndication.NS_URI) continue;
+
+ string ns_uri = row[db.body_meta.info];
+ if (!fallbacks_by_ns.has_key(ns_uri)) {
+ fallbacks_by_ns[ns_uri] = new ArrayList<Xep.FallbackIndication.FallbackLocation>();
+ }
+ fallbacks_by_ns[ns_uri].add(new Xep.FallbackIndication.FallbackLocation(row[db.body_meta.from_char], row[db.body_meta.to_char]));
+ }
+
+ var fallbacks = new ArrayList<Xep.FallbackIndication.Fallback>();
+ foreach (string ns_uri in fallbacks_by_ns.keys) {
+ fallbacks.add(new Xep.FallbackIndication.Fallback(ns_uri, fallbacks_by_ns[ns_uri].to_array()));
+ }
+ this.fallbacks = fallbacks;
+ return fallbacks;
+ }
+
+ public void set_fallbacks(Gee.List<Xep.FallbackIndication.Fallback> fallbacks) {
+ this.fallbacks = fallbacks;
+ }
+
public void set_type_string(string type) {
switch (type) {
case Xmpp.MessageStanza.TYPE_CHAT:
@@ -210,6 +240,13 @@ public class Message : Object {
.value(db.real_jid.real_jid, real_jid.to_string())
.perform();
}
+
+ if (sp.get_name() == "quoted-item-id") {
+ db.reply.upsert()
+ .value(db.reply.message_id, id, true)
+ .value(db.reply.quoted_content_item_id, quoted_item_id)
+ .perform();
+ }
}
}
diff --git a/libdino/src/plugin/interfaces.vala b/libdino/src/plugin/interfaces.vala
index b3402457..6a30f6dc 100644
--- a/libdino/src/plugin/interfaces.vala
+++ b/libdino/src/plugin/interfaces.vala
@@ -148,7 +148,7 @@ public abstract class MetaConversationItem : Object {
}
public interface ConversationItemWidgetInterface: Object {
- public abstract void set_widget(Object object, WidgetType type);
+ public abstract void set_widget(Object object, WidgetType type, int priority);
}
public delegate void MessageActionEvoked(Object button, Plugins.MetaConversationItem evoked_on, Object widget);
diff --git a/libdino/src/service/content_item_store.vala b/libdino/src/service/content_item_store.vala
index 6371e00b..6a9e691f 100644
--- a/libdino/src/service/content_item_store.vala
+++ b/libdino/src/service/content_item_store.vala
@@ -40,7 +40,7 @@ public class ContentItemStore : StreamInteractionModule, Object {
collection_conversations.unset(conversation);
}
- public Gee.List<ContentItem> get_items_from_query(QueryBuilder select, Conversation conversation) {
+ private Gee.List<ContentItem> get_items_from_query(QueryBuilder select, Conversation conversation) {
Gee.TreeSet<ContentItem> items = new Gee.TreeSet<ContentItem>(ContentItem.compare_func);
foreach (var row in select) {
@@ -58,7 +58,7 @@ public class ContentItemStore : StreamInteractionModule, Object {
return ret;
}
- public ContentItem get_item(Conversation conversation, int id, int content_type, int foreign_id, DateTime time) throws Error {
+ private ContentItem get_item(Conversation conversation, int id, int content_type, int foreign_id, DateTime time) throws Error {
switch (content_type) {
case 1:
Message? message = stream_interactor.get_module(MessageStorage.IDENTITY).get_message_by_id(foreign_id, conversation);
diff --git a/libdino/src/service/database.vala b/libdino/src/service/database.vala
index 5f422d2f..bfd85f06 100644
--- a/libdino/src/service/database.vala
+++ b/libdino/src/service/database.vala
@@ -7,7 +7,7 @@ using Dino.Entities;
namespace Dino {
public class Database : Qlite.Database {
- private const int VERSION = 23;
+ private const int VERSION = 24;
public class AccountTable : Table {
public Column<int> id = new Column.Integer("id") { primary_key = true, auto_increment = true };
@@ -97,6 +97,20 @@ public class Database : Qlite.Database {
}
}
+ public class BodyMeta : Table {
+ public Column<int> id = new Column.Integer("id") { primary_key = true, auto_increment = true };
+ public Column<int> message_id = new Column.Integer("message_id");
+ public Column<int> from_char = new Column.Integer("from_char");
+ public Column<int> to_char = new Column.Integer("to_char");
+ public Column<string> info_type = new Column.Text("info_type");
+ public Column<string> info = new Column.Text("info");
+
+ internal BodyMeta(Database db) {
+ base(db, "body_meta");
+ init({id, message_id, from_char, to_char, info_type, info});
+ }
+ }
+
public class MessageCorrectionTable : Table {
public Column<int> id = new Column.Integer("id") { primary_key = true, auto_increment = true };
public Column<int> message_id = new Column.Integer("message_id") { unique=true };
@@ -109,6 +123,20 @@ public class Database : Qlite.Database {
}
}
+ public class ReplyTable : Table {
+ public Column<int> id = new Column.Integer("id") { primary_key = true, auto_increment = true };
+ public Column<int> message_id = new Column.Integer("message_id") { not_null = true, unique=true };
+ public Column<int> quoted_content_item_id = new Column.Integer("quoted_message_id");
+ public Column<string?> quoted_message_stanza_id = new Column.Text("quoted_message_stanza_id");
+ public Column<string?> quoted_message_from = new Column.Text("quoted_message_from");
+
+ internal ReplyTable(Database db) {
+ base(db, "reply");
+ init({id, message_id, quoted_content_item_id, quoted_message_stanza_id, quoted_message_from});
+ index("reply_quoted_message_stanza_id", {quoted_message_stanza_id});
+ }
+ }
+
public class RealJidTable : Table {
public Column<int> message_id = new Column.Integer("message_id") { primary_key = true };
public Column<string> real_jid = new Column.Text("real_jid");
@@ -337,6 +365,8 @@ public class Database : Qlite.Database {
public EntityTable entity { get; private set; }
public ContentItemTable content_item { get; private set; }
public MessageTable message { get; private set; }
+ public BodyMeta body_meta { get; private set; }
+ public ReplyTable reply { get; private set; }
public MessageCorrectionTable message_correction { get; private set; }
public RealJidTable real_jid { get; private set; }
public OccupantIdTable occupantid { get; private set; }
@@ -364,7 +394,9 @@ public class Database : Qlite.Database {
entity = new EntityTable(this);
content_item = new ContentItemTable(this);
message = new MessageTable(this);
+ body_meta = new BodyMeta(this);
message_correction = new MessageCorrectionTable(this);
+ reply = new ReplyTable(this);
occupantid = new OccupantIdTable(this);
real_jid = new RealJidTable(this);
file_transfer = new FileTransferTable(this);
@@ -379,7 +411,7 @@ public class Database : Qlite.Database {
reaction = new ReactionTable(this);
settings = new SettingsTable(this);
conversation_settings = new ConversationSettingsTable(this);
- init({ account, jid, entity, content_item, message, message_correction, real_jid, occupantid, file_transfer, call, call_counterpart, conversation, avatar, entity_identity, entity_feature, roster, mam_catchup, reaction, settings, conversation_settings });
+ init({ account, jid, entity, content_item, message, body_meta, message_correction, reply, real_jid, occupantid, file_transfer, call, call_counterpart, conversation, avatar, entity_identity, entity_feature, roster, mam_catchup, reaction, settings, conversation_settings });
try {
exec("PRAGMA journal_mode = WAL");
diff --git a/libdino/src/service/fallback_body.vala b/libdino/src/service/fallback_body.vala
new file mode 100644
index 00000000..cc9ba9a6
--- /dev/null
+++ b/libdino/src/service/fallback_body.vala
@@ -0,0 +1,67 @@
+using Gee;
+using Qlite;
+
+using Xmpp;
+using Xmpp.Xep;
+using Dino.Entities;
+
+public class Dino.FallbackBody : StreamInteractionModule, Object {
+ public static ModuleIdentity<FallbackBody> IDENTITY = new ModuleIdentity<FallbackBody>("fallback-body");
+ public string id { get { return IDENTITY.id; } }
+
+ private StreamInteractor stream_interactor;
+ private Database db;
+
+ private ReceivedMessageListener received_message_listener;
+
+ public static void start(StreamInteractor stream_interactor, Database db) {
+ FallbackBody m = new FallbackBody(stream_interactor, db);
+ stream_interactor.add_module(m);
+ }
+
+ private FallbackBody(StreamInteractor stream_interactor, Database db) {
+ this.stream_interactor = stream_interactor;
+ this.db = db;
+ this.received_message_listener = new ReceivedMessageListener(stream_interactor, db);
+
+ stream_interactor.get_module(MessageProcessor.IDENTITY).received_pipeline.connect(received_message_listener);
+ }
+
+ private class ReceivedMessageListener : MessageListener {
+
+ public string[] after_actions_const = new string[]{ "STORE" };
+ public override string action_group { get { return "Quote"; } }
+ public override string[] after_actions { get { return after_actions_const; } }
+
+ private StreamInteractor stream_interactor;
+ private Database db;
+
+ public ReceivedMessageListener(StreamInteractor stream_interactor, Database db) {
+ this.stream_interactor = stream_interactor;
+ this.db = db;
+ }
+
+ public override async bool run(Entities.Message message, Xmpp.MessageStanza stanza, Conversation conversation) {
+ Gee.List<Xep.FallbackIndication.Fallback> fallbacks = Xep.FallbackIndication.get_fallbacks(stanza);
+ if (fallbacks.is_empty) return false;
+
+ foreach (var fallback in fallbacks) {
+ if (fallback.ns_uri != Xep.Replies.NS_URI) continue;
+
+ foreach (var location in fallback.locations) {
+ db.body_meta.insert()
+ .value(db.body_meta.message_id, message.id)
+ .value(db.body_meta.info_type, Xep.FallbackIndication.NS_URI)
+ .value(db.body_meta.info, fallback.ns_uri)
+ .value(db.body_meta.from_char, location.from_char)
+ .value(db.body_meta.to_char, location.to_char)
+ .perform();
+ }
+
+ message.set_fallbacks(fallbacks);
+ }
+
+ return false;
+ }
+ }
+} \ No newline at end of file
diff --git a/libdino/src/service/message_correction.vala b/libdino/src/service/message_correction.vala
index d5d15578..2c9078ea 100644
--- a/libdino/src/service/message_correction.vala
+++ b/libdino/src/service/message_correction.vala
@@ -44,6 +44,7 @@ public class MessageCorrection : StreamInteractionModule, MessageListener {
Message out_message = stream_interactor.get_module(MessageProcessor.IDENTITY).create_out_message(correction_text, conversation);
out_message.edit_to = stanza_id;
+ out_message.quoted_item_id = old_message.quoted_item_id;
outstanding_correction_nodes[out_message.stanza_id] = stanza_id;
stream_interactor.get_module(MessageProcessor.IDENTITY).send_xmpp_message(out_message, conversation);
diff --git a/libdino/src/service/message_processor.vala b/libdino/src/service/message_processor.vala
index a290132f..62822658 100644
--- a/libdino/src/service/message_processor.vala
+++ b/libdino/src/service/message_processor.vala
@@ -424,6 +424,26 @@ public class MessageProcessor : StreamInteractionModule, Object {
} else {
new_message.type_ = MessageStanza.TYPE_CHAT;
}
+
+ if (message.quoted_item_id > 0) {
+ ContentItem? content_item = stream_interactor.get_module(ContentItemStore.IDENTITY).get_item_by_id(conversation, message.quoted_item_id);
+ if (content_item != null && content_item.type_ == MessageItem.TYPE) {
+ Message? quoted_message = stream_interactor.get_module(MessageStorage.IDENTITY).get_message_by_id(((MessageItem) content_item).message.id, conversation);
+ if (quoted_message != null) {
+ Xep.Replies.set_reply_to(new_message, new Xep.Replies.ReplyTo(quoted_message.from, quoted_message.stanza_id));
+
+ string body_with_fallback = "> " + Dino.message_body_without_reply_fallback(quoted_message);
+ body_with_fallback.replace("\n", "\n> ");
+ body_with_fallback += "\n";
+ long fallback_length = body_with_fallback.length;
+ body_with_fallback += message.body;
+ new_message.body = body_with_fallback;
+ var fallback_location = new Xep.FallbackIndication.FallbackLocation(0, (int)fallback_length);
+ Xep.FallbackIndication.set_fallback(new_message, new Xep.FallbackIndication.Fallback(Xep.Replies.NS_URI, new Xep.FallbackIndication.FallbackLocation[] { fallback_location }));
+ }
+ }
+ }
+
build_message_stanza(message, new_message, conversation);
pre_message_send(message, new_message, conversation);
if (message.marked == Entities.Message.Marked.UNSENT || message.marked == Entities.Message.Marked.WONTSEND) return;
diff --git a/libdino/src/service/message_storage.vala b/libdino/src/service/message_storage.vala
index a44c0b02..fbdbcf8a 100644
--- a/libdino/src/service/message_storage.vala
+++ b/libdino/src/service/message_storage.vala
@@ -42,6 +42,7 @@ public class MessageStorage : StreamInteractionModule, Object {
.with(db.message.type_, "=", (int) Util.get_message_type_for_conversation(conversation))
.order_by(db.message.time, "DESC")
.outer_join_with(db.message_correction, db.message_correction.message_id, db.message.id)
+ .outer_join_with(db.reply, db.reply.message_id, db.message.id)
.limit(count);
Gee.List<Message> ret = new LinkedList<Message>(Message.equals_func);
@@ -92,6 +93,7 @@ public class MessageStorage : StreamInteractionModule, Object {
RowOption row_option = db.message.select().with(db.message.id, "=", id)
.outer_join_with(db.message_correction, db.message_correction.message_id, db.message.id)
+ .outer_join_with(db.reply, db.reply.message_id, db.message.id)
.row();
return create_message_from_row_opt(row_option, conversation);
@@ -111,7 +113,8 @@ public class MessageStorage : StreamInteractionModule, Object {
.with(db.message.type_, "=", (int) Util.get_message_type_for_conversation(conversation))
.with(db.message.stanza_id, "=", stanza_id)
.order_by(db.message.time, "DESC")
- .outer_join_with(db.message_correction, db.message_correction.message_id, db.message.id);
+ .outer_join_with(db.message_correction, db.message_correction.message_id, db.message.id)
+ .outer_join_with(db.reply, db.reply.message_id, db.message.id);
if (conversation.counterpart.resourcepart == null) {
query.with_null(db.message.counterpart_resource);
@@ -138,7 +141,8 @@ public class MessageStorage : StreamInteractionModule, Object {
.with(db.message.type_, "=", (int) Util.get_message_type_for_conversation(conversation))
.with(db.message.server_id, "=", server_id)
.order_by(db.message.time, "DESC")
- .outer_join_with(db.message_correction, db.message_correction.message_id, db.message.id);
+ .outer_join_with(db.message_correction, db.message_correction.message_id, db.message.id)
+ .outer_join_with(db.reply, db.reply.message_id, db.message.id);
if (conversation.counterpart.resourcepart == null) {
query.with_null(db.message.counterpart_resource);
diff --git a/libdino/src/service/replies.vala b/libdino/src/service/replies.vala
new file mode 100644
index 00000000..6a9bced4
--- /dev/null
+++ b/libdino/src/service/replies.vala
@@ -0,0 +1,130 @@
+using Gee;
+using Qlite;
+
+using Xmpp;
+using Xmpp.Xep;
+using Dino.Entities;
+
+public class Dino.Replies : StreamInteractionModule, Object {
+ public static ModuleIdentity<Replies> IDENTITY = new ModuleIdentity<Replies>("reply");
+ public string id { get { return IDENTITY.id; } }
+
+ private StreamInteractor stream_interactor;
+ private Database db;
+ private HashMap<Conversation, HashMap<string, Gee.List<Message>>> unmapped_replies = new HashMap<Conversation, HashMap<string, Gee.List<Message>>>();
+
+ private ReceivedMessageListener received_message_listener;
+
+ public static void start(StreamInteractor stream_interactor, Database db) {
+ Replies m = new Replies(stream_interactor, db);
+ stream_interactor.add_module(m);
+ }
+
+ private Replies(StreamInteractor stream_interactor, Database db) {
+ this.stream_interactor = stream_interactor;
+ this.db = db;
+ this.received_message_listener = new ReceivedMessageListener(stream_interactor, this);
+
+ stream_interactor.get_module(MessageProcessor.IDENTITY).received_pipeline.connect(received_message_listener);
+ }
+
+ public ContentItem? get_quoted_content_item(Message message, Conversation conversation) {
+ if (message.quoted_item_id == 0) return null;
+
+ RowOption row_option = db.reply.select().with(db.reply.message_id, "=", message.id).row();
+ if (row_option.is_present()) {
+ return stream_interactor.get_module(ContentItemStore.IDENTITY).get_item_by_id(conversation, row_option[db.reply.quoted_content_item_id]);
+ }
+ return null;
+ }
+
+ public void set_message_is_reply_to(Message message, ContentItem reply_to) {
+ message.quoted_item_id = reply_to.id;
+
+ db.reply.upsert()
+ .value(db.reply.message_id, message.id, true)
+ .value(db.reply.quoted_content_item_id, reply_to.id)
+ .value_null(db.reply.quoted_message_stanza_id)
+ .value_null(db.reply.quoted_message_from)
+ .perform();
+ }
+
+ private void on_incoming_message(Entities.Message message, Xmpp.MessageStanza stanza, Conversation conversation) {
+ // Check if a previous message was in reply to this one
+ string relevant_id = conversation.type_ == Conversation.Type.GROUPCHAT ? message.server_id : message.stanza_id;
+
+ var reply_qry = db.reply.select();
+ if (conversation.type_ == Conversation.Type.GROUPCHAT) {
+ reply_qry.with(db.reply.quoted_message_stanza_id, "=", message.server_id);
+ } else {
+ reply_qry.with(db.reply.quoted_message_stanza_id, "=", message.stanza_id);
+ }
+ reply_qry.join_with(db.message, db.reply.message_id, db.message.id)
+ .with(db.message.account_id, "=", conversation.account.id)
+ .with(db.message.counterpart_id, "=", db.get_jid_id(conversation.counterpart))
+ .with(db.message.time, ">", (long)message.time.to_unix())
+ .order_by(db.message.time, "DESC");
+
+ foreach (Row reply_row in reply_qry) {
+ ContentItem? message_item = stream_interactor.get_module(ContentItemStore.IDENTITY).get_item_by_foreign(conversation, 1, message.id);
+ Message? reply_message = stream_interactor.get_module(MessageStorage.IDENTITY).get_message_by_id(reply_row[db.message.id], conversation);
+ if (message_item != null && reply_message != null) {
+ set_message_is_reply_to(reply_message, message_item);
+ }
+ }
+
+ // Handle if this message is a reply
+ Xep.Replies.ReplyTo? reply_to = Xep.Replies.get_reply_to(stanza);
+ if (reply_to == null) return;
+
+ Message? quoted_message = null;
+ if (conversation.type_ == Conversation.Type.GROUPCHAT) {
+ quoted_message = stream_interactor.get_module(MessageStorage.IDENTITY).get_message_by_server_id(reply_to.to_message_id, conversation);
+ } else {
+ quoted_message = stream_interactor.get_module(MessageStorage.IDENTITY).get_message_by_stanza_id(reply_to.to_message_id, conversation);
+ }
+ if (quoted_message == null) {
+ db.reply.upsert()
+ .value(db.reply.message_id, message.id, true)
+ .value(db.reply.quoted_message_stanza_id, reply_to.to_message_id)
+ .value(db.reply.quoted_message_from, reply_to.to_jid.to_string())
+ .perform();
+ return;
+ }
+
+ ContentItem? quoted_content_item = stream_interactor.get_module(ContentItemStore.IDENTITY).get_item_by_foreign(conversation, 1, quoted_message.id);
+ if (quoted_content_item == null) return;
+
+ set_message_is_reply_to(message, quoted_content_item);
+ }
+
+ private class ReceivedMessageListener : MessageListener {
+
+ public string[] after_actions_const = new string[]{ "STORE", "STORE_CONTENT_ITEM" };
+ public override string action_group { get { return "Quote"; } }
+ public override string[] after_actions { get { return after_actions_const; } }
+
+ private Replies outer;
+
+ public ReceivedMessageListener(StreamInteractor stream_interactor, Replies outer) {
+ this.outer = outer;
+ }
+
+ public override async bool run(Entities.Message message, Xmpp.MessageStanza stanza, Conversation conversation) {
+ outer.on_incoming_message(message, stanza, conversation);
+ return false;
+ }
+ }
+}
+
+namespace Dino {
+ public string message_body_without_reply_fallback(Message message) {
+ string body = message.body;
+ foreach (var fallback in message.get_fallbacks()) {
+ if (fallback.ns_uri == Xep.Replies.NS_URI && message.quoted_item_id > 0) {
+ body = body[0:fallback.locations[0].from_char] + body[fallback.locations[0].to_char:body.length];
+ }
+ }
+ return body;
+ }
+}