diff options
Diffstat (limited to 'libdino/src')
-rw-r--r-- | libdino/src/application.vala | 2 | ||||
-rw-r--r-- | libdino/src/entity/conversation.vala | 7 | ||||
-rw-r--r-- | libdino/src/entity/file_transfer.vala | 22 | ||||
-rw-r--r-- | libdino/src/entity/message.vala | 2 | ||||
-rw-r--r-- | libdino/src/plugin/interfaces.vala | 26 | ||||
-rw-r--r-- | libdino/src/plugin/registry.vala | 21 | ||||
-rw-r--r-- | libdino/src/service/connection_manager.vala | 24 | ||||
-rw-r--r-- | libdino/src/service/content_item_store.vala | 279 | ||||
-rw-r--r-- | libdino/src/service/counterpart_interaction_manager.vala | 6 | ||||
-rw-r--r-- | libdino/src/service/database.vala | 105 | ||||
-rw-r--r-- | libdino/src/service/file_manager.vala | 43 | ||||
-rw-r--r-- | libdino/src/service/message_processor.vala | 2 | ||||
-rw-r--r-- | libdino/src/service/message_storage.vala | 59 | ||||
-rw-r--r-- | libdino/src/service/module_manager.vala | 7 | ||||
-rw-r--r-- | libdino/src/service/muc_manager.vala | 8 | ||||
-rw-r--r-- | libdino/src/service/notification_events.vala | 48 | ||||
-rw-r--r-- | libdino/src/service/registration.vala | 42 | ||||
-rw-r--r-- | libdino/src/service/search_processor.vala | 263 |
18 files changed, 848 insertions, 118 deletions
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; + } +} + +} |