From 29ca70a6d534e1cd79963718c793ae740318cff1 Mon Sep 17 00:00:00 2001 From: Marvin W Date: Fri, 10 Mar 2017 19:34:56 +0100 Subject: Initial plugin system --- libdino/src/service/avatar_manager.vala | 134 ++++++ libdino/src/service/avatar_storage.vala | 34 ++ libdino/src/service/chat_interaction.vala | 146 +++++++ libdino/src/service/connection_manager.vala | 222 ++++++++++ libdino/src/service/conversation_manager.vala | 103 +++++ .../service/counterpart_interaction_manager.vala | 99 +++++ libdino/src/service/database.vala | 466 +++++++++++++++++++++ .../src/service/entity_capabilities_storage.vala | 23 + libdino/src/service/message_manager.vala | 193 +++++++++ libdino/src/service/module_manager.vala | 96 +++++ libdino/src/service/muc_manager.vala | 224 ++++++++++ libdino/src/service/pgp_manager.vala | 54 +++ libdino/src/service/presence_manager.vala | 150 +++++++ libdino/src/service/roster_manager.vala | 82 ++++ libdino/src/service/stream_interactor.vala | 70 ++++ 15 files changed, 2096 insertions(+) create mode 100644 libdino/src/service/avatar_manager.vala create mode 100644 libdino/src/service/avatar_storage.vala create mode 100644 libdino/src/service/chat_interaction.vala create mode 100644 libdino/src/service/connection_manager.vala create mode 100644 libdino/src/service/conversation_manager.vala create mode 100644 libdino/src/service/counterpart_interaction_manager.vala create mode 100644 libdino/src/service/database.vala create mode 100644 libdino/src/service/entity_capabilities_storage.vala create mode 100644 libdino/src/service/message_manager.vala create mode 100644 libdino/src/service/module_manager.vala create mode 100644 libdino/src/service/muc_manager.vala create mode 100644 libdino/src/service/pgp_manager.vala create mode 100644 libdino/src/service/presence_manager.vala create mode 100644 libdino/src/service/roster_manager.vala create mode 100644 libdino/src/service/stream_interactor.vala (limited to 'libdino/src/service') diff --git a/libdino/src/service/avatar_manager.vala b/libdino/src/service/avatar_manager.vala new file mode 100644 index 00000000..de44c419 --- /dev/null +++ b/libdino/src/service/avatar_manager.vala @@ -0,0 +1,134 @@ +using Gdk; +using Gee; + +using Xmpp; +using Dino.Entities; + +namespace Dino { + +public class AvatarManager : StreamInteractionModule, Object { + public const string id = "avatar_manager"; + + public signal void received_avatar(Pixbuf avatar, Jid jid, Account account); + + private enum Source { + USER_AVATARS, + VCARD + } + + private StreamInteractor stream_interactor; + private Database db; + private HashMap user_avatars = new HashMap(Jid.hash_func, Jid.equals_func); + private HashMap vcard_avatars = new HashMap(Jid.hash_func, Jid.equals_func); + private AvatarStorage avatar_storage = new AvatarStorage("./"); // TODO ihh + private const int MAX_PIXEL = 192; + + public static void start(StreamInteractor stream_interactor, Database db) { + AvatarManager m = new AvatarManager(stream_interactor, db); + stream_interactor.add_module(m); + } + + private AvatarManager(StreamInteractor stream_interactor, Database db) { + this.stream_interactor = stream_interactor; + this.db = db; + stream_interactor.account_added.connect(on_account_added); + } + + public Pixbuf? get_avatar(Account account, Jid jid) { + Jid jid_ = jid; + if (!MucManager.get_instance(stream_interactor).is_groupchat_occupant(jid, account)) { + jid_ = jid.bare_jid; + } + string? user_avatars_id = user_avatars[jid_]; + if (user_avatars_id != null) { + return avatar_storage.get_image(user_avatars_id); + } + string? vcard_avatars_id = vcard_avatars[jid_]; + if (vcard_avatars_id != null) { + return avatar_storage.get_image(vcard_avatars_id); + } + return null; + } + + public void publish(Account account, string file) { + print(file + "\n"); + try { + Pixbuf pixbuf = new Pixbuf.from_file(file); + if (pixbuf.width >= pixbuf.height && pixbuf.width > MAX_PIXEL) { + int dest_height = (int) ((float) MAX_PIXEL / pixbuf.width * pixbuf.height); + pixbuf = pixbuf.scale_simple(MAX_PIXEL, dest_height, InterpType.BILINEAR); + } else if (pixbuf.height > pixbuf.width && pixbuf.width > MAX_PIXEL) { + int dest_width = (int) ((float) MAX_PIXEL / pixbuf.height * pixbuf.width); + pixbuf = pixbuf.scale_simple(dest_width, MAX_PIXEL, InterpType.BILINEAR); + } + uint8[] buffer; + pixbuf.save_to_buffer(out buffer, "png"); + Core.XmppStream stream = stream_interactor.get_stream(account); + if (stream != null) { + Xep.UserAvatars.Module.get_module(stream).publish_png(stream, buffer, pixbuf.width, pixbuf.height); + on_user_avatar_received(account, account.bare_jid, Base64.encode(buffer)); + } + } catch (Error e) { + print("error " + e.message + "\n"); + } + } + + private class PublishResponseListenerImpl : Object { + public void on_success(Core.XmppStream stream) { + + } + public void on_error(Core.XmppStream stream) { } + } + + public static AvatarManager? get_instance(StreamInteractor stream_interaction) { + return (AvatarManager) stream_interaction.get_module(id); + } + + internal string get_id() { + return id; + } + + private void on_account_added(Account account) { + stream_interactor.module_manager.user_avatars_modules[account].received_avatar.connect((stream, jid, id) => + on_user_avatar_received(account, new Jid(jid), id) + ); + stream_interactor.module_manager.vcard_modules[account].received_avatar.connect((stream, jid, id) => + on_vcard_avatar_received(account, new Jid(jid), id) + ); + + user_avatars = db.get_avatar_hashes(Source.USER_AVATARS); + foreach (Jid jid in user_avatars.keys) { + on_user_avatar_received(account, jid, user_avatars[jid]); + } + vcard_avatars = db.get_avatar_hashes(Source.VCARD); + foreach (Jid jid in vcard_avatars.keys) { + on_vcard_avatar_received(account, jid, vcard_avatars[jid]); + } + } + + private void on_user_avatar_received(Account account, Jid jid, string id) { + if (!user_avatars.has_key(jid) || user_avatars[jid] != id) { + user_avatars[jid] = id; + db.set_avatar_hash(jid, id, Source.USER_AVATARS); + } + Pixbuf? avatar = avatar_storage.get_image(id); + if (avatar != null) { + received_avatar(avatar, jid, account); + } + } + + private void on_vcard_avatar_received(Account account, Jid jid, string id) { + if (!vcard_avatars.has_key(jid) || vcard_avatars[jid] != id) { + vcard_avatars[jid] = id; + if (!jid.is_full()) { // don't save muc avatars + db.set_avatar_hash(jid, id, Source.VCARD); + } + } + Pixbuf? avatar = avatar_storage.get_image(id); + if (avatar != null) { + received_avatar(avatar, jid, account); + } + } +} + +} \ No newline at end of file diff --git a/libdino/src/service/avatar_storage.vala b/libdino/src/service/avatar_storage.vala new file mode 100644 index 00000000..a9a8fb86 --- /dev/null +++ b/libdino/src/service/avatar_storage.vala @@ -0,0 +1,34 @@ +using Gdk; + +using Xmpp; + +namespace Dino { +public class AvatarStorage : Xep.PixbufStorage, Object { + + string folder; + + public AvatarStorage(string folder) { + this.folder = folder; + } + + public void store(string id, uint8[] data) { + File file = File.new_for_path(id); + if (file.query_exists()) file.delete(); //TODO y? + DataOutputStream fos = new DataOutputStream(file.create(FileCreateFlags.REPLACE_DESTINATION)); + fos.write(data); + } + + public bool has_image(string id) { + File file = File.new_for_path(folder + id); + return file.query_exists(); + } + + public Pixbuf? get_image(string id) { + try { + return new Pixbuf.from_file(folder + id); + } catch (Error e) { + return null; + } + } +} +} \ No newline at end of file diff --git a/libdino/src/service/chat_interaction.vala b/libdino/src/service/chat_interaction.vala new file mode 100644 index 00000000..cd6907fa --- /dev/null +++ b/libdino/src/service/chat_interaction.vala @@ -0,0 +1,146 @@ +using Gee; + +using Xmpp; +using Dino.Entities; + +namespace Dino { +public class ChatInteraction : StreamInteractionModule, Object { + private const string id = "chat_interaction"; + + public signal void conversation_read(Conversation conversation); + public signal void conversation_unread(Conversation conversation); + + private StreamInteractor stream_interactor; + private Conversation? selected_conversation; + + private HashMap last_input_interaction = new HashMap(Conversation.hash_func, Conversation.equals_func); + private HashMap last_interface_interaction = new HashMap(Conversation.hash_func, Conversation.equals_func); + private bool focus_in = false; + + public static void start(StreamInteractor stream_interactor) { + ChatInteraction m = new ChatInteraction(stream_interactor); + stream_interactor.add_module(m); + } + + private ChatInteraction(StreamInteractor stream_interactor) { + this.stream_interactor = stream_interactor; + Timeout.add_seconds(30, update_interactions); + MessageManager.get_instance(stream_interactor).message_received.connect(on_message_received); + MessageManager.get_instance(stream_interactor).message_sent.connect(on_message_sent); + } + + public bool is_active_focus(Conversation? conversation = null) { + if (conversation != null) { + return focus_in && conversation.equals(this.selected_conversation); + } else { + return focus_in; + } + } + + public void window_focus_in(Conversation? conversation) { + on_conversation_selected(selected_conversation); + } + + public void window_focus_out(Conversation? conversation) { + focus_in = false; + } + + public void on_message_entered(Conversation conversation) { + if (Settings.instance().send_read) { + if (!last_input_interaction.has_key(conversation) && conversation.type_ != Conversation.Type.GROUPCHAT) { + send_chat_state_notification(conversation, Xep.ChatStateNotifications.STATE_COMPOSING); + } + } + last_input_interaction[conversation] = new DateTime.now_utc(); + last_interface_interaction[conversation] = new DateTime.now_utc(); + } + + public void on_message_cleared(Conversation conversation) { + if (last_input_interaction.has_key(conversation)) { + last_input_interaction.unset(conversation); + last_interface_interaction.unset(conversation); + send_chat_state_notification(conversation, Xep.ChatStateNotifications.STATE_ACTIVE); + } + } + + public void on_conversation_selected(Conversation? conversation) { + selected_conversation = conversation; + focus_in = true; + if (conversation != null) { + conversation_read(selected_conversation); + check_send_read(); + selected_conversation.read_up_to = MessageManager.get_instance(stream_interactor).get_last_message(conversation); + } + } + + internal string get_id() { + return id; + } + + public static ChatInteraction? get_instance(StreamInteractor stream_interactor) { + return (ChatInteraction) stream_interactor.get_module(id); + } + + private void check_send_read() { + if (selected_conversation == null || selected_conversation.type_ == Conversation.Type.GROUPCHAT) return; + Entities.Message? message = MessageManager.get_instance(stream_interactor).get_last_message(selected_conversation); + if (message != null && message.direction == Entities.Message.DIRECTION_RECEIVED && + message.stanza != null && !message.equals(selected_conversation.read_up_to)) { + selected_conversation.read_up_to = message; + send_chat_marker(selected_conversation, message, Xep.ChatMarkers.MARKER_DISPLAYED); + } + } + + private bool update_interactions() { + ArrayList remove_input = new ArrayList(Conversation.equals_func); + ArrayList remove_interface = new ArrayList(Conversation.equals_func); + foreach (Conversation conversation in last_input_interaction.keys) { + if (last_input_interaction.has_key(conversation) && + (new DateTime.now_utc()).difference(last_input_interaction[conversation]) >= 15 * TimeSpan.SECOND) { + remove_input.add(conversation); + send_chat_state_notification(conversation, Xep.ChatStateNotifications.STATE_PAUSED); + } + } + foreach (Conversation conversation in last_interface_interaction.keys) { + if (last_interface_interaction.has_key(conversation) && + (new DateTime.now_utc()).difference(last_interface_interaction[conversation]) >= 1.5 * TimeSpan.MINUTE) { + remove_interface.add(conversation); + send_chat_state_notification(conversation, Xep.ChatStateNotifications.STATE_GONE); + } + } + foreach (Conversation conversation in remove_input) last_input_interaction.unset(conversation); + foreach (Conversation conversation in remove_interface) last_interface_interaction.unset(conversation); + return true; + } + + private void on_message_received(Entities.Message message, Conversation conversation) { + if (is_active_focus(conversation)) { + check_send_read(); + conversation.read_up_to = message; + send_chat_marker(conversation, message, Xep.ChatMarkers.MARKER_DISPLAYED); + } else { + conversation_unread(conversation); + } + } + + private void on_message_sent(Entities.Message message, Conversation conversation) { + last_input_interaction.unset(conversation); + last_interface_interaction.unset(conversation); + conversation.read_up_to = message; + } + + private void send_chat_marker(Conversation conversation, Entities.Message message, string marker) { + Core.XmppStream stream = stream_interactor.get_stream(conversation.account); + if (stream != null && Settings.instance().send_read && Xep.ChatMarkers.Module.requests_marking(message.stanza)) { + Xep.ChatMarkers.Module.get_module(stream).send_marker(stream, message.stanza.from, message.stanza_id, message.get_type_string(), marker); + } + } + + private void send_chat_state_notification(Conversation conversation, string state) { + Core.XmppStream stream = stream_interactor.get_stream(conversation.account); + if (stream != null && Settings.instance().send_read) { + Xep.ChatStateNotifications.Module.get_module(stream).send_state(stream, conversation.counterpart.to_string(), state); + } + } +} +} \ No newline at end of file diff --git a/libdino/src/service/connection_manager.vala b/libdino/src/service/connection_manager.vala new file mode 100644 index 00000000..92827296 --- /dev/null +++ b/libdino/src/service/connection_manager.vala @@ -0,0 +1,222 @@ +using Gee; + +using Xmpp; +using Dino.Entities; + +namespace Dino { + +public class ConnectionManager { + + public signal void stream_opened(Account account, Core.XmppStream stream); + public signal void connection_state_changed(Account account, ConnectionState state); + + public enum ConnectionState { + CONNECTED, + CONNECTING, + DISCONNECTED + } + + private ArrayList connection_todo = new ArrayList(Account.equals_func); + private HashMap stream_states = new HashMap(Account.hash_func, Account.equals_func); + private NetworkManager? network_manager; + private Login1Manager? login1; + private ModuleManager module_manager; + + private class Connection { + public Core.XmppStream stream { get; set; } + public ConnectionState connection_state { get; set; default = ConnectionState.DISCONNECTED; } + public DateTime established { get; set; } + public class Connection(Core.XmppStream stream, DateTime established) { + this.stream = stream; + this.established = established; + } + } + + public ConnectionManager(ModuleManager module_manager) { + this.module_manager = module_manager; + network_manager = get_network_manager(); + if (network_manager != null) { + network_manager.StateChanged.connect(on_nm_state_changed); + } + login1 = get_login1(); + if (login1 != null) { + login1.PrepareForSleep.connect(on_prepare_for_sleep); + } + } + + public Core.XmppStream? get_stream(Account account) { + if (get_connection_state(account) == ConnectionState.CONNECTED) { + return stream_states[account].stream; + } + return null; + } + + public ConnectionState get_connection_state(Account account) { + if (stream_states.has_key(account)){ + return stream_states[account].connection_state; + } + return ConnectionState.DISCONNECTED; + } + + public ArrayList get_managed_accounts() { + return connection_todo; + } + + public Core.XmppStream? connect(Account account) { + if (!connection_todo.contains(account)) connection_todo.add(account); + if (!stream_states.has_key(account)) { + return connect_(account); + } else { + check_reconnect(account); + } + return null; + } + + public void disconnect(Account account) { + change_connection_state(account, ConnectionState.DISCONNECTED); + if (stream_states.has_key(account)) { + try { + stream_states[account].stream.disconnect(); + } catch (Error e) { } + } + connection_todo.remove(account); + } + + private Core.XmppStream? connect_(Account account, string? resource = null) { + if (resource == null) resource = account.resourcepart; + if (stream_states.has_key(account)) { + stream_states[account].stream.remove_modules(); + } + + Core.XmppStream stream = new Core.XmppStream(); + foreach (Core.XmppStreamModule module in module_manager.get_modules(account, resource)) { + stream.add_module(module); + } + stream.debug = false; + + Connection connection = new Connection(stream, new DateTime.now_local()); + stream_states[account] = connection; + change_connection_state(account, ConnectionState.CONNECTING); + stream.stream_negotiated.connect((stream) => { + change_connection_state(account, ConnectionState.CONNECTED); + }); + new Thread (null, () => { + try { + stream.connect(account.domainpart); + } catch (Error e) { + stderr.printf("Stream Error: %s\n", e.message); + change_connection_state(account, ConnectionState.DISCONNECTED); + interpret_reconnect_flags(account, StreamError.Flag.get_flag(stream) ?? + new StreamError.Flag() { reconnection_recomendation = StreamError.Flag.Reconnect.NOW }); + } + return null; + }); + stream_opened(account, stream); + + return stream; + } + + private void interpret_reconnect_flags(Account account, StreamError.Flag stream_error_flag) { + if (!connection_todo.contains(account)) return; + int wait_sec = 10; + if (network_manager != null && network_manager.State != NetworkManager.CONNECTED_GLOBAL) { + wait_sec = 60; + } + switch (stream_error_flag.reconnection_recomendation) { + case StreamError.Flag.Reconnect.NOW: + wait_sec = 10; + break; + case StreamError.Flag.Reconnect.LATER: + case StreamError.Flag.Reconnect.UNKNOWN: + wait_sec = 60; + break; + case StreamError.Flag.Reconnect.NEVER: + return; + } + print(@"recovering in $wait_sec\n"); + Timeout.add_seconds(wait_sec, () => { + if (stream_error_flag.resource_rejected) { + connect_(account, account.resourcepart + "-" + random_uuid()); + } else { + connect_(account); + } + return false; + }); + } + + private void check_reconnects() { + foreach (Account account in connection_todo) { + check_reconnect(account); + } + } + + private void check_reconnect(Account account) { + PingResponseListenerImpl ping_response_listener = new PingResponseListenerImpl(this, account); + Core.XmppStream stream = stream_states[account].stream; + Xep.Ping.Module.get_module(stream).send_ping(stream, account.domainpart, ping_response_listener); + + Timeout.add_seconds(5, () => { + if (stream_states[account].stream != stream) return false; + if (ping_response_listener.acked) return false; + + change_connection_state(account, ConnectionState.DISCONNECTED); + try { + stream_states[account].stream.disconnect(); + } catch (Error e) { } + return false; + }); + } + + private class PingResponseListenerImpl : Xep.Ping.ResponseListener, Object { + public bool acked = false; + ConnectionManager outer; + Account account; + public PingResponseListenerImpl(ConnectionManager outer, Account account) { + this.outer = outer; + this.account = account; + } + public void on_result(Core.XmppStream stream) { + print("ping ok\n"); + acked = true; + outer.change_connection_state(account, ConnectionState.CONNECTED); + } + } + + private void on_nm_state_changed(uint32 state) { + print("nm " + state.to_string() + "\n"); + if (state == NetworkManager.CONNECTED_GLOBAL) { + check_reconnects(); + } else { + foreach (Account account in connection_todo) { + change_connection_state(account, ConnectionState.DISCONNECTED); + } + } + } + + private void on_prepare_for_sleep(bool suspend) { + foreach (Account account in connection_todo) { + change_connection_state(account, ConnectionState.DISCONNECTED); + } + if (suspend) { + print("suspend\n"); + foreach (Account account in connection_todo) { + Xmpp.Presence.Stanza presence = new Xmpp.Presence.Stanza(); + presence.type_ = Xmpp.Presence.Stanza.TYPE_UNAVAILABLE; + try { + Presence.Module.get_module(stream_states[account].stream).send_presence(stream_states[account].stream, presence); + stream_states[account].stream.disconnect(); + } catch (Error e) { print(@"on_prepare_for_sleep error $(e.message)\n"); } + } + } else { + print("un-suspend\n"); + check_reconnects(); + } + } + + private void change_connection_state(Account account, ConnectionState state) { + stream_states[account].connection_state = state; + connection_state_changed(account, state); + } +} + +} diff --git a/libdino/src/service/conversation_manager.vala b/libdino/src/service/conversation_manager.vala new file mode 100644 index 00000000..716c9b39 --- /dev/null +++ b/libdino/src/service/conversation_manager.vala @@ -0,0 +1,103 @@ +using Gee; + +using Xmpp; +using Dino.Entities; + +namespace Dino { +public class ConversationManager : StreamInteractionModule, Object { + + public const string id = "conversation_manager"; + + public signal void conversation_activated(Conversation conversation); + + private StreamInteractor stream_interactor; + private Database db; + + private HashMap> conversations = new HashMap>(Account.hash_func, Account.equals_func); + + public static void start(StreamInteractor stream_interactor, Database db) { + ConversationManager m = new ConversationManager(stream_interactor, db); + stream_interactor.add_module(m); + } + + private ConversationManager(StreamInteractor stream_interactor, Database db) { + this.db = db; + this.stream_interactor = stream_interactor; + stream_interactor.add_module(this); + stream_interactor.account_added.connect(on_account_added); + MucManager.get_instance(stream_interactor).groupchat_joined.connect(on_groupchat_joined); + MessageManager.get_instance(stream_interactor).pre_message_received.connect(on_message_received); + MessageManager.get_instance(stream_interactor).message_sent.connect(on_message_sent); + } + + public Conversation? get_conversation(Jid jid, Account account) { + if (conversations.has_key(account)) { + return conversations[account][jid]; + } + return null; + } + + public Conversation get_add_conversation(Jid jid, Account account) { + ensure_add_conversation(jid, account, Conversation.Type.CHAT); + return get_conversation(jid, account); + } + + public void ensure_start_conversation(Jid jid, Account account) { + ensure_add_conversation(jid, account, Conversation.Type.CHAT); + Conversation? conversation = get_conversation(jid, account); + if (conversation != null) { + conversation.last_active = new DateTime.now_utc(); + if (!conversation.active) { + conversation.active = true; + conversation_activated(conversation); + } + } + + } + + public string get_id() { + return id; + } + + public static ConversationManager? get_instance(StreamInteractor stream_interaction) { + return (ConversationManager) stream_interaction.get_module(id); + } + + private void on_account_added(Account account) { + conversations[account] = new HashMap(Jid.hash_bare_func, Jid.equals_bare_func); + foreach (Conversation conversation in db.get_conversations(account)) { + add_conversation(conversation); + } + } + + private void on_message_received(Entities.Message message, Conversation conversation) { + ensure_start_conversation(conversation.counterpart, conversation.account); + } + + private void on_message_sent(Entities.Message message, Conversation conversation) { + conversation.last_active = message.time; + } + + private void on_groupchat_joined(Account account, Jid jid, string nick) { + ensure_add_conversation(jid, account, Conversation.Type.GROUPCHAT); + ensure_start_conversation(jid, account); + } + + private void ensure_add_conversation(Jid jid, Account account, Conversation.Type type) { + if (conversations.has_key(account) && !conversations[account].has_key(jid)) { + Conversation conversation = new Conversation(jid, account); + conversation.type_ = type; + add_conversation(conversation); + db.add_conversation(conversation); + } + } + + private void add_conversation(Conversation conversation) { + conversations[conversation.account][conversation.counterpart] = conversation; + if (conversation.active) { + conversation_activated(conversation); + } + } +} + +} \ No newline at end of file diff --git a/libdino/src/service/counterpart_interaction_manager.vala b/libdino/src/service/counterpart_interaction_manager.vala new file mode 100644 index 00000000..8ea8ba15 --- /dev/null +++ b/libdino/src/service/counterpart_interaction_manager.vala @@ -0,0 +1,99 @@ +using Gee; + +using Xmpp; +using Dino.Entities; + +namespace Dino { +public class CounterpartInteractionManager : StreamInteractionModule, Object { + public const string id = "counterpart_interaction_manager"; + + 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_message_received(Account account, Jid jid, Entities.Message message); + public signal void received_message_displayed(Account account, Jid jid, Entities.Message message); + + private StreamInteractor stream_interactor; + private HashMap last_read = new HashMap(Jid.hash_bare_func, Jid.equals_bare_func); + private HashMap chat_states = new HashMap(Jid.hash_bare_func, Jid.equals_bare_func); + + public static void start(StreamInteractor stream_interactor) { + CounterpartInteractionManager m = new CounterpartInteractionManager(stream_interactor); + stream_interactor.add_module(m); + } + + private CounterpartInteractionManager(StreamInteractor stream_interactor) { + this.stream_interactor = stream_interactor; + stream_interactor.account_added.connect(on_account_added); + MessageManager.get_instance(stream_interactor).message_received.connect(on_message_received); + } + + public string? get_chat_state(Account account, Jid jid) { + return chat_states[jid]; + } + + public Entities.Message? get_last_read(Account account, Jid jid) { + return last_read[jid]; + } + + public static CounterpartInteractionManager? get_instance(StreamInteractor stream_interactor) { + return (CounterpartInteractionManager) stream_interactor.get_module(id); + } + + internal string get_id() { + return id; + } + + private void on_account_added(Account account) { + stream_interactor.module_manager.chat_markers_modules[account].marker_received.connect( (stream, jid, marker, id) => { + on_chat_marker_received(account, new Jid(jid), marker, id); + }); + stream_interactor.module_manager.message_delivery_receipts_modules[account].receipt_received.connect((stream, jid, id) => { + on_receipt_received(account, new Jid(jid), id); + }); + stream_interactor.module_manager.chat_state_notifications_modules[account].chat_state_received.connect((stream, jid, state) => { + on_chat_state_received(account, new Jid(jid), state); + }); + } + + private void on_chat_state_received(Account account, Jid jid, string state) { + chat_states[jid] = state; + received_state(account, jid, state); + } + + private void on_chat_marker_received(Account account, Jid jid, string marker, string stanza_id) { + Conversation? conversation = ConversationManager.get_instance(stream_interactor).get_conversation(jid, account); + if (conversation != null) { + Gee.List? messages = MessageManager.get_instance(stream_interactor).get_messages(conversation); + if (messages != null) { // TODO not here + foreach (Entities.Message message in messages) { + if (message.stanza_id == stanza_id) { + switch (marker) { + case Xep.ChatMarkers.MARKER_RECEIVED: + received_message_received(account, jid, message); + message.marked = Entities.Message.Marked.RECEIVED; + break; + case Xep.ChatMarkers.MARKER_DISPLAYED: + last_read[jid] = message; + received_message_displayed(account, jid, message); + foreach (Entities.Message m in messages) { + if (m.equals(message)) break; + if (m.marked == Entities.Message.Marked.RECEIVED) m.marked = Entities.Message.Marked.READ; + } + message.marked = Entities.Message.Marked.READ; + break; + } + } + } + } + } + } + + private void on_message_received(Entities.Message message, Conversation conversation) { + on_chat_state_received(conversation.account, conversation.counterpart, Xep.ChatStateNotifications.STATE_ACTIVE); + } + + private void on_receipt_received(Account account, Jid jid, string id) { + on_chat_marker_received(account, jid, Xep.ChatMarkers.MARKER_RECEIVED, id); + } +} +} \ No newline at end of file diff --git a/libdino/src/service/database.vala b/libdino/src/service/database.vala new file mode 100644 index 00000000..13be6222 --- /dev/null +++ b/libdino/src/service/database.vala @@ -0,0 +1,466 @@ +using Gee; +using Sqlite; +using Qlite; + +using Dino.Entities; + +namespace Dino { + +public class Database : Qlite.Database { + private const int VERSION = 0; + + public class AccountTable : Table { + public Column id = new Column.Integer("id") { primary_key = true, auto_increment = true }; + public Column bare_jid = new Column.Text("bare_jid") { unique = true, not_null = true }; + public Column resourcepart = new Column.Text("resourcepart"); + public Column password = new Column.Text("password"); + public Column alias = new Column.Text("alias"); + public Column enabled = new Column.BoolInt("enabled"); + + protected AccountTable(Database db) { + base(db, "account"); + init({id, bare_jid, resourcepart, password, alias, enabled}); + } + } + + public class JidTable : Table { + public Column id = new Column.Integer("id") { primary_key = true, auto_increment = true }; + public Column bare_jid = new Column.Text("bare_jid") { unique = true, not_null = true }; + + protected JidTable(Database db) { + base(db, "jid"); + init({id, bare_jid}); + } + } + + public class MessageTable : Table { + public Column id = new Column.Integer("id") { primary_key = true, auto_increment = true }; + public Column stanza_id = new Column.Text("stanza_id"); + public Column account_id = new Column.Integer("account_id") { not_null = true }; + public Column counterpart_id = new Column.Integer("counterpart_id") { not_null = true }; + public Column counterpart_resource = new Column.Text("counterpart_resource"); + public Column our_resource = new Column.Text("our_resource"); + public Column direction = new Column.BoolInt("direction") { not_null = true }; + public Column type_ = new Column.Integer("type"); + public Column time = new Column.Long("time"); + public Column local_time = new Column.Long("local_time"); + public Column body = new Column.Text("body"); + public Column encryption = new Column.Integer("encryption"); + public Column marked = new Column.Integer("marked"); + + protected MessageTable(Database db) { + base(db, "message"); + init({id, stanza_id, account_id, counterpart_id, our_resource, counterpart_resource, direction, + type_, time, local_time, body, encryption, marked}); + } + } + + public class RealJidTable : Table { + public Column message_id = new Column.Integer("message_id") { primary_key = true }; + public Column real_jid = new Column.Text("real_jid"); + + protected RealJidTable(Database db) { + base(db, "real_jid"); + init({message_id, real_jid}); + } + } + + public class UndecryptedTable : Table { + public Column message_id = new Column.Integer("message_id"); + public Column type_ = new Column.Integer("type"); + public Column data = new Column.Text("data"); + + protected UndecryptedTable(Database db) { + base(db, "undecrypted"); + init({message_id, type_, data}); + } + } + + public class ConversationTable : Table { + public Column id = new Column.Integer("id") { primary_key = true, auto_increment = true }; + public Column account_id = new Column.Integer("account_id") { not_null = true }; + public Column jid_id = new Column.Integer("jid_id") { not_null = true }; + public Column active = new Column.BoolInt("active"); + public Column last_active = new Column.Long("last_active"); + public Column type_ = new Column.Integer("type"); + public Column encryption = new Column.Integer("encryption"); + public Column read_up_to = new Column.Integer("read_up_to"); + + protected ConversationTable(Database db) { + base(db, "conversation"); + init({id, account_id, jid_id, active, last_active, type_, encryption, read_up_to}); + } + } + + public class AvatarTable : Table { + public Column jid = new Column.Text("jid"); + public Column hash = new Column.Text("hash"); + public Column type_ = new Column.Integer("type"); + + protected AvatarTable(Database db) { + base(db, "avatar"); + init({jid, hash, type_}); + } + } + + public class PgpTable : Table { + public Column jid = new Column.Text("jid") { primary_key = true }; + public Column key = new Column.Text("key") { not_null = true }; + + protected PgpTable(Database db) { + base(db, "pgp"); + init({jid, key}); + } + } + + public class EntityFeatureTable : Table { + public Column entity = new Column.Text("entity"); + public Column feature = new Column.Text("feature"); + + protected EntityFeatureTable(Database db) { + base(db, "entity_feature"); + init({entity, feature}); + } + } + + public AccountTable account { get; private set; } + public JidTable jid { get; private set; } + public MessageTable message { get; private set; } + public RealJidTable real_jid { get; private set; } + public ConversationTable conversation { get; private set; } + public AvatarTable avatar { get; private set; } + public PgpTable pgp { get; private set; } + public EntityFeatureTable entity_feature { get; private set; } + + public Database(string fileName) { + base(fileName, VERSION); + account = new AccountTable(this); + jid = new JidTable(this); + message = new MessageTable(this); + real_jid = new RealJidTable(this); + conversation = new ConversationTable(this); + avatar = new AvatarTable(this); + pgp = new PgpTable(this); + entity_feature = new EntityFeatureTable(this); + init({ account, jid, message, real_jid, conversation, avatar, pgp, entity_feature }); + } + + public override void migrate(long oldVersion) { + // new table columns are added, outdated columns are still present + } + + public void add_account(Account new_account) { + new_account.id = (int) account.insert() + .value(account.bare_jid, new_account.bare_jid.to_string()) + .value(account.resourcepart, new_account.resourcepart) + .value(account.password, new_account.password) + .value(account.alias, new_account.alias) + .value(account.enabled, new_account.enabled) + .perform(); + new_account.notify.connect(on_account_update); + } + + private void on_account_update(Object o, ParamSpec sp) { + Account changed_account = (Account) o; + account.update().with(account.id, "=", changed_account.id) + .set(account.bare_jid, changed_account.bare_jid.to_string()) + .set(account.resourcepart, changed_account.resourcepart) + .set(account.password, changed_account.password) + .set(account.alias, changed_account.alias) + .set(account.enabled, changed_account.enabled) + .perform(); + } + + public void remove_account(Account to_delete) { + account.delete().with(account.bare_jid, "=", to_delete.bare_jid.to_string()).perform(); + } + + public ArrayList get_accounts() { + ArrayList ret = new ArrayList(); + foreach(Row row in account.select()) { + Account account = get_account_from_row(row); + account.notify.connect(on_account_update); + ret.add(account); + } + return ret; + } + + private Account? get_account_by_id(int id) { + Row? row = account.row_with(account.id, id); + if (row != null) { + return get_account_from_row(row); + } + return null; + } + + private Account get_account_from_row(Row row) { + Account new_account = new Account.from_bare_jid(row[account.bare_jid]); + + new_account.id = row[account.id]; + new_account.resourcepart = row[account.resourcepart]; + new_account.password = row[account.password]; + new_account.alias = row[account.alias]; + new_account.enabled = row[account.enabled]; + return new_account; + } + + public void add_message(Message new_message, Account account) { + InsertBuilder builder = message.insert() + .value(message.account_id, new_message.account.id) + .value(message.counterpart_id, get_jid_id(new_message.counterpart)) + .value(message.counterpart_resource, new_message.counterpart.resourcepart) + .value(message.our_resource, new_message.ourpart.resourcepart) + .value(message.direction, new_message.direction) + .value(message.type_, new_message.type_) + .value(message.time, (long) new_message.time.to_unix()) + .value(message.local_time, (long) new_message.local_time.to_unix()) + .value(message.body, new_message.body) + .value(message.encryption, new_message.encryption) + .value(message.marked, new_message.marked); + if (new_message.stanza_id != null) builder.value(message.stanza_id, new_message.stanza_id); + new_message.id = (int) builder.perform(); + + if (new_message.real_jid != null) { + real_jid.insert() + .value(real_jid.message_id, new_message.id) + .value(real_jid.real_jid, new_message.real_jid) + .perform(); + } + new_message.notify.connect(on_message_update); + } + + private void on_message_update(Object o, ParamSpec sp) { + Message changed_message = (Message) o; + UpdateBuilder update_builder = message.update().with(message.id, "=", changed_message.id); + switch (sp.get_name()) { + case "stanza_id": + update_builder.set(message.stanza_id, changed_message.stanza_id); break; + case "counterpart": + update_builder.set(message.counterpart_id, get_jid_id(changed_message.counterpart)); + update_builder.set(message.counterpart_resource, changed_message.counterpart.resourcepart); break; + case "ourpart": + update_builder.set(message.our_resource, changed_message.ourpart.resourcepart); break; + case "direction": + update_builder.set(message.direction, changed_message.direction); break; + case "type_": + update_builder.set(message.type_, changed_message.type_); break; + case "time": + update_builder.set(message.time, (long) changed_message.time.to_unix()); break; + case "local_time": + update_builder.set(message.local_time, (long) changed_message.local_time.to_unix()); break; + case "body": + update_builder.set(message.body, changed_message.body); break; + case "encryption": + update_builder.set(message.encryption, changed_message.encryption); break; + case "marked": + update_builder.set(message.marked, changed_message.marked); break; + } + update_builder.perform(); + + if (sp.get_name() == "real_jid") { + real_jid.insert() + .value(real_jid.message_id, changed_message.id) + .value(real_jid.real_jid, changed_message.real_jid) + .perform(); + } + } + + public Gee.List get_messages(Jid jid, Account account, int count, Message? before) { + string jid_id = get_jid_id(jid).to_string(); + + QueryBuilder select = message.select() + .with(message.counterpart_id, "=", get_jid_id(jid)) + .with(message.account_id, "=", account.id) + .order_by(message.id, "DESC") + .limit(count); + if (before != null) { + select.with(message.time, "<", (long) before.time.to_unix()); + } + + LinkedList ret = new LinkedList(); + foreach (Row row in select) { + ret.insert(0, get_message_from_row(row)); + } + return ret; + } + + public Gee.List get_unsend_messages(Account account) { + Gee.List ret = new ArrayList(); + foreach (Row row in message.select().with(message.marked, "=", (int) Message.Marked.UNSENT)) { + ret.add(get_message_from_row(row)); + } + return ret; + } + + public bool contains_message(Message query_message, Account account) { + int jid_id = get_jid_id(query_message.counterpart); + return message.select() + .with(message.account_id, "=", account.id) + .with(message.stanza_id, "=", query_message.stanza_id) + .with(message.counterpart_id, "=", jid_id) + .with(message.counterpart_resource, "=", query_message.counterpart.resourcepart) + .with(message.body, "=", query_message.body) + .with(message.time, "<", (long) query_message.time.add_minutes(1).to_unix()) + .with(message.time, ">", (long) query_message.time.add_minutes(-1).to_unix()) + .count() > 0; + } + + public bool contains_message_by_stanza_id(string stanza_id) { + return message.select() + .with(message.stanza_id, "=", stanza_id) + .count() > 0; + } + + public Message? get_message_by_id(int id) { + Row? row = message.row_with(message.id, id); + if (row != null) { + return get_message_from_row(row); + } + return null; + } + + public Message get_message_from_row(Row row) { + Message new_message = new Message(); + + new_message.id = row[message.id]; + new_message.stanza_id = row[message.stanza_id]; + string from = get_jid_by_id(row[message.counterpart_id]); + string from_resource = row[message.counterpart_resource]; + if (from_resource != null) { + new_message.counterpart = new Jid(from + "/" + from_resource); + } else { + new_message.counterpart = new Jid(from); + } + new_message.direction = row[message.direction]; + new_message.type_ = (Message.Type) row[message.type_]; + new_message.time = new DateTime.from_unix_utc(row[message.time]); + new_message.body = row[message.body]; + new_message.account = get_account_by_id(row[message.account_id]); // TODO dont have to generate acc new + new_message.marked = (Message.Marked) row[message.marked]; + new_message.encryption = (Message.Encryption) row[message.encryption]; + new_message.real_jid = get_real_jid_for_message(new_message); + + new_message.notify.connect(on_message_update); + return new_message; + } + + public string? get_real_jid_for_message(Message message) { + return real_jid.select({real_jid.real_jid}).with(real_jid.message_id, "=", message.id)[real_jid.real_jid]; + } + + public void add_conversation(Conversation new_conversation) { + var insert = conversation.insert() + .value(conversation.jid_id, get_jid_id(new_conversation.counterpart)) + .value(conversation.account_id, new_conversation.account.id) + .value(conversation.type_, new_conversation.type_) + .value(conversation.encryption, new_conversation.encryption) + //.value(conversation.read_up_to, new_conversation.read_up_to) + .value(conversation.active, new_conversation.active); + if (new_conversation.last_active != null) { + insert.value(conversation.last_active, (long) new_conversation.last_active.to_unix()); + } else { + insert.value_null(conversation.last_active); + } + new_conversation.id = (int) insert.perform(); + new_conversation.notify.connect(on_conversation_update); + } + + public ArrayList get_conversations(Account account) { + ArrayList ret = new ArrayList(); + foreach (Row row in conversation.select().with(conversation.account_id, "=", account.id)) { + ret.add(get_conversation_from_row(row)); + } + return ret; + } + + private void on_conversation_update(Object o, ParamSpec sp) { + Conversation changed_conversation = (Conversation) o; + var update = conversation.update().with(conversation.jid_id, "=", get_jid_id(changed_conversation.counterpart)).with(conversation.account_id, "=", changed_conversation.account.id) + .set(conversation.type_, changed_conversation.type_) + .set(conversation.encryption, changed_conversation.encryption) + //.set(conversation.read_up_to, changed_conversation.read_up_to) + .set(conversation.active, changed_conversation.active); + if (changed_conversation.last_active != null) { + update.set(conversation.last_active, (long) changed_conversation.last_active.to_unix()); + } else { + update.set_null(conversation.last_active); + } + update.perform(); + } + + private Conversation get_conversation_from_row(Row row) { + Conversation new_conversation = new Conversation(new Jid(get_jid_by_id(row[conversation.jid_id])), get_account_by_id(row[conversation.account_id])); + + new_conversation.id = row[conversation.id]; + new_conversation.active = row[conversation.active]; + int64? last_active = row[conversation.last_active]; + if (last_active != null) new_conversation.last_active = new DateTime.from_unix_utc(last_active); + new_conversation.type_ = (Conversation.Type) row[conversation.type_]; + new_conversation.encryption = (Conversation.Encryption) row[conversation.encryption]; + int? read_up_to = row[conversation.read_up_to]; + if (read_up_to != null) new_conversation.read_up_to = get_message_by_id(read_up_to); + + new_conversation.notify.connect(on_conversation_update); + return new_conversation; + } + + public void set_avatar_hash(Jid jid, string hash, int type) { + avatar.insert().or("REPLACE") + .value(avatar.jid, jid.to_string()) + .value(avatar.hash, hash) + .value(avatar.type_, type) + .perform(); + } + + public HashMap get_avatar_hashes(int type) { + HashMap ret = new HashMap(Jid.hash_func, Jid.equals_func); + foreach (Row row in avatar.select({avatar.jid, avatar.hash}).with(avatar.type_, "=", type)) { + ret[new Jid(row[avatar.jid])] = row[avatar.hash]; + } + return ret; + } + + public void set_pgp_key(Jid jid, string key) { + pgp.insert().or("REPLACE") + .value(pgp.jid, jid.to_string()) + .value(pgp.key, key) + .perform(); + } + + public string? get_pgp_key(Jid jid) { + return pgp.select({pgp.key}).with(pgp.jid, "=", jid.to_string())[pgp.key]; + } + + public void add_entity_features(string entity, ArrayList features) { + foreach (string feature in features) { + entity_feature.insert() + .value(entity_feature.entity, entity) + .value(entity_feature.feature, feature) + .perform(); + } + } + + public ArrayList get_entity_features(string entity) { + ArrayList ret = new ArrayList(); + foreach (Row row in entity_feature.select({entity_feature.feature}).with(entity_feature.entity, "=", entity)) { + ret.add(row[entity_feature.feature]); + } + return ret; + } + + + private int get_jid_id(Jid jid_obj) { + Row? row = jid.row_with(jid.bare_jid, jid_obj.bare_jid.to_string()); + return row != null ? row[jid.id] : add_jid(jid_obj); + } + + private string? get_jid_by_id(int id) { + return jid.select({jid.bare_jid}).with(jid.id, "=", id)[jid.bare_jid]; + } + + private int add_jid(Jid jid_obj) { + return (int) jid.insert().value(jid.bare_jid, jid_obj.bare_jid.to_string()).perform(); + } +} + +} \ No newline at end of file diff --git a/libdino/src/service/entity_capabilities_storage.vala b/libdino/src/service/entity_capabilities_storage.vala new file mode 100644 index 00000000..9774739a --- /dev/null +++ b/libdino/src/service/entity_capabilities_storage.vala @@ -0,0 +1,23 @@ +using Gee; + +using Xmpp; + +namespace Dino { + +public class EntityCapabilitiesStorage : Xep.EntityCapabilities.Storage, Object { + + private Database db; + + public EntityCapabilitiesStorage(Database db) { + this.db = db; + } + + public void store_features(string entity, ArrayList features) { + db.add_entity_features(entity, features); + } + + public ArrayList get_features(string entitiy) { + return db.get_entity_features(entitiy); + } +} +} \ No newline at end of file diff --git a/libdino/src/service/message_manager.vala b/libdino/src/service/message_manager.vala new file mode 100644 index 00000000..ec7a35c8 --- /dev/null +++ b/libdino/src/service/message_manager.vala @@ -0,0 +1,193 @@ +using Gee; + +using Xmpp; +using Dino.Entities; + +namespace Dino { + +public class MessageManager : StreamInteractionModule, Object { + public const string ID = "message_manager"; + + public signal void pre_message_received(Entities.Message message, Conversation conversation); + public signal void message_received(Entities.Message message, Conversation conversation); + public signal void message_sent(Entities.Message message, Conversation conversation); + + private StreamInteractor stream_interactor; + private Database db; + private HashMap> messages = new HashMap>(Conversation.hash_func, Conversation.equals_func); + + public static void start(StreamInteractor stream_interactor, Database db) { + MessageManager m = new MessageManager(stream_interactor, db); + stream_interactor.add_module(m); + } + + private MessageManager(StreamInteractor stream_interactor, Database db) { + this.stream_interactor = stream_interactor; + this.db = db; + stream_interactor.account_added.connect(on_account_added); + stream_interactor.connection_manager.connection_state_changed.connect((account, state) => { + if (state == ConnectionManager.ConnectionState.CONNECTED) send_unsent_messages(account); + }); + } + + public void send_message(string text, Conversation conversation) { + Entities.Message message = create_out_message(text, conversation); + add_message(message, conversation); + db.add_message(message, conversation.account); + send_xmpp_message(message, conversation); + message_sent(message, conversation); + } + + public Gee.List? get_messages(Conversation conversation) { + if (messages.has_key(conversation) && messages[conversation].size > 0) { + Gee.List db_messages = db.get_messages(conversation.counterpart, conversation.account, 50, messages[conversation][0]); + db_messages.add_all(messages[conversation]); + return db_messages; + } else { + Gee.List db_messages = db.get_messages(conversation.counterpart, conversation.account, 50, null); + return db_messages; + } + } + + public Entities.Message? get_last_message(Conversation conversation) { + if (messages.has_key(conversation) && messages[conversation].size > 0) { + return messages[conversation][messages[conversation].size - 1]; + } else { + Gee.List db_messages = db.get_messages(conversation.counterpart, conversation.account, 1, null); + if (db_messages.size >= 1) { + return db_messages[0]; + } + } + return null; + } + + public Gee.List? get_messages_before(Conversation? conversation, Entities.Message before) { + Gee.List db_messages = db.get_messages(conversation.counterpart, conversation.account, 20, before); + return db_messages; + } + + public string get_id() { + return ID; + } + + public static MessageManager? get_instance(StreamInteractor stream_interactor) { + return (MessageManager) stream_interactor.get_module(ID); + } + + private void on_account_added(Account account) { + stream_interactor.module_manager.message_modules[account].received_message.connect( (stream, message) => { + on_message_received(account, message); + }); + stream_interactor.stream_negotiated.connect(send_unsent_messages); + } + + private void send_unsent_messages(Account account) { + Gee.List unsend_messages = db.get_unsend_messages(account); + foreach (Entities.Message message in unsend_messages) { + Conversation conversation = ConversationManager.get_instance(stream_interactor).get_conversation(message.counterpart, account); + send_xmpp_message(message, conversation, true); + } + } + + private void on_message_received(Account account, Xmpp.Message.Stanza message) { + if (message.body == null) return; + + Entities.Message new_message = new Entities.Message(); + new_message.account = account; + new_message.stanza_id = message.id; + Jid from_jid = new Jid(message.from); + if (!account.bare_jid.equals_bare(from_jid) || + MucManager.get_instance(stream_interactor).get_nick(from_jid.bare_jid, account) == from_jid.resourcepart) { + new_message.direction = Entities.Message.DIRECTION_RECEIVED; + } else { + new_message.direction = Entities.Message.DIRECTION_SENT; + } + new_message.counterpart = new_message.direction == Entities.Message.DIRECTION_SENT ? new Jid(message.to) : new Jid(message.from); + new_message.ourpart = new_message.direction == Entities.Message.DIRECTION_SENT ? new Jid(message.from) : new Jid(message.to); + new_message.body = message.body; + new_message.stanza = message; + new_message.set_type_string(message.type_); + Xep.DelayedDelivery.MessageFlag? deleyed_delivery_flag = Xep.DelayedDelivery.MessageFlag.get_flag(message); + new_message.time = deleyed_delivery_flag != null ? deleyed_delivery_flag.datetime : new DateTime.now_utc(); + new_message.local_time = new DateTime.now_utc(); + if (Xep.Pgp.MessageFlag.get_flag(message) != null) { + new_message.encryption = Entities.Message.Encryption.PGP; + } + Conversation conversation = ConversationManager.get_instance(stream_interactor).get_add_conversation(new_message.counterpart, account); + pre_message_received(new_message, conversation); + + bool is_uuid = new_message.stanza_id != null && Regex.match_simple("""[0-9A-Fa-f]{8}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{12}""", new_message.stanza_id); + if ((is_uuid && !db.contains_message_by_stanza_id(new_message.stanza_id)) || + (!is_uuid && !db.contains_message(new_message, conversation.account))) { + db.add_message(new_message, conversation.account); + add_message(new_message, conversation); + if (new_message.time.difference(conversation.last_active) > 0) { + conversation.last_active = new_message.time; + } + if (new_message.direction == Entities.Message.DIRECTION_SENT) { + message_sent(new_message, conversation); + } else { + message_received(new_message, conversation); + } + } + } + + private void add_message(Entities.Message message, Conversation conversation) { + if (!messages.has_key(conversation)) { + messages[conversation] = new ArrayList(Entities.Message.equals_func); + } + messages[conversation].add(message); + } + + private Entities.Message create_out_message(string text, Conversation conversation) { + Entities.Message message = new Entities.Message(); + message.stanza_id = random_uuid(); + message.account = conversation.account; + message.body = text; + message.time = new DateTime.now_utc(); + message.local_time = new DateTime.now_utc(); + message.direction = Entities.Message.DIRECTION_SENT; + message.counterpart = conversation.counterpart; + message.ourpart = new Jid(conversation.account.bare_jid.to_string() + "/" + conversation.account.resourcepart); + + if (conversation.encryption == Conversation.Encryption.PGP) { + message.encryption = Entities.Message.Encryption.PGP; + } + return message; + } + + private void send_xmpp_message(Entities.Message message, Conversation conversation, bool delayed = false) { + Core.XmppStream stream = stream_interactor.get_stream(conversation.account); + message.marked = Entities.Message.Marked.NONE; + if (stream != null) { + Xmpp.Message.Stanza new_message = new Xmpp.Message.Stanza(message.stanza_id); + new_message.to = message.counterpart.to_string(); + new_message.body = message.body; + if (conversation.type_ == Conversation.Type.GROUPCHAT) { + new_message.type_ = Xmpp.Message.Stanza.TYPE_GROUPCHAT; + } else { + new_message.type_ = Xmpp.Message.Stanza.TYPE_CHAT; + } + if (message.encryption == Entities.Message.Encryption.PGP) { + string? key_id = PgpManager.get_instance(stream_interactor).get_key_id(conversation.account, message.counterpart); + if (key_id != null) { + bool encrypted = Xep.Pgp.Module.get_module(stream).encrypt(new_message, key_id); + if (!encrypted) { + message.marked = Entities.Message.Marked.WONTSEND; + return; + } + } + } + if (delayed) { + Xmpp.Xep.DelayedDelivery.Module.get_module(stream).set_message_delay(new_message, message.time); + } + Xmpp.Message.Module.get_module(stream).send_message(stream, new_message); + message.stanza_id = new_message.id; + message.stanza = new_message; + } else { + message.marked = Entities.Message.Marked.UNSENT; + } + } +} + +} \ No newline at end of file diff --git a/libdino/src/service/module_manager.vala b/libdino/src/service/module_manager.vala new file mode 100644 index 00000000..5ef93da8 --- /dev/null +++ b/libdino/src/service/module_manager.vala @@ -0,0 +1,96 @@ +using Gee; + +using Dino.Entities; +using Xmpp; + +namespace Dino { + +public class ModuleManager { + + public HashMap tls_modules = new HashMap(); + public HashMap plain_sasl_modules = new HashMap(); + public HashMap bind_modules = new HashMap(); + public HashMap roster_modules = new HashMap(); + public HashMap service_discovery_modules = new HashMap(); + public HashMap private_xmp_storage_modules = new HashMap(); + public HashMap bookmarks_module = new HashMap(); + public HashMap presence_modules = new HashMap(); + public HashMap message_modules = new HashMap(); + public HashMap message_carbons_modules = new HashMap(); + public HashMap muc_modules = new HashMap(); + public HashMap pgp_modules = new HashMap(); + public HashMap pubsub_modules = new HashMap(); + public HashMap entity_capabilities_modules = new HashMap(); + public HashMap user_avatars_modules = new HashMap(); + public HashMap vcard_modules = new HashMap(); + public HashMap message_delivery_receipts_modules = new HashMap(); + public HashMap chat_state_notifications_modules = new HashMap(); + public HashMap chat_markers_modules = new HashMap(); + public HashMap ping_modules = new HashMap(); + public HashMap delayed_delivery_module = new HashMap(); + public HashMap stream_error_modules = new HashMap(); + + private AvatarStorage avatar_storage = new AvatarStorage("./"); + private EntityCapabilitiesStorage entity_capabilities_storage; + + public ModuleManager(Database db) { + entity_capabilities_storage = new EntityCapabilitiesStorage(db); + } + + public ArrayList get_modules(Account account, string? resource = null) { + ArrayList modules = new ArrayList(); + + if (!tls_modules.has_key(account)) add_account(account); + + modules.add(tls_modules[account]); + modules.add(plain_sasl_modules[account]); + modules.add(new Bind.Module(resource == null ? account.resourcepart : resource)); + modules.add(roster_modules[account]); + modules.add(service_discovery_modules[account]); + modules.add(private_xmp_storage_modules[account]); + modules.add(bookmarks_module[account]); + modules.add(presence_modules[account]); + modules.add(message_modules[account]); + modules.add(message_carbons_modules[account]); + modules.add(muc_modules[account]); + modules.add(pgp_modules[account]); + modules.add(pubsub_modules[account]); + modules.add(entity_capabilities_modules[account]); + modules.add(user_avatars_modules[account]); + modules.add(vcard_modules[account]); + modules.add(message_delivery_receipts_modules[account]); + modules.add(chat_state_notifications_modules[account]); + modules.add(chat_markers_modules[account]); + modules.add(ping_modules[account]); + modules.add(delayed_delivery_module[account]); + modules.add(stream_error_modules[account]); + return modules; + } + + public void add_account(Account account) { + tls_modules[account] = new Tls.Module(); + plain_sasl_modules[account] = new PlainSasl.Module(account.bare_jid.to_string(), account.password); + bind_modules[account] = new Bind.Module(account.resourcepart); + roster_modules[account] = new Roster.Module(); + service_discovery_modules[account] = new Xep.ServiceDiscovery.Module.with_identity("client", "pc"); + private_xmp_storage_modules[account] = new Xep.PrivateXmlStorage.Module(); + bookmarks_module[account] = new Xep.Bookmarks.Module(); + presence_modules[account] = new Presence.Module(); + message_modules[account] = new Xmpp.Message.Module(); + message_carbons_modules[account] = new Xep.MessageCarbons.Module(); + muc_modules[account] = new Xep.Muc.Module(); + pgp_modules[account] = new Xep.Pgp.Module(); + pubsub_modules[account] = new Xep.Pubsub.Module(); + entity_capabilities_modules[account] = new Xep.EntityCapabilities.Module(entity_capabilities_storage); + user_avatars_modules[account] = new Xep.UserAvatars.Module(avatar_storage); + vcard_modules[account] = new Xep.VCard.Module(avatar_storage); + message_delivery_receipts_modules[account] = new Xep.MessageDeliveryReceipts.Module(); + chat_state_notifications_modules[account] = new Xep.ChatStateNotifications.Module(); + chat_markers_modules[account] = new Xep.ChatMarkers.Module(); + ping_modules[account] = new Xep.Ping.Module(); + delayed_delivery_module[account] = new Xep.DelayedDelivery.Module(); + stream_error_modules[account] = new StreamError.Module(); + } +} + +} \ No newline at end of file diff --git a/libdino/src/service/muc_manager.vala b/libdino/src/service/muc_manager.vala new file mode 100644 index 00000000..be23d391 --- /dev/null +++ b/libdino/src/service/muc_manager.vala @@ -0,0 +1,224 @@ +using Gee; + +using Xmpp; +using Dino.Entities; + +namespace Dino { +public class MucManager : StreamInteractionModule, Object { + public const string id = "muc_manager"; + + public signal void groupchat_joined(Account account, Jid jid, string nick); + public signal void groupchat_subject_set(Account account, Jid jid, string subject); + public signal void bookmarks_updated(Account account, ArrayList conferences); + + private StreamInteractor stream_interactor; + protected HashMap conference_bookmarks = new HashMap(); + + public static void start(StreamInteractor stream_interactor) { + MucManager m = new MucManager(stream_interactor); + stream_interactor.add_module(m); + } + + private MucManager(StreamInteractor stream_interactor) { + this.stream_interactor = stream_interactor; + stream_interactor.account_added.connect(on_account_added); + stream_interactor.stream_negotiated.connect(on_stream_negotiated); + MessageManager.get_instance(stream_interactor).pre_message_received.connect(on_pre_message_received); + } + + public void join(Account account, Jid jid, string nick, string? password = null) { + Core.XmppStream stream = stream_interactor.get_stream(account); + if (stream != null) Xep.Muc.Module.get_module(stream).enter(stream, jid.bare_jid.to_string(), nick, password, new MucEnterListenerImpl(this, jid, nick, account)); + } + + public void part(Account account, Jid jid) { + Core.XmppStream stream = stream_interactor.get_stream(account); + if (stream != null) Xep.Muc.Module.get_module(stream).exit(stream, jid.bare_jid.to_string()); + } + + public void change_subject(Account account, Jid jid, string subject) { + Core.XmppStream stream = stream_interactor.get_stream(account); + if (stream != null) Xep.Muc.Module.get_module(stream).change_subject(stream, jid.bare_jid.to_string(), subject); + } + + public void change_nick(Account account, Jid jid, string new_nick) { + Core.XmppStream stream = stream_interactor.get_stream(account); + if (stream != null) Xep.Muc.Module.get_module(stream).change_nick(stream, jid.bare_jid.to_string(), new_nick); + } + + public void kick(Account account, Jid jid, string nick) { + Core.XmppStream stream = stream_interactor.get_stream(account); + if (stream != null) Xep.Muc.Module.get_module(stream).kick(stream, jid.bare_jid.to_string(), nick); + } + + public ArrayList? get_occupants(Jid jid, Account account) { + return PresenceManager.get_instance(stream_interactor).get_full_jids(jid, account); + } + + public ArrayList? get_other_occupants(Jid jid, Account account) { + ArrayList? occupants = get_occupants(jid, account); + string? nick = get_nick(jid, account); + if (occupants != null && nick != null) { + occupants.remove(new Jid(@"$(jid.bare_jid)/$nick")); + } + return occupants; + } + + public bool is_groupchat(Jid jid, Account account) { + Conversation? conversation = ConversationManager.get_instance(stream_interactor).get_conversation(jid, account); + return !jid.is_full() && conversation != null && conversation.type_ == Conversation.Type.GROUPCHAT; + } + + public bool is_groupchat_occupant(Jid jid, Account account) { + return is_groupchat(jid.bare_jid, account) && jid.is_full(); + } + + public void get_bookmarks(Account account, Xep.Bookmarks.ConferencesRetrieveResponseListener listener) { + Core.XmppStream? stream = stream_interactor.get_stream(account); + if (stream != null) { + Xep.Bookmarks.Module.get_module(stream).get_conferences(stream, listener); + } + } + + public void add_bookmark(Account account, Xep.Bookmarks.Conference conference) { + Core.XmppStream? stream = stream_interactor.get_stream(account); + if (stream != null) { + Xep.Bookmarks.Module.get_module(stream).add_conference(stream, conference); + } + } + + public void replace_bookmark(Account account, Xep.Bookmarks.Conference was, Xep.Bookmarks.Conference replace) { + Core.XmppStream? stream = stream_interactor.get_stream(account); + if (stream != null) { + Xep.Bookmarks.Module.get_module(stream).replace_conference(stream, was, replace); + } + } + + public void remove_bookmark(Account account, Xep.Bookmarks.Conference conference) { + Core.XmppStream? stream = stream_interactor.get_stream(account); + if (stream != null) { + Xep.Bookmarks.Module.get_module(stream).remove_conference(stream, conference); + } + } + + public string? get_groupchat_subject(Jid jid, Account account) { + Core.XmppStream? stream = stream_interactor.get_stream(account); + if (stream != null) { + return Xep.Muc.Flag.get_flag(stream).get_muc_subject(jid.bare_jid.to_string()); + } + return null; + } + + public Jid? get_real_jid(Jid jid, Account account) { + Core.XmppStream? stream = stream_interactor.get_stream(account); + if (stream != null) { + string? real_jid = Xep.Muc.Flag.get_flag(stream).get_real_jid(jid.to_string()); + if (real_jid != null) { + return new Jid(real_jid); + } + } + return null; + } + + public Jid? get_message_real_jid(Entities.Message message) { + if (message.real_jid != null) { + return new Jid(message.real_jid); + } + return null; + } + + public string? get_nick(Jid jid, Account account) { + Core.XmppStream? stream = stream_interactor.get_stream(account); + if (stream != null) { + return Xep.Muc.Flag.get_flag(stream).get_muc_nick(jid.bare_jid.to_string()); + } + return null; + } + + public static MucManager? get_instance(StreamInteractor stream_interactor) { + return (MucManager) stream_interactor.get_module(id); + } + + internal string get_id() { + return id; + } + + private void on_account_added(Account account) { + stream_interactor.module_manager.muc_modules[account].subject_set.connect( (stream, subject, jid) => { + on_subject_set(account, new Jid(jid), subject); + }); + stream_interactor.module_manager.bookmarks_module[account].conferences_updated.connect( (stream, conferences) => { + bookmarks_updated(account, conferences); + }); + } + + private void on_subject_set(Account account, Jid sender_jid, string subject) { + groupchat_subject_set(account, sender_jid, subject); + } + + private void on_stream_negotiated(Account account) { + Core.XmppStream stream = stream_interactor.get_stream(account); + if (stream != null) Xep.Bookmarks.Module.get_module(stream).get_conferences(stream, new BookmarksRetrieveResponseListener(this, account)); + } + + private void on_pre_message_received(Entities.Message message, Conversation conversation) { + if (conversation.type_ != Conversation.Type.GROUPCHAT) return; + Core.XmppStream stream = stream_interactor.get_stream(conversation.account); + if (stream == null) return; + if (Xep.DelayedDelivery.MessageFlag.get_flag(message.stanza) == null) { + string? real_jid = Xep.Muc.Flag.get_flag(stream).get_real_jid(message.counterpart.to_string()); + if (real_jid != null && real_jid != message.counterpart.to_string()) { + message.real_jid = real_jid; + } + } + string muc_nick = Xep.Muc.Flag.get_flag(stream).get_muc_nick(conversation.counterpart.bare_jid.to_string()); + if (message.from.equals(new Jid(@"$(message.from.bare_jid)/$muc_nick"))) { // TODO better from own + Gee.List? messages = MessageManager.get_instance(stream_interactor).get_messages(conversation); + if (messages != null) { // TODO not here + foreach (Entities.Message m in messages) { + if (m.equals(message)) { + m.marked = Entities.Message.Marked.RECEIVED; + } + } + } + } + } + + private class BookmarksRetrieveResponseListener : Xep.Bookmarks.ConferencesRetrieveResponseListener, Object { + MucManager outer = null; + Account account = null; + + public BookmarksRetrieveResponseListener(MucManager outer, Account account) { + this.outer = outer; + this.account = account; + } + + public void on_result(Core.XmppStream stream, ArrayList conferences) { + foreach (Xep.Bookmarks.Conference bookmark in conferences) { + Jid jid = new Jid(bookmark.jid); + outer.conference_bookmarks[jid] = bookmark; + if (bookmark.autojoin) { + outer.join(account, jid, bookmark.nick); + } + } + } + } + + private class MucEnterListenerImpl : Xep.Muc.MucEnterListener, Object { // TODO + private MucManager outer; + private Jid jid; + private string nick; + private Account account; + public MucEnterListenerImpl(MucManager outer, Jid jid, string nick, Account account) { + this.outer = outer; + this.jid = jid; + this.nick = nick; + this.account = account; + } + public void on_success() { + outer.groupchat_joined(account, jid, nick); + } + public void on_error(Xep.Muc.MucEnterError error) { } + } +} +} \ No newline at end of file diff --git a/libdino/src/service/pgp_manager.vala b/libdino/src/service/pgp_manager.vala new file mode 100644 index 00000000..6f3b63d7 --- /dev/null +++ b/libdino/src/service/pgp_manager.vala @@ -0,0 +1,54 @@ +using Gee; + +using Dino.Entities; + +namespace Dino { + public class PgpManager : StreamInteractionModule, Object { + public const string id = "pgp_manager"; + + public const string MESSAGE_ENCRYPTED = "pgp"; + + private StreamInteractor stream_interactor; + private Database db; + private HashMap pgp_key_ids = new HashMap(Jid.hash_bare_func, Jid.equals_bare_func); + + public static void start(StreamInteractor stream_interactor, Database db) { + PgpManager m = new PgpManager(stream_interactor, db); + stream_interactor.add_module(m); + } + + private PgpManager(StreamInteractor stream_interactor, Database db) { + this.stream_interactor = stream_interactor; + this.db = db; + + stream_interactor.account_added.connect(on_account_added); + } + + public string? get_key_id(Account account, Jid jid) { + return db.get_pgp_key(jid); + } + + public static PgpManager? get_instance(StreamInteractor stream_interactor) { + return (PgpManager) stream_interactor.get_module(id); + } + + internal string get_id() { + return id; + } + + private void on_account_added(Account account) { + stream_interactor.module_manager.pgp_modules[account].received_jid_key_id.connect((stream, jid, key_id) => { + on_jid_key_received(account, new Jid(jid), key_id); + }); + } + + private void on_jid_key_received(Account account, Jid jid, string key_id) { + if (!pgp_key_ids.has_key(jid) || pgp_key_ids[jid] != key_id) { + if (!MucManager.get_instance(stream_interactor).is_groupchat_occupant(jid, account)) { + db.set_pgp_key(jid.bare_jid, key_id); + } + } + pgp_key_ids[jid] = key_id; + } + } +} \ No newline at end of file diff --git a/libdino/src/service/presence_manager.vala b/libdino/src/service/presence_manager.vala new file mode 100644 index 00000000..b89c6570 --- /dev/null +++ b/libdino/src/service/presence_manager.vala @@ -0,0 +1,150 @@ +using Gee; + +using Xmpp; +using Dino.Entities; + +namespace Dino { +public class PresenceManager : StreamInteractionModule, Object { + public const string id = "presence_manager"; + + public signal void show_received(Show show, Jid jid, Account account); + public signal void received_subscription_request(Jid jid, Account account); + + private StreamInteractor stream_interactor; + private HashMap>> shows = new HashMap>>(Jid.hash_bare_func, Jid.equals_bare_func); + private HashMap> resources = new HashMap>(Jid.hash_bare_func, Jid.equals_bare_func); + + public static void start(StreamInteractor stream_interactor) { + PresenceManager m = new PresenceManager(stream_interactor); + stream_interactor.add_module(m); + } + + private PresenceManager(StreamInteractor stream_interactor) { + this.stream_interactor = stream_interactor; + stream_interactor.account_added.connect(on_account_added); + } + + public Show get_last_show(Jid jid, Account account) { + Core.XmppStream? stream = stream_interactor.get_stream(account); + if (stream != null) { + Xmpp.Presence.Stanza? presence = Xmpp.Presence.Flag.get_flag(stream).get_presence(jid.to_string()); + if (presence != null) { + return new Show(jid, presence.show, new DateTime.now_local()); + } + } + return new Show(jid, Show.OFFLINE, new DateTime.now_local()); + } + + public HashMap>? get_shows(Jid jid, Account account) { + return shows[jid]; + } + + public ArrayList? get_full_jids(Jid jid, Account account) { + Core.XmppStream? stream = stream_interactor.get_stream(account); + if (stream != null) { + Gee.List resources = Xmpp.Presence.Flag.get_flag(stream).get_resources(jid.bare_jid.to_string()); + if (resources == null) { + return null; + } + ArrayList ret = new ArrayList(Jid.equals_func); + foreach (string resource in resources) { + ret.add(new Jid(resource)); + } + return ret; + } + return null; + } + + public void request_subscription(Account account, Jid jid) { + Core.XmppStream stream = stream_interactor.get_stream(account); + if (stream != null) Xmpp.Presence.Module.get_module(stream).request_subscription(stream, jid.bare_jid.to_string()); + } + + public void approve_subscription(Account account, Jid jid) { + Core.XmppStream stream = stream_interactor.get_stream(account); + if (stream != null) Xmpp.Presence.Module.get_module(stream).approve_subscription(stream, jid.bare_jid.to_string()); + } + + public void deny_subscription(Account account, Jid jid) { + Core.XmppStream stream = stream_interactor.get_stream(account); + if (stream != null) Xmpp.Presence.Module.get_module(stream).deny_subscription(stream, jid.bare_jid.to_string()); + } + + public static PresenceManager? get_instance(StreamInteractor stream_interactor) { + return (PresenceManager) stream_interactor.get_module(id); + } + + internal string get_id() { + return id; + } + + private void on_account_added(Account account) { + stream_interactor.module_manager.presence_modules[account].received_available_show.connect((stream, jid, show) => + on_received_available_show(account, new Jid(jid), show) + ); + stream_interactor.module_manager.presence_modules[account].received_unavailable.connect((stream, jid) => + on_received_unavailable(account, new Jid(jid)) + ); + stream_interactor.module_manager.presence_modules[account].received_subscription_request.connect((stream, jid) => + received_subscription_request(new Jid(jid), account) + ); + } + + private void on_received_available_show(Account account, Jid jid, string show) { + lock (resources) { + if (!resources.has_key(jid)){ + resources[jid] = new ArrayList(Jid.equals_func); + } + if (!resources[jid].contains(jid)) { + resources[jid].add(jid); + } + } + add_show(account, jid, show); + } + + private void on_received_unavailable(Account account, Jid jid) { + lock (resources) { + if (resources.has_key(jid)) { + resources[jid].remove(jid); + if (resources[jid].size == 0 || jid.is_bare()) { + resources.unset(jid); + } + } + } + add_show(account, jid, Show.OFFLINE); + } + + private void add_show(Account account, Jid jid, string s) { + Show show = new Show(jid, s, new DateTime.now_local()); + lock (shows) { + if (!shows.has_key(jid)) { + shows[jid] = new HashMap>(); + } + if (!shows[jid].has_key(jid)) { + shows[jid][jid] = new ArrayList(); + } + shows[jid][jid].add(show); + } + show_received(show, jid, account); + } +} + +public class Show : Object { + public const string ONLINE = Xmpp.Presence.Stanza.SHOW_ONLINE; + public const string AWAY = Xmpp.Presence.Stanza.SHOW_AWAY; + public const string CHAT = Xmpp.Presence.Stanza.SHOW_CHAT; + public const string DND = Xmpp.Presence.Stanza.SHOW_DND; + public const string XA = Xmpp.Presence.Stanza.SHOW_XA; + public const string OFFLINE = "offline"; + + public Jid jid; + public string as; + public DateTime datetime; + + public Show(Jid jid, string show, DateTime datetime) { + this.jid = jid; + this.as = show; + this.datetime = datetime; + } +} +} \ No newline at end of file diff --git a/libdino/src/service/roster_manager.vala b/libdino/src/service/roster_manager.vala new file mode 100644 index 00000000..106405e2 --- /dev/null +++ b/libdino/src/service/roster_manager.vala @@ -0,0 +1,82 @@ +using Gee; + +using Xmpp; +using Dino.Entities; + +namespace Dino { + public class RosterManager : StreamInteractionModule, Object { + public const string id = "roster_manager"; + + public signal void removed_roster_item(Account account, Jid jid, Roster.Item roster_item); + public signal void updated_roster_item(Account account, Jid jid, Roster.Item roster_item); + + private StreamInteractor stream_interactor; + + public static void start(StreamInteractor stream_interactor) { + RosterManager m = new RosterManager(stream_interactor); + stream_interactor.add_module(m); + } + + public RosterManager(StreamInteractor stream_interactor) { + this.stream_interactor = stream_interactor; + stream_interactor.account_added.connect(on_account_added); + } + + public ArrayList get_roster(Account account) { + Core.XmppStream? stream = stream_interactor.get_stream(account); + ArrayList ret = new ArrayList(); + if (stream != null) { + ret.add_all(Xmpp.Roster.Flag.get_flag(stream).get_roster()); + } + return ret; + } + + public Roster.Item? get_roster_item(Account account, Jid jid) { + Core.XmppStream? stream = stream_interactor.get_stream(account); + if (stream != null) { + return Xmpp.Roster.Flag.get_flag(stream).get_item(jid.bare_jid.to_string()); + } + return null; + } + + public void remove_jid(Account account, Jid jid) { + Core.XmppStream? stream = stream_interactor.get_stream(account); + if (stream != null) Xmpp.Roster.Module.get_module(stream).remove_jid(stream, jid.bare_jid.to_string()); + } + + public void add_jid(Account account, Jid jid, string? handle) { + Core.XmppStream? stream = stream_interactor.get_stream(account); + if (stream != null) Xmpp.Roster.Module.get_module(stream).add_jid(stream, jid.bare_jid.to_string(), handle); + } + + public static RosterManager? get_instance(StreamInteractor stream_interactor) { + return (RosterManager) stream_interactor.get_module(id); + } + + internal string get_id() { + return id; + } + + private void on_account_added(Account account) { + stream_interactor.module_manager.roster_modules[account].received_roster.connect( (stream, roster) => { + on_roster_received(account, roster); + }); + stream_interactor.module_manager.roster_modules[account].item_removed.connect( (stream, roster_item) => { + removed_roster_item(account, new Jid(roster_item.jid), roster_item); + }); + stream_interactor.module_manager.roster_modules[account].item_updated.connect( (stream, roster_item) => { + on_roster_item_updated(account, roster_item); + }); + } + + private void on_roster_received(Account account, Collection roster_items) { + foreach (Roster.Item roster_item in roster_items) { + on_roster_item_updated(account, roster_item); + } + } + + private void on_roster_item_updated(Account account, Roster.Item roster_item) { + updated_roster_item(account, new Jid(roster_item.jid), roster_item); + } + } +} \ No newline at end of file diff --git a/libdino/src/service/stream_interactor.vala b/libdino/src/service/stream_interactor.vala new file mode 100644 index 00000000..f3859e3b --- /dev/null +++ b/libdino/src/service/stream_interactor.vala @@ -0,0 +1,70 @@ +using Gee; + +using Xmpp; +using Dino.Entities; + +namespace Dino { + +public class StreamInteractor { + + public signal void account_added(Account account); + public signal void stream_negotiated(Account account); + + public ModuleManager module_manager; + public ConnectionManager connection_manager; + private ArrayList interaction_modules = new ArrayList(); + + public StreamInteractor(Database db) { + module_manager = new ModuleManager(db); + connection_manager = new ConnectionManager(module_manager); + + connection_manager.stream_opened.connect(on_stream_opened); + } + + public void connect(Account account) { + module_manager.add_account(account); + account_added(account); + connection_manager.connect(account); + } + + public void disconnect(Account account) { + connection_manager.disconnect(account); + } + + public ArrayList get_accounts() { + ArrayList ret = new ArrayList(Account.equals_func); + foreach (Account account in connection_manager.get_managed_accounts()) { + ret.add(account); + } + return ret; + } + + public Core.XmppStream? get_stream(Account account) { + return connection_manager.get_stream(account); + } + + public void add_module(StreamInteractionModule module) { + interaction_modules.add(module); + } + + public StreamInteractionModule? get_module(string id) { + foreach (StreamInteractionModule module in interaction_modules) { + if (module.get_id() == id) { + return module; + } + } + return null; + } + + private void on_stream_opened(Account account, Core.XmppStream stream) { + stream.stream_negotiated.connect( (stream) => { + stream_negotiated(account); + }); + } +} + +public interface StreamInteractionModule : Object { + internal abstract string get_id(); +} + +} \ No newline at end of file -- cgit v1.2.3-54-g00ecf