aboutsummaryrefslogtreecommitdiff
path: root/client/src/service
diff options
context:
space:
mode:
Diffstat (limited to 'client/src/service')
-rw-r--r--client/src/service/avatar_manager.vala134
-rw-r--r--client/src/service/avatar_storage.vala34
-rw-r--r--client/src/service/chat_interaction.vala146
-rw-r--r--client/src/service/connection_manager.vala222
-rw-r--r--client/src/service/conversation_manager.vala98
-rw-r--r--client/src/service/counterpart_interaction_manager.vala99
-rw-r--r--client/src/service/database.vala457
-rw-r--r--client/src/service/entity_capabilities_storage.vala23
-rw-r--r--client/src/service/message_manager.vala166
-rw-r--r--client/src/service/module_manager.vala96
-rw-r--r--client/src/service/muc_manager.vala224
-rw-r--r--client/src/service/pgp_manager.vala54
-rw-r--r--client/src/service/presence_manager.vala150
-rw-r--r--client/src/service/roster_manager.vala82
-rw-r--r--client/src/service/stream_interactor.vala68
15 files changed, 2053 insertions, 0 deletions
diff --git a/client/src/service/avatar_manager.vala b/client/src/service/avatar_manager.vala
new file mode 100644
index 00000000..de44c419
--- /dev/null
+++ b/client/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<Jid, string> user_avatars = new HashMap<Jid, string>(Jid.hash_func, Jid.equals_func);
+ private HashMap<Jid, string> vcard_avatars = new HashMap<Jid, string>(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/client/src/service/avatar_storage.vala b/client/src/service/avatar_storage.vala
new file mode 100644
index 00000000..a9a8fb86
--- /dev/null
+++ b/client/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/client/src/service/chat_interaction.vala b/client/src/service/chat_interaction.vala
new file mode 100644
index 00000000..ed805a93
--- /dev/null
+++ b/client/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<Conversation, DateTime> last_input_interaction = new HashMap<Conversation, DateTime>(Conversation.hash_func, Conversation.equals_func);
+ private HashMap<Conversation, DateTime> last_interface_interaction = new HashMap<Conversation, DateTime>(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<Conversation> remove_input = new ArrayList<Conversation>(Conversation.equals_func);
+ ArrayList<Conversation> remove_interface = new ArrayList<Conversation>(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/client/src/service/connection_manager.vala b/client/src/service/connection_manager.vala
new file mode 100644
index 00000000..91664af5
--- /dev/null
+++ b/client/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<Account> connection_todo = new ArrayList<Account>(Account.equals_func);
+ private HashMap<Account, Connection> stream_states = new HashMap<Account, Connection>(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<Account> 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 = true;
+
+ 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<void*> (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 + "-" + UUID.generate_random_unparsed());
+ } 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/client/src/service/conversation_manager.vala b/client/src/service/conversation_manager.vala
new file mode 100644
index 00000000..5337f007
--- /dev/null
+++ b/client/src/service/conversation_manager.vala
@@ -0,0 +1,98 @@
+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<Account, HashMap<Jid, Conversation>> conversations = new HashMap<Account, HashMap<Jid, Conversation>>(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);
+ }
+
+ 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, Conversation>(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_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, int 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/client/src/service/counterpart_interaction_manager.vala b/client/src/service/counterpart_interaction_manager.vala
new file mode 100644
index 00000000..8ea8ba15
--- /dev/null
+++ b/client/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<Jid, Entities.Message> last_read = new HashMap<Jid, Entities.Message>(Jid.hash_bare_func, Jid.equals_bare_func);
+ private HashMap<Jid, string> chat_states = new HashMap<Jid, string>(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<Entities.Message>? 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/client/src/service/database.vala b/client/src/service/database.vala
new file mode 100644
index 00000000..6428d83f
--- /dev/null
+++ b/client/src/service/database.vala
@@ -0,0 +1,457 @@
+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<int> id = new Column.Integer("id") { primary_key = true, auto_increment = true };
+ public Column<string> bare_jid = new Column.Text("bare_jid") { unique = true, not_null = true };
+ public Column<string> resourcepart = new Column.Text("resourcepart");
+ public Column<string> password = new Column.Text("password");
+ public Column<string> alias = new Column.Text("alias");
+ public Column<bool> 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<int> id = new Column.Integer("id") { primary_key = true, auto_increment = true };
+ public Column<string> 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<int> id = new Column.Integer("id") { primary_key = true, auto_increment = true };
+ public Column<string> stanza_id = new Column.Text("stanza_id");
+ public Column<int> account_id = new Column.Integer("account_id");
+ public Column<int> counterpart_id = new Column.Integer("counterpart_id");
+ public Column<string> counterpart_resource = new Column.Text("counterpart_resource");
+ public Column<string> our_resource = new Column.Text("our_resource");
+ public Column<bool> direction = new Column.BoolInt("direction");
+ public Column<int> type_ = new Column.Integer("type");
+ public Column<long> time = new Column.Long("time");
+ public Column<long> local_time = new Column.Long("local_time");
+ public Column<string> body = new Column.Text("body");
+ public Column<int> encryption = new Column.Integer("encryption");
+ public Column<int> 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<int> message_id = new Column.Integer("message_id") { primary_key = true };
+ public Column<string> 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<int> message_id = new Column.Integer("message_id");
+ public Column<int> type_ = new Column.Integer("type");
+ public Column<string> data = new Column.Text("data");
+
+ protected UndecryptedTable(Database db) {
+ base(db, "undecrypted");
+ init({message_id, type_, data});
+ }
+ }
+
+ public class ConversationTable : Table {
+ public Column<int> id = new Column.Integer("id") { primary_key = true, auto_increment = true };
+ public Column<int> account_id = new Column.Integer("account_id") { not_null = true };
+ public Column<int> jid_id = new Column.Integer("jid_id") { not_null = true };
+ public Column<bool> active = new Column.BoolInt("active");
+ public Column<long> last_active = new Column.Long("last_active");
+ public Column<int> type_ = new Column.Integer("type");
+ public Column<int> encryption = new Column.Integer("encryption");
+ public Column<int> 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<string> jid = new Column.Text("jid");
+ public Column<string> hash = new Column.Text("hash");
+ public Column<int> type_ = new Column.Integer("type");
+
+ protected AvatarTable(Database db) {
+ base(db, "avatar");
+ init({jid, hash, type_});
+ }
+ }
+
+ public class PgpTable : Table {
+ public Column<string> jid = new Column.Text("jid") { primary_key = true };
+ public Column<string> key = new Column.Text("key") { not_null = true };
+
+ protected PgpTable(Database db) {
+ base(db, "pgp");
+ init({jid, key});
+ }
+ }
+
+ public class EntityFeatureTable : Table {
+ public Column<string> entity = new Column.Text("entity");
+ public Column<string> 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<Account> get_accounts() {
+ ArrayList<Account> ret = new ArrayList<Account>();
+ 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) {
+ if (new_message.body == null || new_message.stanza_id == null) {
+ return;
+ }
+
+ new_message.id = (int) message.insert()
+ .value(message.stanza_id, new_message.stanza_id)
+ .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)
+ .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<Message> 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<Message> ret = new LinkedList<Message>();
+ foreach (Row row in select) {
+ ret.insert(0, 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)
+ .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);
+ 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<Conversation> get_conversations(Account account) {
+ ArrayList<Conversation> ret = new ArrayList<Conversation>();
+ 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_ = row[conversation.type_];
+ new_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<Jid, string> get_avatar_hashes(int type) {
+ HashMap<Jid, string> ret = new HashMap<Jid, string>(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<string> features) {
+ foreach (string feature in features) {
+ entity_feature.insert()
+ .value(entity_feature.entity, entity)
+ .value(entity_feature.feature, feature)
+ .perform();
+ }
+ }
+
+ public ArrayList<string> get_entity_features(string entity) {
+ ArrayList<string> ret = new ArrayList<string>();
+ 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/client/src/service/entity_capabilities_storage.vala b/client/src/service/entity_capabilities_storage.vala
new file mode 100644
index 00000000..9774739a
--- /dev/null
+++ b/client/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<string> features) {
+ db.add_entity_features(entity, features);
+ }
+
+ public ArrayList<string> get_features(string entitiy) {
+ return db.get_entity_features(entitiy);
+ }
+}
+} \ No newline at end of file
diff --git a/client/src/service/message_manager.vala b/client/src/service/message_manager.vala
new file mode 100644
index 00000000..a268e619
--- /dev/null
+++ b/client/src/service/message_manager.vala
@@ -0,0 +1,166 @@
+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<Conversation, ArrayList<Entities.Message>> messages = new HashMap<Conversation, ArrayList<Entities.Message>>(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);
+ }
+
+ public void send_message(string text, Conversation conversation) {
+ Entities.Message message = new Entities.Message();
+ 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);
+
+ Core.XmppStream stream = stream_interactor.get_stream(conversation.account);
+
+ if (stream != null) {
+ Xmpp.Message.Stanza new_message = new Xmpp.Message.Stanza();
+ 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 (conversation.encryption == Conversation.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.encryption = Entities.Message.Encryption.PGP;
+ }
+ }
+ Xmpp.Message.Module.get_module(stream).send_message(stream, new_message);
+ message.stanza_id = new_message.id;
+ message.stanza = new_message;
+ db.add_message(message, conversation.account);
+ } else {
+ // save for resend
+ }
+
+ conversation.last_active = message.time;
+ add_message(message, conversation);
+ message_sent(message, conversation);
+ }
+
+ public Gee.List<Entities.Message>? get_messages(Conversation conversation) {
+ if (messages.has_key(conversation) && messages[conversation].size > 0) {
+ Gee.List<Entities.Message> 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<Entities.Message> 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<Entities.Message> 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<Entities.Message>? get_messages_before(Conversation? conversation, Entities.Message before) {
+ Gee.List<Entities.Message> 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);
+ });
+ }
+
+ 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_);
+ new_message.time = Xep.DelayedDelivery.Module.get_send_time(message);
+ if (new_message.time == null) {
+ new_message.time = 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>(Entities.Message.equals_func);
+ }
+ messages[conversation].add(message);
+ }
+}
+
+} \ No newline at end of file
diff --git a/client/src/service/module_manager.vala b/client/src/service/module_manager.vala
new file mode 100644
index 00000000..5ef93da8
--- /dev/null
+++ b/client/src/service/module_manager.vala
@@ -0,0 +1,96 @@
+using Gee;
+
+using Dino.Entities;
+using Xmpp;
+
+namespace Dino {
+
+public class ModuleManager {
+
+ public HashMap<Account, Tls.Module> tls_modules = new HashMap<Account, Tls.Module>();
+ public HashMap<Account, PlainSasl.Module> plain_sasl_modules = new HashMap<Account, PlainSasl.Module>();
+ public HashMap<Account, Bind.Module> bind_modules = new HashMap<Account, Bind.Module>();
+ public HashMap<Account, Roster.Module> roster_modules = new HashMap<Account, Roster.Module>();
+ public HashMap<Account, Xep.ServiceDiscovery.Module> service_discovery_modules = new HashMap<Account, Xep.ServiceDiscovery.Module>();
+ public HashMap<Account, Xep.PrivateXmlStorage.Module> private_xmp_storage_modules = new HashMap<Account, Xep.PrivateXmlStorage.Module>();
+ public HashMap<Account, Xep.Bookmarks.Module> bookmarks_module = new HashMap<Account, Xep.Bookmarks.Module>();
+ public HashMap<Account, Presence.Module> presence_modules = new HashMap<Account, Presence.Module>();
+ public HashMap<Account, Xmpp.Message.Module> message_modules = new HashMap<Account, Xmpp.Message.Module>();
+ public HashMap<Account, Xep.MessageCarbons.Module> message_carbons_modules = new HashMap<Account, Xep.MessageCarbons.Module>();
+ public HashMap<Account, Xep.Muc.Module> muc_modules = new HashMap<Account, Xep.Muc.Module>();
+ public HashMap<Account, Xep.Pgp.Module> pgp_modules = new HashMap<Account, Xep.Pgp.Module>();
+ public HashMap<Account, Xep.Pubsub.Module> pubsub_modules = new HashMap<Account, Xep.Pubsub.Module>();
+ public HashMap<Account, Xep.EntityCapabilities.Module> entity_capabilities_modules = new HashMap<Account, Xep.EntityCapabilities.Module>();
+ public HashMap<Account, Xep.UserAvatars.Module> user_avatars_modules = new HashMap<Account, Xep.UserAvatars.Module>();
+ public HashMap<Account, Xep.VCard.Module> vcard_modules = new HashMap<Account, Xep.VCard.Module>();
+ public HashMap<Account, Xep.MessageDeliveryReceipts.Module> message_delivery_receipts_modules = new HashMap<Account, Xep.MessageDeliveryReceipts.Module>();
+ public HashMap<Account, Xep.ChatStateNotifications.Module> chat_state_notifications_modules = new HashMap<Account, Xep.ChatStateNotifications.Module>();
+ public HashMap<Account, Xep.ChatMarkers.Module> chat_markers_modules = new HashMap<Account, Xep.ChatMarkers.Module>();
+ public HashMap<Account, Xep.Ping.Module> ping_modules = new HashMap<Account, Xep.Ping.Module>();
+ public HashMap<Account, Xep.DelayedDelivery.Module> delayed_delivery_module = new HashMap<Account, Xep.DelayedDelivery.Module>();
+ public HashMap<Account, StreamError.Module> stream_error_modules = new HashMap<Account, StreamError.Module>();
+
+ private AvatarStorage avatar_storage = new AvatarStorage("./");
+ private EntityCapabilitiesStorage entity_capabilities_storage;
+
+ public ModuleManager(Database db) {
+ entity_capabilities_storage = new EntityCapabilitiesStorage(db);
+ }
+
+ public ArrayList<Core.XmppStreamModule> get_modules(Account account, string? resource = null) {
+ ArrayList<Core.XmppStreamModule> modules = new ArrayList<Core.XmppStreamModule>();
+
+ 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/client/src/service/muc_manager.vala b/client/src/service/muc_manager.vala
new file mode 100644
index 00000000..ead09306
--- /dev/null
+++ b/client/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<Xep.Bookmarks.Conference> conferences);
+
+ private StreamInteractor stream_interactor;
+ protected HashMap<Jid, Xep.Bookmarks.Conference> conference_bookmarks = new HashMap<Jid, Xep.Bookmarks.Conference>();
+
+ 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) {
+ 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, null, 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<Jid>? get_occupants(Jid jid, Account account) {
+ return PresenceManager.get_instance(stream_interactor).get_full_jids(jid, account);
+ }
+
+ public ArrayList<Jid>? get_other_occupants(Jid jid, Account account) {
+ ArrayList<Jid>? 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<Entities.Message>? 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<Xep.Bookmarks.Conference> 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/client/src/service/pgp_manager.vala b/client/src/service/pgp_manager.vala
new file mode 100644
index 00000000..6f3b63d7
--- /dev/null
+++ b/client/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<Jid, string> pgp_key_ids = new HashMap<Jid, string>(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/client/src/service/presence_manager.vala b/client/src/service/presence_manager.vala
new file mode 100644
index 00000000..53bdf4ce
--- /dev/null
+++ b/client/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<Jid, HashMap<Jid, ArrayList<Show>>> shows = new HashMap<Jid, HashMap<Jid, ArrayList<Show>>>(Jid.hash_bare_func, Jid.equals_bare_func);
+ private HashMap<Jid, ArrayList<Jid>> resources = new HashMap<Jid, ArrayList<Jid>>(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<Jid, ArrayList<Show>>? get_shows(Jid jid, Account account) {
+ return shows[jid];
+ }
+
+ public ArrayList<Jid>? get_full_jids(Jid jid, Account account) {
+ Core.XmppStream? stream = stream_interactor.get_stream(account);
+ if (stream != null) {
+ ArrayList<string> resources = Xmpp.Presence.Flag.get_flag(stream).get_resources(jid.bare_jid.to_string());
+ if (resources == null) {
+ return null;
+ }
+ ArrayList<Jid> ret = new ArrayList<Jid>(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>(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<Jid, ArrayList<Show>>();
+ }
+ if (!shows[jid].has_key(jid)) {
+ shows[jid][jid] = new ArrayList<Show>();
+ }
+ 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/client/src/service/roster_manager.vala b/client/src/service/roster_manager.vala
new file mode 100644
index 00000000..106405e2
--- /dev/null
+++ b/client/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<Roster.Item> get_roster(Account account) {
+ Core.XmppStream? stream = stream_interactor.get_stream(account);
+ ArrayList<Roster.Item> ret = new ArrayList<Roster.Item>();
+ 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.Item> 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/client/src/service/stream_interactor.vala b/client/src/service/stream_interactor.vala
new file mode 100644
index 00000000..56591cf0
--- /dev/null
+++ b/client/src/service/stream_interactor.vala
@@ -0,0 +1,68 @@
+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<StreamInteractionModule> interaction_modules = new ArrayList<StreamInteractionModule>();
+
+ 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<Account> get_accounts() {
+ ArrayList<Account> ret = new ArrayList<Account>(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