aboutsummaryrefslogtreecommitdiff
path: root/libdino
diff options
context:
space:
mode:
Diffstat (limited to 'libdino')
-rw-r--r--libdino/CMakeLists.txt3
-rw-r--r--libdino/src/application.vala2
-rw-r--r--libdino/src/entity/conversation.vala7
-rw-r--r--libdino/src/entity/file_transfer.vala22
-rw-r--r--libdino/src/entity/message.vala2
-rw-r--r--libdino/src/plugin/interfaces.vala26
-rw-r--r--libdino/src/plugin/registry.vala21
-rw-r--r--libdino/src/service/connection_manager.vala24
-rw-r--r--libdino/src/service/content_item_store.vala279
-rw-r--r--libdino/src/service/counterpart_interaction_manager.vala6
-rw-r--r--libdino/src/service/database.vala105
-rw-r--r--libdino/src/service/file_manager.vala43
-rw-r--r--libdino/src/service/message_processor.vala2
-rw-r--r--libdino/src/service/message_storage.vala59
-rw-r--r--libdino/src/service/module_manager.vala7
-rw-r--r--libdino/src/service/muc_manager.vala8
-rw-r--r--libdino/src/service/notification_events.vala48
-rw-r--r--libdino/src/service/registration.vala42
-rw-r--r--libdino/src/service/search_processor.vala263
19 files changed, 851 insertions, 118 deletions
diff --git a/libdino/CMakeLists.txt b/libdino/CMakeLists.txt
index 62c73eca..f45d08f5 100644
--- a/libdino/CMakeLists.txt
+++ b/libdino/CMakeLists.txt
@@ -29,6 +29,7 @@ SOURCES
src/service/blocking_manager.vala
src/service/chat_interaction.vala
src/service/connection_manager.vala
+ src/service/content_item_store.vala
src/service/conversation_manager.vala
src/service/counterpart_interaction_manager.vala
src/service/database.vala
@@ -40,7 +41,9 @@ 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/search_processor.vala
src/service/stream_interactor.vala
src/service/util.vala
diff --git a/libdino/src/application.vala b/libdino/src/application.vala
index 370618b2..d307c746 100644
--- a/libdino/src/application.vala
+++ b/libdino/src/application.vala
@@ -37,7 +37,9 @@ public interface Dino.Application : GLib.Application {
ConversationManager.start(stream_interactor, db);
ChatInteraction.start(stream_interactor);
FileManager.start(stream_interactor, db);
+ ContentItemStore.start(stream_interactor, db);
NotificationEvents.start(stream_interactor);
+ SearchProcessor.start(stream_interactor, db);
create_actions();
diff --git a/libdino/src/entity/conversation.vala b/libdino/src/entity/conversation.vala
index 9026e33f..585db07e 100644
--- a/libdino/src/entity/conversation.vala
+++ b/libdino/src/entity/conversation.vala
@@ -105,8 +105,11 @@ public class Conversation : Object {
Xmpp.XmppStream? stream = stream_interactor.get_stream(account);
if (!Application.get_default().settings.notifications) return NotifySetting.OFF;
if (type_ == Type.GROUPCHAT) {
- bool members_only = stream.get_flag(Xmpp.Xep.Muc.Flag.IDENTITY).has_room_feature(counterpart.bare_jid, Xmpp.Xep.Muc.Feature.MEMBERS_ONLY);
- return members_only ? NotifySetting.ON : NotifySetting.HIGHLIGHT;
+ Xmpp.Xep.Muc.Flag flag = stream.get_flag(Xmpp.Xep.Muc.Flag.IDENTITY);
+ if (flag != null) {
+ bool members_only = flag.has_room_feature(counterpart.bare_jid, Xmpp.Xep.Muc.Feature.MEMBERS_ONLY);
+ return members_only ? NotifySetting.ON : NotifySetting.HIGHLIGHT;
+ }
}
return NotifySetting.ON;
}
diff --git a/libdino/src/entity/file_transfer.vala b/libdino/src/entity/file_transfer.vala
index e2542e74..be472796 100644
--- a/libdino/src/entity/file_transfer.vala
+++ b/libdino/src/entity/file_transfer.vala
@@ -23,7 +23,21 @@ public class FileTransfer : Object {
public DateTime? local_time { get; set; }
public Encryption encryption { get; set; }
- public InputStream input_stream { get; set; }
+ private InputStream? input_stream_ = null;
+ public InputStream input_stream {
+ get {
+ if (input_stream_ == null) {
+ File file = File.new_for_path(Path.build_filename(storage_dir, path ?? file_name));
+ try {
+ input_stream_ = file.read();
+ } catch (Error e) { }
+ }
+ return input_stream_;
+ }
+ set {
+ input_stream_ = value;
+ }
+ }
public OutputStream output_stream { get; set; }
public string file_name { get; set; }
@@ -41,9 +55,11 @@ public class FileTransfer : Object {
public string info { get; set; }
private Database? db;
+ private string storage_dir;
- public FileTransfer.from_row(Database db, Qlite.Row row) {
+ public FileTransfer.from_row(Database db, Qlite.Row row, string storage_dir) {
this.db = db;
+ this.storage_dir = storage_dir;
id = row[db.file_transfer.id];
account = db.get_account_by_id(row[db.file_transfer.account_id]); // TODO don’t have to generate acc new
@@ -61,7 +77,7 @@ public class FileTransfer : Object {
}
direction = row[db.file_transfer.direction];
time = new DateTime.from_unix_utc(row[db.file_transfer.time]);
- local_time = new DateTime.from_unix_utc(row[db.file_transfer.time]);
+ local_time = new DateTime.from_unix_utc(row[db.file_transfer.local_time]);
encryption = (Encryption) row[db.file_transfer.encryption];
file_name = row[db.file_transfer.file_name];
path = row[db.file_transfer.path];
diff --git a/libdino/src/entity/message.vala b/libdino/src/entity/message.vala
index 6e34e458..ac54a7c2 100644
--- a/libdino/src/entity/message.vala
+++ b/libdino/src/entity/message.vala
@@ -82,7 +82,7 @@ public class Message : Object {
}
direction = row[db.message.direction];
time = new DateTime.from_unix_utc(row[db.message.time]);
- local_time = new DateTime.from_unix_utc(row[db.message.time]);
+ local_time = new DateTime.from_unix_utc(row[db.message.local_time]);
body = row[db.message.body];
marked = (Message.Marked) row[db.message.marked];
encryption = (Encryption) row[db.message.encryption];
diff --git a/libdino/src/plugin/interfaces.vala b/libdino/src/plugin/interfaces.vala
index 09d4d921..01cd525a 100644
--- a/libdino/src/plugin/interfaces.vala
+++ b/libdino/src/plugin/interfaces.vala
@@ -75,11 +75,13 @@ public interface ConversationTitlebarWidget : Object {
public abstract interface ConversationItemPopulator : Object {
public abstract string id { get; }
public abstract void init(Conversation conversation, ConversationItemCollection summary, WidgetType type);
- public virtual void populate_timespan(Conversation conversation, DateTime from, DateTime to) { }
- public virtual void populate_between_widgets(Conversation conversation, DateTime from, DateTime to) { }
public abstract void close(Conversation conversation);
}
+public abstract interface ConversationAdditionPopulator : ConversationItemPopulator {
+ public virtual void populate_timespan(Conversation conversation, DateTime from, DateTime to) { }
+}
+
public abstract interface NotificationPopulator : Object {
public abstract string id { get; }
public abstract void init(Conversation conversation, NotificationCollection summary, WidgetType type);
@@ -87,9 +89,8 @@ public abstract interface NotificationPopulator : Object {
}
public abstract class MetaConversationItem : Object {
+ public virtual string populator_id { get; set; }
public virtual Jid? jid { get; set; default=null; }
- public virtual string color { get; set; default=null; }
- public virtual string display_name { get; set; default=null; }
public virtual bool dim { get; set; default=false; }
public virtual DateTime? sort_time { get; set; default=null; }
public virtual double seccondary_sort_indicator { get; set; }
@@ -118,21 +119,4 @@ public interface NotificationCollection : Object {
public signal void remove_meta_notification(MetaConversationNotification item);
}
-public interface MessageDisplayProvider : Object {
- public abstract string id { get; set; }
- public abstract double priority { get; set; }
- public abstract bool can_display(Entities.Message? message);
- public abstract MetaConversationItem? get_item(Entities.Message message, Entities.Conversation conversation);
-}
-
-public interface FileWidget : Object {
- public abstract Object? get_widget(WidgetType type);
-}
-
-public interface FileDisplayProvider : Object {
- public abstract double priority { get; }
- public abstract bool can_display(Entities.Message? message);
- public abstract FileWidget? get_item(Entities.Message? message);
-}
-
}
diff --git a/libdino/src/plugin/registry.vala b/libdino/src/plugin/registry.vala
index fbdf2c5c..9c211a6d 100644
--- a/libdino/src/plugin/registry.vala
+++ b/libdino/src/plugin/registry.vala
@@ -7,8 +7,7 @@ public class Registry {
internal ArrayList<AccountSettingsEntry> account_settings_entries = new ArrayList<AccountSettingsEntry>();
internal ArrayList<ContactDetailsProvider> contact_details_entries = new ArrayList<ContactDetailsProvider>();
internal Map<string, TextCommand> text_commands = new HashMap<string, TextCommand>();
- internal Gee.List<MessageDisplayProvider> message_displays = new ArrayList<MessageDisplayProvider>();
- internal Gee.List<ConversationItemPopulator> conversation_item_populators = new ArrayList<ConversationItemPopulator>();
+ internal Gee.List<ConversationAdditionPopulator> conversation_addition_populators = new ArrayList<ConversationAdditionPopulator>();
internal Gee.List<NotificationPopulator> notification_populators = new ArrayList<NotificationPopulator>();
internal Gee.Collection<ConversationTitlebarEntry> conversation_titlebar_entries = new Gee.TreeSet<ConversationTitlebarEntry>((a, b) => {
if (a.order < b.order) {
@@ -71,22 +70,12 @@ public class Registry {
}
}
- public bool register_message_display(MessageDisplayProvider provider) {
- lock (message_displays) {
- foreach(MessageDisplayProvider p in message_displays) {
- if (p.id == provider.id) return false;
- }
- message_displays.add(provider);
- return true;
- }
- }
-
- public bool register_conversation_item_populator(ConversationItemPopulator populator) {
- lock (conversation_item_populators) {
- foreach(ConversationItemPopulator p in conversation_item_populators) {
+ public bool register_conversation_addition_populator(ConversationAdditionPopulator populator) {
+ lock (conversation_addition_populators) {
+ foreach(ConversationItemPopulator p in conversation_addition_populators) {
if (p.id == populator.id) return false;
}
- conversation_item_populators.add(populator);
+ conversation_addition_populators.add(populator);
return true;
}
}
diff --git a/libdino/src/service/connection_manager.vala b/libdino/src/service/connection_manager.vala
index 4413dfd7..2abbc9cb 100644
--- a/libdino/src/service/connection_manager.vala
+++ b/libdino/src/service/connection_manager.vala
@@ -121,7 +121,7 @@ public class ConnectionManager {
}
public void make_offline_all() {
- foreach (Account account in connection_todo) {
+ foreach (Account account in connections.keys) {
make_offline(account);
}
}
@@ -134,13 +134,17 @@ public class ConnectionManager {
}
public void disconnect(Account account) {
- make_offline(account);
- try {
- connections[account].stream.disconnect();
- } catch (Error e) { print(@"on_prepare_for_sleep error $(e.message)\n"); }
- connection_todo.remove(account);
if (connections.has_key(account)) {
- connections.unset(account);
+ make_offline(account);
+ try {
+ connections[account].stream.disconnect();
+ } catch (Error e) {
+ warning(@"Error disconnecting stream $(e.message)\n");
+ }
+ connection_todo.remove(account);
+ if (connections.has_key(account)) {
+ connections.unset(account);
+ }
}
}
@@ -162,7 +166,7 @@ public class ConnectionManager {
stream.attached_modules.connect((stream) => {
change_connection_state(account, ConnectionState.CONNECTED);
});
- stream.get_module(PlainSasl.Module.IDENTITY).received_auth_failure.connect((stream, node) => {
+ stream.get_module(Sasl.Module.IDENTITY).received_auth_failure.connect((stream, node) => {
set_connection_error(account, new ConnectionError(ConnectionError.Source.SASL, null));
change_connection_state(account, ConnectionState.DISCONNECTED);
});
@@ -283,7 +287,9 @@ public class ConnectionManager {
try {
make_offline(account);
connections[account].stream.disconnect();
- } catch (Error e) { print(@"on_prepare_for_sleep error $(e.message)\n"); }
+ } catch (Error e) {
+ warning(@"Error disconnecting stream $(e.message)\n");
+ }
}
} else {
print("Device un-suspend\n");
diff --git a/libdino/src/service/content_item_store.vala b/libdino/src/service/content_item_store.vala
new file mode 100644
index 00000000..9eba26ba
--- /dev/null
+++ b/libdino/src/service/content_item_store.vala
@@ -0,0 +1,279 @@
+using Gee;
+
+using Dino.Entities;
+using Qlite;
+using Xmpp;
+
+namespace Dino {
+
+public class ContentItemStore : StreamInteractionModule, Object {
+ public static ModuleIdentity<ContentItemStore> IDENTITY = new ModuleIdentity<ContentItemStore>("content_item_store");
+ public string id { get { return IDENTITY.id; } }
+
+ public signal void new_item(ContentItem item, Conversation conversation);
+
+ private StreamInteractor stream_interactor;
+ private Database db;
+ private Gee.List<ContentFilter> filters = new ArrayList<ContentFilter>();
+ private HashMap<Conversation, ContentItemCollection> collection_conversations = new HashMap<Conversation, ContentItemCollection>(Conversation.hash_func, Conversation.equals_func);
+
+ public static void start(StreamInteractor stream_interactor, Database db) {
+ ContentItemStore m = new ContentItemStore(stream_interactor, db);
+ stream_interactor.add_module(m);
+ }
+
+ public ContentItemStore(StreamInteractor stream_interactor, Database db) {
+ this.stream_interactor = stream_interactor;
+ this.db = db;
+
+ stream_interactor.get_module(FileManager.IDENTITY).received_file.connect(insert_file_transfer);
+ stream_interactor.get_module(MessageProcessor.IDENTITY).message_received.connect(announce_message);
+ stream_interactor.get_module(MessageProcessor.IDENTITY).message_sent.connect(announce_message);
+ }
+
+ public void init(Conversation conversation, ContentItemCollection item_collection) {
+ collection_conversations[conversation] = item_collection;
+ }
+
+ public void uninit(Conversation conversation, ContentItemCollection item_collection) {
+ collection_conversations.unset(conversation);
+ }
+
+ public Gee.List<ContentItem> get_items_from_query(QueryBuilder select, Conversation conversation) {
+ Gee.TreeSet<ContentItem> items = new Gee.TreeSet<ContentItem>(ContentItem.compare);
+
+ foreach (var row in select) {
+ int provider = row[db.content_item.content_type];
+ int foreign_id = row[db.content_item.foreign_id];
+ switch (provider) {
+ case 1:
+ RowOption row_option = db.message.select().with(db.message.id, "=", foreign_id).row();
+ if (row_option.is_present()) {
+ Message message = stream_interactor.get_module(MessageStorage.IDENTITY).get_message_by_id(foreign_id, conversation);
+ if (message == null) {
+ message = new Message.from_row(db, row_option.inner);
+ }
+ items.add(new MessageItem(message, conversation, row[db.content_item.id]));
+ }
+ break;
+ case 2:
+ RowOption row_option = db.file_transfer.select().with(db.file_transfer.id, "=", foreign_id).row();
+ if (row_option.is_present()) {
+ string storage_dir = FileManager.get_storage_dir();
+ FileTransfer file_transfer = new FileTransfer.from_row(db, row_option.inner, storage_dir);
+ items.add(new FileItem(file_transfer, row[db.content_item.id]));
+ }
+ break;
+ }
+ }
+
+ Gee.List<ContentItem> ret = new ArrayList<ContentItem>();
+ foreach (ContentItem item in items) {
+ ret.add(item);
+ }
+ return ret;
+ }
+
+ public ContentItem? get_item(Conversation conversation, int type, int foreign_id) {
+ QueryBuilder select = db.content_item.select()
+ .with(db.content_item.content_type, "=", type)
+ .with(db.content_item.foreign_id, "=", foreign_id);
+
+ Gee.List<ContentItem> item = get_items_from_query(select, conversation);
+
+ return item.size > 0 ? item[0] : null;
+ }
+
+ public ContentItem? get_latest(Conversation conversation) {
+ Gee.List<ContentItem> items = get_n_latest(conversation, 1);
+ if (items.size > 0) {
+ return items.get(0);
+ }
+ return null;
+ }
+
+ public Gee.List<ContentItem> get_n_latest(Conversation conversation, int count) {
+ QueryBuilder select = db.content_item.select()
+ .with(db.content_item.conversation_id, "=", conversation.id)
+ .with(db.content_item.hide, "=", false)
+ .order_by(db.content_item.local_time, "DESC")
+ .order_by(db.content_item.time, "DESC")
+ .limit(count);
+
+ return get_items_from_query(select, conversation);
+ }
+
+ public Gee.List<ContentItem> get_before(Conversation conversation, ContentItem item, int count) {
+ long local_time = (long) item.sort_time.to_unix();
+ long time = (long) item.display_time.to_unix();
+ QueryBuilder select = db.content_item.select()
+ .where(@"local_time < ? OR (local_time = ? AND time < ?) OR (local_time = ? AND time = ? AND id < ?)", { local_time.to_string(), local_time.to_string(), time.to_string(), local_time.to_string(), time.to_string(), item.id.to_string() })
+ .with(db.content_item.conversation_id, "=", conversation.id)
+ .with(db.content_item.hide, "=", false)
+ .order_by(db.content_item.local_time, "DESC")
+ .order_by(db.content_item.time, "DESC")
+ .limit(count);
+
+ return get_items_from_query(select, conversation);
+ }
+
+ public Gee.List<ContentItem> get_after(Conversation conversation, ContentItem item, int count) {
+ long local_time = (long) item.sort_time.to_unix();
+ long time = (long) item.display_time.to_unix();
+ QueryBuilder select = db.content_item.select()
+ .where(@"local_time > ? OR (local_time = ? AND time > ?) OR (local_time = ? AND time = ? AND id > ?)", { local_time.to_string(), local_time.to_string(), time.to_string(), local_time.to_string(), time.to_string(), item.id.to_string() })
+ .with(db.content_item.conversation_id, "=", conversation.id)
+ .with(db.content_item.hide, "=", false)
+ .order_by(db.content_item.local_time, "ASC")
+ .order_by(db.content_item.time, "ASC")
+ .limit(count);
+
+ return get_items_from_query(select, conversation);
+ }
+
+ public void add_filter(ContentFilter content_filter) {
+ filters.add(content_filter);
+ }
+
+ public void insert_message(Message message, Conversation conversation, bool hide = false) {
+ MessageItem item = new MessageItem(message, conversation, -1);
+ item.id = db.add_content_item(conversation, message.time, message.local_time, 1, message.id, hide);
+ }
+
+ private void announce_message(Message message, Conversation conversation) {
+ QueryBuilder select = db.content_item.select();
+ select.with(db.content_item.foreign_id, "=", message.id);
+ select.with(db.content_item.content_type, "=", 1);
+ foreach (Row row in select) {
+ MessageItem item = new MessageItem(message, conversation, row[db.content_item.id]);
+ if (!discard(item)) {
+ if (collection_conversations.has_key(conversation)) {
+ collection_conversations.get(conversation).insert_item(item);
+ }
+ new_item(item, conversation);
+ }
+ break;
+ }
+ }
+
+ private void insert_file_transfer(FileTransfer file_transfer, Conversation conversation) {
+ FileItem item = new FileItem(file_transfer, -1);
+ item.id = db.add_content_item(conversation, file_transfer.time, file_transfer.local_time, 2, file_transfer.id, false);
+ if (!discard(item)) {
+ if (collection_conversations.has_key(conversation)) {
+ collection_conversations.get(conversation).insert_item(item);
+ }
+ new_item(item, conversation);
+ }
+ }
+
+ public void set_item_hide(ContentItem content_item, bool hide) {
+ db.content_item.update()
+ .with(db.content_item.id, "=", content_item.id)
+ .set(db.content_item.hide, hide)
+ .perform();
+ }
+
+ private bool discard(ContentItem content_item) {
+ foreach (ContentFilter filter in filters) {
+ if (filter.discard(content_item)) {
+ return true;
+ }
+ }
+ return false;
+ }
+}
+
+public interface ContentItemCollection : Object {
+ public abstract void insert_item(ContentItem item);
+ public abstract void remove_item(ContentItem item);
+}
+
+public interface ContentFilter : Object {
+ public abstract bool discard(ContentItem content_item);
+}
+
+public abstract class ContentItem : Object {
+ public int id { get; set; }
+ public string type_ { get; set; }
+ public Jid? jid { get; set; default=null; }
+ public DateTime? sort_time { get; set; default=null; }
+ public DateTime? display_time { get; set; default=null; }
+ public Encryption? encryption { get; set; default=null; }
+ public Entities.Message.Marked? mark { get; set; default=null; }
+
+ public ContentItem(int id, string ty, Jid jid, DateTime sort_time, DateTime display_time, Encryption encryption, Entities.Message.Marked mark) {
+ this.id = id;
+ this.type_ = ty;
+ this.jid = jid;
+ this.sort_time = sort_time;
+ this.display_time = display_time;
+ this.encryption = encryption;
+ this.mark = mark;
+ }
+
+ public static int compare(ContentItem a, ContentItem b) {
+ int res = a.sort_time.compare(b.sort_time);
+ if (res == 0) {
+ res = a.display_time.compare(b.display_time);
+ }
+ if (res == 0) {
+ res = a.id - b.id > 0 ? 1 : -1;
+ }
+ return res;
+ }
+}
+
+public class MessageItem : ContentItem {
+ public const string TYPE = "message";
+
+ public Message message;
+ public Conversation conversation;
+
+ public MessageItem(Message message, Conversation conversation, int id) {
+ base(id, TYPE, message.from, message.local_time, message.time, message.encryption, message.marked);
+ this.message = message;
+ this.conversation = conversation;
+
+ WeakRef weak_message = WeakRef(message);
+ message.notify["marked"].connect(() => {
+ Message? m = weak_message.get() as Message;
+ if (m == null) return;
+ mark = m.marked;
+ });
+ }
+}
+
+public class FileItem : ContentItem {
+ public const string TYPE = "file";
+
+ public FileTransfer file_transfer;
+ public Conversation conversation;
+
+ public FileItem(FileTransfer file_transfer, int id) {
+ Jid jid = file_transfer.direction == FileTransfer.DIRECTION_SENT ? file_transfer.account.bare_jid.with_resource(file_transfer.account.resourcepart) : file_transfer.counterpart;
+ base(id, TYPE, jid, file_transfer.local_time, file_transfer.time, file_transfer.encryption, file_to_message_state(file_transfer.state));
+
+ this.file_transfer = file_transfer;
+
+ file_transfer.notify["state"].connect_after(() => {
+ this.mark = file_to_message_state(file_transfer.state);
+ });
+ }
+
+ private static Entities.Message.Marked file_to_message_state(FileTransfer.State state) {
+ switch (state) {
+ case FileTransfer.State.IN_PROCESS:
+ return Entities.Message.Marked.UNSENT;
+ case FileTransfer.State.COMPLETE:
+ return Entities.Message.Marked.NONE;
+ case FileTransfer.State.NOT_STARTED:
+ return Entities.Message.Marked.UNSENT;
+ case FileTransfer.State.FAILED:
+ return Entities.Message.Marked.WONTSEND;
+ }
+ assert_not_reached();
+ }
+}
+
+}
diff --git a/libdino/src/service/counterpart_interaction_manager.vala b/libdino/src/service/counterpart_interaction_manager.vala
index fb10d20c..b4df9b8d 100644
--- a/libdino/src/service/counterpart_interaction_manager.vala
+++ b/libdino/src/service/counterpart_interaction_manager.vala
@@ -9,7 +9,7 @@ public class CounterpartInteractionManager : StreamInteractionModule, Object {
public string id { get { return IDENTITY.id; } }
public signal void received_state(Account account, Jid jid, string state);
- public signal void received_marker(Account account, Jid jid, Entities.Message message, string marker);
+ public signal void received_marker(Account account, Jid jid, Entities.Message message, Entities.Message.Marked marker);
public signal void received_message_received(Account account, Jid jid, Entities.Message message);
public signal void received_message_displayed(Account account, Jid jid, Entities.Message message);
@@ -69,12 +69,12 @@ public class CounterpartInteractionManager : StreamInteractionModule, Object {
if (marker != Xep.ChatMarkers.MARKER_DISPLAYED && marker != Xep.ChatMarkers.MARKER_ACKNOWLEDGED) return;
Conversation? conversation = stream_interactor.get_module(MessageStorage.IDENTITY).get_conversation_for_stanza_id(account, stanza_id);
if (conversation == null) return;
- Entities.Message? message = stream_interactor.get_module(MessageStorage.IDENTITY).get_message_by_id(stanza_id, conversation);
+ Entities.Message? message = stream_interactor.get_module(MessageStorage.IDENTITY).get_message_by_stanza_id(stanza_id, conversation);
if (message == null) return;
conversation.read_up_to = message;
} else {
foreach (Conversation conversation in stream_interactor.get_module(ConversationManager.IDENTITY).get_conversations(jid, account)) {
- Entities.Message? message = stream_interactor.get_module(MessageStorage.IDENTITY).get_message_by_id(stanza_id, conversation);
+ Entities.Message? message = stream_interactor.get_module(MessageStorage.IDENTITY).get_message_by_stanza_id(stanza_id, conversation);
if (message != null) {
switch (marker) {
case Xep.ChatMarkers.MARKER_RECEIVED:
diff --git a/libdino/src/service/database.vala b/libdino/src/service/database.vala
index 25db82f8..e5ddd0f2 100644
--- a/libdino/src/service/database.vala
+++ b/libdino/src/service/database.vala
@@ -6,7 +6,7 @@ using Dino.Entities;
namespace Dino {
public class Database : Qlite.Database {
- private const int VERSION = 6;
+ private const int VERSION = 9;
public class AccountTable : Table {
public Column<int> id = new Column.Integer("id") { primary_key = true, auto_increment = true };
@@ -34,6 +34,23 @@ public class Database : Qlite.Database {
}
}
+ public class ContentItemTable : Table {
+ public Column<int> id = new Column.Integer("id") { primary_key = true, auto_increment = true };
+ public Column<int> conversation_id = new Column.Integer("conversation_id") { not_null = true };
+ public Column<long> time = new Column.Long("time") { not_null = true };
+ public Column<long> local_time = new Column.Long("local_time") { not_null = true };
+ public Column<int> content_type = new Column.Integer("content_type") { not_null = true };
+ public Column<int> foreign_id = new Column.Integer("foreign_id") { not_null = true };
+ public Column<bool> hide = new Column.BoolInt("hide") { default = "0", not_null = true, min_version = 9 };
+
+ internal ContentItemTable(Database db) {
+ base(db, "content_item");
+ init({id, conversation_id, time, local_time, content_type, foreign_id, hide});
+ index("contentitem_localtime_counterpart_idx", {local_time, conversation_id});
+ unique({content_type, foreign_id}, "IGNORE");
+ }
+ }
+
public class MessageTable : Table {
public Column<int> id = new Column.Integer("id") { primary_key = true, auto_increment = true };
public Column<string> stanza_id = new Column.Text("stanza_id");
@@ -54,6 +71,7 @@ public class Database : Qlite.Database {
init({id, stanza_id, account_id, counterpart_id, our_resource, counterpart_resource, direction,
type_, time, local_time, body, encryption, marked});
index("message_localtime_counterpart_idx", {local_time, counterpart_id});
+ fts({body});
}
}
@@ -173,6 +191,7 @@ public class Database : Qlite.Database {
public AccountTable account { get; private set; }
public JidTable jid { get; private set; }
+ public ContentItemTable content_item { get; private set; }
public MessageTable message { get; private set; }
public RealJidTable real_jid { get; private set; }
public FileTransferTable file_transfer { get; private set; }
@@ -190,6 +209,7 @@ public class Database : Qlite.Database {
base(fileName, VERSION);
account = new AccountTable(this);
jid = new JidTable(this);
+ content_item = new ContentItemTable(this);
message = new MessageTable(this);
real_jid = new RealJidTable(this);
file_transfer = new FileTransferTable(this);
@@ -198,7 +218,7 @@ public class Database : Qlite.Database {
entity_feature = new EntityFeatureTable(this);
roster = new RosterTable(this);
settings = new SettingsTable(this);
- init({ account, jid, message, real_jid, file_transfer, conversation, avatar, entity_feature, roster, settings });
+ init({ account, jid, content_item, message, real_jid, file_transfer, conversation, avatar, entity_feature, roster, settings });
try {
exec("PRAGMA synchronous=0");
} catch (Error e) { }
@@ -206,6 +226,45 @@ public class Database : Qlite.Database {
public override void migrate(long oldVersion) {
// new table columns are added, outdated columns are still present
+ if (oldVersion < 7) {
+ message.fts_rebuild();
+ }
+ if (oldVersion < 8) {
+ exec("""
+ insert into content_item (conversation_id, time, local_time, content_type, foreign_id, hide)
+ select conversation.id, message.time, message.local_time, 1, message.id, 0
+ from message join conversation on
+ message.account_id=conversation.account_id and
+ message.counterpart_id=conversation.jid_id and
+ message.type=conversation.type+1 and
+ (message.counterpart_resource=conversation.resource or message.type != 3)
+ where
+ message.body not in (select info from file_transfer where info not null) and
+ message.id not in (select info from file_transfer where info not null)
+ union
+ select conversation.id, message.time, message.local_time, 2, file_transfer.id, 0
+ from file_transfer
+ join message on
+ file_transfer.info=message.id
+ join conversation on
+ file_transfer.account_id=conversation.account_id and
+ file_transfer.counterpart_id=conversation.jid_id and
+ message.type=conversation.type+1 and
+ (message.counterpart_resource=conversation.resource or message.type != 3)""");
+ }
+ if (oldVersion < 9) {
+ exec("""
+ insert into content_item (conversation_id, time, local_time, content_type, foreign_id, hide)
+ select conversation.id, message.time, message.local_time, 1, message.id, 1
+ from message join conversation on
+ message.account_id=conversation.account_id and
+ message.counterpart_id=conversation.jid_id and
+ message.type=conversation.type+1 and
+ (message.counterpart_resource=conversation.resource or message.type != 3)
+ where
+ message.body in (select info from file_transfer where info not null) or
+ message.id in (select info from file_transfer where info not null)""");
+ }
}
public ArrayList<Account> get_accounts() {
@@ -232,11 +291,42 @@ public class Database : Qlite.Database {
}
}
- public Gee.List<Message> get_messages(Xmpp.Jid jid, Account account, Message.Type? type, int count, DateTime? before) {
- QueryBuilder select = message.select()
- .with(message.counterpart_id, "=", get_jid_id(jid))
+ public int add_content_item(Conversation conversation, DateTime time, DateTime local_time, int content_type, int foreign_id, bool hide) {
+ return (int) content_item.insert()
+ .value(content_item.conversation_id, conversation.id)
+ .value(content_item.local_time, (long) local_time.to_unix())
+ .value(content_item.time, (long) time.to_unix())
+ .value(content_item.content_type, content_type)
+ .value(content_item.foreign_id, foreign_id)
+ .value(content_item.hide, hide)
+ .perform();
+ }
+
+ public Gee.List<Message> get_messages(Xmpp.Jid jid, Account account, Message.Type? type, int count, DateTime? before, DateTime? after, int id) {
+ QueryBuilder select = message.select();
+
+ if (before != null) {
+ if (id > 0) {
+ select.where(@"local_time < ? OR (local_time = ? AND id < ?)", { before.to_unix().to_string(), before.to_unix().to_string(), id.to_string() });
+ } else {
+ select.with(message.id, "<", id);
+ }
+ }
+ if (after != null) {
+ if (id > 0) {
+ select.where(@"local_time > ? OR (local_time = ? AND id > ?)", { after.to_unix().to_string(), after.to_unix().to_string(), id.to_string() });
+ } else {
+ select.with(message.local_time, ">", (long) after.to_unix());
+ }
+ if (id > 0) {
+ select.with(message.id, ">", id);
+ }
+ } else {
+ select.order_by(message.id, "DESC");
+ }
+
+ select.with(message.counterpart_id, "=", get_jid_id(jid))
.with(message.account_id, "=", account.id)
- .order_by(message.id, "DESC")
.limit(count);
if (jid.resourcepart != null) {
select.with(message.counterpart_resource, "=", jid.resourcepart);
@@ -244,9 +334,6 @@ public class Database : Qlite.Database {
if (type != null) {
select.with(message.type_, "=", (int) type);
}
- if (before != null) {
- select.with(message.local_time, "<", (long) before.to_unix());
- }
LinkedList<Message> ret = new LinkedList<Message>();
foreach (Row row in select) {
diff --git a/libdino/src/service/file_manager.vala b/libdino/src/service/file_manager.vala
index 3def24af..340205af 100644
--- a/libdino/src/service/file_manager.vala
+++ b/libdino/src/service/file_manager.vala
@@ -11,7 +11,7 @@ public class FileManager : StreamInteractionModule, Object {
public string id { get { return IDENTITY.id; } }
public signal void upload_available(Account account);
- public signal void received_file(FileTransfer file_transfer);
+ public signal void received_file(FileTransfer file_transfer, Conversation conversation);
private StreamInteractor stream_interactor;
private Database db;
@@ -68,7 +68,7 @@ public class FileManager : StreamInteractionModule, Object {
file_sender.send_file(conversation, file_transfer);
}
}
- received_file(file_transfer);
+ received_file(file_transfer, conversation);
}
public bool is_upload_available(Conversation conversation) {
@@ -78,21 +78,38 @@ public class FileManager : StreamInteractionModule, Object {
return false;
}
- public Gee.List<FileTransfer> get_file_transfers(Account account, Jid counterpart, DateTime after, DateTime before) {
+ public Gee.List<FileTransfer> get_latest_transfers(Account account, Jid counterpart, int n) {
+ Qlite.QueryBuilder select = db.file_transfer.select()
+ .with(db.file_transfer.counterpart_id, "=", db.get_jid_id(counterpart))
+ .with(db.file_transfer.account_id, "=", account.id)
+ .order_by(db.file_transfer.local_time, "DESC")
+ .limit(n);
+ return get_transfers_from_qry(select);
+ }
+
+ public Gee.List<FileTransfer> get_transfers_before(Account account, Jid counterpart, DateTime before, int n) {
Qlite.QueryBuilder select = db.file_transfer.select()
.with(db.file_transfer.counterpart_id, "=", db.get_jid_id(counterpart))
.with(db.file_transfer.account_id, "=", account.id)
- .with(db.file_transfer.local_time, ">", (long)after.to_unix())
.with(db.file_transfer.local_time, "<", (long)before.to_unix())
- .order_by(db.file_transfer.id, "DESC");
+ .order_by(db.file_transfer.local_time, "DESC")
+ .limit(n);
+ return get_transfers_from_qry(select);
+ }
+
+ public Gee.List<FileTransfer> get_transfers_after(Account account, Jid counterpart, DateTime after, int n) {
+ Qlite.QueryBuilder select = db.file_transfer.select()
+ .with(db.file_transfer.counterpart_id, "=", db.get_jid_id(counterpart))
+ .with(db.file_transfer.account_id, "=", account.id)
+ .with(db.file_transfer.local_time, ">", (long)after.to_unix())
+ .limit(n);
+ return get_transfers_from_qry(select);
+ }
+ private Gee.List<FileTransfer> get_transfers_from_qry(Qlite.QueryBuilder select) {
Gee.List<FileTransfer> ret = new ArrayList<FileTransfer>();
foreach (Qlite.Row row in select) {
- FileTransfer file_transfer = new FileTransfer.from_row(db, row);
- File file = File.new_for_path(Path.build_filename(get_storage_dir(), file_transfer.path ?? file_transfer.file_name));
- try {
- file_transfer.input_stream = file.read();
- } catch (Error e) { }
+ FileTransfer file_transfer = new FileTransfer.from_row(db, row, get_storage_dir());
ret.insert(0, file_transfer);
}
return ret;
@@ -117,7 +134,7 @@ public class FileManager : StreamInteractionModule, Object {
outgoing_processors.add(processor);
}
- private void handle_incomming_file(FileTransfer file_transfer) {
+ private void handle_incomming_file(FileTransfer file_transfer, Conversation conversation) {
foreach (IncommingFileProcessor processor in incomming_processors) {
if (processor.can_process(file_transfer)) {
processor.process(file_transfer);
@@ -131,7 +148,7 @@ public class FileManager : StreamInteractionModule, Object {
} catch (Error e) { }
file_transfer.persist(db);
- received_file(file_transfer);
+ received_file(file_transfer, conversation);
}
private void save_file(FileTransfer file_transfer) {
@@ -152,7 +169,7 @@ public class FileManager : StreamInteractionModule, Object {
}
public interface FileProvider : Object {
- public signal void file_incoming(FileTransfer file_transfer);
+ public signal void file_incoming(FileTransfer file_transfer, Conversation conversation);
}
public interface FileSender : Object {
diff --git a/libdino/src/service/message_processor.vala b/libdino/src/service/message_processor.vala
index d0e3e79a..f7f13a40 100644
--- a/libdino/src/service/message_processor.vala
+++ b/libdino/src/service/message_processor.vala
@@ -180,7 +180,7 @@ public class MessageProcessor : StreamInteractionModule, Object {
private class StoreMessageListener : MessageListener {
- public string[] after_actions_const = new string[]{ "DEDUPLICATE" };
+ public string[] after_actions_const = new string[]{ "DEDUPLICATE", "DECRYPT" };
public override string action_group { get { return "STORE"; } }
public override string[] after_actions { get { return after_actions_const; } }
diff --git a/libdino/src/service/message_storage.vala b/libdino/src/service/message_storage.vala
index 35e05074..9c077109 100644
--- a/libdino/src/service/message_storage.vala
+++ b/libdino/src/service/message_storage.vala
@@ -1,4 +1,5 @@
using Gee;
+using Qlite;
using Dino.Entities;
@@ -27,6 +28,7 @@ public class MessageStorage : StreamInteractionModule, Object {
message.persist(db);
init_conversation(conversation);
messages[conversation].add(message);
+ stream_interactor.get_module(ContentItemStore.IDENTITY).insert_message(message, conversation);
}
public Gee.List<Message> get_messages(Conversation conversation, int count = 50) {
@@ -51,26 +53,47 @@ public class MessageStorage : StreamInteractionModule, Object {
return null;
}
- public Gee.List<Message>? get_messages_before_message(Conversation? conversation, Message message, int count = 20) {
- SortedSet<Message>? before = messages[conversation].head_set(message);
- if (before != null && before.size >= count) {
- Gee.List<Message> ret = new ArrayList<Message>(Message.equals_func);
- Iterator<Message> iter = before.iterator();
- iter.next();
- for (int from_index = before.size - count; iter.has_next() && from_index > 0; from_index--) iter.next();
- while(iter.has_next()) {
- Message m = iter.get();
- ret.add(m);
- iter.next();
- }
- return ret;
- } else {
- Gee.List<Message> db_messages = db.get_messages(conversation.counterpart, conversation.account, Util.get_message_type_for_conversation(conversation), count, message.local_time);
- return db_messages;
+ public Gee.List<MessageItem> get_messages_before_message(Conversation? conversation, DateTime before, int id, int count = 20) {
+// SortedSet<Message>? before = messages[conversation].head_set(message);
+// if (before != null && before.size >= count) {
+// Gee.List<Message> ret = new ArrayList<Message>(Message.equals_func);
+// Iterator<Message> iter = before.iterator();
+// iter.next();
+// for (int from_index = before.size - count; iter.has_next() && from_index > 0; from_index--) iter.next();
+// while(iter.has_next()) {
+// Message m = iter.get();
+// ret.add(m);
+// iter.next();
+// }
+// return ret;
+// } else {
+ Gee.List<Message> db_messages = db.get_messages(conversation.counterpart, conversation.account, Util.get_message_type_for_conversation(conversation), count, before, null, id);
+ Gee.List<MessageItem> ret = new ArrayList<MessageItem>();
+ foreach (Message message in db_messages) {
+ ret.add(new MessageItem(message, conversation, -1));
+ }
+ return ret;
+// }
+ }
+
+ public Gee.List<MessageItem> get_messages_after_message(Conversation? conversation, DateTime after, int id, int count = 20) {
+ Gee.List<Message> db_messages = db.get_messages(conversation.counterpart, conversation.account, Util.get_message_type_for_conversation(conversation), count, null, after, id);
+ Gee.List<MessageItem> ret = new ArrayList<MessageItem>();
+ foreach (Message message in db_messages) {
+ ret.add(new MessageItem(message, conversation, -1));
+ }
+ return ret;
+ }
+
+ public Message? get_message_by_id(int id, Conversation conversation) {
+ init_conversation(conversation);
+ foreach (Message message in messages[conversation]) {
+ if (message.id == id) return message;
}
+ return null;
}
- public Message? get_message_by_id(string stanza_id, Conversation conversation) {
+ public Message? get_message_by_stanza_id(string stanza_id, Conversation conversation) {
init_conversation(conversation);
foreach (Message message in messages[conversation]) {
if (message.stanza_id == stanza_id) return message;
@@ -100,7 +123,7 @@ public class MessageStorage : StreamInteractionModule, Object {
}
return res;
});
- Gee.List<Message> db_messages = db.get_messages(conversation.counterpart, conversation.account, Util.get_message_type_for_conversation(conversation), 50, null);
+ Gee.List<Message> db_messages = db.get_messages(conversation.counterpart, conversation.account, Util.get_message_type_for_conversation(conversation), 50, null, null, -1);
messages[conversation].add_all(db_messages);
}
}
diff --git a/libdino/src/service/module_manager.vala b/libdino/src/service/module_manager.vala
index 78819bb3..d16dc935 100644
--- a/libdino/src/service/module_manager.vala
+++ b/libdino/src/service/module_manager.vala
@@ -41,8 +41,8 @@ public class ModuleManager {
foreach (XmppStreamModule module in module_map[account]) {
if (module.get_id() == Bind.Module.IDENTITY.id) {
(module as Bind.Module).requested_resource = resource ?? account.resourcepart;
- } else if (module.get_id() == PlainSasl.Module.IDENTITY.id) {
- (module as PlainSasl.Module).password = account.password;
+ } else if (module.get_id() == Sasl.Module.IDENTITY.id) {
+ (module as Sasl.Module).password = account.password;
}
}
return modules;
@@ -54,7 +54,7 @@ public class ModuleManager {
module_map[account].add(new Iq.Module());
module_map[account].add(new Tls.Module());
module_map[account].add(new Xep.SrvRecordsTls.Module());
- module_map[account].add(new PlainSasl.Module(account.bare_jid.to_string(), account.password));
+ module_map[account].add(new Sasl.Module(account.bare_jid.to_string(), account.password));
module_map[account].add(new Xep.StreamManagement.Module());
module_map[account].add(new Bind.Module(account.resourcepart));
module_map[account].add(new Session.Module());
@@ -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/muc_manager.vala b/libdino/src/service/muc_manager.vala
index b69d71f2..98700c60 100644
--- a/libdino/src/service/muc_manager.vala
+++ b/libdino/src/service/muc_manager.vala
@@ -12,6 +12,7 @@ public class MucManager : StreamInteractionModule, Object {
public signal void enter_error(Account account, Jid jid, Xep.Muc.MucEnterError error);
public signal void left(Account account, Jid jid);
public signal void subject_set(Account account, Jid jid, string? subject);
+ public signal void room_name_set(Account account, Jid jid, string? room_name);
public signal void bookmarks_updated(Account account, Gee.List<Xep.Bookmarks.Conference> conferences);
private StreamInteractor stream_interactor;
@@ -42,7 +43,7 @@ public class MucManager : StreamInteractionModule, Object {
Entities.Message? last_message = stream_interactor.get_module(MessageStorage.IDENTITY).get_last_message(conversation);
if (last_message != null) history_since = last_message.time;
}
-
+
stream.get_module(Xep.Muc.Module.IDENTITY).enter(stream, jid.bare_jid, nick_, password, history_since);
}
@@ -125,7 +126,7 @@ public class MucManager : StreamInteractionModule, Object {
}
public bool is_groupchat_occupant(Jid jid, Account account) {
- return is_groupchat(jid.bare_jid, account) && jid.is_full();
+ return is_groupchat(jid.bare_jid, account) && jid.resourcepart != null;
}
public void get_bookmarks(Account account, owned Xep.Bookmarks.Module.OnResult listener) {
@@ -242,6 +243,9 @@ public class MucManager : StreamInteractionModule, Object {
stream_interactor.module_manager.get_module(account, Xep.Muc.Module.IDENTITY).subject_set.connect( (stream, subject, jid) => {
subject_set(account, jid, subject);
});
+ stream_interactor.module_manager.get_module(account, Xep.Muc.Module.IDENTITY).room_name_set.connect( (stream, jid, room_name) => {
+ room_name_set(account, jid, room_name);
+ });
stream_interactor.module_manager.get_module(account, Xep.Bookmarks.Module.IDENTITY).received_conferences.connect( (stream, conferences) => {
sync_autojoin_active(account, conferences);
bookmarks_updated(account, conferences);
diff --git a/libdino/src/service/notification_events.vala b/libdino/src/service/notification_events.vala
index 13fef3e3..010341e3 100644
--- a/libdino/src/service/notification_events.vala
+++ b/libdino/src/service/notification_events.vala
@@ -9,13 +9,14 @@ public class NotificationEvents : StreamInteractionModule, Object {
public static ModuleIdentity<NotificationEvents> IDENTITY = new ModuleIdentity<NotificationEvents>("notification_events");
public string id { get { return IDENTITY.id; } }
- public signal void notify_message(Message message, Conversation conversation);
+ public signal void notify_content_item(ContentItem content_item, Conversation conversation);
public signal void notify_subscription_request(Conversation conversation);
+ public signal void notify_connection_error(Account account, ConnectionManager.ConnectionError error);
private StreamInteractor stream_interactor;
- private HashMap<Account, HashMap<Conversation, Entities.Message>> mam_potential_new = new HashMap<Account, HashMap<Conversation, Entities.Message>>(Account.hash_func, Account.equals_func);
- private Gee.List<Account> synced_accounts = new ArrayList<Account>();
+ private HashMap<Account, HashMap<Conversation, ContentItem>> mam_potential_new = new HashMap<Account, HashMap<Conversation, ContentItem>>(Account.hash_func, Account.equals_func);
+ private Gee.List<Account> synced_accounts = new ArrayList<Account>(Account.equals_func);
public static void start(StreamInteractor stream_interactor) {
NotificationEvents m = new NotificationEvents(stream_interactor);
@@ -25,42 +26,55 @@ public class NotificationEvents : StreamInteractionModule, Object {
public NotificationEvents(StreamInteractor stream_interactor) {
this.stream_interactor = stream_interactor;
- stream_interactor.get_module(MessageProcessor.IDENTITY).message_received.connect(on_message_received);
+ 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(MessageProcessor.IDENTITY).history_synced.connect((account) => {
synced_accounts.add(account);
if (!mam_potential_new.has_key(account)) return;
foreach (Conversation c in mam_potential_new[account].keys) {
- Entities.Message m = mam_potential_new[account][c];
- Entities.Message last_message = stream_interactor.get_module(MessageStorage.IDENTITY).get_last_message(c);
- if (m.equals(last_message) && !c.read_up_to.equals(m)) {
- on_message_received(m, c);
+ ContentItem last_mam_item = mam_potential_new[account][c];
+ ContentItem last_item = stream_interactor.get_module(ContentItemStore.IDENTITY).get_latest(c);
+ if (last_mam_item == last_item /* && !c.read_up_to.equals(m) */) {
+ on_content_item_received(last_mam_item, c);
}
}
mam_potential_new[account].clear();
});
+ stream_interactor.connection_manager.connection_error.connect((account, error) => notify_connection_error(account, error));
}
- private void on_message_received(Entities.Message message, Conversation conversation) {
+ private void on_content_item_received(ContentItem item, Conversation conversation) {
if (!synced_accounts.contains(conversation.account)) {
if (!mam_potential_new.has_key(conversation.account)) {
- mam_potential_new[conversation.account] = new HashMap<Conversation, Entities.Message>(Conversation.hash_func, Conversation.equals_func);
+ mam_potential_new[conversation.account] = new HashMap<Conversation, ContentItem>(Conversation.hash_func, Conversation.equals_func);
}
- mam_potential_new[conversation.account][conversation] = message;
+ mam_potential_new[conversation.account][conversation] = item;
return;
}
- if (!should_notify_message(message, conversation)) return;
- if (!should_notify_message(message, conversation)) return;
+ if (!should_notify(item, conversation)) return;
if (stream_interactor.get_module(ChatInteraction.IDENTITY).is_active_focus()) return;
- notify_message(message, conversation);
+ notify_content_item(item, conversation);
}
- private bool should_notify_message(Entities.Message message, Conversation conversation) {
+ private bool should_notify(ContentItem content_item, Conversation conversation) {
Conversation.NotifySetting notify = conversation.get_notification_setting(stream_interactor);
+ switch (content_item.type_) {
+ case MessageItem.TYPE:
+ Message message = (content_item as MessageItem).message;
+ if (message.direction == Message.DIRECTION_SENT) return false;
+ break;
+ case FileItem.TYPE:
+ FileTransfer file_transfer = (content_item as FileItem).file_transfer;
+ if (file_transfer.direction == FileTransfer.DIRECTION_SENT) return false;
+ break;
+ }
if (notify == Conversation.NotifySetting.OFF) return false;
Jid? nick = stream_interactor.get_module(MucManager.IDENTITY).get_own_jid(conversation.counterpart, conversation.account);
- if (notify == Conversation.NotifySetting.HIGHLIGHT && nick != null) {
- return Regex.match_simple("""\b""" + Regex.escape_string(nick.resourcepart) + """\b""", message.body, RegexCompileFlags.CASELESS);
+ if (content_item.type_ == MessageItem.TYPE) {
+ Entities.Message message = (content_item as MessageItem).message;
+ if (notify == Conversation.NotifySetting.HIGHLIGHT && nick != null) {
+ return Regex.match_simple("\\b" + Regex.escape_string(nick.resourcepart) + "\\b", message.body, RegexCompileFlags.CASELESS);
+ }
}
return true;
}
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/libdino/src/service/search_processor.vala b/libdino/src/service/search_processor.vala
new file mode 100644
index 00000000..6a08d6b8
--- /dev/null
+++ b/libdino/src/service/search_processor.vala
@@ -0,0 +1,263 @@
+using Gee;
+
+using Xmpp;
+using Qlite;
+using Dino.Entities;
+
+namespace Dino {
+
+public class SearchProcessor : StreamInteractionModule, Object {
+ public static ModuleIdentity<SearchProcessor> IDENTITY = new ModuleIdentity<SearchProcessor>("search_processor");
+ public string id { get { return IDENTITY.id; } }
+
+ private StreamInteractor stream_interactor;
+ private Database db;
+
+ public static void start(StreamInteractor stream_interactor, Database db) {
+ SearchProcessor m = new SearchProcessor(stream_interactor, db);
+ stream_interactor.add_module(m);
+ }
+
+ public SearchProcessor(StreamInteractor stream_interactor, Database db) {
+ this.stream_interactor = stream_interactor;
+ this.db = db;
+ }
+
+ private QueryBuilder prepare_search(string query, bool join_content) {
+ string words = "";
+ string? with = null;
+ string? in_ = null;
+ string? from = null;
+ foreach(string word in query.split(" ")) {
+ if (word.has_prefix("with:")) {
+ if (with == null) {
+ with = word.substring(5);
+ } else {
+ return db.message.select().where("0");
+ }
+ } else if (word.has_prefix("in:")) {
+ if (in_ == null) {
+ in_ = word.substring(3);
+ } else {
+ return db.message.select().where("0");
+ }
+ } else if (word.has_prefix("from:")) {
+ if (from == null) {
+ from = word.substring(5);
+ } else {
+ return db.message.select().where("0");
+ }
+ } else {
+ words += word + "* ";
+ }
+ }
+ if (in_ != null && with != null) {
+ return db.message.select().where("0");
+ }
+
+ QueryBuilder rows = db.message
+ .match(db.message.body, words)
+ .order_by(db.message.id, "DESC")
+ .join_with(db.jid, db.jid.id, db.message.counterpart_id)
+ .join_with(db.account, db.account.id, db.message.account_id)
+ .outer_join_with(db.real_jid, db.real_jid.message_id, db.message.id)
+ .with(db.account.enabled, "=", true);
+ if (join_content) {
+ rows.join_on(db.content_item, "message.id=content_item.foreign_id AND content_item.content_type=1")
+ .with(db.content_item.content_type, "=", 1);
+ }
+ if (with != null) {
+ if (with.index_of("/") > 0) {
+ rows.with(db.message.type_, "=", Message.Type.GROUPCHAT_PM)
+ .with(db.jid.bare_jid, "LIKE", with.substring(0, with.index_of("/")))
+ .with(db.message.counterpart_resource, "LIKE", with.substring(with.index_of("/") + 1));
+ } else {
+ rows.where(@"($(db.message.type_) = $((int)Message.Type.CHAT) AND $(db.jid.bare_jid) LIKE ?)"
+ + @" OR ($(db.message.type_) = $((int)Message.Type.GROUPCHAT_PM) AND $(db.real_jid.real_jid) LIKE ?)"
+ + @" OR ($(db.message.type_) = $((int)Message.Type.GROUPCHAT_PM) AND $(db.message.counterpart_resource) LIKE ?)", {with, with, with});
+ }
+ } else if (in_ != null) {
+ rows.with(db.jid.bare_jid, "LIKE", in_)
+ .with(db.message.type_, "=", Message.Type.GROUPCHAT);
+ }
+ if (from != null) {
+ rows.where(@"($(db.message.direction) = 1 AND $(db.account.bare_jid) LIKE ?)"
+ + @" OR ($(db.message.direction) = 1 AND $(db.message.type_) IN ($((int)Message.Type.GROUPCHAT), $((int)Message.Type.GROUPCHAT_PM)) AND $(db.message.our_resource) LIKE ?)"
+ + @" OR ($(db.message.direction) = 0 AND $(db.message.type_) = $((int)Message.Type.CHAT) AND $(db.jid.bare_jid) LIKE ?)"
+ + @" OR ($(db.message.direction) = 0 AND $(db.message.type_) IN ($((int)Message.Type.GROUPCHAT), $((int)Message.Type.GROUPCHAT_PM)) AND $(db.real_jid.real_jid) LIKE ?)"
+ + @" OR ($(db.message.direction) = 0 AND $(db.message.type_) IN ($((int)Message.Type.GROUPCHAT), $((int)Message.Type.GROUPCHAT_PM)) AND $(db.message.counterpart_resource) LIKE ?)", {from, from, from, from, from});
+ }
+ return rows;
+ }
+
+ public Gee.List<SearchSuggestion> suggest_auto_complete(string query, int cursor_position, int limit = 5) {
+ int after_prev_space = query.substring(0, cursor_position).last_index_of(" ") + 1;
+ int next_space = query.index_of(" ", after_prev_space);
+ if (next_space < 0) next_space = query.length;
+ string current_query = query.substring(after_prev_space, next_space - after_prev_space);
+ Gee.List<SearchSuggestion> suggestions = new ArrayList<SearchSuggestion>();
+
+ if (current_query.has_prefix("from:")) {
+ if (cursor_position < after_prev_space + 5) return suggestions;
+ string current_from = current_query.substring(5);
+ string[] splitted = query.split(" ");
+ foreach(string s in splitted) {
+ if (s.has_prefix("from:") && s != "from:" + current_from) {
+ // Already have an from: filter -> no useful autocompletion possible
+ return suggestions;
+ }
+ }
+ string? current_in = null;
+ string? current_with = null;
+ foreach(string s in splitted) {
+ if (s.has_prefix("in:")) {
+ current_in = s.substring(3);
+ } else if (s.has_prefix("with:")) {
+ current_with = s.substring(5);
+ }
+ }
+ if (current_in != null && current_with != null) {
+ // in: and with: -> no useful autocompletion possible
+ return suggestions;
+ }
+ if (current_with != null) {
+ // Can only be the other one or us
+
+ // Normal chat
+ QueryBuilder chats = db.conversation.select()
+ .join_with(db.jid, db.jid.id, db.conversation.jid_id)
+ .join_with(db.account, db.account.id, db.conversation.account_id)
+ .with(db.jid.bare_jid, "=", current_with)
+ .with(db.account.enabled, "=", true)
+ .with(db.conversation.type_, "=", Conversation.Type.CHAT)
+ .order_by(db.conversation.last_active, "DESC");
+ foreach(Row chat in chats) {
+ if (suggestions.size == 0) {
+ suggestions.add(new SearchSuggestion(new Account.from_row(db, chat), new Jid(chat[db.jid.bare_jid]), "from:"+chat[db.jid.bare_jid], after_prev_space, next_space));
+ }
+ suggestions.add(new SearchSuggestion(new Account.from_row(db, chat), new Jid(chat[db.account.bare_jid]), "from:"+chat[db.account.bare_jid], after_prev_space, next_space));
+ }
+ return suggestions;
+ }
+ if (current_in != null) {
+ // All members of the MUC with history
+ QueryBuilder msgs = db.message.select()
+ .select_string(@"account.*, $(db.message.counterpart_resource)")
+ .join_with(db.jid, db.jid.id, db.message.counterpart_id)
+ .join_with(db.account, db.account.id, db.message.account_id)
+ .with(db.jid.bare_jid, "=", current_in)
+ .with(db.account.enabled, "=", true)
+ .with(db.message.type_, "=", Message.Type.GROUPCHAT)
+ .with(db.message.counterpart_resource, "LIKE", @"%$current_from%")
+ .group_by({db.message.counterpart_resource})
+ .order_by_name(@"MAX($(db.message.time))", "DESC")
+ .limit(5);
+ foreach(Row msg in msgs) {
+ suggestions.add(new SearchSuggestion(new Account.from_row(db, msg), new Jid(current_in).with_resource(msg[db.message.counterpart_resource]), "from:"+msg[db.message.counterpart_resource], after_prev_space, next_space));
+ }
+ }
+ // TODO: auto complete from
+ } else if (current_query.has_prefix("with:")) {
+ if (cursor_position < after_prev_space + 5) return suggestions;
+ string current_with = current_query.substring(5);
+ string[] splitted = query.split(" ");
+ foreach(string s in splitted) {
+ if ((s.has_prefix("with:") && s != "with:" + current_with) || s.has_prefix("in:")) {
+ // Already have an in: or with: filter -> no useful autocompletion possible
+ return suggestions;
+ }
+ }
+
+ // Normal chat
+ QueryBuilder chats = db.conversation.select()
+ .join_with(db.jid, db.jid.id, db.conversation.jid_id)
+ .join_with(db.account, db.account.id, db.conversation.account_id)
+ .outer_join_on(db.roster, @"$(db.jid.bare_jid) = $(db.roster.jid) AND $(db.account.id) = $(db.roster.account_id)")
+ .where(@"$(db.jid.bare_jid) LIKE ? OR $(db.roster.handle) LIKE ?", {@"%$current_with%", @"%$current_with%"})
+ .with(db.account.enabled, "=", true)
+ .with(db.conversation.type_, "=", Conversation.Type.CHAT)
+ .order_by(db.conversation.last_active, "DESC")
+ .limit(limit);
+ foreach(Row chat in chats) {
+ suggestions.add(new SearchSuggestion(new Account.from_row(db, chat), new Jid(chat[db.jid.bare_jid]), "with:"+chat[db.jid.bare_jid], after_prev_space, next_space) { order = chat[db.conversation.last_active]});
+ }
+
+ // Groupchat PM
+ if (suggestions.size < 5) {
+ chats = db.conversation.select()
+ .join_with(db.jid, db.jid.id, db.conversation.jid_id)
+ .join_with(db.account, db.account.id, db.conversation.account_id)
+ .where(@"$(db.jid.bare_jid) LIKE ? OR $(db.conversation.resource) LIKE ?", {@"%$current_with%", @"%$current_with%"})
+ .with(db.account.enabled, "=", true)
+ .with(db.conversation.type_, "=", Conversation.Type.GROUPCHAT_PM)
+ .order_by(db.conversation.last_active, "DESC")
+ .limit(limit - suggestions.size);
+ foreach(Row chat in chats) {
+ suggestions.add(new SearchSuggestion(new Account.from_row(db, chat), new Jid(chat[db.jid.bare_jid]).with_resource(chat[db.conversation.resource]), "with:"+chat[db.jid.bare_jid]+"/"+chat[db.conversation.resource], after_prev_space, next_space) { order = chat[db.conversation.last_active]});
+ }
+ suggestions.sort((a, b) => (int)(b.order - a.order));
+ }
+ } else if (current_query.has_prefix("in:")) {
+ if (cursor_position < after_prev_space + 3) return suggestions;
+ string current_in = current_query.substring(3);
+ string[] splitted = query.split(" ");
+ foreach(string s in splitted) {
+ if ((s.has_prefix("in:") && s != "in:" + current_in) || s.has_prefix("with:")) {
+ // Already have an in: or with: filter -> no useful autocompletion possible
+ return suggestions;
+ }
+ }
+ QueryBuilder groupchats = db.conversation.select()
+ .join_with(db.jid, db.jid.id, db.conversation.jid_id)
+ .join_with(db.account, db.account.id, db.conversation.account_id)
+ .with(db.jid.bare_jid, "LIKE", @"%$current_in%")
+ .with(db.account.enabled, "=", true)
+ .with(db.conversation.type_, "=", Conversation.Type.GROUPCHAT)
+ .order_by(db.conversation.last_active, "DESC")
+ .limit(limit);
+ foreach(Row chat in groupchats) {
+ suggestions.add(new SearchSuggestion(new Account.from_row(db, chat), new Jid(chat[db.jid.bare_jid]), "in:"+chat[db.jid.bare_jid], after_prev_space, next_space));
+ }
+ } else {
+ // Other auto complete?
+ }
+ return suggestions;
+ }
+
+ public Gee.List<MessageItem> match_messages(string query, int offset = -1) {
+ Gee.List<MessageItem> ret = new ArrayList<MessageItem>();
+ QueryBuilder rows = prepare_search(query, true).limit(10);
+ if (offset > 0) {
+ rows.offset(offset);
+ }
+ foreach (Row row in rows) {
+ Message message = new Message.from_row(db, row);
+ Conversation? conversation = stream_interactor.get_module(ConversationManager.IDENTITY).get_conversation_for_message(message);
+ ret.add(new MessageItem(message, conversation, row[db.content_item.id]));
+ }
+ return ret;
+ }
+
+ public int count_match_messages(string query) {
+ return (int)prepare_search(query, false).select({db.message.id}).count();
+ }
+}
+
+public class SearchSuggestion : Object {
+ public Account account { get; private set; }
+ public Jid? jid { get; private set; }
+ public string completion { get; private set; }
+ public int start_index { get; private set; }
+ public int end_index { get; private set; }
+ public long order { get; set; }
+
+ public SearchSuggestion(Account account, Jid? jid, string completion, int start_index, int end_index) {
+ this.account = account;
+ this.jid = jid;
+ this.completion = completion;
+ this.start_index = start_index;
+ this.end_index = end_index;
+ }
+}
+
+}