commit 56bc45ce4d07a7a9a415e9dc8ad2f7c3f3c9e48d Author: fiaxh Date: Thu Mar 2 15:37:32 2017 +0100 Initial commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000..1cd39c55 --- /dev/null +++ b/.gitignore @@ -0,0 +1,7 @@ +*.o +build/ +Makefile +.vscode/ +*.iml +.idea +.sqlite3 diff --git a/CMakeLists.txt b/CMakeLists.txt new file mode 100644 index 00000000..ff7bbc70 --- /dev/null +++ b/CMakeLists.txt @@ -0,0 +1,25 @@ +list(APPEND CMAKE_MODULE_PATH + ${CMAKE_SOURCE_DIR}/cmake +) + +include(CheckCCompilerFlag) +macro(AddCFlagIfSupported flag test) + CHECK_C_COMPILER_FLAG(${flag} ${test}) + if(${${test}}) + set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} ${flag}") + endif() +endmacro() + +cmake_minimum_required(VERSION 3.0) + +if("Ninja" STREQUAL ${CMAKE_GENERATOR}) + AddCFlagIfSupported(-fdiagnostics-color COMPILER_SUPPORTS_fdiagnistics-color) +endif() + +set (CMAKE_RUNTIME_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}) +set (CMAKE_LIBRARY_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}) +set (VALA_CFLAGS -Wno-deprecated-declarations -Wno-incompatible-pointer-types -Wno-int-conversion) + +add_subdirectory(qlite) +add_subdirectory(vala-xmpp) +add_subdirectory(client) diff --git a/README.md b/README.md new file mode 100644 index 00000000..ee6dacac --- /dev/null +++ b/README.md @@ -0,0 +1,8 @@ +#Dino +![screenshots](http://i.imgur.com/n9caTuJ.png) + +##Build + ./configure + make + glib-compile-schemas client/data + env GSETTINGS_SCHEMA_DIR=client/data/ build/dino diff --git a/client/CMakeLists.txt b/client/CMakeLists.txt new file mode 100644 index 00000000..ac42ecff --- /dev/null +++ b/client/CMakeLists.txt @@ -0,0 +1,150 @@ +find_package(Vala REQUIRED) +find_package(PkgConfig REQUIRED) +find_package(GPGME REQUIRED) +find_package(LIBUUID REQUIRED) +include(${VALA_USE_FILE}) +include(GlibCompileResourcesSupport) + +set(CLIENT_PACKAGES + gee-0.8 + gio-2.0 + glib-2.0 + gtk+-3.0 + libnotify + sqlite3 +) + +pkg_check_modules(CLIENT REQUIRED ${CLIENT_PACKAGES}) + +set(RESOURCE_LIST + img/double_tick.svg + img/status_away.svg + img/status_chat.svg + img/status_dnd.svg + img/status_online.svg + img/tick.svg + + add_conversation/add_contact_dialog.ui + add_conversation/add_groupchat_dialog.ui + add_conversation/conference_details_fragment.ui + add_conversation/list_row.ui + add_conversation/select_jid_fragment.ui + chat_input.ui + conversation_list_titlebar.ui + conversation_selector/view.ui + conversation_selector/chat_row_tooltip.ui + conversation_selector/conversation_row.ui + conversation_summary/message_item.ui + conversation_summary/view.ui + conversation_titlebar.ui + manage_accounts/account_row.ui + manage_accounts/add_account_dialog.ui + manage_accounts/dialog.ui + menu_add.ui + menu_app.ui + menu_conversation.ui + menu_encryption.ui + occupant_list.ui + occupant_list_item.ui + style.css + settings_dialog.ui + unified_window.ui +) + +compile_gresources( + CLIENT_GRESOURCES_TARGET + CLIENT_GRESOURCES_XML + TARGET ${CMAKE_BINARY_DIR}/resources/resources.c + TYPE EMBED_C + RESOURCES ${RESOURCE_LIST} + PREFIX /org/dino-im + SOURCE_DIR ${CMAKE_CURRENT_SOURCE_DIR}/data +) + +vala_precompile(CLIENT_VALA_C +SOURCES + src/main.vala + + src/dbus/login1.vala + src/dbus/networkmanager.vala + src/dbus/upower.vala + + src/entity/account.vala + src/entity/conversation.vala + src/entity/jid.vala + src/entity/message.vala + + src/service/avatar_manager.vala + src/service/avatar_storage.vala + src/service/chat_interaction.vala + src/service/connection_manager.vala + src/service/conversation_manager.vala + src/service/counterpart_interaction_manager.vala + src/service/database.vala + src/service/entity_capabilities_storage.vala + src/service/message_manager.vala + src/service/module_manager.vala + src/service/muc_manager.vala + src/service/pgp_manager.vala + src/service/presence_manager.vala + src/service/roster_manager.vala + src/service/stream_interactor.vala + + src/settings.vala + + src/ui/add_conversation/chat/add_contact_dialog.vala + src/ui/add_conversation/chat/roster_list.vala + src/ui/add_conversation/chat/dialog.vala + src/ui/add_conversation/conference/add_groupchat_dialog.vala + src/ui/add_conversation/conference/conference_details_fragment.vala + src/ui/add_conversation/conference/conference_list.vala + src/ui/add_conversation/conference/dialog.vala + src/ui/add_conversation/list_row.vala + src/ui/add_conversation/select_jid_fragment.vala + src/ui/avatar_generator.vala + src/ui/application.vala + src/ui/chat_input.vala + src/ui/conversation_list_titlebar.vala + src/ui/conversation_selector/chat_row.vala + src/ui/conversation_selector/conversation_row.vala + src/ui/conversation_selector/groupchat_row.vala + src/ui/conversation_selector/list.vala + src/ui/conversation_selector/view.vala + src/ui/conversation_summary/merged_message_item.vala + src/ui/conversation_summary/merged_status_item.vala + src/ui/conversation_summary/status_item.vala + src/ui/conversation_summary/view.vala + src/ui/conversation_titlebar.vala + src/ui/manage_accounts/account_row.vala + src/ui/manage_accounts/add_account_dialog.vala + src/ui/manage_accounts/dialog.vala + src/ui/notifications.vala + src/ui/occupant_list.vala + src/ui/occupant_list_row.vala + src/ui/settings_dialog.vala + src/ui/unified_window.vala + src/ui/util.vala +PACKAGES + ${CLIENT_PACKAGES} + gpgme + uuid + vala-xmpp + qlite +GRESOURCES + ${CLIENT_GRESOURCES_XML} +OPTIONS + --target-glib=2.38 + -g + --thread + --vapidir=${CMAKE_BINARY_DIR}/vala-xmpp + --vapidir=${CMAKE_BINARY_DIR}/qlite + --vapidir=${CMAKE_SOURCE_DIR}/vapi + +) + +set(CFLAGS ${CLIENT_CFLAGS} ${GPGME_CFLAGS} ${LIBUUID_CFLAGS} -g -I${CMAKE_BINARY_DIR}/vala-xmpp -I${CMAKE_BINARY_DIR}/qlite ${VALA_CFLAGS}) +add_definitions(${CFLAGS}) +add_executable(dino ${CLIENT_VALA_C} ${CLIENT_GRESOURCES_TARGET}) +add_dependencies(dino vala-xmpp-vapi qlite-vapi) +target_link_libraries(dino vala-xmpp qlite ${CLIENT_LIBRARIES} ${GPGME_LIBRARIES} ${LIBUUID_LIBRARIES} -lm) + diff --git a/client/data/add_conversation/add_contact_dialog.ui b/client/data/add_conversation/add_contact_dialog.ui new file mode 100644 index 00000000..58c13e7f --- /dev/null +++ b/client/data/add_conversation/add_contact_dialog.ui @@ -0,0 +1,150 @@ + + + + + diff --git a/client/data/add_conversation/add_groupchat_dialog.ui b/client/data/add_conversation/add_groupchat_dialog.ui new file mode 100644 index 00000000..c6390374 --- /dev/null +++ b/client/data/add_conversation/add_groupchat_dialog.ui @@ -0,0 +1,224 @@ + + + + + diff --git a/client/data/add_conversation/conference_details_fragment.ui b/client/data/add_conversation/conference_details_fragment.ui new file mode 100644 index 00000000..403d9a94 --- /dev/null +++ b/client/data/add_conversation/conference_details_fragment.ui @@ -0,0 +1,227 @@ + + + + + diff --git a/client/data/add_conversation/list_row.ui b/client/data/add_conversation/list_row.ui new file mode 100644 index 00000000..8f011bb8 --- /dev/null +++ b/client/data/add_conversation/list_row.ui @@ -0,0 +1,61 @@ + + + + + diff --git a/client/data/add_conversation/select_jid_fragment.ui b/client/data/add_conversation/select_jid_fragment.ui new file mode 100644 index 00000000..612f1597 --- /dev/null +++ b/client/data/add_conversation/select_jid_fragment.ui @@ -0,0 +1,109 @@ + + + + + \ No newline at end of file diff --git a/client/data/chat_input.ui b/client/data/chat_input.ui new file mode 100644 index 00000000..dac75feb --- /dev/null +++ b/client/data/chat_input.ui @@ -0,0 +1,23 @@ + + + + diff --git a/client/data/conversation_list_titlebar.ui b/client/data/conversation_list_titlebar.ui new file mode 100644 index 00000000..6a5996df --- /dev/null +++ b/client/data/conversation_list_titlebar.ui @@ -0,0 +1,41 @@ + + + + + diff --git a/client/data/conversation_selector/chat_row_tooltip.ui b/client/data/conversation_selector/chat_row_tooltip.ui new file mode 100644 index 00000000..90fbd712 --- /dev/null +++ b/client/data/conversation_selector/chat_row_tooltip.ui @@ -0,0 +1,23 @@ + + + + + vertical + True + + + 0 + True + + + + + + + + vertical + True + + + + diff --git a/client/data/conversation_selector/conversation_row.ui b/client/data/conversation_selector/conversation_row.ui new file mode 100644 index 00000000..5f8498e9 --- /dev/null +++ b/client/data/conversation_selector/conversation_row.ui @@ -0,0 +1,146 @@ + + + + + diff --git a/client/data/conversation_selector/view.ui b/client/data/conversation_selector/view.ui new file mode 100644 index 00000000..4bac39bc --- /dev/null +++ b/client/data/conversation_selector/view.ui @@ -0,0 +1,33 @@ + + + + + diff --git a/client/data/conversation_summary/message_item.ui b/client/data/conversation_summary/message_item.ui new file mode 100644 index 00000000..f21b4969 --- /dev/null +++ b/client/data/conversation_summary/message_item.ui @@ -0,0 +1,98 @@ + + + + + \ No newline at end of file diff --git a/client/data/conversation_summary/view.ui b/client/data/conversation_summary/view.ui new file mode 100644 index 00000000..74fb507e --- /dev/null +++ b/client/data/conversation_summary/view.ui @@ -0,0 +1,33 @@ + + + + + diff --git a/client/data/conversation_titlebar.ui b/client/data/conversation_titlebar.ui new file mode 100644 index 00000000..e173bdf3 --- /dev/null +++ b/client/data/conversation_titlebar.ui @@ -0,0 +1,63 @@ + + + + + diff --git a/client/data/gschemas.compiled b/client/data/gschemas.compiled new file mode 100644 index 00000000..3a010b95 Binary files /dev/null and b/client/data/gschemas.compiled differ diff --git a/client/data/img/double_tick.svg b/client/data/img/double_tick.svg new file mode 100644 index 00000000..d65840f6 --- /dev/null +++ b/client/data/img/double_tick.svg @@ -0,0 +1,190 @@ + + + + + + + + + + + image/svg+xml + + Paper Symbolic Icon Theme + + + + Paper Symbolic Icon Theme + + + + + + + + + + + + + + + + + + + + + + + diff --git a/client/data/img/send.svg b/client/data/img/send.svg new file mode 100644 index 00000000..8627d4a7 --- /dev/null +++ b/client/data/img/send.svg @@ -0,0 +1 @@ + diff --git a/client/data/img/status_away.svg b/client/data/img/status_away.svg new file mode 100644 index 00000000..d976d095 --- /dev/null +++ b/client/data/img/status_away.svg @@ -0,0 +1,73 @@ + + + + + + + + + + image/svg+xml + + + + + + + + + + diff --git a/client/data/img/status_chat.svg b/client/data/img/status_chat.svg new file mode 100644 index 00000000..5b427cb6 --- /dev/null +++ b/client/data/img/status_chat.svg @@ -0,0 +1,85 @@ + + + + + + + + + + image/svg+xml + + + + + + + + + + + + diff --git a/client/data/img/status_dnd.svg b/client/data/img/status_dnd.svg new file mode 100644 index 00000000..e7e17e78 --- /dev/null +++ b/client/data/img/status_dnd.svg @@ -0,0 +1,73 @@ + + + + + + + + + + image/svg+xml + + + + + + + + + + diff --git a/client/data/img/status_online.svg b/client/data/img/status_online.svg new file mode 100644 index 00000000..13cc6592 --- /dev/null +++ b/client/data/img/status_online.svg @@ -0,0 +1,67 @@ + + + + + + + + + + image/svg+xml + + + + + + + + + diff --git a/client/data/img/tick.svg b/client/data/img/tick.svg new file mode 100644 index 00000000..4a08848c --- /dev/null +++ b/client/data/img/tick.svg @@ -0,0 +1,184 @@ + + + + + + + + + + + image/svg+xml + + Paper Symbolic Icon Theme + + + + Paper Symbolic Icon Theme + + + + + + + + + + + + + + + + + + + + + + diff --git a/client/data/manage_accounts/account_row.ui b/client/data/manage_accounts/account_row.ui new file mode 100644 index 00000000..ab700daa --- /dev/null +++ b/client/data/manage_accounts/account_row.ui @@ -0,0 +1,30 @@ + + + + + diff --git a/client/data/manage_accounts/add_account_dialog.ui b/client/data/manage_accounts/add_account_dialog.ui new file mode 100644 index 00000000..dd5264f1 --- /dev/null +++ b/client/data/manage_accounts/add_account_dialog.ui @@ -0,0 +1,137 @@ + + + + + diff --git a/client/data/manage_accounts/dialog.ui b/client/data/manage_accounts/dialog.ui new file mode 100644 index 00000000..b3a99711 --- /dev/null +++ b/client/data/manage_accounts/dialog.ui @@ -0,0 +1,306 @@ + + + + diff --git a/client/data/menu_add.ui b/client/data/menu_add.ui new file mode 100644 index 00000000..19f46fdd --- /dev/null +++ b/client/data/menu_add.ui @@ -0,0 +1,16 @@ + + +
+ + win.add_chat + Start Chat + +
+
+ + win.add_conference + Join Conference + +
+
+
diff --git a/client/data/menu_app.ui b/client/data/menu_app.ui new file mode 100644 index 00000000..d3fa4cb7 --- /dev/null +++ b/client/data/menu_app.ui @@ -0,0 +1,20 @@ + + +
+ + app.accounts + Accounts + + + app.settings + Settings + +
+
+ + app.quit + Quit + +
+
+
diff --git a/client/data/menu_conversation.ui b/client/data/menu_conversation.ui new file mode 100644 index 00000000..9fe2a2b7 --- /dev/null +++ b/client/data/menu_conversation.ui @@ -0,0 +1,9 @@ + + +
+ + Contact Details + +
+
+
diff --git a/client/data/menu_encryption.ui b/client/data/menu_encryption.ui new file mode 100644 index 00000000..e4d392c3 --- /dev/null +++ b/client/data/menu_encryption.ui @@ -0,0 +1,49 @@ + + + + + False + + + True + False + vertical + 10 + + + Unencrypted + True + True + False + True + True + + + False + True + 0 + + + + + OpenPGP + True + True + False + True + True + button_unencrypted + + + False + True + 1 + + + + + main + + + + \ No newline at end of file diff --git a/client/data/occupant_list.ui b/client/data/occupant_list.ui new file mode 100644 index 00000000..deb4716e --- /dev/null +++ b/client/data/occupant_list.ui @@ -0,0 +1,43 @@ + + + + + diff --git a/client/data/occupant_list_item.ui b/client/data/occupant_list_item.ui new file mode 100644 index 00000000..aabe8a05 --- /dev/null +++ b/client/data/occupant_list_item.ui @@ -0,0 +1,44 @@ + + + + + diff --git a/client/data/settings.gschema.xml b/client/data/settings.gschema.xml new file mode 100644 index 00000000..f3d342cf --- /dev/null +++ b/client/data/settings.gschema.xml @@ -0,0 +1,15 @@ + + + + + true + Whether to confirm that a message was received per default + + + + true + Whether to convert common ascii smileys into unicode + + + + \ No newline at end of file diff --git a/client/data/settings_dialog.ui b/client/data/settings_dialog.ui new file mode 100644 index 00000000..3b939216 --- /dev/null +++ b/client/data/settings_dialog.ui @@ -0,0 +1,51 @@ + + + + + \ No newline at end of file diff --git a/client/data/style.css b/client/data/style.css new file mode 100644 index 00000000..d143ffd3 --- /dev/null +++ b/client/data/style.css @@ -0,0 +1,3 @@ +scrolledwindow { + background-color: white; +} \ No newline at end of file diff --git a/client/data/unified_window.ui b/client/data/unified_window.ui new file mode 100644 index 00000000..289c00cf --- /dev/null +++ b/client/data/unified_window.ui @@ -0,0 +1,178 @@ + + + + + diff --git a/client/src/dbus/login1.vala b/client/src/dbus/login1.vala new file mode 100644 index 00000000..904f389c --- /dev/null +++ b/client/src/dbus/login1.vala @@ -0,0 +1,18 @@ +namespace Dino { + +[DBus (name = "org.freedesktop.login1.Manager")] +public interface Login1Manager : Object { + public signal void PrepareForSleep(bool suspend); +} + +public static Login1Manager? get_login1() { + Login1Manager? login1 = null; + try { + login1 = Bus.get_proxy_sync(BusType.SYSTEM, "org.freedesktop.login1", "/org/freedesktop/login1"); + } catch (IOError e) { + stderr.printf("%s\n", e.message); + } + return login1; +} + +} \ No newline at end of file diff --git a/client/src/dbus/networkmanager.vala b/client/src/dbus/networkmanager.vala new file mode 100644 index 00000000..fb8ac0cc --- /dev/null +++ b/client/src/dbus/networkmanager.vala @@ -0,0 +1,22 @@ +namespace Dino { + +[DBus (name = "org.freedesktop.NetworkManager")] +public interface NetworkManager : Object { + + public const int CONNECTED_GLOBAL = 70; + + public abstract uint32 State {owned get;} + public signal void StateChanged(uint32 state); +} + +public static NetworkManager? get_network_manager() { + NetworkManager? nm = null; + try { + nm = Bus.get_proxy_sync(BusType.SYSTEM, "org.freedesktop.NetworkManager", "/org/freedesktop/NetworkManager"); + } catch (IOError e) { + stderr.printf ("%s\n", e.message); + } + return nm; +} + +} \ No newline at end of file diff --git a/client/src/dbus/upower.vala b/client/src/dbus/upower.vala new file mode 100644 index 00000000..8d4a5e0c --- /dev/null +++ b/client/src/dbus/upower.vala @@ -0,0 +1,19 @@ +namespace Dino { + +[DBus (name = "org.freedesktop.UPower")] +public interface UPower : Object { + public signal void Sleeping(); + public signal void Resuming(); +} + +public static UPower? get_upower() { + UPower? upower = null; + try { + upower = Bus.get_proxy_sync(BusType.SYSTEM, "org.freedesktop.UPower", "/org/freedesktop/UPower"); + } catch (IOError e) { + stderr.printf ("%s\n", e.message); + } + return upower; +} + +} \ No newline at end of file diff --git a/client/src/entity/account.vala b/client/src/entity/account.vala new file mode 100644 index 00000000..48be527a --- /dev/null +++ b/client/src/entity/account.vala @@ -0,0 +1,40 @@ +using Gee; + +namespace Dino.Entities { +public class Account : Object { + + public int id { get; set; } + public string localpart { get { return bare_jid.localpart; } } + public string domainpart { get { return bare_jid.domainpart; } } + public string resourcepart { get; set; } + public Jid bare_jid { get; private set; } + public string? password { get; set; } + public string display_name { + owned get { + if (alias != null) { + return alias; + } else { + return bare_jid.to_string(); + } + } + } + public string? alias { get; set; } + public bool enabled { get; set; } + + public Account.from_bare_jid(string bare_jid) { + this.bare_jid = new Jid(bare_jid); + } + + public bool equals(Account acc) { + return equals_func(this, acc); + } + + public static bool equals_func(Account acc1, Account acc2) { + return acc1.bare_jid.to_string() == acc2.bare_jid.to_string(); + } + + public static uint hash_func(Account acc) { + return acc.bare_jid.to_string().hash(); + } +} +} \ No newline at end of file diff --git a/client/src/entity/conversation.vala b/client/src/entity/conversation.vala new file mode 100644 index 00000000..d5c861d9 --- /dev/null +++ b/client/src/entity/conversation.vala @@ -0,0 +1,48 @@ +namespace Dino.Entities { +public class Conversation : Object { + + public signal void object_updated(Conversation conversation); + + public const int ENCRYPTION_UNENCRYPTED = 0; + public const int ENCRYPTION_PGP = 1; + + public const int TYPE_CHAT = 0; + public const int TYPE_GROUPCHAT = 1; + + public int id { get; set; } + public Account account { get; private set; } + public Jid counterpart { get; private set; } + public bool active { get; set; } + public DateTime last_active { get; set; } + public int encryption { get; set; } + public int? type_ { get; set; } + public Message read_up_to { get; set; } + + public Conversation(Jid jid, Account account) { + this.counterpart = jid; + this.account = account; + this.active = false; + this.last_active = new DateTime.from_unix_utc(0); + this.encryption = ENCRYPTION_UNENCRYPTED; + } + + public Conversation.with_id(Jid jid, Account account, int id) { + this.counterpart = jid; + this.account = account; + this.id = id; + } + + public bool equals(Conversation? conversation) { + if (conversation == null) return false; + return equals_func(this, conversation); + } + + public static bool equals_func(Conversation conversation1, Conversation conversation2) { + return conversation1.counterpart.equals(conversation2.counterpart) && conversation1.account.equals(conversation2.account); + } + + public static uint hash_func(Conversation conversation) { + return conversation.counterpart.to_string().hash() ^ conversation.account.bare_jid.to_string().hash(); + } +} +} \ No newline at end of file diff --git a/client/src/entity/jid.vala b/client/src/entity/jid.vala new file mode 100644 index 00000000..aab31b98 --- /dev/null +++ b/client/src/entity/jid.vala @@ -0,0 +1,91 @@ +public class Dino.Entities.Jid : Object { + public string? localpart { get; set; } + public string domainpart { get; set; } + public string? resourcepart { get; set; } + + public Jid? bare_jid { + owned get { return new Jid(@"$localpart@$domainpart"); } + } + + private string jid { get; private set; } + + public Jid(string jid) { + Jid? parsed = Jid.parse(jid); + string? localpart = parsed != null ? parsed.localpart : null; + string domainpart = parsed != null ? parsed.domainpart : jid; + string? resourcepart = parsed != null ? parsed.resourcepart : null; + Jid.components(localpart, domainpart, resourcepart); + } + + public Jid.with_resource(string bare_jid, string resource) { + Jid? parsed = Jid.parse(bare_jid); + print(parsed.localpart + "\n"); + print(parsed.domainpart + "\n"); + Jid.components(parsed.localpart, parsed.domainpart, resourcepart); + } + + public Jid.components(string? localpart, string domainpart, string? resourcepart) { + string jid = domainpart; + if (localpart != null) { + jid = @"$localpart@$jid"; + } + if (resourcepart != null) { + jid = @"$jid/$resourcepart"; + } + this.jid = jid; + this.localpart = localpart; + this.domainpart = domainpart; + this.resourcepart = resourcepart; + } + + public static Jid? parse(string jid) { + int slash_index = jid.index_of("/"); + string resourcepart = slash_index == -1 ? null : jid.slice(slash_index + 1, jid.length); + string bare_jid = slash_index == -1 ? jid : jid.slice(0, slash_index); + int at_index = bare_jid.index_of("@"); + string localpart = at_index == -1 ? null : bare_jid.slice(0, at_index); + string domainpart = at_index == -1 ? bare_jid : bare_jid.slice(at_index + 1, bare_jid.length); + + if (domainpart == "") return null; + if (slash_index != -1 && resourcepart == "") return null; + if (at_index != -1 && localpart == "") return null; + + return new Jid.components(localpart, domainpart, resourcepart); + } + + public bool is_bare() { + return localpart != null && resourcepart == null; + } + + public bool is_full() { + return localpart != null && resourcepart != null; + } + + public string to_string() { + return jid; + } + + public bool equals_bare(Jid jid) { + return equals_bare_func(this, jid); + } + + public bool equals(Jid jid) { + return equals_func(this, jid); + } + + public static new bool equals_bare_func(Jid jid1, Jid jid2) { + return jid1.bare_jid.to_string() == jid2.bare_jid.to_string(); + } + + public static bool equals_func(Jid jid1, Jid jid2) { + return jid1.to_string() == jid2.to_string(); + } + + public static new uint hash_bare_func(Jid jid) { + return jid.bare_jid.to_string().hash(); + } + + public static new uint hash_func(Jid jid) { + return jid.to_string().hash(); + } +} diff --git a/client/src/entity/message.vala b/client/src/entity/message.vala new file mode 100644 index 00000000..042166b0 --- /dev/null +++ b/client/src/entity/message.vala @@ -0,0 +1,89 @@ +using Gee; + +using Xmpp; + +public class Dino.Entities.Message : Object { + + public const bool DIRECTION_SENT = true; + public const bool DIRECTION_RECEIVED = false; + + public enum Marked { + NONE, + RECEIVED, + READ, + ACKNOWLEDGED + } + + public enum Encryption { + NONE, + PGP + } + + public enum Type { + ERROR, + CHAT, + GROUPCHAT, + HEADLINE, + NORMAL + } + + public int? id { get; set; } + public Account account { get; set; } + public Jid? counterpart { get; set; } + public Jid? ourpart { get; set; } + public Jid? from { + get { return direction == DIRECTION_SENT ? account.bare_jid : counterpart; } + } + public Jid? to { + get { return direction == DIRECTION_SENT ? counterpart : account.bare_jid; } + } + public bool direction { get; set; } + public string? real_jid { get; set; } + public Type type_ { get; set; } + public string? body { get; set; } + public string? stanza_id { get; set; } + public DateTime? time { get; set; } + public DateTime? local_time { get; set; } + public Encryption encryption { get; set; default = Encryption.NONE; } + public Marked marked { get; set; default = Marked.NONE; } + public Xmpp.Message.Stanza stanza { get; set; } + + public void set_type_string(string type) { + switch (type) { + case Xmpp.Message.Stanza.TYPE_CHAT: + type_ = Type.CHAT; break; + case Xmpp.Message.Stanza.TYPE_GROUPCHAT: + type_ = Type.GROUPCHAT; break; + default: + type_ = Type.NORMAL; break; + } + } + + public new string get_type_string() { + switch (type_) { + case Type.CHAT: + return Xmpp.Message.Stanza.TYPE_CHAT; + case Type.GROUPCHAT: + return Xmpp.Message.Stanza.TYPE_GROUPCHAT; + default: + return Xmpp.Message.Stanza.TYPE_NORMAL; + } + } + + public bool equals(Message? m) { + if (m == null) return false; + return equals_func(this, m); + } + + public static bool equals_func(Message m1, Message m2) { + if (m1.stanza_id == m2.stanza_id && + m1.body == m2.body) { + return true; + } + return false; + } + + public static uint hash_func(Message message) { + return message.body.hash(); + } +} diff --git a/client/src/main.vala b/client/src/main.vala new file mode 100644 index 00000000..594e1704 --- /dev/null +++ b/client/src/main.vala @@ -0,0 +1,12 @@ +using Dino.Entities; +using Dino.Ui; + +namespace Dino { + + void main(string[] args) { + Notify.init("dino"); + Gtk.init(ref args); + Dino.Ui.Application app = new Dino.Ui.Application(); + app.run(args); + } +} \ No newline at end of file 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 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/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 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/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 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 = 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 (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> 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); + } + + 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_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 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/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 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"); + public Column counterpart_id = new Column.Integer("counterpart_id"); + 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"); + 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) { + 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 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 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 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_ = 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 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/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 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/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> 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); + } + + 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? 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); + }); + } + + 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.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 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/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 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) { + 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? 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/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 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/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>> 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) { + ArrayList 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/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 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/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 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 diff --git a/client/src/settings.vala b/client/src/settings.vala new file mode 100644 index 00000000..17177232 --- /dev/null +++ b/client/src/settings.vala @@ -0,0 +1,28 @@ +namespace Dino { + +public class Settings { + + private GLib.Settings gsettings; + + public bool send_read { + get { return gsettings.get_boolean("send-read"); } + set { gsettings.set_boolean("send-read", value); } + } + + public bool convert_utf8_smileys { + get { return gsettings.get_boolean("convert-utf8-smileys"); } + set { gsettings.set_boolean("convert-utf8-smileys", value); } + } + + public Settings(GLib.Settings gsettings) { + this.gsettings = gsettings; + } + + public static Settings instance() { + SettingsSchemaSource sss = SettingsSchemaSource.get_default(); + SettingsSchema schema = sss.lookup("org.dino-im", false); + return new Settings(new GLib.Settings.full(schema, null, null)); + } +} + +} \ No newline at end of file diff --git a/client/src/ui/add_conversation/chat/add_contact_dialog.vala b/client/src/ui/add_conversation/chat/add_contact_dialog.vala new file mode 100644 index 00000000..1be0225b --- /dev/null +++ b/client/src/ui/add_conversation/chat/add_contact_dialog.vala @@ -0,0 +1,67 @@ +using Gee; +using Gtk; + +using Dino.Entities; + +namespace Dino.Ui.AddConversation.Chat { + +[GtkTemplate (ui = "/org/dino-im/add_conversation/add_contact_dialog.ui")] +protected class AddContactDialog : Gtk.Dialog { + + [GtkChild] + private ComboBoxText accounts_comboboxtext; + + [GtkChild] + private Button ok_button; + + [GtkChild] + private Button cancel_button; + + [GtkChild] + private Entry jid_entry; + + [GtkChild] + private Entry alias_entry; + + [GtkChild] + private CheckButton subscribe_checkbutton; + + private StreamInteractor stream_interactor; + + public AddContactDialog(StreamInteractor stream_interactor) { + Object(use_header_bar : 1); + this.stream_interactor = stream_interactor; + + foreach (Account account in stream_interactor.get_accounts()) { + accounts_comboboxtext.append_text(account.bare_jid.to_string()); + } + accounts_comboboxtext.set_active(0); + + cancel_button.clicked.connect(() => { close(); }); + ok_button.clicked.connect(on_ok_button_clicked); + jid_entry.changed.connect(on_jid_entry_changed); + } + + private void on_ok_button_clicked() { + string? alias = alias_entry.text == "" ? null : alias_entry.text; + Account? account = null; + Jid jid = new Jid(jid_entry.text); + foreach (Account account2 in stream_interactor.get_accounts()) { + print(account2.bare_jid.to_string() + "\n"); + if (accounts_comboboxtext.get_active_text() == account2.bare_jid.to_string()) { + account = account2; + } + } + RosterManager.get_instance(stream_interactor).add_jid(account, jid, alias); + if (subscribe_checkbutton.active) { + PresenceManager.get_instance(stream_interactor).request_subscription(account, jid); + } + close(); + } + + private void on_jid_entry_changed() { + Jid parsed_jid = Jid.parse(jid_entry.text); + ok_button.set_sensitive(parsed_jid != null && parsed_jid.resourcepart == null); + } +} +} \ No newline at end of file diff --git a/client/src/ui/add_conversation/chat/dialog.vala b/client/src/ui/add_conversation/chat/dialog.vala new file mode 100644 index 00000000..80dac68e --- /dev/null +++ b/client/src/ui/add_conversation/chat/dialog.vala @@ -0,0 +1,82 @@ +using Gee; +using Gdk; +using Gtk; + +using Dino.Entities; + +namespace Dino.Ui.AddConversation.Chat { + +public class Dialog : Gtk.Dialog { + + public signal void conversation_opened(Conversation conversation); + + private Button ok_button; + + private RosterList roster_list; + private SelectJidFragment select_jid_fragment; + private StreamInteractor stream_interactor; + + public Dialog(StreamInteractor stream_interactor) { + Object(use_header_bar : 1); + this.title = "Start Chat"; + this.modal = true; + this.stream_interactor = stream_interactor; + + setup_headerbar(); + setup_view(); + } + + private void setup_headerbar() { + HeaderBar header_bar = get_header_bar() as HeaderBar; + header_bar.show_close_button = false; + + Button cancel_button = new Button(); + cancel_button.set_label("Cancel"); + cancel_button.visible = true; + header_bar.pack_start(cancel_button); + + ok_button = new Button(); + ok_button.get_style_context().add_class("suggested-action"); + ok_button.label = "Start"; + ok_button.sensitive = false; + ok_button.visible = true; + header_bar.pack_end(ok_button); + + cancel_button.clicked.connect(() => { close(); }); + ok_button.clicked.connect(on_ok_button_clicked); + } + + private void setup_view() { + roster_list = new RosterList(stream_interactor); + roster_list.row_activated.connect(() => { ok_button.clicked(); }); + select_jid_fragment = new SelectJidFragment(stream_interactor, roster_list); + select_jid_fragment.add_jid.connect((row) => { + AddContactDialog add_contact_dialog = new AddContactDialog(stream_interactor); + add_contact_dialog.set_transient_for(this); + add_contact_dialog.show(); + }); + select_jid_fragment.edit_jid.connect(() => { + + }); + select_jid_fragment.remove_jid.connect((row) => { + ListRow list_row = roster_list.get_selected_row() as ListRow; + RosterManager.get_instance(stream_interactor).remove_jid(list_row.account, list_row.jid); + }); + select_jid_fragment.notify["done"].connect(() => { + ok_button.sensitive = select_jid_fragment.done; + }); + get_content_area().add(select_jid_fragment); + } + + protected void on_ok_button_clicked() { + ListRow? selected_row = roster_list.get_selected_row() as ListRow; + if (selected_row != null) { + // TODO move in list to front immediately + ConversationManager.get_instance(stream_interactor).ensure_start_conversation(selected_row.jid, selected_row.account); + Conversation conversation = ConversationManager.get_instance(stream_interactor).get_conversation(selected_row.jid, selected_row.account); + conversation_opened(conversation); + } + close(); + } +} +} \ No newline at end of file diff --git a/client/src/ui/add_conversation/chat/roster_list.vala b/client/src/ui/add_conversation/chat/roster_list.vala new file mode 100644 index 00000000..9e970d8c --- /dev/null +++ b/client/src/ui/add_conversation/chat/roster_list.vala @@ -0,0 +1,77 @@ +using Gee; +using Gtk; + +using Dino.Entities; +using Xmpp; + +namespace Dino.Ui.AddConversation.Chat { +protected class RosterList : FilterableList { + + public signal void conversation_selected(Conversation? conversation); + private StreamInteractor stream_interactor; + + private HashMap rows = new HashMap(Jid.hash_func, Jid.equals_func); + + public RosterList(StreamInteractor stream_interactor) { + this.stream_interactor = stream_interactor; + + set_filter_func(filter); + set_header_func(header); + set_sort_func(sort); + + RosterManager.get_instance(stream_interactor).removed_roster_item.connect( (account, jid, roster_item) => { + Idle.add(() => { on_removed_roster_item(account, jid, roster_item); return false;});}); + RosterManager.get_instance(stream_interactor).updated_roster_item.connect( (account, jid, roster_item) => { + Idle.add(() => { on_updated_roster_item(account, jid, roster_item); return false;});}); + + foreach (Account account in stream_interactor.get_accounts()) { + foreach (Roster.Item roster_item in RosterManager.get_instance(stream_interactor).get_roster(account)) { + on_updated_roster_item(account, new Jid(roster_item.jid), roster_item); + } + } + } + + private void on_removed_roster_item(Account account, Jid jid, Roster.Item roster_item) { + if (rows.has_key(jid)) { + remove(rows[jid]); + rows.unset(jid); + } + } + + private void on_updated_roster_item(Account account, Jid jid, Roster.Item roster_item) { + on_removed_roster_item(account, jid, roster_item); + ListRow row = new ListRow.from_jid(stream_interactor, new Jid(roster_item.jid), account); + rows[jid] = row; + add(row); + invalidate_sort(); + invalidate_filter(); + } + + private void header(ListBoxRow row, ListBoxRow? before_row) { + if (row.get_header() == null && before_row != null) { + row.set_header(new Separator(Orientation.HORIZONTAL)); + } + } + + private bool filter(ListBoxRow r) { + if (r.get_type().is_a(typeof(ListRow))) { + ListRow row = r as ListRow; + if (filter_values != null) { + foreach (string filter in filter_values) { + if (!(row.name_label.label.down().contains(filter.down()) || + row.jid.to_string().down().contains(filter.down()))) { + return false; + } + } + } + } + return true; + } + + public override int sort(ListBoxRow row1, ListBoxRow row2) { + ListRow c1 = (row1 as ListRow); + ListRow c2 = (row2 as ListRow); + return c1.name_label.label.collate(c2.name_label.label); + } +} +} \ No newline at end of file diff --git a/client/src/ui/add_conversation/conference/add_groupchat_dialog.vala b/client/src/ui/add_conversation/conference/add_groupchat_dialog.vala new file mode 100644 index 00000000..aa86958d --- /dev/null +++ b/client/src/ui/add_conversation/conference/add_groupchat_dialog.vala @@ -0,0 +1,107 @@ +using Gee; +using Gtk; + +using Dino.Entities; + +namespace Dino.Ui.AddConversation.Conference { + +[GtkTemplate (ui = "/org/dino-im/add_conversation/add_groupchat_dialog.ui")] +protected class AddGroupchatDialog : Gtk.Dialog { + + [GtkChild] + private Stack accounts_stack; + + [GtkChild] + private ComboBoxText accounts_comboboxtext; + + [GtkChild] + private Label account_label; + + [GtkChild] + private Button ok_button; + + [GtkChild] + private Button cancel_button; + + [GtkChild] + private Entry jid_entry; + + [GtkChild] + private Entry alias_entry; + + [GtkChild] + private Entry nick_entry; + + [GtkChild] + private CheckButton autojoin_checkbutton; + + private StreamInteractor stream_interactor; + private Xmpp.Xep.Bookmarks.Conference? edit_confrence = null; + private bool alias_entry_changed = false; + + public AddGroupchatDialog(StreamInteractor stream_interactor) { + Object(use_header_bar : 1); + this.stream_interactor = stream_interactor; + ok_button.label = "Add"; + ok_button.get_style_context().add_class("suggested-action"); // TODO why doesn't it work in XML + accounts_stack.set_visible_child_name("combobox"); + foreach (Account account in stream_interactor.get_accounts()) { + accounts_comboboxtext.append_text(account.bare_jid.to_string()); + } + accounts_comboboxtext.set_active(0); + + cancel_button.clicked.connect(() => { close(); }); + ok_button.clicked.connect(on_ok_button_clicked); + jid_entry.key_press_event.connect_after(after_jid_entry_key_press); + nick_entry.key_press_event.connect(check_ok); + } + + public AddGroupchatDialog.for_conference(StreamInteractor stream_interactor, Account account, Xmpp.Xep.Bookmarks.Conference conference) { + this(stream_interactor); + edit_confrence = conference; + ok_button.label = "Save"; + ok_button.sensitive = true; + accounts_stack.set_visible_child_name("label"); + account_label.label = account.bare_jid.to_string(); + jid_entry.text = conference.jid; + nick_entry.text = conference.nick; + autojoin_checkbutton.active = conference.autojoin; + alias_entry.text = conference.name; + } + + private bool after_jid_entry_key_press() { + check_ok(); + if (!alias_entry_changed) { + Jid? parsed_jid = Jid.parse(jid_entry.text); + alias_entry.text = parsed_jid != null && parsed_jid.localpart != null ? parsed_jid.localpart : jid_entry.text; + } + return false; + } + + private bool check_ok() { + Jid? parsed_jid = Jid.parse(jid_entry.text); + ok_button.sensitive = parsed_jid != null && parsed_jid.localpart != null && parsed_jid.resourcepart == null && + nick_entry.text != "" && alias_entry.text != null; + return false; + } + + private void on_ok_button_clicked() { + Account? account = null; + foreach (Account account2 in stream_interactor.get_accounts()) { + if (accounts_comboboxtext.get_active_text() == account2.bare_jid.to_string()) { + account = account2; + } + } + Xmpp.Xep.Bookmarks.Conference conference = new Xmpp.Xep.Bookmarks.Conference(jid_entry.text); + conference.nick = nick_entry.text; + conference.name = alias_entry.text; + conference.autojoin = autojoin_checkbutton.active; + if (edit_confrence == null) { + MucManager.get_instance(stream_interactor).add_bookmark(account, conference); + } else { + MucManager.get_instance(stream_interactor).replace_bookmark(account, edit_confrence, conference); + } + close(); + } +} +} \ No newline at end of file diff --git a/client/src/ui/add_conversation/conference/conference_details_fragment.vala b/client/src/ui/add_conversation/conference/conference_details_fragment.vala new file mode 100644 index 00000000..edfeab9d --- /dev/null +++ b/client/src/ui/add_conversation/conference/conference_details_fragment.vala @@ -0,0 +1,148 @@ +using Gtk; + +using Dino.Entities; + +namespace Dino.Ui.AddConversation.Conference { + +[GtkTemplate (ui = "/org/dino-im/add_conversation/conference_details_fragment.ui")] +protected class ConferenceDetailsFragment : Box { + + public bool done { + get { + Jid? parsed_jid = Jid.parse(jid); + return parsed_jid != null && parsed_jid.localpart != null && + parsed_jid.resourcepart == null && nick != ""; + } + private set {} + } + + public Account account { + owned get { + foreach (Account account in stream_interactor.get_accounts()) { + if (accounts_comboboxtext.get_active_text() == account.bare_jid.to_string()) { + return account; + } + } + return null; + } + set { + accounts_label.label = value.bare_jid.to_string(); + accounts_comboboxtext.set_active_id(value.bare_jid.to_string()); + } + } + public string jid { + get { return jid_label.label; } + set { + jid_label.label = value; + jid_entry.text = value; + } + } + public string nick { + get { return nick_label.label; } + set { + nick_label.label = value; + nick_entry.text = value; + } + } + public string password { + get { return password_label.label; } + set { + password_label.label = value; + password_entry.text = value; + } + } + + [GtkChild] + private Stack accounts_stack; + + [GtkChild] + private Stack jid_stack; + + [GtkChild] + private Stack nick_stack; + + [GtkChild] + private Stack password_stack; + + [GtkChild] + private Button accounts_button; + + [GtkChild] + private Button jid_button; + + [GtkChild] + private Button nick_button; + + [GtkChild] + private Button password_button; + + [GtkChild] + private Label accounts_label; + + [GtkChild] + private Label jid_label; + + [GtkChild] + private Label nick_label; + + [GtkChild] + private Label password_label; + + [GtkChild] + private ComboBoxText accounts_comboboxtext; + + [GtkChild] + private Entry jid_entry; + + [GtkChild] + private Entry nick_entry; + + [GtkChild] + private Entry password_entry; + + private StreamInteractor stream_interactor; + + public ConferenceDetailsFragment(StreamInteractor stream_interactor) { + this.stream_interactor = stream_interactor; + + accounts_stack.set_visible_child_name("label"); + jid_stack.set_visible_child_name("label"); + nick_stack.set_visible_child_name("label"); + password_stack.set_visible_child_name("label"); + + accounts_button.clicked.connect(() => { set_active_stack(accounts_stack); }); + jid_button.clicked.connect(() => { set_active_stack(jid_stack); }); + nick_button.clicked.connect(() => { set_active_stack(nick_stack); }); + password_button.clicked.connect(() => { set_active_stack(password_stack); }); + + accounts_comboboxtext.changed.connect(() => { accounts_label.label = accounts_comboboxtext.get_active_text(); }); + jid_entry.key_press_event.connect(() => { jid_label.label = jid_entry.text; return false; }); + nick_entry.key_press_event.connect(() => { nick_label.label = nick_entry.text; return false; }); + password_entry.key_press_event.connect(() => { password_label.label = password_entry.text; return false; }); + + jid_entry.key_press_event.connect(() => { done = true; return false; }); // just for notifying + nick_entry.key_press_event.connect(() => { done = true; return false; }); + + foreach (Account account in stream_interactor.get_accounts()) { + accounts_comboboxtext.append_text(account.bare_jid.to_string()); + } + accounts_comboboxtext.set_active(0); + } + + public void clear() { + jid = ""; + nick = ""; + password = ""; + } + + private void set_active_stack(Stack stack) { + stack.set_visible_child_name("entry"); + if (stack != accounts_stack) accounts_stack.set_visible_child_name("label"); + if (stack != jid_stack) jid_stack.set_visible_child_name("label"); + if (stack != nick_stack) nick_stack.set_visible_child_name("label"); + if (stack != password_stack) password_stack.set_visible_child_name("label"); + } + +} + +} \ No newline at end of file diff --git a/client/src/ui/add_conversation/conference/conference_list.vala b/client/src/ui/add_conversation/conference/conference_list.vala new file mode 100644 index 00000000..2e461472 --- /dev/null +++ b/client/src/ui/add_conversation/conference/conference_list.vala @@ -0,0 +1,105 @@ +using Gee; +using Gtk; + +using Xmpp; +using Dino.Entities; + +namespace Dino.Ui.AddConversation.Conference { +protected class ConferenceList : FilterableList { + + public signal void conversation_selected(Conversation? conversation); + + private StreamInteractor stream_interactor; + private HashMap> lists = new HashMap>(); + + public ConferenceList(StreamInteractor stream_interactor) { + this.stream_interactor = stream_interactor; + + set_filter_func(filter); + set_header_func(header); + set_sort_func(sort); + + MucManager.get_instance(stream_interactor).bookmarks_updated.connect((account, conferences) => { + Idle.add(() => { + lists[account] = conferences; + refresh_conferences(); + return false; + }); + }); + + foreach (Account account in stream_interactor.get_accounts()) { + MucManager.get_instance(stream_interactor).get_bookmarks(account, new BookmarksListener(this, stream_interactor, account)); + } + } + + public void refresh_conferences() { + @foreach((widget) => { remove(widget); }); + foreach (Account account in lists.keys) { + foreach (Xep.Bookmarks.Conference conference in lists[account]) { + add(new ConferenceListRow(stream_interactor, conference, account)); + } + } + } + + private class BookmarksListener : Xep.Bookmarks.ConferencesRetrieveResponseListener, Object { + ConferenceList outer; + Account account; + public BookmarksListener(ConferenceList outer, StreamInteractor stream_interactor, Account account) { + this.outer = outer; + this.account = account; + } + + public void on_result(Core.XmppStream stream, ArrayList conferences) { + outer.lists[account] = conferences; + Idle.add(() => { outer.refresh_conferences(); return false; }); + } + } + + private void header(ListBoxRow row, ListBoxRow? before_row) { + if (row.get_header() == null && before_row != null) { + row.set_header(new Separator(Orientation.HORIZONTAL)); + } + } + + private bool filter(ListBoxRow r) { + if (r.get_type().is_a(typeof(ListRow))) { + ListRow row = r as ListRow; + if (filter_values != null) { + foreach (string filter in filter_values) { + if (!(row.name_label.label.down().contains(filter.down()) || + row.jid.to_string().down().contains(filter.down()))) { + return false; + } + } + } + } + return true; + } + + public override int sort(ListBoxRow row1, ListBoxRow row2) { + ListRow c1 = (row1 as ListRow); + ListRow c2 = (row2 as ListRow); + return c1.name_label.label.collate(c2.name_label.label); + } +} + +internal class ConferenceListRow : ListRow { + + public Xep.Bookmarks.Conference bookmark; + + public ConferenceListRow(StreamInteractor stream_interactor, Xep.Bookmarks.Conference bookmark, Account account) { + this.jid = new Jid(bookmark.jid); + this.account = account; + this.bookmark = bookmark; + + if (bookmark.name != "" && bookmark.name != bookmark.jid) { + name_label.label = bookmark.name; + via_label.label = bookmark.jid; + } else { + name_label.label = bookmark.jid; + via_label.visible = false; + } + image.set_from_pixbuf((new AvatarGenerator(35, 35)).set_stateless(true).draw_jid(stream_interactor, jid, account)); + } +} +} \ No newline at end of file diff --git a/client/src/ui/add_conversation/conference/dialog.vala b/client/src/ui/add_conversation/conference/dialog.vala new file mode 100644 index 00000000..8bf29bb4 --- /dev/null +++ b/client/src/ui/add_conversation/conference/dialog.vala @@ -0,0 +1,165 @@ +using Gee; +using Gtk; + +using Dino.Entities; + +namespace Dino.Ui.AddConversation.Conference { + +public class Dialog : Gtk.Dialog { + + public signal void conversation_opened(Conversation conversation); + + private Stack stack = new Stack(); + private Button cancel_button; + private Button ok_button; + private Label cancel_label = new Label("Cancel") {visible=true}; + private Image cancel_image = new Image.from_icon_name("go-previous-symbolic", IconSize.MENU) {visible=true}; + + private SelectJidFragment select_fragment; + private ConferenceDetailsFragment details_fragment; + private ConferenceList conference_list; + + private StreamInteractor stream_interactor; + + public Dialog(StreamInteractor stream_interactor) { + Object(use_header_bar : 1); + this.title = "Join Conference"; + this.modal = true; + this.stream_interactor = stream_interactor; + + stack.visible = true; + stack.vhomogeneous = false; + get_content_area().add(stack); + + setup_headerbar(); + setup_jid_add_view(); + setup_conference_details_view(); + show_jid_add_view(); + } + + private void show_jid_add_view() { + cancel_button.remove(cancel_image); + cancel_button.add(cancel_label); + cancel_button.clicked.disconnect(show_jid_add_view); + cancel_button.clicked.connect(close); + ok_button.label = "Next"; + ok_button.sensitive = select_fragment.done; + ok_button.clicked.disconnect(on_ok_button_clicked); + ok_button.clicked.connect(on_next_button_clicked); + details_fragment.notify["done"].disconnect(set_ok_sensitive_from_details); + select_fragment.notify["done"].connect(set_ok_sensitive_from_select); + stack.transition_type = StackTransitionType.SLIDE_RIGHT; + stack.set_visible_child_name("select"); + } + + private void show_conference_details_view() { + cancel_button.remove(cancel_label); + cancel_button.add(cancel_image); + cancel_button.clicked.disconnect(close); + cancel_button.clicked.connect(show_jid_add_view); + ok_button.label = "Join"; + ok_button.sensitive = details_fragment.done; + ok_button.clicked.disconnect(show_conference_details_view); + ok_button.clicked.connect(on_ok_button_clicked); + select_fragment.notify["done"].disconnect(set_ok_sensitive_from_select); + details_fragment.notify["done"].connect(set_ok_sensitive_from_details); + stack.transition_type = StackTransitionType.SLIDE_LEFT; + stack.set_visible_child_name("details"); + animate_window_resize(); + } + + private void setup_headerbar() { + HeaderBar header_bar = get_header_bar() as HeaderBar; + header_bar.show_close_button = false; + + cancel_button = new Button(); + header_bar.pack_start(cancel_button); + cancel_button.visible = true; + + ok_button = new Button(); + header_bar.pack_end(ok_button); + ok_button.get_style_context().add_class("suggested-action"); + ok_button.visible = true; + ok_button.can_focus = true; + ok_button.can_default = true; + ok_button.has_default = true; + } + + private void setup_jid_add_view() { + conference_list = new ConferenceList(stream_interactor); + conference_list.row_activated.connect(() => { ok_button.clicked(); }); + select_fragment = new SelectJidFragment(stream_interactor, conference_list); + select_fragment.add_jid.connect((row) => { + AddGroupchatDialog dialog = new AddGroupchatDialog(stream_interactor); + dialog.set_transient_for(this); + dialog.show(); + }); + select_fragment.edit_jid.connect((row) => { + ConferenceListRow conference_row = row as ConferenceListRow; + AddGroupchatDialog dialog = new AddGroupchatDialog.for_conference(stream_interactor, conference_row.account, conference_row.bookmark); + dialog.set_transient_for(this); + dialog.show(); + }); + select_fragment.remove_jid.connect((row) => { + ConferenceListRow conference_row = row as ConferenceListRow; + MucManager.get_instance(stream_interactor).remove_bookmark(conference_row.account, conference_row.bookmark); + }); + stack.add_named(select_fragment, "select"); + } + + private void setup_conference_details_view() { + details_fragment = new ConferenceDetailsFragment(stream_interactor); + stack.add_named(details_fragment, "details"); + } + + private void set_ok_sensitive_from_select() { + ok_button.sensitive = select_fragment.done; + } + + private void set_ok_sensitive_from_details() { + ok_button.sensitive = select_fragment.done; + } + + private void on_next_button_clicked() { + details_fragment.clear(); + ListRow? row = conference_list.get_selected_row() as ListRow; + ConferenceListRow? conference_row = conference_list.get_selected_row() as ConferenceListRow; + if (conference_row != null) { + details_fragment.jid = conference_row.bookmark.jid; + details_fragment.nick = conference_row.bookmark.nick; + if (conference_row.bookmark.password != null) details_fragment.password = conference_row.bookmark.password; + ok_button.grab_focus(); + } else if (row != null) { + details_fragment.jid = row.jid.to_string(); + } + show_conference_details_view(); + } + + private void on_ok_button_clicked() { + MucManager.get_instance(stream_interactor).join(details_fragment.account, new Jid(details_fragment.jid), details_fragment.nick); + close(); + } + + private void close() { + base.close(); + } + + private void animate_window_resize() { + int def_height, curr_width, curr_height; + get_size(out curr_width, out curr_height); + stack.get_preferred_height(null, out def_height); + int difference = def_height - curr_height; + Timer timer = new Timer(); + Timeout.add((int) (stack.transition_duration / 30), + () => { + ulong microsec; + timer.elapsed(out microsec); + ulong millisec = microsec / 1000; + double partial = double.min(1, (double) millisec / stack.transition_duration); + resize(curr_width, (int) (curr_height + difference * partial)); + return millisec < stack.transition_duration; + }); + } +} + +} \ No newline at end of file diff --git a/client/src/ui/add_conversation/list_row.vala b/client/src/ui/add_conversation/list_row.vala new file mode 100644 index 00000000..5c2eff97 --- /dev/null +++ b/client/src/ui/add_conversation/list_row.vala @@ -0,0 +1,43 @@ +using Gee; +using Gtk; + +using Dino.Entities; + +namespace Dino.Ui.AddConversation { + +[GtkTemplate (ui = "/org/dino-im/add_conversation/list_row.ui")] +public class ListRow : ListBoxRow { + + [GtkChild] + public Image image; + + [GtkChild] + public Label name_label; + + [GtkChild] + public Label via_label; + + public Jid? jid; + public Account? account; + + public ListRow() {} + + public ListRow.from_jid(StreamInteractor stream_interactor, Jid jid, Account account) { + this.jid = jid; + this.account = account; + + string display_name = Util.get_display_name(stream_interactor, jid, account); + if (stream_interactor.get_accounts().size > 1) { + via_label.label = @"via $(account.bare_jid)"; + this.has_tooltip = true; + set_tooltip_text(jid.to_string()); + } else if (display_name != jid.bare_jid.to_string()){ + via_label.label = jid.bare_jid.to_string(); + } else { + via_label.visible = false; + } + name_label.label = display_name; + image.set_from_pixbuf((new AvatarGenerator(35, 35)).draw_jid(stream_interactor, jid, account)); + } +} +} \ No newline at end of file diff --git a/client/src/ui/add_conversation/select_jid_fragment.vala b/client/src/ui/add_conversation/select_jid_fragment.vala new file mode 100644 index 00000000..847a9ecb --- /dev/null +++ b/client/src/ui/add_conversation/select_jid_fragment.vala @@ -0,0 +1,124 @@ +using Gee; +using Gtk; + +using Dino.Entities; + +namespace Dino.Ui.AddConversation { + +[GtkTemplate (ui = "/org/dino-im/add_conversation/select_jid_fragment.ui")] +public class SelectJidFragment : Gtk.Box { + + public signal void add_jid(); + public signal void edit_jid(ListRow row); + public signal void remove_jid(ListRow row); + public bool done { + get { + return filterable_list.get_selected_row() != null; + } + private set {} } + + [GtkChild] + private Entry entry; + + [GtkChild] + private Box box; + + [GtkChild] + private Button add_button; + + [GtkChild] + private Button edit_button; + + [GtkChild] + private Button remove_button; + + private FilterableList filterable_list; + private ArrayList added_rows = new ArrayList(); + private StreamInteractor stream_interactor; + + public SelectJidFragment(StreamInteractor stream_interactor, FilterableList filterable_list) { + this.stream_interactor = stream_interactor; + this.filterable_list = filterable_list; + + filterable_list.visible = true; + filterable_list.activate_on_single_click = false; + filterable_list.vexpand = true; + box.add(filterable_list); + + filterable_list.set_sort_func(sort); + filterable_list.row_selected.connect(check_buttons_active); + filterable_list.row_selected.connect(() => { done = true; }); // just for notifying + entry.changed.connect(on_entry_changed); + add_button.clicked.connect(() => { add_jid(); }); + remove_button.clicked.connect(() => { remove_jid(filterable_list.get_selected_row() as ListRow); }); + edit_button.clicked.connect(() => { edit_jid(filterable_list.get_selected_row() as ListRow); }); + } + + private void on_entry_changed() { + foreach (AddListRow row in added_rows) { + filterable_list.remove(row); + } + added_rows.clear(); + + string[] ? values; + string str = entry.get_text(); + values = str == "" ? null : str.split(" "); + filterable_list.set_filter_values(values); + Jid? parsed_jid = Jid.parse(str); + if (parsed_jid != null && parsed_jid.localpart != null) { + foreach (Account account in stream_interactor.get_accounts()) { + AddListRow row = new AddListRow(stream_interactor, str, account); + filterable_list.add(row); + added_rows.add(row); + } + } + } + + private void check_buttons_active() { + ListBoxRow? row = filterable_list.get_selected_row(); + bool active = row != null && !row.get_type().is_a(typeof(AddListRow)); + edit_button.sensitive = active; + remove_button.sensitive = active; + } + + private int sort(ListBoxRow row1, ListBoxRow row2) { + AddListRow al1 = (row1 as AddListRow); + AddListRow al2 = (row2 as AddListRow); + if (al1 != null && al2 == null) { + return -1; + } else if (al2 != null && al1 == null) { + return 1; + } + return filterable_list.sort(row1, row2); + } + + private class AddListRow : ListRow { + + public AddListRow(StreamInteractor stream_interactor, string jid, Account account) { + this.account = account; + this.jid = new Jid(jid); + + name_label.label = jid; + if (stream_interactor.get_accounts().size > 1) { + via_label.label = account.bare_jid.to_string(); + } else { + via_label.visible = false; + } + image.set_from_pixbuf((new AvatarGenerator(35, 35)).set_greyscale(true).draw_text("?")); + } + } +} + +public abstract class FilterableList : Gtk.ListBox { + public string[]? filter_values; + + public void set_filter_values(string[] values) { + if (filter_values == values) return; + filter_values = values; + invalidate_filter(); + } + + public abstract int sort(ListBoxRow row1, ListBoxRow row2); +} + +} \ No newline at end of file diff --git a/client/src/ui/application.vala b/client/src/ui/application.vala new file mode 100644 index 00000000..c3f0e302 --- /dev/null +++ b/client/src/ui/application.vala @@ -0,0 +1,112 @@ +using Gtk; + +using Dino.Entities; + +public class Dino.Ui.Application : Gtk.Application { + + private Database db; + private StreamInteractor stream_interaction; + + private Notifications notifications; + private UnifiedWindow? window; + private ConversationSelector.View? filterable_conversation_list; + private ConversationSelector.List? conversation_list; + private ConversationSummary.View? conversation_frame; + private ChatInput? chat_input; + + public Application() { + this.db = new Database("store.sqlite3"); + this.stream_interaction = new StreamInteractor(db); + + AvatarManager.start(stream_interaction, db); + MessageManager.start(stream_interaction, db); + CounterpartInteractionManager.start(stream_interaction); + PresenceManager.start(stream_interaction); + MucManager.start(stream_interaction); + PgpManager.start(stream_interaction, db); + RosterManager.start(stream_interaction); + ConversationManager.start(stream_interaction, db); + ChatInteraction.start(stream_interaction); + + notifications = new Notifications(stream_interaction); + notifications.start(); + + load_css(); + } + + public override void activate() { + create_set_app_menu(); + create_window(); + window.show_all(); + restore(); + } + + private void create_window() { + window = new UnifiedWindow(this, stream_interaction); + + filterable_conversation_list = window.filterable_conversation_list; + conversation_list = window.filterable_conversation_list.conversation_list; + conversation_frame = window.conversation_frame; + chat_input = window.chat_input; + } + + private void show_accounts_window() { + ManageAccounts.Dialog dialog = new ManageAccounts.Dialog(stream_interaction, db); + dialog.set_transient_for(window); + dialog.account_enabled.connect(add_connection); + dialog.account_disabled.connect(remove_connection); + dialog.show(); + } + + private void show_settings_window() { + SettingsDialog dialog = new SettingsDialog(); + dialog.set_transient_for(window); + dialog.show(); + } + + private void create_set_app_menu() { + SimpleAction accounts_action = new SimpleAction("accounts", null); + accounts_action.activate.connect(show_accounts_window); + add_action(accounts_action); + + SimpleAction settings_action = new SimpleAction("settings", null); + settings_action.activate.connect(show_settings_window); + add_action(settings_action); + + SimpleAction quit_action = new SimpleAction("quit", null); + quit_action.activate.connect(quit); + add_action(quit_action); + add_accelerator("Q", "app.quit", null); + + Builder builder = new Builder.from_resource("/org/dino-im/menu_app.ui"); + MenuModel menu = builder.get_object("menu_app") as MenuModel; + + set_app_menu(menu); + } + + private void restore() { + foreach (Account account in db.get_accounts()) { + if (account.enabled) add_connection(account); + } + } + + private void add_connection(Account account) { + stream_interaction.connect(account); + } + + private void remove_connection(Account account) { + stream_interaction.disconnect(account); + } + + private void load_css() { + var css_provider = new Gtk.CssProvider (); + try { + var file = File.new_for_uri("resource:///org/dino-im/style.css"); + css_provider.load_from_file (file); + } catch (GLib.Error e) { + warning ("loading css: %s", e.message); + } + Gtk.StyleContext.add_provider_for_screen (Gdk.Screen.get_default(), css_provider, Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION); + } +} + diff --git a/client/src/ui/avatar_generator.vala b/client/src/ui/avatar_generator.vala new file mode 100644 index 00000000..e168c4a4 --- /dev/null +++ b/client/src/ui/avatar_generator.vala @@ -0,0 +1,233 @@ +using Cairo; +using Gee; +using Gdk; +using Gtk; + +using Dino.Entities; + +namespace Dino.Ui { +public class AvatarGenerator { + + private const string COLOR_GREY = "E0E0E0"; + private const string GROUPCHAT_ICON = "system-users-symbolic"; + + StreamInteractor? stream_interactor; + bool greyscale = false; + bool stateless = false; + int width; + int height; + int scale_factor; + + public AvatarGenerator(int width, int height, int scale_factor = 1) { + this.width = width; + this.height = height; + this.scale_factor = scale_factor; + } + + public Pixbuf draw_jid(StreamInteractor stream_interactor, Jid jid, Account account) { + this.stream_interactor = stream_interactor; + return crop_corners(draw_tile(jid, account, width * scale_factor, height * scale_factor)); + } + + public Pixbuf draw_message(StreamInteractor stream_interactor, Message message) { + Jid? real_jid = MucManager.get_instance(stream_interactor).get_message_real_jid(message); + return draw_jid(stream_interactor, real_jid != null ? real_jid : message.from, message.account); + } + + public Pixbuf draw_conversation(StreamInteractor stream_interactor, Conversation conversation) { + return draw_jid(stream_interactor, conversation.counterpart, conversation.account); + } + + public Pixbuf draw_account(StreamInteractor stream_interactor, Account account) { + return draw_jid(stream_interactor, account.bare_jid, account); + } + + public Pixbuf draw_text(string text) { + string color = greyscale ? COLOR_GREY : Util.get_avatar_hex_color(text); + Pixbuf pixbuf = draw_colored_rectangle_text(color, text, width, height); + return crop_corners(pixbuf); + } + + public AvatarGenerator set_greyscale(bool greyscale) { + this.greyscale = greyscale; + return this; + } + + public AvatarGenerator set_stateless(bool stateless) { + this.stateless = stateless; + return this; + } + + private int get_left_border() { + return (int)Math.floor(scale_factor/2.0); + } + + private int get_right_border() { + return (int)Math.ceil(scale_factor/2.0); + } + + private void add_tile_to_pixbuf(Pixbuf pixbuf, Jid jid, Account account, int width, int height, int x, int y) { + Pixbuf tile = draw_chat_tile(jid, account, width, height); + tile.copy_area(0, 0, width, height, pixbuf, x, y); + } + + private Pixbuf draw_tile(Jid jid, Account account, int width, int height) { + if (MucManager.get_instance(stream_interactor).is_groupchat(jid, account)) { + return draw_groupchat_tile(jid, account, width, height); + } else { + return draw_chat_tile(jid, account, width, height); + } + } + + private Pixbuf draw_chat_tile(Jid jid, Account account, int width, int height) { + if (MucManager.get_instance(stream_interactor).is_groupchat_occupant(jid, account)) { + Jid? real_jid = MucManager.get_instance(stream_interactor).get_real_jid(jid, account); + if (real_jid != null) { + return draw_tile(real_jid, account, width, height); + } + } + Pixbuf? avatar = AvatarManager.get_instance(stream_interactor).get_avatar(account, jid); + if (avatar != null) { + double desired_ratio = (double) width / height; + double avatar_ratio = (double) avatar.width / avatar.height; + if (avatar_ratio > desired_ratio) { + int comp_width = width * avatar.height / height; + avatar = new Pixbuf.subpixbuf(avatar, avatar.width / 2 - comp_width / 2, 0, comp_width, avatar.height); + } else if (avatar_ratio < desired_ratio) { + int comp_height = height * avatar.width / width; + avatar = new Pixbuf.subpixbuf(avatar, 0, avatar.height / 2 - comp_height / 2, avatar.width, comp_height); + } + avatar = avatar.scale_simple(width, height, InterpType.BILINEAR); + if (greyscale) avatar = convert_to_greyscale(avatar); + return avatar; + } else { + string display_name = Util.get_display_name(stream_interactor, jid, account); + string color = greyscale ? COLOR_GREY : Util.get_avatar_hex_color(display_name); + return draw_colored_rectangle_text(color, display_name.get_char(0).toupper().to_string(), width, height); + } + } + + private Pixbuf draw_groupchat_tile(Jid jid, Account account, int width, int height) { + ArrayList? occupants = MucManager.get_instance(stream_interactor).get_other_occupants(jid, account); + if (stateless || occupants == null || occupants.size == 0) { + return draw_chat_tile(jid, account, width, height); + } + Pixbuf pixbuf = initialize_pixbuf(width, height); + if (occupants.size == 1 || occupants.size == 2 || occupants.size == 3) { + add_tile_to_pixbuf(pixbuf, occupants[0], account, width / 2 - get_right_border(), height, 0, 0); + if (occupants.size == 1) { + add_tile_to_pixbuf(pixbuf, account.bare_jid, account, width / 2 - get_left_border(), height, width / 2 + get_left_border(), 0); + } else if (occupants.size == 2) { + add_tile_to_pixbuf(pixbuf, occupants[1], account, width / 2 - get_left_border(), height, width / 2 + get_left_border(), 0); + } else if (occupants.size == 3) { + add_tile_to_pixbuf(pixbuf, occupants[1], account, width / 2 - get_left_border(), height / 2 - get_right_border(), width / 2 + get_left_border(), 0); + add_tile_to_pixbuf(pixbuf, occupants[2], account, width / 2 - get_left_border(), height / 2 - get_left_border(), width / 2 + get_left_border(), height / 2 + get_left_border()); + } + } else if (occupants.size >= 4) { + add_tile_to_pixbuf(pixbuf, occupants[0], account, width / 2 - get_right_border(), height / 2 - get_right_border(), 0, 0); + add_tile_to_pixbuf(pixbuf, occupants[1], account, width / 2 - get_left_border(), height / 2 - get_right_border(), width / 2 + get_left_border(), 0); + add_tile_to_pixbuf(pixbuf, occupants[2], account, width / 2 - get_right_border(), height / 2 - get_left_border(), 0, height / 2 + get_left_border()); + if (occupants.size == 4) { + add_tile_to_pixbuf(pixbuf, occupants[3], account, width / 2 - get_left_border(), height / 2 - get_left_border(), width / 2 + get_left_border(), height / 2 + get_left_border()); + } else if (occupants.size > 4) { + Pixbuf plus_pixbuf = draw_colored_rectangle_text("555753", "+", width / 2 - get_left_border(), height / 2 - get_left_border()); + if (greyscale) plus_pixbuf = convert_to_greyscale(plus_pixbuf); + plus_pixbuf.copy_area(0, 0, width / 2 - get_left_border(), height / 2 - get_left_border(), pixbuf, width / 2 + get_left_border(), height / 2 + get_left_border()); + } + } + return pixbuf; + } + + public Pixbuf draw_colored_icon(string hex_color, string icon, int width, int height) { + int ICON_SIZE = width > 20 * scale_factor ? 17 * scale_factor : 14 * scale_factor; + + Context rectancle_context = new Context(new ImageSurface(Format.ARGB32, width, height)); + draw_colored_rectangle(rectancle_context, hex_color, width, height); + + Pixbuf icon_pixbuf = IconTheme.get_default().load_icon(icon, ICON_SIZE, IconLookupFlags.FORCE_SIZE); + Surface icon_surface = cairo_surface_create_from_pixbuf(icon_pixbuf, 1, null); + Context context = new Context(icon_surface); + context.set_operator(Operator.IN); + context.set_source_rgba(1, 1, 1, 1); + context.rectangle(0, 0, width, height); + context.fill(); + + rectancle_context.set_source_surface(icon_surface, width / 2 - ICON_SIZE / 2, height / 2 - ICON_SIZE / 2); + rectancle_context.paint(); + + return pixbuf_get_from_surface(rectancle_context.get_target(), 0, 0, width, height); + } + + public Pixbuf draw_colored_rectangle_text(string hex_color, string text, int width, int height) { + Context ctx = new Context(new ImageSurface(Format.ARGB32, width, height)); + draw_colored_rectangle(ctx, hex_color, width, height); + draw_center_text(ctx, text, width < 40 * scale_factor ? 17 * scale_factor : 25 * scale_factor, width, height); + return pixbuf_get_from_surface(ctx.get_target(), 0, 0, width, height); + } + + private static void draw_center_text(Context ctx, string text, int fontsize, int width, int height) { + ctx.select_font_face("Sans", Cairo.FontSlant.NORMAL, Cairo.FontWeight.NORMAL); + ctx.set_font_size(fontsize); + Cairo.TextExtents extents; + ctx.text_extents(text, out extents); + double x_pos = width/2 - (extents.width/2 + extents.x_bearing); + double y_pos = height/2 - (extents.height/2 + extents.y_bearing); + ctx.move_to(x_pos, y_pos); + ctx.set_source_rgba(1, 1, 1, 1); + ctx.show_text(text); + } + + private static void draw_colored_rectangle(Context ctx, string hex_color, int width, int height) { + set_source_hex_color(ctx, hex_color); + ctx.rectangle(0, 0, width, height); + ctx.fill(); + } + + private static Pixbuf convert_to_greyscale(Pixbuf pixbuf) { + Surface surface = cairo_surface_create_from_pixbuf(pixbuf, 1, null); + Context context = new Context(surface); + // convert to greyscale + context.set_operator(Operator.HSL_COLOR); + context.set_source_rgb(1, 1, 1); + context.rectangle(0, 0, pixbuf.width, pixbuf.height); + context.fill(); + // make the visible part more light + context.set_operator(Operator.ATOP); + context.set_source_rgba(1, 1, 1, 0.7); + context.rectangle(0, 0, pixbuf.width, pixbuf.height); + context.fill(); + return pixbuf_get_from_surface(context.get_target(), 0, 0, pixbuf.width, pixbuf.height); + } + + private Pixbuf crop_corners(Pixbuf pixbuf, double radius = 3) { + radius *= scale_factor; + Context ctx = new Context(new ImageSurface(Format.ARGB32, pixbuf.width, pixbuf.height)); + cairo_set_source_pixbuf(ctx, pixbuf, 0, 0); + double degrees = Math.PI / 180.0; + ctx.new_sub_path(); + ctx.arc(pixbuf.width - radius, radius, radius, -90 * degrees, 0 * degrees); + ctx.arc(pixbuf.width - radius, pixbuf.height - radius, radius, 0 * degrees, 90 * degrees); + ctx.arc(radius, pixbuf.height - radius, radius, 90 * degrees, 180 * degrees); + ctx.arc(radius, radius, radius, 180 * degrees, 270 * degrees); + ctx.close_path(); + ctx.clip(); + ctx.paint(); + return pixbuf_get_from_surface(ctx.get_target(), 0, 0, pixbuf.width, pixbuf.height); + } + + private static Pixbuf initialize_pixbuf(int width, int height) { + Context ctx = new Context(new ImageSurface(Format.ARGB32, width, height)); + ctx.set_source_rgba(1, 1, 1, 0); + ctx.rectangle(0, 0, width, height); + ctx.fill(); + return pixbuf_get_from_surface(ctx.get_target(), 0, 0, width, height); + } + + private static void set_source_hex_color(Context ctx, string hex_color) { + ctx.set_source_rgba((double) hex_color.substring(0, 2).to_long(null, 16) / 255, + (double) hex_color.substring(2, 2).to_long(null, 16) / 255, + (double) hex_color.substring(4, 2).to_long(null, 16) / 255, + hex_color.length > 6 ? (double) hex_color.substring(6, 2).to_long(null, 16) / 255 : 1); + } +} +} diff --git a/client/src/ui/chat_input.vala b/client/src/ui/chat_input.vala new file mode 100644 index 00000000..d2f9c562 --- /dev/null +++ b/client/src/ui/chat_input.vala @@ -0,0 +1,123 @@ +using Gdk; +using Gee; +using Gtk; + +using Dino.Entities; +using Xmpp; + +namespace Dino.Ui { +[GtkTemplate (ui = "/org/dino-im/chat_input.ui")] +public class ChatInput : Grid { + + [GtkChild] + private TextView text_input; + + private Conversation? conversation; + private StreamInteractor stream_interactor; + private HashMap entry_cache = new HashMap(Conversation.hash_func, Conversation.equals_func); + private static HashMap smiley_translations = new HashMap(); + + static construct { + smiley_translations[":)"] = "🙂"; + smiley_translations[":D"] = "😀"; + smiley_translations[";)"] = "😉"; + smiley_translations["O:)"] = "😇"; + smiley_translations["]:>"] = "😈"; + smiley_translations[":o"] = "😮"; + smiley_translations[":P"] = "😛"; + smiley_translations[";P"] = "😜"; + smiley_translations[":("] = "🙁"; + smiley_translations[":'("] = "😢"; + smiley_translations[":/"] = "😕"; + smiley_translations["-.-"] = "😑"; + } + + public ChatInput(StreamInteractor stream_interactor) { + this.stream_interactor = stream_interactor; + } + + public void initialize_for_conversation(Conversation conversation) { + if (this.conversation != null) { + if (text_input.buffer.text != "") { + entry_cache[this.conversation] = text_input.buffer.text; + } else { + entry_cache.unset(this.conversation); + } + } + this.conversation = conversation; + text_input.buffer.text = ""; + if (entry_cache.has_key(conversation)) { + text_input.buffer.text = entry_cache[conversation]; + } + text_input.key_press_event.connect(on_text_input_key_press); + text_input.key_release_event.connect(on_text_input_key_release); + text_input.grab_focus(); + } + + private void send_text() { + string text = text_input.buffer.text; + if (text.has_prefix("/")) { + string[] token = text.split(" ", 2); + switch(token[0]) { + case "/kick": + MucManager.get_instance(stream_interactor).kick(conversation.account, conversation.counterpart, token[1]); + break; + case "/me": + MessageManager.get_instance(stream_interactor).send_message(text, conversation); + break; + case "/nick": + MucManager.get_instance(stream_interactor).change_nick(conversation.account, conversation.counterpart, token[1]); + break; + case "/ping": // TODO remove this + Xep.Ping.Module.get_module(stream_interactor.get_stream(conversation.account)) + .send_ping(stream_interactor.get_stream(conversation.account), @"$(conversation.counterpart.bare_jid)/$(token[1])"); + Xep.Ping.Module.get_module(stream_interactor.get_stream(conversation.account)).get_id(); + break; + case "/topic": + MucManager.get_instance(stream_interactor).change_subject(conversation.account, conversation.counterpart, token[1]); + break; + } + } else { + MessageManager.get_instance(stream_interactor).send_message(text, conversation); + } + text_input.buffer.text = ""; + } + + private bool on_text_input_key_press(EventKey event) { + if (event.keyval == Key.space || event.keyval == Key.Return) { + check_convert_smiley(); + } + if (event.keyval == Key.Return) { + if (event.state == ModifierType.SHIFT_MASK) { + text_input.buffer.insert_at_cursor("\n", 1); + } else if (text_input.buffer.text != ""){ + send_text(); + } + return true; + } + return false; + } + + private void check_convert_smiley() { + if (Dino.Settings.instance().convert_utf8_smileys) { + foreach (string smiley in smiley_translations.keys) { + if (text_input.buffer.text.has_suffix(smiley)) { + if (text_input.buffer.text.length == smiley.length || + text_input.buffer.text[text_input.buffer.text.length - smiley.length - 1] == ' ') { + text_input.buffer.text = text_input.buffer.text.substring(0, text_input.buffer.text.length - smiley.length) + smiley_translations[smiley]; + } + } + } + } + } + + private bool on_text_input_key_release(EventKey event) { + if (text_input.buffer.text != "") { + ChatInteraction.get_instance(stream_interactor).on_message_entered(conversation); + } else { + ChatInteraction.get_instance(stream_interactor).on_message_cleared(conversation); + } + return false; + } +} +} \ No newline at end of file diff --git a/client/src/ui/conversation_list_titlebar.vala b/client/src/ui/conversation_list_titlebar.vala new file mode 100644 index 00000000..4835ec66 --- /dev/null +++ b/client/src/ui/conversation_list_titlebar.vala @@ -0,0 +1,47 @@ +using Gtk; + +using Dino.Entities; + +[GtkTemplate (ui = "/org/dino-im/conversation_list_titlebar.ui")] +public class Dino.Ui.ConversationListTitlebar : Gtk.HeaderBar { + + public signal void conversation_opened(Conversation conversation); + + [GtkChild] + private MenuButton add_button; + + [GtkChild] + public ToggleButton search_button; + + private StreamInteractor stream_interactor; + + public ConversationListTitlebar(ApplicationWindow application, StreamInteractor stream_interactor) { + this.stream_interactor = stream_interactor; + create_add_menu(application); + } + + private void create_add_menu(ApplicationWindow application) { + SimpleAction contacts_action = new SimpleAction("add_chat", null); + contacts_action.activate.connect(() => { + AddConversation.Chat.Dialog add_chat_dialog = new AddConversation.Chat.Dialog(stream_interactor); + add_chat_dialog.set_transient_for((ApplicationWindow) get_toplevel()); + add_chat_dialog.conversation_opened.connect((conversation) => conversation_opened(conversation)); + add_chat_dialog.show(); + }); + application.add_action(contacts_action); + + SimpleAction conference_action = new SimpleAction("add_conference", null); + conference_action.activate.connect(() => { + AddConversation.Conference.Dialog add_conference_dialog = new AddConversation.Conference.Dialog(stream_interactor); + add_conference_dialog.set_transient_for((ApplicationWindow) get_toplevel()); + add_conference_dialog.conversation_opened.connect((conversation) => conversation_opened(conversation)); + add_conference_dialog.show(); + }); + application.add_action(conference_action); + + Builder builder = new Builder.from_resource("/org/dino-im/menu_add.ui"); + MenuModel menu = builder.get_object("menu_add") as MenuModel; + add_button.set_menu_model(menu); + } +} + diff --git a/client/src/ui/conversation_selector/chat_row.vala b/client/src/ui/conversation_selector/chat_row.vala new file mode 100644 index 00000000..1613b404 --- /dev/null +++ b/client/src/ui/conversation_selector/chat_row.vala @@ -0,0 +1,88 @@ +using Gdk; +using Gee; +using Gtk; + +using Xmpp; +using Dino.Entities; + +namespace Dino.Ui.ConversationSelector { +public class ChatRow : ConversationRow { + + public ChatRow(StreamInteractor stream_interactor, Conversation conversation) { + base(stream_interactor, conversation); + has_tooltip = true; + query_tooltip.connect ((x, y, keyboard_tooltip, tooltip) => { + tooltip.set_custom(generate_tooltip()); + return true; + }); + update_avatar(); + } + + public override void on_show_received(Show show) { + update_avatar(); + } + + public override void network_connection(bool connected) { + if (!connected) { + set_avatar((new AvatarGenerator(AVATAR_SIZE, AVATAR_SIZE, image.scale_factor)).set_greyscale(true).draw_conversation(stream_interactor, conversation), image.scale_factor); + } else { + update_avatar(); + } + } + + public void on_updated_roster_item(Roster.Item roster_item) { + if (roster_item.name != null) { + display_name = roster_item.name; + update_name(); + } + update_avatar(); + } + + public void update_avatar() { + ArrayList full_jids = PresenceManager.get_instance(stream_interactor).get_full_jids(conversation.counterpart, conversation.account); + set_avatar((new AvatarGenerator(AVATAR_SIZE, AVATAR_SIZE, image.scale_factor)) + .set_greyscale(full_jids == null) + .draw_conversation(stream_interactor, conversation), image.scale_factor); + } + + private Widget generate_tooltip() { + Builder builder = new Builder.from_resource("/org/dino-im/conversation_selector/chat_row_tooltip.ui"); + Box main_box = builder.get_object("main_box") as Box; + Box inner_box = builder.get_object("inner_box") as Box; + Label jid_label = builder.get_object("jid_label") as Label; + + jid_label.label = conversation.counterpart.to_string(); + + ArrayList? full_jids = PresenceManager.get_instance(stream_interactor).get_full_jids(conversation.counterpart, conversation.account); + if (full_jids != null) { + for (int i = 0; i < full_jids.size; i++) { + Box box = new Box(Orientation.HORIZONTAL, 5); + + Show show = PresenceManager.get_instance(stream_interactor).get_last_show(full_jids[i], conversation.account); + Image image = new Image(); + Pixbuf pixbuf; + int icon_size = 13 * image.scale_factor; + if (show.as == Show.AWAY) { + pixbuf = new Pixbuf.from_resource_at_scale("/org/dino-im/img/status_away.svg", icon_size, icon_size, true); + } else if (show.as == Show.XA || show.as == Show.DND) { + pixbuf = new Pixbuf.from_resource_at_scale("/org/dino-im/img/status_dnd.svg", icon_size, icon_size, true); + } else if (show.as == Show.CHAT) { + pixbuf = new Pixbuf.from_resource_at_scale("/org/dino-im/img/status_chat.svg", icon_size, icon_size, true); + } else { + pixbuf = new Pixbuf.from_resource_at_scale("/org/dino-im/img/status_online.svg", icon_size, icon_size, true); + } + Util.image_set_from_scaled_pixbuf(image, pixbuf); + box.add(image); + + Label resource = new Label(full_jids[i].resourcepart); + resource.xalign = 0; + box.add(resource); + box.show_all(); + + inner_box.add(box); + } + } + return main_box; + } +} +} \ No newline at end of file diff --git a/client/src/ui/conversation_selector/conversation_row.vala b/client/src/ui/conversation_selector/conversation_row.vala new file mode 100644 index 00000000..e641cab2 --- /dev/null +++ b/client/src/ui/conversation_selector/conversation_row.vala @@ -0,0 +1,175 @@ +using Gee; +using Gdk; +using Gtk; +using Pango; + +using Xmpp; +using Dino.Entities; + +namespace Dino.Ui.ConversationSelector { + +[GtkTemplate (ui = "/org/dino-im/conversation_selector/conversation_row.ui")] +public abstract class ConversationRow : ListBoxRow { + + [GtkChild] + protected Image image; + + [GtkChild] + private Label name_label; + + [GtkChild] + private Label time_label; + + [GtkChild] + private Label message_label; + + [GtkChild] + protected Button x_button; + + [GtkChild] + private Revealer time_revealer; + + [GtkChild] + private Revealer xbutton_revealer; + + [GtkChild] + public Revealer main_revealer; + + public Conversation conversation { get; private set; } + + protected const int AVATAR_SIZE = 40; + + protected string display_name; + protected string message; + protected DateTime time; + protected bool read = true; + + + protected StreamInteractor stream_interactor; + + construct { + name_label.attributes = new AttrList(); + } + + public ConversationRow(StreamInteractor stream_interactor, Conversation conversation) { + this.conversation = conversation; + this.stream_interactor = stream_interactor; + + x_button.clicked.connect(on_x_button_clicked); + + update_name(Util.get_conversation_display_name(stream_interactor, conversation)); + Entities.Message message = MessageManager.get_instance(stream_interactor).get_last_message(conversation); + if (message != null) { + message_received(message); + } + } + + public void update() { + update_time(); + } + + public void message_received(Entities.Message message) { + update_message(message.body.replace("\n", " ")); + update_time(message.time.to_local()); + } + + public void set_avatar(Pixbuf pixbuf, int scale_factor = 1) { + Util.image_set_from_scaled_pixbuf(image, pixbuf, scale_factor); + image.queue_draw(); + } + + public void mark_read() { + update_read(true); + } + + public void mark_unread() { + update_read(false); + } + + public abstract void on_show_received(Show presence); + public abstract void network_connection(bool connected); + + protected void update_name(string? new_name = null) { + if (new_name != null) { + display_name = new_name; + } + name_label.label = display_name; + } + + protected void update_time(DateTime? new_time = null) { + time_label.visible = true; + if (new_time != null) { + time = new_time; + } + if (time != null) { + time_label.label = get_relative_time(time); + } + } + + protected void update_message(string? new_message = null) { + if (new_message != null) { + message = new_message; + } + if (message != null) { + message_label.visible = true; + message_label.label = message; + } + } + + protected void update_read(bool read) { + this.read = read; + if (read) { + name_label.attributes.filter((attr) => attr.equal(attr_weight_new(Weight.BOLD))); + time_label.attributes.filter((attr) => attr.equal(attr_weight_new(Weight.BOLD))); + message_label.attributes.filter((attr) => attr.equal(attr_weight_new(Weight.BOLD))); + } else { + name_label.attributes.insert(attr_weight_new(Weight.BOLD)); + time_label.attributes.insert(attr_weight_new(Weight.BOLD)); + message_label.attributes.insert(attr_weight_new(Weight.BOLD)); + } + name_label.label = name_label.label; // TODO initializes redrawing, which would otherwise not happen. nicer? + time_label.label = time_label.label; + message_label.label = message_label.label; + } + + private void on_x_button_clicked() { + main_revealer.set_transition_type(RevealerTransitionType.SLIDE_UP); + main_revealer.set_reveal_child(false); + main_revealer.notify["child-revealed"].connect(() => { + conversation.active = false; + }); + } + + public override void state_flags_changed(StateFlags flags) { + StateFlags curr_flags = get_state_flags(); + if ((curr_flags & StateFlags.PRELIGHT) != 0) { + time_revealer.set_reveal_child(false); + xbutton_revealer.set_reveal_child(true); + } else { + time_revealer.set_reveal_child(true); + xbutton_revealer.set_reveal_child(false); + } + } + + private static string get_relative_time(DateTime datetime) { + DateTime now = new DateTime.now_local(); + TimeSpan timespan = now.difference(datetime); + if (timespan > 365 * TimeSpan.DAY) { + return datetime.get_year().to_string(); + } else if (timespan > 7 * TimeSpan.DAY) { + return datetime.format("%d.%m"); + } else if (timespan > 2 * TimeSpan.DAY) { + return datetime.format("%a"); + } else if (timespan > 1 * TimeSpan.DAY) { + return "Yesterday"; + } else if (timespan > 9 * TimeSpan.MINUTE) { + return datetime.format("%H:%M"); + } else if (timespan > 1 * TimeSpan.MINUTE) { + return (timespan / TimeSpan.MINUTE).to_string() + " min ago"; + } else { + return "Just now"; + } + } + +} +} diff --git a/client/src/ui/conversation_selector/groupchat_row.vala b/client/src/ui/conversation_selector/groupchat_row.vala new file mode 100644 index 00000000..bec2181e --- /dev/null +++ b/client/src/ui/conversation_selector/groupchat_row.vala @@ -0,0 +1,33 @@ +using Dino.Entities; + +namespace Dino.Ui.ConversationSelector { +public class GroupchatRow : ConversationRow { + + public GroupchatRow(StreamInteractor stream_interactor, Conversation conversation) { + base(stream_interactor, conversation); + has_tooltip = true; + set_tooltip_text(conversation.counterpart.bare_jid.to_string()); + set_avatar((new AvatarGenerator(AVATAR_SIZE, AVATAR_SIZE, image.scale_factor)) + .set_greyscale(true) + .draw_conversation(stream_interactor, conversation), image.scale_factor); + x_button.clicked.connect(on_x_button_clicked); + } + + + public override void on_show_received(Show show) { + set_avatar((new AvatarGenerator(AVATAR_SIZE, AVATAR_SIZE, image.scale_factor)) + .draw_conversation(stream_interactor, conversation), image.scale_factor); + } + + public override void network_connection(bool connected) { + set_avatar((new AvatarGenerator(AVATAR_SIZE, AVATAR_SIZE, image.scale_factor)) + .set_greyscale(!connected || + MucManager.get_instance(stream_interactor).get_nick(conversation.counterpart, conversation.account) == null) // TODO better currently joined + .draw_conversation(stream_interactor, conversation), image.scale_factor); + } + + private void on_x_button_clicked() { + MucManager.get_instance(stream_interactor).part(conversation.account, conversation.counterpart); + } +} +} \ No newline at end of file diff --git a/client/src/ui/conversation_selector/list.vala b/client/src/ui/conversation_selector/list.vala new file mode 100644 index 00000000..b114c3fa --- /dev/null +++ b/client/src/ui/conversation_selector/list.vala @@ -0,0 +1,173 @@ +using Gee; +using Gtk; + +using Xmpp; +using Dino.Entities; + +namespace Dino.Ui.ConversationSelector { +public class List : ListBox { + + public signal void conversation_selected(Conversation conversation); + + private StreamInteractor stream_interactor; + private string[]? filter_values; + private HashMap rows = new HashMap(Conversation.hash_func, Conversation.equals_func); + + public List(StreamInteractor stream_interactor) { + this.stream_interactor = stream_interactor; + + get_style_context().add_class("sidebar"); + set_filter_func(filter); + set_header_func(header); + set_sort_func(sort); + + ChatInteraction.get_instance(stream_interactor).conversation_read.connect((conversation) => { + Idle.add(() => {rows[conversation].mark_read(); return false;}); + }); + ChatInteraction.get_instance(stream_interactor).conversation_unread.connect((conversation) => { + Idle.add(() => {rows[conversation].mark_unread(); return false;}); + }); + ConversationManager.get_instance(stream_interactor).conversation_activated.connect((conversation) => { + Idle.add(() => {add_conversation(conversation); return false;}); + }); + MessageManager.get_instance(stream_interactor).message_received.connect((message, conversation) => { + Idle.add(() => {message_received(message, conversation); return false;}); + }); + MessageManager.get_instance(stream_interactor).message_sent.connect((message, conversation) => { + Idle.add(() => {message_received(message, conversation); return false;}); + }); + PresenceManager.get_instance(stream_interactor).show_received.connect((show, jid, account) => { + Idle.add(() => { + Conversation? conversation = ConversationManager.get_instance(stream_interactor).get_conversation(jid, account); + if (conversation != null && rows.has_key(conversation)) rows[conversation].on_show_received(show); + return false; + }); + }); + RosterManager.get_instance(stream_interactor).updated_roster_item.connect((account, jid, roster_item) => { + Idle.add(() => { + Conversation? conversation = ConversationManager.get_instance(stream_interactor).get_conversation(jid, account); + if (conversation != null && rows.has_key(conversation)) { + ChatRow row = rows[conversation] as ChatRow; + if (row != null) row.on_updated_roster_item(roster_item); + } + return false; + }); + }); + AvatarManager.get_instance(stream_interactor).received_avatar.connect((avatar, jid, account) => { + Idle.add(() => { + Conversation? conversation = ConversationManager.get_instance(stream_interactor).get_conversation(jid, account); + if (conversation != null && rows.has_key(conversation)) { + ChatRow row = rows[conversation] as ChatRow; + if (row != null) row.update_avatar(); + } + return false; + }); + }); + stream_interactor.connection_manager.connection_state_changed.connect((account, state) => { + Idle.add(() => { + foreach (ConversationRow row in rows.values) { + if (row.conversation.account.equals(account)) row.network_connection(state == ConnectionManager.ConnectionState.CONNECTED); + } + return false; + }); + }); + Timeout.add_seconds(60, () => { + foreach (ConversationRow row in rows.values) row.update(); + return true; + }); + } + + public override void row_activated(ListBoxRow r) { + if (r.get_type().is_a(typeof(ConversationRow))) { + ConversationRow row = r as ConversationRow; + conversation_selected(row.conversation); + } + } + + public void set_filter_values(string[]? values) { + if (filter_values == values) { + return; + } + filter_values = values; + invalidate_filter(); + } + + public void add_conversation(Conversation conversation) { + ConversationRow row; + if (!rows.has_key(conversation)) { + if (conversation.type_ == Conversation.TYPE_GROUPCHAT) { + row = new GroupchatRow(stream_interactor, conversation); + } else { + row = new ChatRow(stream_interactor, conversation); + } + rows[conversation] = row; + add(row); + row.main_revealer.set_reveal_child(true); + conversation.notify["active"].connect((s, p) => { + if (rows.has_key(conversation) && !conversation.active) { + remove_conversation(conversation); + } + }); + } + invalidate_sort(); + queue_draw(); + } + + public void remove_conversation(Conversation conversation) { + remove(rows[conversation]); + rows.unset(conversation); + } + + public void on_conversation_selected(Conversation conversation) { + if (!rows.has_key(conversation)) { + add_conversation(conversation); + } + this.select_row(rows[conversation]); + } + + private void message_received(Entities.Message message, Conversation conversation) { + if (rows.has_key(conversation)) { + rows[conversation].message_received(message); + invalidate_sort(); + } + } + + private void header(ListBoxRow row, ListBoxRow? before_row) { + if (row.get_header() == null && before_row != null) { + row.set_header(new Separator(Orientation.HORIZONTAL)); + } + } + + private bool filter(ListBoxRow r) { + if (r.get_type().is_a(typeof(ConversationRow))) { + ConversationRow row = r as ConversationRow; + if (filter_values != null && filter_values.length != 0) { + foreach (string filter in filter_values) { + if (!(Util.get_conversation_display_name(stream_interactor, row.conversation).down().contains(filter.down()) || + row.conversation.counterpart.to_string().down().contains(filter.down()))) { + return false; + } + } + } + } + return true; + } + + private int sort(ListBoxRow row1, ListBoxRow row2) { + ConversationRow cr1 = row1 as ConversationRow; + ConversationRow cr2 = row2 as ConversationRow; + if (cr1 != null && cr2 != null) { + Conversation c1 = cr1.conversation; + Conversation c2 = cr2.conversation; + int comp = c2.last_active.compare(c1.last_active); + if (comp == 0) { + return Util.get_conversation_display_name(stream_interactor, c1) + .collate(Util.get_conversation_display_name(stream_interactor, c2)); + } else { + return comp; + } + } + return 0; + } +} +} \ No newline at end of file diff --git a/client/src/ui/conversation_selector/view.vala b/client/src/ui/conversation_selector/view.vala new file mode 100644 index 00000000..72e8bbec --- /dev/null +++ b/client/src/ui/conversation_selector/view.vala @@ -0,0 +1,56 @@ +using Gee; +using Gtk; +using Gdk; + +using Dino.Entities; + +namespace Dino.Ui.ConversationSelector { + +[GtkTemplate (ui = "/org/dino-im/conversation_selector/view.ui")] +public class View : Grid { + public List conversation_list; + + [GtkChild] + public SearchEntry search_entry; + + [GtkChild] + public SearchBar search_bar; + + [GtkChild] + private ScrolledWindow scrolled; + + public View(StreamInteractor stream_interactor) { + conversation_list = new List(stream_interactor); + scrolled.add(conversation_list); + search_entry.key_press_event.connect(search_key_press_event); + search_entry.search_changed.connect(search_changed); + } + + public void conversation_selected(Conversation? conversation) { + search_entry.set_text(""); + } + + private void refilter() { + string[]? values = null; + string str = search_entry.get_text (); + if (str != "") values = str.split(" "); + conversation_list.set_filter_values(values); + } + + private void search_changed(Editable editable) { + refilter(); + } + + private bool search_key_press_event(EventKey event) { + conversation_list.select_row(conversation_list.get_row_at_y(0)); + if (event.keyval == Key.Down) { + ConversationRow? row = (ConversationRow) conversation_list.get_row_at_index(0); + if (row != null) { + conversation_list.select_row(row); + row.grab_focus(); + } + } + return false; + } +} +} \ No newline at end of file diff --git a/client/src/ui/conversation_summary/merged_message_item.vala b/client/src/ui/conversation_summary/merged_message_item.vala new file mode 100644 index 00000000..b1e99d3e --- /dev/null +++ b/client/src/ui/conversation_summary/merged_message_item.vala @@ -0,0 +1,164 @@ +using Gee; +using Gdk; +using Gtk; +using Markup; + +using Dino.Entities; + +namespace Dino.Ui.ConversationSummary { + +[GtkTemplate (ui = "/org/dino-im/conversation_summary/message_item.ui")] +public class MergedMessageItem : Grid { + + public Conversation conversation { get; set; } + public Jid from { get; private set; } + public DateTime initial_time { get; private set; } + public ArrayList messages = new ArrayList(Message.equals_func); + + [GtkChild] + private Image image; + + [GtkChild] + private Label time_label; + + [GtkChild] + private Label name_label; + + [GtkChild] + private Image encryption_image; + + [GtkChild] + private Image received_image; + + [GtkChild] + private TextView message_text_view; + + public MergedMessageItem(StreamInteractor stream_interactor, Conversation conversation, Message message) { + this.conversation = conversation; + this.from = message.from; + this.initial_time = message.time; + setup_tags(); + add_message(message); + + time_label.label = get_relative_time(initial_time.to_local()); + string display_name = Util.get_message_display_name(stream_interactor, message, conversation.account); + name_label.set_markup(@"$display_name"); + Util.image_set_from_scaled_pixbuf(image, (new AvatarGenerator(30, 30, image.scale_factor)).draw_message(stream_interactor, message)); + if (message.encryption == Entities.Message.Encryption.PGP) { + encryption_image.visible = true; + encryption_image.set_from_icon_name("changes-prevent-symbolic", IconSize.SMALL_TOOLBAR); + } + } + + public void update() { + time_label.label = get_relative_time(initial_time.to_local()); + } + + public void add_message(Message message) { + TextIter end; + message_text_view.buffer.get_end_iter(out end); + if (messages.size > 0) { + message_text_view.buffer.insert(ref end, "\n", -1); + } + message_text_view.buffer.insert(ref end, message.body, -1); + format_suffix_urls(message.body); + messages.add(message); + message.notify["marked"].connect_after(update_received); // TODO other thread? not main? css error? gtk main? + update_received(); + } + + private void update_received() { + bool all_received = true; + bool all_read = true; + foreach (Message message in messages) { + if (message.marked != Message.Marked.READ) { + all_read = false; + if (message.marked != Message.Marked.RECEIVED) { + all_received = false; + } + } + } + if (all_read) { + received_image.visible = true; + received_image.set_from_resource("/org/dino-im/img/double_tick.svg"); + } else if (all_received) { + received_image.visible = true; + received_image.set_from_resource("/org/dino-im/img/tick.svg"); + } else if (received_image.visible) { + received_image.set_from_icon_name("image-loading-symbolic", IconSize.SMALL_TOOLBAR); + } + } + + private void format_suffix_urls(string text) { + int absolute_start = message_text_view.buffer.text.length - text.length; + + Regex url_regex = new Regex("""(?i)\b((?:[a-z][\w-]+:(?:\/{1,3}|[a-z0-9%])|www\d{0,3}[.]|[a-z0-9.\-]+[.][a-z]{2,4}\/)(?:[^\s()<>]+|\(([^\s()<>]+|(\([^\s()<>]+\)))*\))+(?:\(([^\s()<>]+|(\([^\s()<>]+\)))*\)|[^\s`!()\[\]{};:'".,<>?«»“”‘’]))"""); + MatchInfo match_info; + url_regex.match(text, 0, out match_info); + for (; match_info.matches(); match_info.next()) { + string? url = match_info.fetch(0); + int start; + int end; + match_info.fetch_pos(0, out start, out end); + TextIter start_iter; + TextIter end_iter; + message_text_view.buffer.get_iter_at_offset(out start_iter, absolute_start + start); + message_text_view.buffer.get_iter_at_offset(out end_iter, absolute_start + end); + message_text_view.buffer.apply_tag_by_name("url", start_iter, end_iter); + } + } + + private void setup_tags() { + message_text_view.buffer.create_tag("url", underline: Pango.Underline.SINGLE, foreground: "blue"); + message_text_view.button_release_event.connect(open_url); + message_text_view.motion_notify_event.connect(change_cursor_over_url); + } + + private bool open_url(EventButton event_button) { + int buffer_x, buffer_y; + message_text_view.window_to_buffer_coords(TextWindowType.TEXT, (int) event_button.x, (int) event_button.y, out buffer_x, out buffer_y); + TextIter iter; + message_text_view.get_iter_at_location(out iter, buffer_x, buffer_y); + TextIter start_iter = iter, end_iter = iter; + if (start_iter.backward_to_tag_toggle(null) && end_iter.forward_to_tag_toggle(null)) { + string url = start_iter.get_text(end_iter); + try{ + AppInfo.launch_default_for_uri(url, null); + } catch (Error err) { + print("Tryed to open " + url); + } + } + return false; + } + + private bool change_cursor_over_url(EventMotion event_motion) { + TextIter iter; + message_text_view.get_iter_at_location(out iter, (int) event_motion.x, (int) event_motion.y); + if (iter.has_tag(message_text_view.buffer.tag_table.lookup("url"))) { + event_motion.window.set_cursor(new Cursor.for_display(get_display(), CursorType.HAND2)); + } else { + event_motion.window.set_cursor(new Cursor.for_display(get_display(), CursorType.XTERM)); + } + return false; + } + + private static string get_relative_time(DateTime datetime) { + DateTime now = new DateTime.now_local(); + TimeSpan timespan = now.difference(datetime); + if (timespan > 365 * TimeSpan.DAY) { + return datetime.format("%d.%m.%Y %H:%M"); + } else if (timespan > 7 * TimeSpan.DAY) { + return datetime.format("%d.%m %H:%M"); + } else if (timespan > 1 * TimeSpan.DAY) { + return datetime.format("%a, %H:%M"); + } else if (timespan > 9 * TimeSpan.MINUTE) { + return datetime.format("%H:%M"); + } else if (timespan > TimeSpan.MINUTE) { + return (timespan / TimeSpan.MINUTE).to_string() + " min ago"; + } else { + return "Just now"; + } + } +} + +} diff --git a/client/src/ui/conversation_summary/merged_status_item.vala b/client/src/ui/conversation_summary/merged_status_item.vala new file mode 100644 index 00000000..78b156e9 --- /dev/null +++ b/client/src/ui/conversation_summary/merged_status_item.vala @@ -0,0 +1,30 @@ +using Gee; +using Gtk; + +using Dino.Entities; + +namespace Dino.Ui.ConversationSummary { + +private class MergedStatusItem : Expander { + + private StreamInteractor stream_interactor; + private Conversation conversation; + private ArrayList statuses = new ArrayList(); + + public MergedStatusItem(StreamInteractor stream_interactor, Conversation conversation, Show show) { + set_hexpand(true); + add_status(show); + } + + public void add_status(Show show) { + statuses.add(show); + StatusItem status_item = new StatusItem(stream_interactor, conversation, @"is $(show.as)"); + if (statuses.size == 1) { + label = show.as; + } else { + label = @"changed their status $(statuses.size) times"; + add(new Label(show.as)); + } + } +} +} \ No newline at end of file diff --git a/client/src/ui/conversation_summary/status_item.vala b/client/src/ui/conversation_summary/status_item.vala new file mode 100644 index 00000000..5918d008 --- /dev/null +++ b/client/src/ui/conversation_summary/status_item.vala @@ -0,0 +1,29 @@ +using Gtk; +using Markup; + +using Dino.Entities; + +namespace Dino.Ui.ConversationSummary { + +private class StatusItem : Grid { + + private Image image = new Image(); + private Label label = new Label(""); + + private StreamInteractor stream_interactor; + private Conversation conversation; + + public StatusItem(StreamInteractor stream_interactor, Conversation conversation, string? text) { + Object(column_spacing : 7); + set_hexpand(true); + this.stream_interactor = stream_interactor; + this.conversation = conversation; + image.set_from_pixbuf((new AvatarGenerator(30, 30)).set_greyscale(true).draw_conversation(stream_interactor, conversation)); + attach(image, 0, 0, 1, 1); + attach(label, 1, 0, 1, 1); + string display_name = Util.get_display_name(stream_interactor, conversation.counterpart, conversation.account); + label.set_markup(@" $(escape_text(display_name)) $text "); + show_all(); + } +} +} \ No newline at end of file diff --git a/client/src/ui/conversation_summary/view.vala b/client/src/ui/conversation_summary/view.vala new file mode 100644 index 00000000..0ea1a32c --- /dev/null +++ b/client/src/ui/conversation_summary/view.vala @@ -0,0 +1,221 @@ +using Gee; +using Gtk; +using Pango; + +using Dino.Entities; +using Xmpp; + +namespace Dino.Ui.ConversationSummary { + +[GtkTemplate (ui = "/org/dino-im/conversation_summary/view.ui")] +public class View : Box { + + public Conversation? conversation { get; private set; } + public HashMap message_items = new HashMap(Entities.Message.hash_func, Entities.Message.equals_func); + + [GtkChild] + private ScrolledWindow scrolled; + + [GtkChild] + private Box main; + + private StreamInteractor stream_interactor; + private MergedMessageItem? last_message_item; + private StatusItem typing_status; + private Entities.Message? earliest_message; + double? was_value; + double? was_upper; + double? was_page_size; + Object reloading_lock = new Object(); + bool reloading = false; + + public View(StreamInteractor stream_interactor) { + Object(homogeneous : false, spacing : 0); + this.stream_interactor = stream_interactor; + scrolled.vadjustment.notify["upper"].connect_after(on_upper_notify); + scrolled.vadjustment.notify["value"].connect(on_value_notify); + + CounterpartInteractionManager.get_instance(stream_interactor).received_state.connect((account, jid, state) => { + Idle.add(() => { on_received_state(account, jid, state); return false; }); + }); + MessageManager.get_instance(stream_interactor).message_received.connect((message, conversation) => { + Idle.add(() => { show_message(message, conversation, true); return false; }); + }); + MessageManager.get_instance(stream_interactor).message_sent.connect((message, conversation) => { + Idle.add(() => { show_message(message, conversation, true); return false; }); + }); + PresenceManager.get_instance(stream_interactor).show_received.connect((show, jid, account) => { + Idle.add(() => { on_show_received(show, jid, account); return false; }); + }); + Timeout.add_seconds(60, () => { + foreach (MergedMessageItem message_item in message_items.values) { + message_item.update(); + } + return true; + }); + } + + public void initialize_for_conversation(Conversation? conversation) { + this.conversation = conversation; + clear(); + message_items.clear(); + was_upper = null; + was_page_size = null; + last_message_item = null; + + ArrayList objects = new ArrayList(); + Gee.List? messages = MessageManager.get_instance(stream_interactor).get_messages(conversation); + if (messages != null && messages.size > 0) { + earliest_message = messages[0]; + objects.add_all(messages); + } + HashMap>? shows = PresenceManager.get_instance(stream_interactor).get_shows(conversation.counterpart, conversation.account); + if (shows != null) { + foreach (Jid jid in shows.keys) objects.add_all(shows[jid]); + } + objects.sort((a, b) => { + DateTime? dt1 = null; + DateTime? dt2 = null; + Entities.Message m1 = a as Entities.Message; + if (m1 != null) dt1 = m1.time; + Show s1 = a as Show; + if (s1 != null) dt1 = s1.datetime; + Entities.Message m2 = b as Entities.Message; + if (m2 != null) dt2 = m2.time; + Show s2 = b as Show; + if (s2 != null) dt2 = s2.datetime; + return dt1.compare(dt2); + }); + foreach (Object o in objects) { + Entities.Message message = o as Entities.Message; + Show show = o as Show; + if (message != null) { + show_message(message, conversation); + } else if (show != null) { + on_show_received(show, conversation.counterpart, conversation.account); + } + } + update_chat_state(); + } + + private void on_received_state(Account account, Jid jid, string state) { + if (conversation != null && conversation.account.equals(account) && conversation.counterpart.equals_bare(jid)) { + update_chat_state(state); + } + } + + private void update_chat_state(string? state = null) { + string? state_ = state; + if (state_ == null) { + state_ = CounterpartInteractionManager.get_instance(stream_interactor).get_chat_state(conversation.account, conversation.counterpart); + } + if (typing_status != null) { + main.remove(typing_status); + } + if (state_ != null) { + if (state_ == Xep.ChatStateNotifications.STATE_COMPOSING || state_ == Xep.ChatStateNotifications.STATE_PAUSED) { + if (state_ == Xep.ChatStateNotifications.STATE_COMPOSING) { + typing_status = new StatusItem(stream_interactor, conversation, "is typing..."); + } else if (state_ == Xep.ChatStateNotifications.STATE_PAUSED) { + typing_status = new StatusItem(stream_interactor, conversation, "has stoped typing"); + } + main.add(typing_status); + } + } + } + + private void on_show_received(Show show, Jid jid, Account account) { + + } + + private void on_upper_notify() { + if (was_upper == null || scrolled.vadjustment.value > was_upper - was_page_size - 1 || + scrolled.vadjustment.value > was_upper - was_page_size - 1) { // scrolled down or content smaller than page size + scrolled.vadjustment.value = scrolled.vadjustment.upper - scrolled.vadjustment.page_size; // scroll down + } else if (scrolled.vadjustment.value < scrolled.vadjustment.upper - scrolled.vadjustment.page_size - 1){ + scrolled.vadjustment.value = scrolled.vadjustment.upper - was_upper + scrolled.vadjustment.value; // stay at same content + } + was_upper = scrolled.vadjustment.upper; + was_page_size = scrolled.vadjustment.page_size; + lock(reloading_lock) { + reloading = false; + } + } + + private void on_value_notify() { + if (scrolled.vadjustment.value < 200) { + load_earlier_messages(); + } + } + + private void load_earlier_messages() { + was_value = scrolled.vadjustment.value; + lock(reloading_lock) { + if(reloading) return; + reloading = true; + } + Gee.List? messages = MessageManager.get_instance(stream_interactor).get_messages_before(conversation, earliest_message); + if (messages != null && messages.size > 0) { + earliest_message = messages[0]; + MergedMessageItem? current_item = null; + int items_added = 0; + for (int i = 0; i < messages.size; i++) { + if (current_item != null && should_merge_message(current_item, messages[i])) { + current_item.add_message(messages[i]); + } else { + current_item = new MergedMessageItem(stream_interactor, conversation, messages[i]); + force_alloc_width(current_item, main.get_allocated_width()); + main.add(current_item); + message_items[messages[i]] = current_item; + main.reorder_child(current_item, items_added); + items_added++; + } + } + return; + } + reloading = false; + } + + private void show_message(Entities.Message message, Conversation conversation, bool animate = false) { + if (this.conversation != null && this.conversation.equals(conversation)) { + if (should_merge_message(last_message_item, message)) { + last_message_item.add_message(message); + } else { + MergedMessageItem message_item = new MergedMessageItem(stream_interactor, conversation, message); + if (animate) { + Revealer revealer = new Revealer() {transition_duration = 200, transition_type = RevealerTransitionType.SLIDE_UP, visible = true}; + revealer.add(message_item); + force_alloc_width(revealer, main.get_allocated_width()); + main.add(revealer); + revealer.set_reveal_child(true); + } else { + force_alloc_width(message_item, main.get_allocated_width()); + main.add(message_item); + } + last_message_item = message_item; + } + message_items[message] = last_message_item; + update_chat_state(); + } + } + + private bool should_merge_message(MergedMessageItem? message_item, Entities.Message message) { + return message_item != null && + message_item.from.equals(message.from) && + message_item.messages.get(0).encryption == message.encryption && + message.time.difference(message_item.initial_time) < TimeSpan.MINUTE; + } + + private void force_alloc_width(Widget widget, int width) { + Allocation alloc = Allocation(); + widget.get_preferred_width(out alloc.width, null); + widget.get_preferred_height(out alloc.height, null); + alloc.width = width; + widget.size_allocate(alloc); + } + + private void clear() { + main.@foreach((widget) => { main.remove(widget); }); + } +} +} diff --git a/client/src/ui/conversation_titlebar.vala b/client/src/ui/conversation_titlebar.vala new file mode 100644 index 00000000..cd21353c --- /dev/null +++ b/client/src/ui/conversation_titlebar.vala @@ -0,0 +1,124 @@ +using Gtk; + +using Dino.Entities; + +[GtkTemplate (ui = "/org/dino-im/conversation_titlebar.ui")] +public class Dino.Ui.ConversationTitlebar : Gtk.HeaderBar { + + [GtkChild] + private MenuButton menu_button; + + [GtkChild] + private MenuButton encryption_button; + private RadioButton? button_unencrypted; + private RadioButton? button_pgp; + + [GtkChild] + private MenuButton groupchat_button; + + private StreamInteractor stream_interactor; + private Conversation? conversation; + + public ConversationTitlebar(StreamInteractor stream_interactor) { + this.stream_interactor = stream_interactor; + MucManager.get_instance(stream_interactor).groupchat_subject_set.connect((account, jid, subject) => { + Idle.add(() => { on_groupchat_subject_set(account, jid, subject); return false; }); + }); + create_conversation_menu(); + create_encryption_menu(); + } + + public void initialize_for_conversation(Conversation conversation) { + this.conversation = conversation; + update_encryption_menu_state(); + update_encryption_menu_icon(); + update_groupchat_menu(); + update_title(); + update_subtitle(); + } + + private void update_encryption_menu_state() { + string? pgp_id = PgpManager.get_instance(stream_interactor).get_key_id(conversation.account, conversation.counterpart); + button_pgp.set_sensitive(pgp_id != null); + switch (conversation.encryption) { + case Conversation.ENCRYPTION_UNENCRYPTED: + button_unencrypted.set_active(true); + break; + case Conversation.ENCRYPTION_PGP: + button_pgp.set_active(true); + break; + } + } + + private void update_encryption_menu_icon() { + encryption_button.visible = conversation.type_ == Conversation.TYPE_CHAT; + if (conversation.type_ == Conversation.TYPE_CHAT) { + if (conversation.encryption == Conversation.ENCRYPTION_UNENCRYPTED) { + encryption_button.set_image(new Image.from_icon_name("changes-allow-symbolic", IconSize.BUTTON)); + } else { + encryption_button.set_image(new Image.from_icon_name("changes-prevent-symbolic", IconSize.BUTTON)); + } + } + } + + private void update_groupchat_menu() { + groupchat_button.visible = conversation.type_ == Conversation.TYPE_GROUPCHAT; + if (conversation.type_ == Conversation.TYPE_GROUPCHAT) { + groupchat_button.set_use_popover(true); + Popover popover = new Popover(null); + OccupantList occupant_list = new OccupantList(stream_interactor, conversation); + popover.add(occupant_list); + occupant_list.show_all(); + groupchat_button.set_popover(popover); + } + } + + private void update_title() { + set_title(Util.get_conversation_display_name(stream_interactor, conversation)); + } + + private void update_subtitle(string? subtitle = null) { + if (subtitle != null) { + set_subtitle(subtitle); + } else if (conversation.type_ == Conversation.TYPE_GROUPCHAT) { + string subject = MucManager.get_instance(stream_interactor).get_groupchat_subject(conversation.counterpart, conversation.account); + set_subtitle(subject != "" ? subject : null); + } else { + set_subtitle(null); + } + } + + private void create_conversation_menu() { + Builder builder = new Builder.from_resource("/org/dino-im/menu_conversation.ui"); + MenuModel menu = builder.get_object("menu_conversation") as MenuModel; + menu_button.set_menu_model(menu); + } + + private void create_encryption_menu() { + Builder builder = new Builder.from_resource("/org/dino-im/menu_encryption.ui"); + PopoverMenu menu = builder.get_object("menu_encryption") as PopoverMenu; + button_unencrypted = builder.get_object("button_unencrypted") as RadioButton; + button_pgp = builder.get_object("button_pgp") as RadioButton; + encryption_button.set_use_popover(true); + encryption_button.set_popover(menu); + encryption_button.set_image(new Image.from_icon_name("changes-allow-symbolic", IconSize.BUTTON)); + + button_unencrypted.toggled.connect(() => { + if (conversation != null) { + if (button_unencrypted.get_active()) { + conversation.encryption = Conversation.ENCRYPTION_UNENCRYPTED; + } else if (button_pgp.get_active()) { + conversation.encryption = Conversation.ENCRYPTION_PGP; + } + update_encryption_menu_icon(); + } + }); + } + + private void on_groupchat_subject_set(Account account, Jid jid, string subject) { + if (conversation != null && conversation.counterpart.equals_bare(jid) && conversation.account.equals(account)) { + update_subtitle(subject); + } + } +} + diff --git a/client/src/ui/manage_accounts/account_row.vala b/client/src/ui/manage_accounts/account_row.vala new file mode 100644 index 00000000..6ca4daf6 --- /dev/null +++ b/client/src/ui/manage_accounts/account_row.vala @@ -0,0 +1,24 @@ +using Gtk; + +using Dino.Entities; + +namespace Dino.Ui.ManageAccounts { + +[GtkTemplate (ui = "/org/dino-im/manage_accounts/account_row.ui")] +public class AccountRow : Gtk.ListBoxRow { + + [GtkChild] + public Image image; + + [GtkChild] + public Label jid_label; + + public Account account; + + public AccountRow(StreamInteractor stream_interactor, Account account) { + this.account = account; + Util.image_set_from_scaled_pixbuf(image, (new AvatarGenerator(40, 40, image.scale_factor)).draw_account(stream_interactor, account)); + jid_label.set_label(account.bare_jid.to_string()); + } +} +} \ No newline at end of file diff --git a/client/src/ui/manage_accounts/add_account_dialog.vala b/client/src/ui/manage_accounts/add_account_dialog.vala new file mode 100644 index 00000000..b22fca3a --- /dev/null +++ b/client/src/ui/manage_accounts/add_account_dialog.vala @@ -0,0 +1,70 @@ +using Gee; +using Gtk; + +using Dino.Entities; + +namespace Dino.Ui.ManageAccounts { + +[GtkTemplate (ui = "/org/dino-im/manage_accounts/add_account_dialog.ui")] +public class AddAccountDialog : Gtk.Dialog { + + public signal void added(Account account); + + [GtkChild] + private Button cancel_button; + + [GtkChild] + private Button ok_button; + + [GtkChild] + private Entry alias_entry; + + [GtkChild] + private Entry jid_entry; + + [GtkChild] + private Entry password_entry; + + public AddAccountDialog(StreamInteractor stream_interactor) { + Object(use_header_bar : 1); + this.title = "Add Account"; + + cancel_button.clicked.connect(() => { close(); }); + ok_button.clicked.connect(on_ok_button_clicked); + jid_entry.changed.connect(on_jid_entry_changed); + jid_entry.focus_out_event.connect(on_jid_entry_focus_out_event); + } + + private void on_jid_entry_changed() { + Jid? jid = Jid.parse(jid_entry.text); + if (jid != null && jid.localpart != null && jid.resourcepart == null) { + ok_button.set_sensitive(true); + jid_entry.secondary_icon_name = null; + } else { + ok_button.set_sensitive(false); + } + } + + private bool on_jid_entry_focus_out_event() { + Jid? jid = Jid.parse(jid_entry.text); + if (jid == null || jid.localpart == null || jid.resourcepart != null) { + jid_entry.secondary_icon_name = "dialog-warning-symbolic"; + // TODO why doesn't the tooltip work + jid_entry.set_icon_tooltip_text(EntryIconPosition.SECONDARY, "JID should be of the form \"user@example.com\""); + } else { + jid_entry.secondary_icon_name = null; + } + return false; + } + + private void on_ok_button_clicked() { + Account account = new Account.from_bare_jid(jid_entry.get_text()); + account.resourcepart = "dino"; + account.alias = alias_entry.get_text(); + account.enabled = false; + account.password = password_entry.get_text(); + added(account); + close(); + } +} +} diff --git a/client/src/ui/manage_accounts/dialog.vala b/client/src/ui/manage_accounts/dialog.vala new file mode 100644 index 00000000..d3695019 --- /dev/null +++ b/client/src/ui/manage_accounts/dialog.vala @@ -0,0 +1,221 @@ +using Gdk; +using Gtk; + +using Dino.Entities; + +namespace Dino.Ui.ManageAccounts { + +[GtkTemplate (ui = "/org/dino-im/manage_accounts/dialog.ui")] +public class Dialog : Gtk.Window { + + public signal void account_enabled(Account account); + public signal void account_disabled(Account account); + + [GtkChild] + public Stack main_stack; + + [GtkChild] + public ListBox account_list; + + [GtkChild] + public Button no_accounts_add; + + [GtkChild] + public ToolButton add_button; + + [GtkChild] + public ToolButton remove_button; + + [GtkChild] + public Image image; + + [GtkChild] Button image_button; + + [GtkChild] + public Label jid_label; + + [GtkChild] + public Switch active_switch; + + [GtkChild] + public Stack password_stack; + + [GtkChild] + public Label password_label; + + [GtkChild] + public Button password_button; + + [GtkChild] + public Entry password_entry; + + [GtkChild] + public Stack alias_stack; + + [GtkChild] + public Label alias_label; + + [GtkChild] + public Button alias_button; + + [GtkChild] + public Entry alias_entry; + + private Database db; + private StreamInteractor stream_interactor; + + construct { + account_list.row_selected.connect(account_list_row_selected); + add_button.clicked.connect(add_button_clicked); + no_accounts_add.clicked.connect(add_button_clicked); + remove_button.clicked.connect(remove_button_clicked); + password_entry.key_press_event.connect(on_password_entry_key_press_event); + alias_entry.key_press_event.connect(on_alias_entry_key_press_event); + image_button.clicked.connect(on_image_button_clicked); + + main_stack.set_visible_child_name("no_accounts"); + } + + public Dialog(StreamInteractor stream_interactor, Database db) { + this.db = db; + this.stream_interactor = stream_interactor; + foreach (Account account in db.get_accounts()) { + add_account(account); + } + + AvatarManager.get_instance(stream_interactor).received_avatar.connect((pixbuf, jid, account) => { + Idle.add(() => { + on_received_avatar(pixbuf, jid, account); + return false; + });}); + + if (account_list.get_row_at_index(0) != null) account_list.select_row(account_list.get_row_at_index(0)); + } + + public AccountRow add_account(Account account) { + AccountRow account_item = new AccountRow (stream_interactor, account); + account_list.add(account_item); + main_stack.set_visible_child_name("accounts_exist"); + return account_item; + } + + private void add_button_clicked() { + AddAccountDialog add_account_dialog = new AddAccountDialog(stream_interactor); + add_account_dialog.set_transient_for(this); + add_account_dialog.added.connect((account) => { + db.add_account(account); + AccountRow account_item = add_account(account); + account_list.select_row(account_item); + account_list.queue_draw(); + }); + add_account_dialog.show(); + } + + private void remove_button_clicked() { + AccountRow account_item = account_list.get_selected_row() as AccountRow; + if (account_item != null) { + account_list.remove(account_item); + account_list.queue_draw(); + if (account_item.account.enabled) account_disabled(account_item.account); + db.remove_account(account_item.account); + if (account_list.get_row_at_index(0) != null) { + account_list.select_row(account_list.get_row_at_index(0)); + } else { + main_stack.set_visible_child_name("no_accounts"); + } + } + } + + private void account_list_row_selected(ListBoxRow? row) { + AccountRow? account_item = row as AccountRow; + if (account_item != null) populate_grid_data(account_item.account); + } + + private void populate_grid_data(Account account) { + active_switch.state_set.disconnect(on_active_switch_state_changed); + + Util.image_set_from_scaled_pixbuf(image, (new AvatarGenerator(50, 50, image.scale_factor)).draw_account(stream_interactor, account)); + active_switch.set_active(account.enabled); + jid_label.label = account.bare_jid.to_string(); + + string filler = ""; + for (int i = 0; i < account.password.length; i++) filler += password_entry.get_invisible_char().to_string(); + password_label.label = filler; + password_stack.set_visible_child_name("label"); + password_button.clicked.connect(() => { + password_stack.set_visible_child_name("entry"); + alias_stack.set_visible_child_name("label"); + set_focus(password_entry); + }); + password_entry.text = account.password; + + alias_label.label = account.alias; + alias_stack.set_visible_child_name("label"); + alias_button.clicked.connect(() => { + alias_stack.set_visible_child_name("entry"); + password_stack.set_visible_child_name("label"); + set_focus(alias_entry); + }); + alias_entry.text = account.alias; + + active_switch.state_set.connect(on_active_switch_state_changed); + } + + private void on_image_button_clicked() { + FileChooserDialog chooser = new FileChooserDialog ( + "Select avatar", this, FileChooserAction.OPEN, + "Cancel", ResponseType.CANCEL, + "Select", ResponseType.ACCEPT); + FileFilter filter = new FileFilter(); + filter.add_mime_type("image/*"); + chooser.set_filter(filter); + if (chooser.run() == Gtk.ResponseType.ACCEPT) { + string uri = chooser.get_filename(); + Account account = (account_list.get_selected_row() as AccountRow).account; + AvatarManager.get_instance(stream_interactor).publish(account, uri); + } + chooser.close(); + } + + private bool on_active_switch_state_changed(bool state) { + Account account = (account_list.get_selected_row() as AccountRow).account; + account.enabled = state; + if (state) { + account_enabled(account); + } else { + account_disabled(account); + } + return false; + } + + private bool on_password_entry_key_press_event(EventKey event) { + Account account = (account_list.get_selected_row() as AccountRow).account; + if (event.keyval == Key.Return) { + account.password = password_entry.text; + string filler = ""; + for (int i = 0; i < account.password.length; i++) filler += password_entry.get_invisible_char().to_string(); + password_label.label = filler; + password_stack.set_visible_child_name("label"); + } + return false; + } + + private bool on_alias_entry_key_press_event(EventKey event) { + Account account = (account_list.get_selected_row() as AccountRow).account; + if (event.keyval == Key.Return) { + account.alias = alias_entry.text; + alias_label.label = alias_entry.text; + alias_stack.set_visible_child_name("label"); + } + return false; + } + + private void on_received_avatar(Pixbuf pixbuf, Jid jid, Account account) { + Account curr_account = (account_list.get_selected_row() as AccountRow).account; + if (curr_account.equals(account) && jid.equals(account.bare_jid)) { + Util.image_set_from_scaled_pixbuf(image, (new AvatarGenerator(50, 50, image.scale_factor)).draw_account(stream_interactor, account)); + } + } +} +} + diff --git a/client/src/ui/notifications.vala b/client/src/ui/notifications.vala new file mode 100644 index 00000000..46bc6bf5 --- /dev/null +++ b/client/src/ui/notifications.vala @@ -0,0 +1,55 @@ +using Dino.Entities; +using Xmpp; + +namespace Dino.Ui { +public class Notifications : GLib.Object { + + private StreamInteractor stream_interactor; + private Notify.Notification notification = new Notify.Notification("", null, null); + + public Notifications(StreamInteractor stream_interactor) { + this.stream_interactor = stream_interactor; + } + + public void start() { + MessageManager.get_instance(stream_interactor).message_received.connect(on_message_received); + PresenceManager.get_instance(stream_interactor).received_subscription_request.connect(on_received_subscription_request); + } + + private void on_message_received(Entities.Message message, Conversation conversation) { + if (!ChatInteraction.get_instance(stream_interactor).is_active_focus()) { + string display_name = Util.get_conversation_display_name(stream_interactor, conversation); + if (MucManager.get_instance(stream_interactor).is_groupchat(conversation.counterpart, conversation.account)) { + string muc_occupant = Util.get_display_name(stream_interactor, message.from, conversation.account); + display_name = muc_occupant + " in " + display_name; + } + notification.update(display_name, message.body, null); + notification.set_image_from_pixbuf((new AvatarGenerator(40, 40)).draw_conversation(stream_interactor, conversation)); + notification.set_timeout(3); + try { + notification.show(); + } catch (Error error) { } + } + } + + private void on_received_subscription_request(Jid jid, Account account) { + Notify.Notification notification = new Notify.Notification("Subscription request", jid.bare_jid.to_string(), null); + notification.set_image_from_pixbuf((new AvatarGenerator(40, 40)).draw_jid(stream_interactor, jid, account)); + notification.add_action("accept", "Accept", () => { + PresenceManager.get_instance(stream_interactor).approve_subscription(account, jid); + try { + notification.close(); + } catch (Error error) { } + }); + notification.add_action("deny", "Deny", () => { + PresenceManager.get_instance(stream_interactor).deny_subscription(account, jid); + try { + notification.close(); + } catch (Error error) { } + }); + try { + notification.show(); + } catch (Error error) { } + } +} +} \ No newline at end of file diff --git a/client/src/ui/occupant_list.vala b/client/src/ui/occupant_list.vala new file mode 100644 index 00000000..921f7e70 --- /dev/null +++ b/client/src/ui/occupant_list.vala @@ -0,0 +1,112 @@ +using Gee; +using Gtk; + +using Dino.Entities; + +namespace Dino.Ui{ +[GtkTemplate (ui = "/org/dino-im/occupant_list.ui")] +public class OccupantList : Box { + + public signal void conversation_selected(Conversation? conversation); + private StreamInteractor stream_interactor; + + [GtkChild] + private ListBox list_box; + + [GtkChild] + private SearchEntry search_entry; + + private Conversation? conversation; + private string[]? filter_values; + private HashMap rows = new HashMap(Jid.hash_func, Jid.equals_func); + + public OccupantList(StreamInteractor stream_interactor, Conversation conversation) { + this.stream_interactor = stream_interactor; + list_box.set_header_func(header); + list_box.set_sort_func(sort); + list_box.set_filter_func(filter); + search_entry.search_changed.connect(search_changed); + + PresenceManager.get_instance(stream_interactor).show_received.connect((show, jid, account) => { + Idle.add(() => { on_show_received(show, jid, account); return false; }); + }); + RosterManager.get_instance(stream_interactor).updated_roster_item.connect(on_updated_roster_item); + + initialize_for_conversation(conversation); + } + + public void initialize_for_conversation(Conversation conversation) { + this.conversation = conversation; + ArrayList? occupants = MucManager.get_instance(stream_interactor).get_occupants(conversation.counterpart, conversation.account); + if (occupants != null) { + foreach (Jid occupant in occupants) { + add_occupant(occupant); + } + } + } + + private void refilter() { + string[]? values = null; + string str = search_entry.get_text (); + if (str != "") values = str.split(" "); + if (filter_values == values) return; + filter_values = values; + list_box.invalidate_filter(); + } + + private void search_changed(Editable editable) { + refilter(); + } + + public void add_occupant(Jid jid) { + rows[jid] = new OccupantListRow(stream_interactor, conversation.account, jid); + list_box.add(rows[jid]); + list_box.invalidate_filter(); + list_box.invalidate_sort(); + } + + public void remove_occupant(Jid jid) { + list_box.remove(rows[jid]); + rows.unset(jid); + } + + private void on_updated_roster_item(Account account, Jid jid, Xmpp.Roster.Item roster_item) { + + } + + private void on_show_received(Show show, Jid jid, Account account) { + if (conversation != null && conversation.counterpart.equals_bare(jid)) { + if (show.as == Show.OFFLINE && rows.has_key(jid)) { + remove_occupant(jid); + } else if (show.as != Show.OFFLINE && !rows.has_key(jid)) { + add_occupant(jid); + } + } + } + + private void header(ListBoxRow row, ListBoxRow? before_row) { + if (row.get_header() == null && before_row != null) { + row.set_header(new Separator(Orientation.HORIZONTAL)); + } + } + + private bool filter(ListBoxRow r) { + if (r.get_type().is_a(typeof(OccupantListRow))) { + OccupantListRow row = r as OccupantListRow; + foreach (string filter in filter_values) { + return row.name_label.label.down().contains(filter.down()); + } + } + return true; + } + + private int sort(ListBoxRow row1, ListBoxRow row2) { + if (row1.get_type().is_a(typeof(OccupantListRow)) && row2.get_type().is_a(typeof(OccupantListRow))) { + OccupantListRow c1 = row1 as OccupantListRow; + OccupantListRow c2 = row2 as OccupantListRow; + return c1.name_label.label.collate(c2.name_label.label); + } + return 0; + } +} +} \ No newline at end of file diff --git a/client/src/ui/occupant_list_row.vala b/client/src/ui/occupant_list_row.vala new file mode 100644 index 00000000..067455b5 --- /dev/null +++ b/client/src/ui/occupant_list_row.vala @@ -0,0 +1,27 @@ +using Gtk; + +using Dino.Entities; +using Xmpp; + +namespace Dino.Ui { + +[GtkTemplate (ui = "/org/dino-im/occupant_list_item.ui")] +public class OccupantListRow : ListBoxRow { + + [GtkChild] + private Image image; + + [GtkChild] + public Label name_label; + + public OccupantListRow(StreamInteractor stream_interactor, Account account, Jid jid) { + name_label.label = Util.get_display_name(stream_interactor, jid, account); + Util.image_set_from_scaled_pixbuf(image, (new AvatarGenerator(30, 30, image.scale_factor)).draw_jid(stream_interactor, jid, account)); + //has_tooltip = true; + } + + public void on_presence_received(Presence.Stanza presence) { + + } +} +} \ No newline at end of file diff --git a/client/src/ui/settings_dialog.vala b/client/src/ui/settings_dialog.vala new file mode 100644 index 00000000..600ec873 --- /dev/null +++ b/client/src/ui/settings_dialog.vala @@ -0,0 +1,27 @@ +using Gtk; + +namespace Dino.Ui { + +[GtkTemplate (ui = "/org/dino-im/settings_dialog.ui")] +class SettingsDialog : Dialog { + + [GtkChild] + private CheckButton marker_checkbutton; + + [GtkChild] + private CheckButton emoji_checkbutton; + + Dino.Settings settings = Dino.Settings.instance(); + + public SettingsDialog() { + Object(use_header_bar : 1); + + marker_checkbutton.active = settings.send_read; + emoji_checkbutton.active = settings.convert_utf8_smileys; + + marker_checkbutton.toggled.connect(() => { settings.send_read = marker_checkbutton.active; }); + emoji_checkbutton.toggled.connect(() => { settings.convert_utf8_smileys = emoji_checkbutton.active; }); + } +} + +} \ No newline at end of file diff --git a/client/src/ui/unified_window.vala b/client/src/ui/unified_window.vala new file mode 100644 index 00000000..9d5f1dfd --- /dev/null +++ b/client/src/ui/unified_window.vala @@ -0,0 +1,78 @@ +using Gtk; + +using Dino.Entities; + +public class Dino.Ui.UnifiedWindow : ApplicationWindow { + public ChatInput chat_input; + public ConversationListTitlebar conversation_list_titlebar; + public ConversationSelector.View filterable_conversation_list; + public ConversationSummary.View conversation_frame; + public ConversationTitlebar conversation_titlebar; + public Paned paned; + + private StreamInteractor stream_interactor; + private Conversation? conversation; + + public UnifiedWindow(Application application, StreamInteractor stream_interactor) { + Object(application : application); + this.stream_interactor = stream_interactor; + focus_in_event.connect(on_focus_in_event); + focus_out_event.connect(on_focus_out_event); + + default_width = 1200; + default_height = 700; + + chat_input = new ChatInput(stream_interactor); + conversation_frame = new ConversationSummary.View(stream_interactor); + conversation_titlebar = new ConversationTitlebar(stream_interactor); + paned = new Paned(Orientation.HORIZONTAL); + paned.set_position(300); + filterable_conversation_list = new ConversationSelector.View(stream_interactor); + conversation_list_titlebar = new ConversationListTitlebar(this, stream_interactor); + conversation_list_titlebar.search_button.bind_property("active", filterable_conversation_list.search_bar, "search-mode-enabled", + BindingFlags.SYNC_CREATE | BindingFlags.BIDIRECTIONAL); + Grid grid = new Grid(); + grid.orientation = Orientation.VERTICAL; + Paned toolbar_paned = new Paned(Orientation.HORIZONTAL); + + add(paned); + paned.add1(filterable_conversation_list); + paned.add2(grid); + + grid.add(conversation_frame); + grid.add(new Separator(Orientation.HORIZONTAL)); + grid.add(chat_input); + + conversation_frame.show_all(); + + toolbar_paned.add1(conversation_list_titlebar); + toolbar_paned.add2(conversation_titlebar); + paned.bind_property("position", toolbar_paned, "position", BindingFlags.SYNC_CREATE | BindingFlags.BIDIRECTIONAL); + set_titlebar(toolbar_paned); + + filterable_conversation_list.conversation_list.conversation_selected.connect(on_conversation_selected); + conversation_list_titlebar.conversation_opened.connect(on_conversation_selected); + } + + private void on_conversation_selected(Conversation conversation) { + this.conversation = conversation; + ChatInteraction.get_instance(stream_interactor).on_conversation_selected(conversation); + conversation.active = true; // only for conversation_selected + filterable_conversation_list.conversation_list.on_conversation_selected(conversation); // only for conversation_opened + + chat_input.initialize_for_conversation(conversation); + conversation_frame.initialize_for_conversation(conversation); + conversation_titlebar.initialize_for_conversation(conversation); + } + + private bool on_focus_in_event() { + ChatInteraction.get_instance(stream_interactor).window_focus_in(conversation); + return false; + } + + private bool on_focus_out_event() { + ChatInteraction.get_instance(stream_interactor).window_focus_out(conversation); + return false; + } +} + diff --git a/client/src/ui/util.vala b/client/src/ui/util.vala new file mode 100644 index 00000000..d06afe67 --- /dev/null +++ b/client/src/ui/util.vala @@ -0,0 +1,71 @@ +using Gtk; + +using Dino.Entities; +using Xmpp; + +public class Dino.Ui.Util : GLib.Object { + + private const string[] tango_colors_light = {"FCE94F", "FCAF3E", "E9B96E", "8AE234", "729FCF", "AD7FA8", "EF2929"}; + private const string[] tango_colors_medium = {"EDD400", "F57900", "C17D11", "73D216", "3465A4", "75507B", "CC0000"}; + private const string[] material_colors_500 = {"F44336", "E91E63", "9C27B0", "673AB7", "3f51B5", "2196F3", "03A9f4", "00BCD4", "009688", "4CAF50", "8BC34a", "CDDC39", "FFEB3B", "FFC107", "FF9800", "FF5722", "795548"}; + private const string[] material_colors_300 = {"E57373", "F06292", "BA68C8", "9575CD", "7986CB", "64B5F6", "4FC3F7", "4DD0E1", "4DB6AC", "81C784", "AED581", "DCE775", "FFF176", "FFD54F", "FFB74D", "FF8A65", "A1887F"}; + private const string[] material_colors_200 = {"EF9A9A", "F48FB1", "CE93D8", "B39DDB", "9FA8DA", "90CAF9", "81D4FA", "80DEEA", "80CBC4", "A5D6A7", "C5E1A5", "E6EE9C", "FFF59D", "FFE082", "FFCC80", "FFAB91", "BCAAA4"}; + + public static string get_avatar_hex_color(string name) { + return material_colors_300[name.hash() % material_colors_300.length]; +// return tango_colors_light[name.hash() % tango_colors_light.length]; + } + + public static string get_name_hex_color(string name) { + return material_colors_500[name.hash() % material_colors_500.length]; +// return tango_colors_medium[name.hash() % tango_colors_medium.length]; + } + + public static string color_for_show(string show) { + switch(show) { + case "online": return "#9CCC65"; + case "away": return "#FFCA28"; + case "chat": return "#66BB6A"; + case "xa": return "#EF5350"; + case "dnd": return "#EF5350"; + default: return "#BDBDBD"; + } + } + + public static string get_conversation_display_name(StreamInteractor stream_interactor, Conversation conversation) { + return get_display_name(stream_interactor, conversation.counterpart, conversation.account); + } + + public static string get_display_name(StreamInteractor stream_interactor, Jid jid, Account account) { + if (MucManager.get_instance(stream_interactor).is_groupchat_occupant(jid, account)) { + return jid.resourcepart; + } else { + if (jid.bare_jid.equals(account.bare_jid.bare_jid)) { + if (account.alias == null || account.alias == "") { + return account.bare_jid.to_string(); + } else { + return account.alias; + } + } + Roster.Item roster_item = RosterManager.get_instance(stream_interactor).get_roster_item(account, jid); + if (roster_item != null && roster_item.name != null) { + return roster_item.name; + } + return jid.bare_jid.to_string(); + } + } + + public static string get_message_display_name(StreamInteractor stream_interactor, Entities.Message message, Account account) { + Jid? real_jid = MucManager.get_instance(stream_interactor).get_message_real_jid(message); + if (real_jid != null) { + return get_display_name(stream_interactor, real_jid, account); + } else { + return get_display_name(stream_interactor, message.from, account); + } + } + + public static void image_set_from_scaled_pixbuf(Image image, Gdk.Pixbuf pixbuf, int scale = 0) { + if (scale == 0) scale = image.get_scale_factor(); + image.set_from_surface(Gdk.cairo_surface_create_from_pixbuf(pixbuf, scale, image.get_window())); + } +} diff --git a/cmake/BuildTargetScript.cmake b/cmake/BuildTargetScript.cmake new file mode 100644 index 00000000..72434498 --- /dev/null +++ b/cmake/BuildTargetScript.cmake @@ -0,0 +1,57 @@ +# This file is used to be invoked at build time. It generates the needed +# resource XML file. + +# Input variables that need to provided when invoking this script: +# GXML_OUTPUT The output file path where to save the XML file. +# GXML_COMPRESS_ALL Sets all COMPRESS flags in all resources in resource +# list. +# GXML_NO_COMPRESS_ALL Removes all COMPRESS flags in all resources in +# resource list. +# GXML_STRIPBLANKS_ALL Sets all STRIPBLANKS flags in all resources in +# resource list. +# GXML_NO_STRIPBLANKS_ALL Removes all STRIPBLANKS flags in all resources in +# resource list. +# GXML_TOPIXDATA_ALL Sets all TOPIXDATA flags i nall resources in resource +# list. +# GXML_NO_TOPIXDATA_ALL Removes all TOPIXDATA flags in all resources in +# resource list. +# GXML_PREFIX Overrides the resource prefix that is prepended to +# each relative name in registered resources. +# GXML_RESOURCES The list of resource files. Whether absolute or +# relative path is equal. + +# Include the GENERATE_GXML() function. +include(${CMAKE_CURRENT_LIST_DIR}/GenerateGXML.cmake) + +# Set flags to actual invocation flags. +if(GXML_COMPRESS_ALL) + set(GXML_COMPRESS_ALL COMPRESS_ALL) +endif() +if(GXML_NO_COMPRESS_ALL) + set(GXML_NO_COMPRESS_ALL NO_COMPRESS_ALL) +endif() +if(GXML_STRIPBLANKS_ALL) + set(GXML_STRIPBLANKS_ALL STRIPBLANKS_ALL) +endif() +if(GXML_NO_STRIPBLANKS_ALL) + set(GXML_NO_STRIPBLANKS_ALL NO_STRIPBLANKS_ALL) +endif() +if(GXML_TOPIXDATA_ALL) + set(GXML_TOPIXDATA_ALL TOPIXDATA_ALL) +endif() +if(GXML_NO_TOPIXDATA_ALL) + set(GXML_NO_TOPIXDATA_ALL NO_TOPIXDATA_ALL) +endif() + +# Replace " " with ";" to import the list over the command line. Otherwise +# CMake would interprete the passed resources as a whole string. +string(REPLACE " " ";" GXML_RESOURCES ${GXML_RESOURCES}) + +# Invoke the gresource XML generation function. +generate_gxml(${GXML_OUTPUT} + ${GXML_COMPRESS_ALL} ${GXML_NO_COMPRESS_ALL} + ${GXML_STRIPBLANKS_ALL} ${GXML_NO_STRIPBLANKS_ALL} + ${GXML_TOPIXDATA_ALL} ${GXML_NO_TOPIXDATA_ALL} + PREFIX ${GXML_PREFIX} + RESOURCES ${GXML_RESOURCES}) + diff --git a/cmake/CompileGResources.cmake b/cmake/CompileGResources.cmake new file mode 100644 index 00000000..e9a8d179 --- /dev/null +++ b/cmake/CompileGResources.cmake @@ -0,0 +1,221 @@ +include(CMakeParseArguments) + +# Path to this file. +set(GCR_CMAKE_MACRO_DIR ${CMAKE_CURRENT_LIST_DIR}) + +# Compiles a gresource resource file from given resource files. Automatically +# creates the XML controlling file. +# The type of resource to generate (header, c-file or bundle) is automatically +# determined from TARGET file ending, if no TYPE is explicitly specified. +# The output file is stored in the provided variable "output". +# "xml_out" contains the variable where to output the XML path. Can be used to +# create custom targets or doing postprocessing. +# If you want to use preprocessing, you need to manually check the existence +# of the tools you use. This function doesn't check this for you, it just +# generates the XML file. glib-compile-resources will then throw a +# warning/error. +function(COMPILE_GRESOURCES output xml_out) + # Available options: + # COMPRESS_ALL, NO_COMPRESS_ALL Overrides the COMPRESS flag in all + # registered resources. + # STRIPBLANKS_ALL, NO_STRIPBLANKS_ALL Overrides the STRIPBLANKS flag in all + # registered resources. + # TOPIXDATA_ALL, NO_TOPIXDATA_ALL Overrides the TOPIXDATA flag in all + # registered resources. + set(CG_OPTIONS COMPRESS_ALL NO_COMPRESS_ALL + STRIPBLANKS_ALL NO_STRIPBLANKS_ALL + TOPIXDATA_ALL NO_TOPIXDATA_ALL) + + # Available one value options: + # TYPE Type of resource to create. Valid options are: + # EMBED_C: A C-file that can be compiled with your project. + # EMBED_H: A header that can be included into your project. + # BUNDLE: Generates a resource bundle file that can be loaded + # at runtime. + # AUTO: Determine from target file ending. Need to specify + # target argument. + # PREFIX Overrides the resource prefix that is prepended to each + # relative file name in registered resources. + # SOURCE_DIR Overrides the resources base directory to search for resources. + # Normally this is set to the source directory with that CMake + # was invoked (CMAKE_SOURCE_DIR). + # TARGET Overrides the name of the output file/-s. Normally the output + # names from glib-compile-resources tool is taken. + set(CG_ONEVALUEARGS TYPE PREFIX SOURCE_DIR TARGET) + + # Available multi-value options: + # RESOURCES The list of resource files. Whether absolute or relative path is + # equal, absolute paths are stripped down to relative ones. If the + # absolute path is not inside the given base directory SOURCE_DIR + # or CMAKE_SOURCE_DIR (if SOURCE_DIR is not overriden), this + # function aborts. + # OPTIONS Extra command line options passed to glib-compile-resources. + set(CG_MULTIVALUEARGS RESOURCES OPTIONS) + + # Parse the arguments. + cmake_parse_arguments(CG_ARG + "${CG_OPTIONS}" + "${CG_ONEVALUEARGS}" + "${CG_MULTIVALUEARGS}" + "${ARGN}") + + # Variable to store the double-quote (") string. Since escaping + # double-quotes in strings is not possible we need a helper variable that + # does this job for us. + set(Q \") + + # Check invocation validity with the _UNPARSED_ARGUMENTS variable. + # If other not recognized parameters were passed, throw error. + if (CG_ARG_UNPARSED_ARGUMENTS) + set(CG_WARNMSG "Invocation of COMPILE_GRESOURCES with unrecognized") + set(CG_WARNMSG "${CG_WARNMSG} parameters. Parameters are:") + set(CG_WARNMSG "${CG_WARNMSG} ${CG_ARG_UNPARSED_ARGUMENTS}.") + message(WARNING ${CG_WARNMSG}) + endif() + + # Check invocation validity depending on generation mode (EMBED_C, EMBED_H + # or BUNDLE). + if ("${CG_ARG_TYPE}" STREQUAL "EMBED_C") + # EMBED_C mode, output compilable C-file. + set(CG_GENERATE_COMMAND_LINE "--generate-source") + set(CG_TARGET_FILE_ENDING "c") + elseif ("${CG_ARG_TYPE}" STREQUAL "EMBED_H") + # EMBED_H mode, output includable header file. + set(CG_GENERATE_COMMAND_LINE "--generate-header") + set(CG_TARGET_FILE_ENDING "h") + elseif ("${CG_ARG_TYPE}" STREQUAL "BUNDLE") + # BUNDLE mode, output resource bundle. Don't do anything since + # glib-compile-resources outputs a bundle when not specifying + # something else. + set(CG_TARGET_FILE_ENDING "gresource") + else() + # Everything else is AUTO mode, determine from target file ending. + if (CG_ARG_TARGET) + set(CG_GENERATE_COMMAND_LINE "--generate") + else() + set(CG_ERRMSG "AUTO mode given, but no target specified. Can't") + set(CG_ERRMSG "${CG_ERRMSG} determine output type. In function") + set(CG_ERRMSG "${CG_ERRMSG} COMPILE_GRESOURCES.") + message(FATAL_ERROR ${CG_ERRMSG}) + endif() + endif() + + # Check flag validity. + if (CG_ARG_COMPRESS_ALL AND CG_ARG_NO_COMPRESS_ALL) + set(CG_ERRMSG "COMPRESS_ALL and NO_COMPRESS_ALL simultaneously set. In") + set(CG_ERRMSG "${CG_ERRMSG} function COMPILE_GRESOURCES.") + message(FATAL_ERROR ${CG_ERRMSG}) + endif() + if (CG_ARG_STRIPBLANKS_ALL AND CG_ARG_NO_STRIPBLANKS_ALL) + set(CG_ERRMSG "STRIPBLANKS_ALL and NO_STRIPBLANKS_ALL simultaneously") + set(CG_ERRMSG "${CG_ERRMSG} set. In function COMPILE_GRESOURCES.") + message(FATAL_ERROR ${CG_ERRMSG}) + endif() + if (CG_ARG_TOPIXDATA_ALL AND CG_ARG_NO_TOPIXDATA_ALL) + set(CG_ERRMSG "TOPIXDATA_ALL and NO_TOPIXDATA_ALL simultaneously set.") + set(CG_ERRMSG "${CG_ERRMSG} In function COMPILE_GRESOURCES.") + message(FATAL_ERROR ${CG_ERRMSG}) + endif() + + # Check if there are any resources. + if (NOT CG_ARG_RESOURCES) + set(CG_ERRMSG "No resource files to process. In function") + set(CG_ERRMSG "${CG_ERRMSG} COMPILE_GRESOURCES.") + message(FATAL_ERROR ${CG_ERRMSG}) + endif() + + # Extract all dependencies for targets from resource list. + foreach(res ${CG_ARG_RESOURCES}) + if (NOT(("${res}" STREQUAL "COMPRESS") OR + ("${res}" STREQUAL "STRIPBLANKS") OR + ("${res}" STREQUAL "TOPIXDATA"))) + + add_custom_command( + OUTPUT "${CMAKE_BINARY_DIR}/resources/${res}" + COMMAND ${CMAKE_COMMAND} -E copy "${CG_ARG_SOURCE_DIR}/${res}" "${CMAKE_BINARY_DIR}/resources/${res}" + MAIN_DEPENDENCY "${CG_ARG_SOURCE_DIR}/${res}") + list(APPEND CG_RESOURCES_DEPENDENCIES "${CMAKE_BINARY_DIR}/resources/${res}") + endif() + endforeach() + + + # Construct .gresource.xml path. + set(CG_XML_FILE_PATH "${CMAKE_BINARY_DIR}/resources/.gresource.xml") + + # Generate gresources XML target. + list(APPEND CG_CMAKE_SCRIPT_ARGS "-D") + list(APPEND CG_CMAKE_SCRIPT_ARGS "GXML_OUTPUT=${Q}${CG_XML_FILE_PATH}${Q}") + if(CG_ARG_COMPRESS_ALL) + list(APPEND CG_CMAKE_SCRIPT_ARGS "-D") + list(APPEND CG_CMAKE_SCRIPT_ARGS "GXML_COMPRESS_ALL") + endif() + if(CG_ARG_NO_COMPRESS_ALL) + list(APPEND CG_CMAKE_SCRIPT_ARGS "-D") + list(APPEND CG_CMAKE_SCRIPT_ARGS "GXML_NO_COMPRESS_ALL") + endif() + if(CG_ARG_STRPIBLANKS_ALL) + list(APPEND CG_CMAKE_SCRIPT_ARGS "-D") + list(APPEND CG_CMAKE_SCRIPT_ARGS "GXML_STRIPBLANKS_ALL") + endif() + if(CG_ARG_NO_STRIPBLANKS_ALL) + list(APPEND CG_CMAKE_SCRIPT_ARGS "-D") + list(APPEND CG_CMAKE_SCRIPT_ARGS "GXML_NO_STRIPBLANKS_ALL") + endif() + if(CG_ARG_TOPIXDATA_ALL) + list(APPEND CG_CMAKE_SCRIPT_ARGS "-D") + list(APPEND CG_CMAKE_SCRIPT_ARGS "GXML_TOPIXDATA_ALL") + endif() + if(CG_ARG_NO_TOPIXDATA_ALL) + list(APPEND CG_CMAKE_SCRIPT_ARGS "-D") + list(APPEND CG_CMAKE_SCRIPT_ARGS "GXML_NO_TOPIXDATA_ALL") + endif() + list(APPEND CG_CMAKE_SCRIPT_ARGS "-D") + list(APPEND CG_CMAKE_SCRIPT_ARGS "GXML_PREFIX=${Q}${CG_ARG_PREFIX}${Q}") + list(APPEND CG_CMAKE_SCRIPT_ARGS "-D") + list(APPEND CG_CMAKE_SCRIPT_ARGS + "GXML_RESOURCES=${Q}${CG_ARG_RESOURCES}${Q}") + list(APPEND CG_CMAKE_SCRIPT_ARGS "-P") + list(APPEND CG_CMAKE_SCRIPT_ARGS + "${Q}${GCR_CMAKE_MACRO_DIR}/BuildTargetScript.cmake${Q}") + + get_filename_component(CG_XML_FILE_PATH_ONLY_NAME + "${CG_XML_FILE_PATH}" NAME) + set(CG_XML_CUSTOM_COMMAND_COMMENT + "Creating gresources XML file (${CG_XML_FILE_PATH_ONLY_NAME})") + add_custom_command(OUTPUT ${CG_XML_FILE_PATH} + COMMAND ${CMAKE_COMMAND} + ARGS ${CG_CMAKE_SCRIPT_ARGS} + DEPENDS ${CG_RESOURCES_DEPENDENCIES} + WORKING_DIRECTORY ${CMAKE_BINARY_DIR} + COMMENT ${CG_XML_CUSTOM_COMMAND_COMMENT}) + + # Create target manually if not set (to make sure glib-compile-resources + # doesn't change behaviour with it's naming standards). + if (NOT CG_ARG_TARGET) + set(CG_ARG_TARGET "${CMAKE_BINARY_DIR}/resources") + set(CG_ARG_TARGET "${CG_ARG_TARGET}.${CG_TARGET_FILE_ENDING}") + endif() + + # Create source directory automatically if not set. + if (NOT CG_ARG_SOURCE_DIR) + set(CG_ARG_SOURCE_DIR "${CMAKE_SOURCE_DIR}") + endif() + + # Add compilation target for resources. + add_custom_command(OUTPUT ${CG_ARG_TARGET} + COMMAND ${GLIB_COMPILE_RESOURCES_EXECUTABLE} + ARGS + ${OPTIONS} + "--target=${Q}${CG_ARG_TARGET}${Q}" + "--sourcedir=${Q}${CG_ARG_SOURCE_DIR}${Q}" + ${CG_GENERATE_COMMAND_LINE} + ${CG_XML_FILE_PATH} + MAIN_DEPENDENCY ${CG_XML_FILE_PATH} + DEPENDS ${CG_RESOURCES_DEPENDENCIES} + WORKING_DIRECTORY ${CMAKE_BUILD_DIR}) + + # Set output and XML_OUT to parent scope. + set(${xml_out} ${CG_XML_FILE_PATH} PARENT_SCOPE) + set(${output} ${CG_ARG_TARGET} PARENT_SCOPE) + +endfunction() diff --git a/cmake/FindGPGME.cmake b/cmake/FindGPGME.cmake new file mode 100644 index 00000000..fd096363 --- /dev/null +++ b/cmake/FindGPGME.cmake @@ -0,0 +1,27 @@ +# TODO: Windows related stuff + +find_program(GPGME_CONFIG_EXECUTABLE NAMES gpgme-config) +mark_as_advanced(GPGME_CONFIG_EXECUTABLE) + +if(GPGME_CONFIG_EXECUTABLE) + execute_process(COMMAND ${GPGME_CONFIG_EXECUTABLE} --version + OUTPUT_VARIABLE GPGME_VERSION + OUTPUT_STRIP_TRAILING_WHITESPACE) + + execute_process(COMMAND ${GPGME_CONFIG_EXECUTABLE} --api-version + OUTPUT_VARIABLE GPGME_API_VERSION + OUTPUT_STRIP_TRAILING_WHITESPACE) + + execute_process(COMMAND ${GPGME_CONFIG_EXECUTABLE} --cflags + OUTPUT_VARIABLE GPGME_CFLAGS + OUTPUT_STRIP_TRAILING_WHITESPACE) + + execute_process(COMMAND ${GPGME_CONFIG_EXECUTABLE} --libs + OUTPUT_VARIABLE GPGME_LIBRARIES + OUTPUT_STRIP_TRAILING_WHITESPACE) +endif(GPGME_CONFIG_EXECUTABLE) + +include(FindPackageHandleStandardArgs) +find_package_handle_standard_args(GPGME + REQUIRED_VARS GPGME_CONFIG_EXECUTABLE + VERSION_VAR GPGME_VERSION) \ No newline at end of file diff --git a/cmake/FindLIBUUID.cmake b/cmake/FindLIBUUID.cmake new file mode 100644 index 00000000..bfc364f3 --- /dev/null +++ b/cmake/FindLIBUUID.cmake @@ -0,0 +1,42 @@ +# - Find libuuid +# Find the libuuid library +# +# This module defines the following variables: +# LIBUUID_FOUND - True if library and include directory are found +# If set to TRUE, the following are also defined: +# LIBUUID_INCLUDE_DIRS - The directory where to find the header file +# LIBUUID_LIBRARIES - Where to find the library file +# +# For conveniance, these variables are also set. They have the same values +# than the variables above. The user can thus choose his/her prefered way +# to write them. +# LIBUUID_INCLUDE_DIR +# LIBUUID_LIBRARY +# +# This file is in the public domain + +include(FindPkgConfig) +pkg_check_modules(LIBUUID uuid) + +if(NOT LIBUUID_FOUND) + find_path(LIBUUID_INCLUDE_DIRS NAMES uuid/uuid.h + PATH_SUFFIXES uuid + DOC "The libuuid include directory") + + find_library(LIBUUID_LIBRARIES NAMES uuid + DOC "The libuuid library") + + # Use some standard module to handle the QUIETLY and REQUIRED arguments, and + # set LIBUUID_FOUND to TRUE if these two variables are set. + include(FindPackageHandleStandardArgs) + find_package_handle_standard_args(LIBUUID REQUIRED_VARS LIBUUID_LIBRARIES LIBUUID_INCLUDE_DIRS) + + # Compatibility for all the ways of writing these variables + if(LIBUUID_FOUND) + set(LIBUUID_INCLUDE_DIR ${LIBUUID_INCLUDE_DIRS}) + set(LIBUUID_LIBRARY ${LIBUUID_LIBRARIES}) + set(LIBUUID_CFLAGS -I${LIBUUID_INCLUDE_DIRS}) + endif() +endif() + +mark_as_advanced(LIBUUID_INCLUDE_DIRS LIBUUID_LIBRARIES LIBUUID_CFLAGS) diff --git a/cmake/FindVala.cmake b/cmake/FindVala.cmake new file mode 100644 index 00000000..5150a7d9 --- /dev/null +++ b/cmake/FindVala.cmake @@ -0,0 +1,70 @@ +## +# Find module for the Vala compiler (valac) +# +# This module determines wheter a Vala compiler is installed on the current +# system and where its executable is. +# +# Call the module using "find_package(Vala) from within your CMakeLists.txt. +# +# The following variables will be set after an invocation: +# +# VALA_FOUND Whether the vala compiler has been found or not +# VALA_EXECUTABLE Full path to the valac executable if it has been found +# VALA_VERSION Version number of the available valac +# VALA_USE_FILE Include this file to define the vala_precompile function +## + +## +# Copyright 2009-2010 Jakob Westhoff. All rights reserved. +# Copyright 2010-2011 Daniel Pfeifer +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: +# +# 1. Redistributions of source code must retain the above copyright notice, +# this list of conditions and the following disclaimer. +# +# 2. Redistributions in binary form must reproduce the above copyright notice, +# this list of conditions and the following disclaimer in the documentation +# and/or other materials provided with the distribution. +# +# THIS SOFTWARE IS PROVIDED BY JAKOB WESTHOFF ``AS IS'' AND ANY EXPRESS OR +# IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +# MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO +# EVENT SHALL JAKOB WESTHOFF OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, +# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR +# PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +# LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE +# OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF +# ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +# +# The views and conclusions contained in the software and documentation are those +# of the authors and should not be interpreted as representing official policies, +# either expressed or implied, of Jakob Westhoff +## + +# Search for the valac executable in the usual system paths +# Some distributions rename the valac to contain the major.minor in the binary name +find_program(VALA_EXECUTABLE NAMES valac valac-0.20 valac-0.18 valac-0.16 valac-0.14 valac-0.12 valac-0.10) +mark_as_advanced(VALA_EXECUTABLE) + +# Determine the valac version +if(VALA_EXECUTABLE) + execute_process(COMMAND ${VALA_EXECUTABLE} "--version" + OUTPUT_VARIABLE VALA_VERSION + OUTPUT_STRIP_TRAILING_WHITESPACE) + string(REPLACE "Vala " "" VALA_VERSION "${VALA_VERSION}") +endif(VALA_EXECUTABLE) + +# Handle the QUIETLY and REQUIRED arguments, which may be given to the find call. +# Furthermore set VALA_FOUND to TRUE if Vala has been found (aka. +# VALA_EXECUTABLE is set) + +include(FindPackageHandleStandardArgs) +find_package_handle_standard_args(Vala + REQUIRED_VARS VALA_EXECUTABLE + VERSION_VAR VALA_VERSION) + +set(VALA_USE_FILE "${CMAKE_CURRENT_LIST_DIR}/UseVala.cmake") + diff --git a/cmake/GenerateGXML.cmake b/cmake/GenerateGXML.cmake new file mode 100644 index 00000000..4be96041 --- /dev/null +++ b/cmake/GenerateGXML.cmake @@ -0,0 +1,124 @@ +include(CMakeParseArguments) + +# Generates the resource XML controlling file from resource list (and saves it +# to xml_path). It's not recommended to use this function directly, since it +# doesn't handle invalid arguments. It is used by the function +# COMPILE_GRESOURCES() to create a custom command, so that this function is +# invoked at build-time in script mode from CMake. +function(GENERATE_GXML xml_path) + # Available options: + # COMPRESS_ALL, NO_COMPRESS_ALL Overrides the COMPRESS flag in all + # registered resources. + # STRIPBLANKS_ALL, NO_STRIPBLANKS_ALL Overrides the STRIPBLANKS flag in all + # registered resources. + # TOPIXDATA_ALL, NO_TOPIXDATA_ALL Overrides the TOPIXDATA flag in all + # registered resources. + set(GXML_OPTIONS COMPRESS_ALL NO_COMPRESS_ALL + STRIPBLANKS_ALL NO_STRIPBLANKS_ALL + TOPIXDATA_ALL NO_TOPIXDATA_ALL) + + # Available one value options: + # PREFIX Overrides the resource prefix that is prepended to each + # relative file name in registered resources. + set(GXML_ONEVALUEARGS PREFIX) + + # Available multi-value options: + # RESOURCES The list of resource files. Whether absolute or relative path is + # equal, absolute paths are stripped down to relative ones. If the + # absolute path is not inside the given base directory SOURCE_DIR + # or CMAKE_SOURCE_DIR (if SOURCE_DIR is not overriden), this + # function aborts. + set(GXML_MULTIVALUEARGS RESOURCES) + + # Parse the arguments. + cmake_parse_arguments(GXML_ARG + "${GXML_OPTIONS}" + "${GXML_ONEVALUEARGS}" + "${GXML_MULTIVALUEARGS}" + "${ARGN}") + + # Variable to store the double-quote (") string. Since escaping + # double-quotes in strings is not possible we need a helper variable that + # does this job for us. + set(Q \") + + # Process resources and generate XML file. + # Begin with the XML header and header nodes. + set(GXML_XML_FILE "") + set(GXML_XML_FILE "${GXML_XML_FILE}") + + # Process each resource. + foreach(res ${GXML_ARG_RESOURCES}) + if ("${res}" STREQUAL "COMPRESS") + set(GXML_COMPRESSION_FLAG ON) + elseif ("${res}" STREQUAL "STRIPBLANKS") + set(GXML_STRIPBLANKS_FLAG ON) + elseif ("${res}" STREQUAL "TOPIXDATA") + set(GXML_TOPIXDATA_FLAG ON) + else() + # The file name. + set(GXML_RESOURCE_PATH "${res}") + + # Append to real resource file dependency list. + list(APPEND GXML_RESOURCES_DEPENDENCIES ${GXML_RESOURCE_PATH}) + + # Assemble node. + set(GXML_RES_LINE "${GXML_RESOURCE_PATH}") + + # Append to file string. + set(GXML_XML_FILE "${GXML_XML_FILE}${GXML_RES_LINE}") + + # Unset variables. + unset(GXML_COMPRESSION_FLAG) + unset(GXML_STRIPBLANKS_FLAG) + unset(GXML_TOPIXDATA_FLAG) + endif() + + endforeach() + + # Append closing nodes. + set(GXML_XML_FILE "${GXML_XML_FILE}") + + # Use "file" function to generate XML controlling file. + get_filename_component(xml_path_only_name "${xml_path}" NAME) + file(WRITE ${xml_path} ${GXML_XML_FILE}) + +endfunction() + diff --git a/cmake/GlibCompileResourcesSupport.cmake b/cmake/GlibCompileResourcesSupport.cmake new file mode 100644 index 00000000..2950af34 --- /dev/null +++ b/cmake/GlibCompileResourcesSupport.cmake @@ -0,0 +1,11 @@ +# Path to this file. +set(GCR_CMAKE_MACRO_DIR ${CMAKE_CURRENT_LIST_DIR}) + +# Finds the glib-compile-resources executable. +find_program(GLIB_COMPILE_RESOURCES_EXECUTABLE glib-compile-resources) +mark_as_advanced(GLIB_COMPILE_RESOURCES_EXECUTABLE) + +# Include the cmake files containing the functions. +include(${GCR_CMAKE_MACRO_DIR}/CompileGResources.cmake) +include(${GCR_CMAKE_MACRO_DIR}/GenerateGXML.cmake) + diff --git a/cmake/UseVala.cmake b/cmake/UseVala.cmake new file mode 100644 index 00000000..a5d14a0f --- /dev/null +++ b/cmake/UseVala.cmake @@ -0,0 +1,271 @@ +## +# Compile vala files to their c equivalents for further processing. +# +# The "vala_precompile" function takes care of calling the valac executable on +# the given source to produce c files which can then be processed further using +# default cmake functions. +# +# The first parameter provided is a variable, which will be filled with a list +# of c files outputted by the vala compiler. This list can than be used in +# conjuction with functions like "add_executable" or others to create the +# neccessary compile rules with CMake. +# +# The following sections may be specified afterwards to provide certain options +# to the vala compiler: +# +# SOURCES +# A list of .vala files to be compiled. Please take care to add every vala +# file belonging to the currently compiled project or library as Vala will +# otherwise not be able to resolve all dependencies. +# +# PACKAGES +# A list of vala packages/libraries to be used during the compile cycle. The +# package names are exactly the same, as they would be passed to the valac +# "--pkg=" option. +# +# OPTIONS +# A list of optional options to be passed to the valac executable. This can be +# used to pass "--thread" for example to enable multi-threading support. +# +# DEFINITIONS +# A list of symbols to be used for conditional compilation. They are the same +# as they would be passed using the valac "--define=" option. +# +# CUSTOM_VAPIS +# A list of custom vapi files to be included for compilation. This can be +# useful to include freshly created vala libraries without having to install +# them in the system. +# +# GENERATE_VAPI +# Pass all the needed flags to the compiler to create a vapi for +# the compiled library. The provided name will be used for this and a +# .vapi file will be created. +# +# GENERATE_HEADER +# Let the compiler generate a header file for the compiled code. There will +# be a header file as well as an internal header file being generated called +# .h and _internal.h +# +# The following call is a simple example to the vala_precompile macro showing +# an example to every of the optional sections: +# +# find_package(Vala "0.12" REQUIRED) +# include(${VALA_USE_FILE}) +# +# vala_precompile(VALA_C +# SOURCES +# source1.vala +# source2.vala +# source3.vala +# PACKAGES +# gtk+-2.0 +# gio-1.0 +# posix +# DIRECTORY +# gen +# OPTIONS +# --thread +# CUSTOM_VAPIS +# some_vapi.vapi +# GENERATE_VAPI +# myvapi +# GENERATE_HEADER +# myheader +# ) +# +# Most important is the variable VALA_C which will contain all the generated c +# file names after the call. +## + +## +# Copyright 2009-2010 Jakob Westhoff. All rights reserved. +# Copyright 2010-2011 Daniel Pfeifer +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: +# +# 1. Redistributions of source code must retain the above copyright notice, +# this list of conditions and the following disclaimer. +# +# 2. Redistributions in binary form must reproduce the above copyright notice, +# this list of conditions and the following disclaimer in the documentation +# and/or other materials provided with the distribution. +# +# THIS SOFTWARE IS PROVIDED BY JAKOB WESTHOFF ``AS IS'' AND ANY EXPRESS OR +# IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +# MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO +# EVENT SHALL JAKOB WESTHOFF OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, +# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR +# PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +# LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE +# OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF +# ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +# +# The views and conclusions contained in the software and documentation are those +# of the authors and should not be interpreted as representing official policies, +# either expressed or implied, of Jakob Westhoff +## + +include(CMakeParseArguments) + +function(_vala_mkdir_for_file file) + get_filename_component(dir "${file}" DIRECTORY) + file(MAKE_DIRECTORY "${dir}") +endfunction() + +function(vala_precompile output) + cmake_parse_arguments(ARGS "" "DIRECTORY;GENERATE_HEADER;GENERATE_VAPI" + "SOURCES;PACKAGES;OPTIONS;DEFINITIONS;CUSTOM_VAPIS;GRESOURCES" ${ARGN}) + + if(ARGS_DIRECTORY) + get_filename_component(DIRECTORY ${ARGS_DIRECTORY} ABSOLUTE) + else(ARGS_DIRECTORY) + set(DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}) + endif(ARGS_DIRECTORY) + include_directories(${DIRECTORY}) + + set(vala_pkg_opts "") + foreach(pkg ${ARGS_PACKAGES}) + list(APPEND vala_pkg_opts "--pkg=${pkg}") + endforeach(pkg ${ARGS_PACKAGES}) + + set(vala_define_opts "") + foreach(def ${ARGS_DEFINTIONS}) + list(APPEND vala_define_opts "--define=${def}") + endforeach(def ${ARGS_DEFINTIONS}) + + set(custom_vapi_arguments "") + if(ARGS_CUSTOM_VAPIS) + foreach(vapi ${ARGS_CUSTOM_VAPIS}) + if(${vapi} MATCHES ${CMAKE_SOURCE_DIR} OR ${vapi} MATCHES ${CMAKE_BINARY_DIR}) + list(APPEND custom_vapi_arguments ${vapi}) + else (${vapi} MATCHES ${CMAKE_SOURCE_DIR} OR ${vapi} MATCHES ${CMAKE_BINARY_DIR}) + list(APPEND custom_vapi_arguments ${CMAKE_CURRENT_SOURCE_DIR}/${vapi}) + endif(${vapi} MATCHES ${CMAKE_SOURCE_DIR} OR ${vapi} MATCHES ${CMAKE_BINARY_DIR}) + endforeach(vapi ${ARGS_CUSTOM_VAPIS}) + endif(ARGS_CUSTOM_VAPIS) + + set(gresources_args "") + if(ARGS_GRESOURCES) + set(gresources_args --gresources "${ARGS_GRESOURCES}") + endif(ARGS_GRESOURCES) + + set(in_files "") + set(fast_vapi_files "") + set(out_files "") + set(out_extra_files "") + + set(vapi_arguments "") + if(ARGS_GENERATE_VAPI) + list(APPEND out_extra_files "${DIRECTORY}/${ARGS_GENERATE_VAPI}.vapi") + set(vapi_arguments "--internal-vapi=${ARGS_GENERATE_VAPI}.vapi") + + # Header and internal header is needed to generate internal vapi + if (NOT ARGS_GENERATE_HEADER) + set(ARGS_GENERATE_HEADER ${ARGS_GENERATE_VAPI}) + endif(NOT ARGS_GENERATE_HEADER) + endif(ARGS_GENERATE_VAPI) + + set(header_arguments "") + if(ARGS_GENERATE_HEADER) + list(APPEND out_extra_files "${DIRECTORY}/${ARGS_GENERATE_HEADER}.h") + list(APPEND out_extra_files "${DIRECTORY}/${ARGS_GENERATE_HEADER}_internal.h") + list(APPEND header_arguments "--header=${DIRECTORY}/${ARGS_GENERATE_HEADER}.h") + list(APPEND header_arguments "--internal-header=${DIRECTORY}/${ARGS_GENERATE_HEADER}_internal.h") + endif(ARGS_GENERATE_HEADER) + + foreach(src ${ARGS_SOURCES} ${ARGS_UNPARSED_ARGUMENTS}) + set(in_file "${CMAKE_CURRENT_SOURCE_DIR}/${src}") + list(APPEND in_files "${in_file}") + string(REPLACE ".vala" ".c" src ${src}) + string(REPLACE ".gs" ".c" src ${src}) + string(REPLACE ".c" ".vapi" fast_vapi ${src}) + set(fast_vapi_file "${DIRECTORY}/${fast_vapi}") + list(APPEND fast_vapi_files "${fast_vapi_file}") + list(APPEND out_files "${DIRECTORY}/${src}") + + _vala_mkdir_for_file("${fast_vapi_file}") + + add_custom_command(OUTPUT ${fast_vapi_file} + COMMAND + ${VALA_EXECUTABLE} + ARGS + --fast-vapi ${fast_vapi_file} + ${ARGS_OPTIONS} + ${in_file} + DEPENDS + ${in_file} + COMMENT + "Generating fast VAPI ${fast_vapi}" + ) + endforeach(src ${ARGS_SOURCES} ${ARGS_UNPARSED_ARGUMENTS}) + + foreach(src ${ARGS_SOURCES} ${ARGS_UNPARSED_ARGUMENTS}) + set(in_file "${CMAKE_CURRENT_SOURCE_DIR}/${src}") + string(REPLACE ".vala" ".c" c_code ${src}) + string(REPLACE ".gs" ".c" c_code ${c_code}) + string(REPLACE ".c" ".vapi" fast_vapi ${c_code}) + set(my_fast_vapi_file "${DIRECTORY}/${fast_vapi}") + set(c_code_file "${DIRECTORY}/${c_code}") + set(fast_vapi_flags "") + set(fast_vapi_stamp "") + foreach(fast_vapi_file ${fast_vapi_files}) + if(NOT "${fast_vapi_file}" STREQUAL "${my_fast_vapi_file}") + list(APPEND fast_vapi_flags --use-fast-vapi "${fast_vapi_file}") + list(APPEND fast_vapi_stamp "${fast_vapi_file}") + endif() + endforeach(fast_vapi_file) + + _vala_mkdir_for_file("${fast_vapi_file}") + get_filename_component(dir "${c_code_file}" DIRECTORY) + + add_custom_command(OUTPUT ${c_code_file} + COMMAND + ${VALA_EXECUTABLE} + ARGS + "-C" + "-d" ${dir} + ${vala_pkg_opts} + ${vala_define_opts} + ${gresources_args} + ${ARGS_OPTIONS} + ${fast_vapi_flags} + ${in_file} + ${custom_vapi_arguments} + DEPENDS + ${fast_vapi_stamp} + ${in_file} + ${ARGS_CUSTOM_VAPIS} + ${ARGS_GRESOURCES} + COMMENT + "Generating C source ${c_code}" + ) + endforeach(src) + + if(NOT "${out_extra_files}" STREQUAL "") + add_custom_command(OUTPUT ${out_extra_files} + COMMAND + ${VALA_EXECUTABLE} + ARGS + -C -q --disable-warnings + ${header_arguments} + ${vapi_arguments} + "-b" ${CMAKE_CURRENT_SOURCE_DIR} + "-d" ${DIRECTORY} + ${vala_pkg_opts} + ${vala_define_opts} + ${gresources_args} + ${ARGS_OPTIONS} + ${in_files} + ${custom_vapi_arguments} + DEPENDS + ${in_files} + ${ARGS_CUSTOM_VAPIS} + ${ARGS_GRESOURCES} + COMMENT + "Generating VAPI and headers for linking" + ) + endif() + set(${output} ${out_files} PARENT_SCOPE) +endfunction(vala_precompile) diff --git a/configure b/configure new file mode 100755 index 00000000..e98db6ec --- /dev/null +++ b/configure @@ -0,0 +1,82 @@ +#!/bin/bash + +cont() { + read c + if [ "$c" != "yes" ] && [ "$c" != "Yes" ] && [ "$c" != "y" ] && [ "$c" != "Y" ] + then + exit 3 + fi +} + +if [ ! -e `which cmake` ] +then + echo "CMake required." + exit 1 +fi + +if [ -x "$(which ninja 2>/dev/null)" ]; then + echo "Using Ninja ($(which ninja))" + cmake_type="Ninja" + exec_bin="ninja" +elif [ -x "$(which ninja-build 2>/dev/null)" ]; then + echo "Using Ninja ($(which ninja-build))" + cmake_type="Ninja" + exec_bin="ninja-build" +elif [ -x "$(which make 2>/dev/null)" ]; then + echo "Using Make ($(which make))" + cmake_type="Unix Makefiles" + exec_bin="make" + printf "Using Ninja improves build experience, continue with Make? [y/N] " + cont +else + echo "No compatible build system (Ninja, Make) found." + exit 4 +fi + +if [ -f ./build ] +then + echo "./build file exists. ./configure can't continue" + exit 2 +fi + +if [ -d build ] +then + if [ ! -f "build/.cmake_type" ] + then + printf "./build exists but was not created by ./configure script, continue? [y/N] " + cont + fi + last_type=`cat build/.cmake_type` + if [ "$cmake_type" != "$last_type" ] + then + echo "Using different build system, cleaning build system files" + cd build + rm -r CMakeCache.txt CMakeFiles + cd .. + fi +fi + +mkdir -p build +cd build + +echo "$cmake_type" > .cmake_type +cmake -G "$cmake_type" .. + +if [ "$cmake_type" == "Ninja" ] +then +cat << EOF > Makefile +default: + @sh -c "$exec_bin" +%: + @sh -c "$exec_bin \"\$@\"" +EOF +fi + +cd .. + +cat << EOF > Makefile +default: + @sh -c "cd build; $exec_bin" +%: + @sh -c "cd build; $exec_bin \"\$@\"" +EOF diff --git a/qlite/CMakeLists.txt b/qlite/CMakeLists.txt new file mode 100644 index 00000000..d19ed2d2 --- /dev/null +++ b/qlite/CMakeLists.txt @@ -0,0 +1,46 @@ +find_package(Vala REQUIRED) +find_package(PkgConfig REQUIRED) +include(${VALA_USE_FILE}) + +set(QLITE_PACKAGES + gee-0.8 + glib-2.0 + sqlite3 +) + +pkg_check_modules(QLITE REQUIRED ${QLITE_PACKAGES}) + +vala_precompile(QLITE_VALA_C +SOURCES + "src/database.vala" + "src/table.vala" + "src/column.vala" + "src/row.vala" + + "src/statement_builder.vala" + "src/query_builder.vala" + "src/insert_builder.vala" + "src/update_builder.vala" + "src/delete_builder.vala" +PACKAGES + ${QLITE_PACKAGES} +GENERATE_VAPI + qlite +GENERATE_HEADER + qlite +OPTIONS + -g + --thread + --vapidir=${CMAKE_SOURCE_DIR}/vapi +) + +set(CFLAGS ${QLITE_CFLAGS} -g ${VALA_CFLAGS}) +add_definitions(${CFLAGS}) +add_library(qlite SHARED ${QLITE_VALA_C}) +target_link_libraries(qlite ${QLITE_LIBRARIES}) + +add_custom_target(qlite-vapi +DEPENDS + ${CMAKE_BINARY_DIR}/qlite/qlite.vapi +) + diff --git a/qlite/src/column.vala b/qlite/src/column.vala new file mode 100644 index 00000000..f7b3114f --- /dev/null +++ b/qlite/src/column.vala @@ -0,0 +1,188 @@ +using Sqlite; + +namespace Qlite { + +public abstract class Column { + public string name { get; private set; } + public string default { get; set; } + public int sqlite_type { get; private set; } + public bool primary_key { get; set; } + public bool auto_increment { get; set; } + public bool unique { get; set; } + public bool not_null { get; set; } + public long min_version { get; set; default = -1; } + public long max_version { get; set; default = long.MAX; } + + public abstract T get(Row row); + + public virtual bool is_null(Row row) { + return false; + } + + public virtual void bind(Statement stmt, int index, T value) { + throw new DatabaseError.NOT_SUPPORTED(@"bind() was not implemented for field $name"); + } + + public string to_string() { + string res = name; + switch (sqlite_type) { + case INTEGER: + res += " INTEGER"; + break; + case FLOAT: + res += " REAL"; + break; + case TEXT: + res += " TEXT"; + break; + default: + res += " UNKNOWN"; + break; + } + if (primary_key) { + res += " PRIMARY KEY"; + if (auto_increment) res += " AUTOINCREMENT"; + } + if (not_null) res += " NOT NULL"; + if (unique) res += " UNIQUE"; + if (default != null) res += @" DEFAULT $default"; + + return res; + } + + public Column(string name, int type) { + this.name = name; + this.sqlite_type = type; + } + + public class Integer : Column { + public Integer(string name) { + base(name, INTEGER); + } + + public override int get(Row row) { + return (int) row.get_integer(name); + } + + public override bool is_null(Row row) { + return !row.has_integer(name); + } + + public override void bind(Statement stmt, int index, int value) { + stmt.bind_int(index, value); + } + } + + public class Long : Column { + public Long(string name) { + base(name, INTEGER); + } + + public override long get(Row row) { + return (long) row.get_integer(name); + } + + public override bool is_null(Row row) { + return !row.has_integer(name); + } + + public override void bind(Statement stmt, int index, long value) { + stmt.bind_int64(index, value); + } + } + + public class Real : Column { + public Real(string name) { + base(name, FLOAT); + } + + public override double get(Row row) { + return row.get_real(name); + } + + public override bool is_null(Row row) { + return !row.has_real(name); + } + + public override void bind(Statement stmt, int index, double value) { + stmt.bind_double(index, value); + } + } + + public class Text : Column { + public Text(string name) { + base(name, TEXT); + } + + public override string? get(Row row) { + return row.get_text(name); + } + + public override bool is_null(Row row) { + return get(row) == null; + } + + public override void bind(Statement stmt, int index, string? value) { + if (value != null) { + stmt.bind_text(index, value); + } else { + stmt.bind_null(index); + } + } + } + + public class BoolText : Column { + public BoolText(string name) { + base(name, TEXT); + } + + public override bool get(Row row) { + return row.get_text(name) == "1"; + } + + public override void bind(Statement stmt, int index, bool value) { + stmt.bind_text(index, value ? "1" : "0"); + } + } + + public class BoolInt : Column { + public BoolInt(string name) { + base(name, INTEGER); + } + + public override bool get(Row row) { + return row.get_integer(name) == 1; + } + + public override void bind(Statement stmt, int index, bool value) { + stmt.bind_int(index, value ? 1 : 0); + } + } + + public class RowReference : Column { + private Table table; + private Column id_column; + + public RowReference(string name, Table table, Column id_column) throws DatabaseError { + base(name, INTEGER); + if (!table.is_known_column(id_column.name)) throw new DatabaseError.ILLEGAL_REFERENCE(@"$(id_column.name) is not a column in $(table.name)"); + if (!id_column.primary_key && !id_column.unique) throw new DatabaseError.NON_UNIQUE(@"$(id_column.name) is not suited to identify a row, but used with RowReference"); + this.table = table; + this.id_column = id_column; + } + + public override Row? get(Row row) { + return table.row_with(id_column, (int)row.get_integer(name)); + } + + public override void bind(Statement stmt, int index, Row? value) { + if (value != null) { + stmt.bind_int(index, id_column.get(value)); + } else { + stmt.bind_null(index); + } + } + } +} + +} \ No newline at end of file diff --git a/qlite/src/database.vala b/qlite/src/database.vala new file mode 100644 index 00000000..285e10a8 --- /dev/null +++ b/qlite/src/database.vala @@ -0,0 +1,152 @@ +using Sqlite; + +namespace Qlite { + +public errordomain DatabaseError { + ILLEGAL_QUERY, + NOT_SUPPORTED, + OPEN_ERROR, + PREPARE_ERROR, + EXEC_ERROR, + NON_UNIQUE, + ILLEGAL_REFERENCE, + NOT_INITIALIZED +} + +public class Database { + private string file_name; + private Sqlite.Database db; + private long expected_version; + private Table[] tables; + + private Column meta_name = new Column.Text("name") { primary_key = true }; + private Column meta_int_val = new Column.Long("int_val"); + private Column meta_text_val = new Column.Text("text_val"); + private Table meta_table; + + public bool debug = false; + + public Database(string file_name, long expected_version) { + this.file_name = file_name; + this.expected_version = expected_version; + meta_table = new Table(this, "_meta"); + meta_table.init({meta_name, meta_int_val, meta_text_val}); + } + + public void init(Table[] tables) throws DatabaseError { + print(@"Intializing database at $file_name\n"); + Sqlite.config(Config.SERIALIZED); + int ec = Sqlite.Database.open_v2(file_name, out db, OPEN_READWRITE | OPEN_CREATE | 0x00010000); + if (ec != Sqlite.OK) { + throw new DatabaseError.OPEN_ERROR(@"SQLite error: $(db.errcode()) - $(db.errmsg())"); + } + this.tables = tables; + start_migration(); + } + + public void ensure_init() throws DatabaseError { + if (tables == null) throw new DatabaseError.NOT_INITIALIZED(@"Database $file_name was not initialized, call init()"); + } + + private void start_migration() throws DatabaseError { + meta_table.create_table_at_version(expected_version); + long old_version = 0; + try { + Row? row = meta_table.row_with(meta_name, "version"); + old_version = row == null ? -1 : (long) row[meta_int_val]; + } catch (DatabaseError e) { + old_version = -1; + } + foreach (Table t in tables) { + t.create_table_at_version(old_version); + } + if (expected_version != old_version) { + foreach (Table t in tables) { + t.add_columns_for_version(old_version, expected_version); + } + migrate(old_version); + foreach (Table t in tables) { + t.delete_columns_for_version(old_version, expected_version); + } + if (old_version == -1) { + meta_table.insert().value(meta_name, "version").value(meta_int_val, expected_version).perform(); + } else { + meta_table.update().with(meta_name, "=", "version").set(meta_int_val, expected_version).perform(); + } + } + } + + internal int errcode() { + return db.errcode(); + } + + internal string errmsg() { + return db.errmsg(); + } + + internal int64 last_insert_rowid() { + return db.last_insert_rowid(); + } + + // To be implemented by actual implementation if required + // new table columns are added, outdated columns are still present and will be removed afterwards + public virtual void migrate(long old_version) throws DatabaseError { + } + + public QueryBuilder select(Column[]? columns = null) throws DatabaseError { + ensure_init(); + return new QueryBuilder(this).select(columns); + } + + public InsertBuilder insert() throws DatabaseError { + ensure_init(); + return new InsertBuilder(this); + } + + public UpdateBuilder update(Table table) throws DatabaseError { + ensure_init(); + return new UpdateBuilder(this, table); + } + + public UpdateBuilder update_named(string table) throws DatabaseError { + ensure_init(); + return new UpdateBuilder.for_name(this, table); + } + + public DeleteBuilder delete() throws DatabaseError { + ensure_init(); + return new DeleteBuilder(this); + } + + public Row.RowIterator query_sql(string sql, string[]? args = null) throws DatabaseError { + ensure_init(); + return new Row.RowIterator(this, sql, args); + } + + public Statement prepare(string sql) throws DatabaseError { + ensure_init(); + if (debug) print(@"prepare: $sql\n"); + Sqlite.Statement statement; + if (db.prepare_v2(sql, sql.length, out statement) != OK) { + throw new DatabaseError.PREPARE_ERROR(@"SQLite error: $(db.errcode()) - $(db.errmsg())"); + } + return statement; + } + + public void exec(string sql) throws DatabaseError { + ensure_init(); + if (db.exec(sql) != OK) { + throw new DatabaseError.EXEC_ERROR(@"SQLite error: $(db.errcode()) - $(db.errmsg())"); + } + } + + public bool is_known_column(string table, string field) throws DatabaseError { + ensure_init(); + foreach (Table t in tables) { + if (t.is_known_column(field)) return true; + } + return false; + } +} + +} \ No newline at end of file diff --git a/qlite/src/delete_builder.vala b/qlite/src/delete_builder.vala new file mode 100644 index 00000000..5999dc40 --- /dev/null +++ b/qlite/src/delete_builder.vala @@ -0,0 +1,75 @@ +using Sqlite; + +namespace Qlite { + +public class DeleteBuilder : StatementBuilder { + + // DELETE FROM [...] + private Table table; + private string table_name; + + // WHERE [...] + private string selection; + private StatementBuilder.Field[] selection_args; + + protected DeleteBuilder(Database db) { + base(db); + } + + public DeleteBuilder from(Table table) { + if (table != null) throw new DatabaseError.ILLEGAL_QUERY("cannot use from() multiple times."); + this.table = table; + this.table_name = table.name; + return this; + } + + public DeleteBuilder from_name(string table) { + this.table_name = table; + return this; + } + + public DeleteBuilder where(string selection, string[]? selection_args = null) { + if (selection != null) throw new DatabaseError.ILLEGAL_QUERY("selection was already done, but where() was called."); + this.selection = selection; + if (selection_args != null) { + this.selection_args = new StatementBuilder.Field[selection_args.length]; + for (int i = 0; i < selection_args.length; i++) { + this.selection_args[i] = new StatementBuilder.StringField(selection_args[i]); + } + } + return this; + } + + public DeleteBuilder with(Column column, string comp, T value) { + if (selection == null) { + selection = @"$(column.name) $comp ?"; + selection_args = { new StatementBuilder.Field(column, value) }; + } else { + selection = @"($selection) AND $(column.name) $comp ?"; + StatementBuilder.Field[] selection_args_new = new StatementBuilder.Field[selection_args.length+1]; + for (int i = 0; i < selection_args.length; i++) { + selection_args_new[i] = selection_args[i]; + } + selection_args_new[selection_args.length] = new Field(column, value); + selection_args = selection_args_new; + } + return this; + } + + public override Statement prepare() { + Statement stmt = db.prepare(@"DELETE FROM $table_name $(selection != null ? @"WHERE $selection": "")"); + for (int i = 0; i < selection_args.length; i++) { + selection_args[i].bind(stmt, i+1); + } + return stmt; + } + + public void perform() { + if (prepare().step() != DONE) { + throw new DatabaseError.EXEC_ERROR(@"SQLite error: $(db.errcode()) - $(db.errmsg())"); + } + } + +} + +} \ No newline at end of file diff --git a/qlite/src/insert_builder.vala b/qlite/src/insert_builder.vala new file mode 100644 index 00000000..654935a6 --- /dev/null +++ b/qlite/src/insert_builder.vala @@ -0,0 +1,102 @@ +using Sqlite; + +namespace Qlite { + +public class InsertBuilder : StatementBuilder { + + // INSERT [OR ...] + private bool replace_val; + private string or_val; + + // INTO [...] + private Table table; + private string table_name; + + // VALUES [...] + private StatementBuilder.Field[] fields; + + protected InsertBuilder(Database db) { + base(db); + } + + public InsertBuilder replace() { + this.replace_val = true; + return this; + } + + public InsertBuilder or(string or) { + this.or_val = or; + return this; + } + + public InsertBuilder into(Table table) { + this.table = table; + this.table_name = table.name; + return this; + } + + public InsertBuilder into_name(string table) { + this.table_name = table; + return this; + } + + public InsertBuilder value(Column column, T value) { + if (fields == null) { + fields = { new StatementBuilder.Field(column, value) }; + } else { + StatementBuilder.Field[] fields_new = new StatementBuilder.Field[fields.length+1]; + for (int i = 0; i < fields.length; i++) { + fields_new[i] = fields[i]; + } + fields_new[fields.length] = new Field(column, value); + fields = fields_new; + } + return this; + } + + public InsertBuilder value_null(Column column) { + if (column.not_null) throw new DatabaseError.ILLEGAL_QUERY(@"Can't set non-null column $(column.name) to null"); + if (fields == null) { + fields = { new NullField(column) }; + } else { + StatementBuilder.Field[] fields_new = new StatementBuilder.Field[fields.length+1]; + for (int i = 0; i < fields.length; i++) { + fields_new[i] = fields[i]; + } + fields_new[fields.length] = new NullField(column); + fields = fields_new; + } + return this; + } + + public override Statement prepare() throws DatabaseError { + string fields_text = ""; + string value_qs = ""; + for (int i = 0; i < fields.length; i++) { + if (i != 0) { + value_qs += ", "; + fields_text += ", "; + } + fields_text += fields[i].column.name; + value_qs += "?"; + } + string sql = replace_val ? "REPLACE" : "INSERT"; + if (!replace_val && or_val != null) sql += @" OR $or_val"; + sql += @" INTO $table_name ( $fields_text ) VALUES ($value_qs)"; + Statement stmt = db.prepare(sql); + for (int i = 0; i < fields.length; i++) { + fields[i].bind(stmt, i+1); + } + return stmt; + } + + public int64 perform() throws DatabaseError { + if (prepare().step() != DONE) { + throw new DatabaseError.EXEC_ERROR(@"SQLite error: $(db.errcode()) - $(db.errmsg())"); + } + return db.last_insert_rowid(); + } + +} + +} \ No newline at end of file diff --git a/qlite/src/query_builder.vala b/qlite/src/query_builder.vala new file mode 100644 index 00000000..0c9f4d98 --- /dev/null +++ b/qlite/src/query_builder.vala @@ -0,0 +1,196 @@ +using Sqlite; + +namespace Qlite { + +public class QueryBuilder : StatementBuilder { + private bool finished; + private bool single_result; + + // SELECT [...] + private string column_selector = "*"; + private Column[] columns; + + // FROM [...] + private Table table; + private string table_name; + + // WHERE [...] + private string selection; + private StatementBuilder.Field[] selection_args; + + // ORDER BY [...] + private OrderingTerm[] order_by_terms; + + // LIMIT [...] + private int limit_val; + + private Row[] result; + + protected QueryBuilder(Database db) { + base(db); + } + + public QueryBuilder select(Column[]? columns = null) { + this.columns = columns; + if (columns != null) { + for (int i = 0; i < columns.length; i++) { + if (column_selector == "*") { + column_selector = columns[0].name; + } else { + column_selector += ", " + columns[i].name; + } + } + } else { + column_selector = "*"; + } + return this; + } + + public QueryBuilder select_string(string column_selector) { + this.columns = null; + this.column_selector = column_selector; + return this; + } + + public QueryBuilder from(Table table) throws DatabaseError { + if (this.table_name != null) throw new DatabaseError.ILLEGAL_QUERY("cannot use from() multiple times."); + this.table = table; + this.table_name = table.name; + return this; + } + + public QueryBuilder from_name(string table) { + this.table_name = table; + return this; + } + + public QueryBuilder where(string selection, string[]? selection_args = null) throws DatabaseError { + if (this.selection != null) throw new DatabaseError.ILLEGAL_QUERY("selection was already done, but where() was called."); + this.selection = selection; + if (selection_args != null) { + this.selection_args = new StatementBuilder.Field[selection_args.length]; + for (int i = 0; i < selection_args.length; i++) { + this.selection_args[i] = new StatementBuilder.StringField(selection_args[i]); + } + } + return this; + } + + public QueryBuilder with(Column column, string comp, T value) { + if ((column.unique || column.primary_key) && comp == "=") single_result = true; + if (selection == null) { + selection = @"$(column.name) $comp ?"; + selection_args = { new StatementBuilder.Field(column, value) }; + } else { + selection = @"($selection) AND $(column.name) $comp ?"; + StatementBuilder.Field[] selection_args_new = new StatementBuilder.Field[selection_args.length+1]; + for (int i = 0; i < selection_args.length; i++) { + selection_args_new[i] = selection_args[i]; + } + selection_args_new[selection_args.length] = new Field(column, value); + selection_args = selection_args_new; + } + return this; + } + + public QueryBuilder with_null(Column column) { + selection = @"($selection) AND $(column.name) ISNULL"; + return this; + } + + public QueryBuilder without_null(Column column) { + selection = @"($selection) AND $(column.name) NOT NULL"; + return this; + } + + private void add_order_by(OrderingTerm term) { + if (order_by_terms == null) { + order_by_terms = { term }; + } else { + OrderingTerm[] order_by_terms_new = new OrderingTerm[order_by_terms.length+1]; + for (int i = 0; i < order_by_terms.length; i++) { + order_by_terms_new[i] = order_by_terms[i]; + } + order_by_terms_new[order_by_terms.length] = term; + order_by_terms = order_by_terms_new; + } + } + + public QueryBuilder order_by(Column column, string dir = "ASC") { + add_order_by(new OrderingTerm(column, dir)); + return this; + } + + public QueryBuilder order_by_name(string name, string dir) { + add_order_by(new OrderingTerm.by_name(name, dir)); + return this; + } + + public QueryBuilder limit(int limit) { + this.limit_val = limit; + return this; + } + + public int64 count() throws DatabaseError { + this.column_selector = @"COUNT($column_selector) AS count"; + this.single_result = true; + return row().get_integer("count"); + } + + public Row? row() throws DatabaseError { + if (!single_result) throw new DatabaseError.NON_UNIQUE("query is not suited to return a single row, but row() was called."); + return iterator().next_value(); + } + + public T get(Column field) throws DatabaseError { + Row row = row(); + if (row != null) { + return row[field]; + } + return null; + } + + public override Statement prepare() throws DatabaseError { + Statement stmt = db.prepare(@"SELECT $column_selector FROM $table_name $(selection != null ? @"WHERE $selection" : "") $(order_by_terms != null ? OrderingTerm.all_to_string(order_by_terms) : "") $(limit_val > 0 ? @" LIMIT $limit_val" : "")"); + for (int i = 0; i < selection_args.length; i++) { + selection_args[i].bind(stmt, i+1); + } + return stmt; + } + + public Row.RowIterator iterator() throws DatabaseError { + return new Row.RowIterator.from_query_builder(this); + } + + class OrderingTerm { + Column column; + string column_name; + string dir; + + public OrderingTerm(Column column, string dir) { + this.column = column; + this.column_name = column.name; + this.dir = dir; + } + + public OrderingTerm.by_name(string column_name, string dir) { + this.column_name = column_name; + this.dir = dir; + } + + public string to_string() { + return @"$column_name $dir"; + } + + public static string all_to_string(OrderingTerm[] terms) { + if (terms.length == 0) return ""; + string res = "ORDER BY "+terms[0].to_string(); + for (int i = 1; i < terms.length; i++) { + res += @", $(terms[i])"; + } + return res; + } + } +} + +} \ No newline at end of file diff --git a/qlite/src/row.vala b/qlite/src/row.vala new file mode 100644 index 00000000..905d12a1 --- /dev/null +++ b/qlite/src/row.vala @@ -0,0 +1,79 @@ +using Gee; +using Sqlite; + +namespace Qlite { + +public class Row { + private Map text_map = new HashMap(); + private Map int_map = new HashMap(); + private Map real_map = new HashMap(); + + public Row(Statement stmt) { + for (int i = 0; i < stmt.column_count(); i++) { + switch(stmt.column_type(i)) { + case TEXT: + text_map[stmt.column_name(i)] = stmt.column_text(i); + break; + case INTEGER: + int_map[stmt.column_name(i)] = (long) stmt.column_int64(i); + break; + case FLOAT: + real_map[stmt.column_name(i)] = stmt.column_double(i); + break; + } + } + } + + public T get(Column field) { + return field[this]; + } + + public string? get_text(string field) { + if (text_map.contains(field)) { + return text_map[field]; + } + return null; + } + + public long get_integer(string field) { + return int_map[field]; + } + + public bool has_integer(string field) { + return int_map.contains(field); + } + + public double get_real(string field) { + return real_map[field]; + } + + public bool has_real(string field) { + return real_map.contains(field) && real_map[field] != null; + } + + public class RowIterator { + private Statement stmt; + + public RowIterator.from_query_builder(QueryBuilder query) throws DatabaseError { + this.stmt = query.prepare(); + } + + public RowIterator(Database db, string sql, string[]? args = null) { + this.stmt = db.prepare(sql); + if (args != null) { + for (int i = 0; i < args.length; i++) { + stmt.bind_text(i, sql, sql.length); + } + } + } + + public Row? next_value() { + if (stmt.step() == Sqlite.ROW) { + return new Row(stmt); + } + return null; + } + } +} + +} \ No newline at end of file diff --git a/qlite/src/statement_builder.vala b/qlite/src/statement_builder.vala new file mode 100644 index 00000000..8df069dd --- /dev/null +++ b/qlite/src/statement_builder.vala @@ -0,0 +1,53 @@ +using Sqlite; + +namespace Qlite { + +public abstract class StatementBuilder { + protected Database db; + + public StatementBuilder(Database db) { + this.db = db; + } + + public abstract Statement prepare() throws DatabaseError; + + protected class Field { + public T value; + public Column? column; + + public Field(Column? column, T value) { + this.column = column; + this.value = value; + } + + public virtual void bind(Statement stmt, int index) { + if (column != null) { + column.bind(stmt, index, value); + } else { + throw new DatabaseError.NOT_SUPPORTED("binding was not implemented for this field."); + } + } + } + + protected class NullField : Field { + public NullField(Column? column) { + base(column, null); + } + + public override void bind(Statement stmt, int index) { + stmt.bind_null(index); + } + } + + protected class StringField : Field { + public StringField(string value) { + base(null, value); + } + + public override void bind(Statement stmt, int index) { + stmt.bind_text(index, value); + } + } +} + +} \ No newline at end of file diff --git a/qlite/src/table.vala b/qlite/src/table.vala new file mode 100644 index 00000000..209a5a96 --- /dev/null +++ b/qlite/src/table.vala @@ -0,0 +1,84 @@ +using Sqlite; + +namespace Qlite { + +public class Table { + protected Database db; + public string name { get; private set; } + protected Column[] columns; + + public Table(Database db, string name) { + this.db = db; + this.name = name; + } + + public void init(Column[] columns) { + this.columns = columns; + } + + private void ensure_init() throws DatabaseError { + if (columns == null) throw new DatabaseError.NOT_INITIALIZED(@"Table $name was not initialized, call init()"); + } + + public QueryBuilder select(Column[]? columns = null) throws DatabaseError { + ensure_init(); + return db.select(columns).from(this); + } + + public InsertBuilder insert() throws DatabaseError { + ensure_init(); + return db.insert().into(this); + } + + public UpdateBuilder update() throws DatabaseError { + ensure_init(); + return db.update(this); + } + + public DeleteBuilder delete() throws DatabaseError { + ensure_init(); + return db.delete().from(this); + } + + public Row? row_with(Column column, T value) throws DatabaseError { + ensure_init(); + if (!column.unique && !column.primary_key) throw new DatabaseError.NON_UNIQUE(@"$(column.name) is not suited to identify a row, but used with row_with()"); + return select().with(column, "=", value).row(); + } + + public bool is_known_column(string column) throws DatabaseError { + ensure_init(); + foreach (Column c in columns) { + if (c.name == column) return true; + } + return false; + } + + public void create_table_at_version(long version) throws DatabaseError { + ensure_init(); + string sql = @"CREATE TABLE IF NOT EXISTS $name ("; + for(int i = 0; i < columns.length; i++) { + Column c = columns[i]; + if (c.min_version <= version && c.max_version >= version) { + sql += @"$(i > 0 ? "," : "") $c"; + } + } + sql += ")"; + db.exec(sql); + } + + public void add_columns_for_version(long old_version, long new_version) throws DatabaseError { + ensure_init(); + foreach (Column c in columns) { + if (c.min_version <= new_version && c.max_version >= new_version && c.min_version > old_version && c.max_version < old_version) { + db.exec(@"ALTER TABLE $name ADD COLUMN $c"); + } + } + } + + public void delete_columns_for_version(long old_version, long new_version) throws DatabaseError { + // TODO: Rename old table, create table at new_version, transfer data + } +} + +} \ No newline at end of file diff --git a/qlite/src/update_builder.vala b/qlite/src/update_builder.vala new file mode 100644 index 00000000..f6729772 --- /dev/null +++ b/qlite/src/update_builder.vala @@ -0,0 +1,133 @@ +using Sqlite; + +namespace Qlite { + +public class UpdateBuilder : StatementBuilder { + + // UPDATE [OR ...] + private string or_val; + + // [...] + private Table table; + private string table_name; + + // SET [...] + private StatementBuilder.Field[] fields; + + // WHERE [...] + private string selection; + private StatementBuilder.Field[] selection_args; + + protected UpdateBuilder(Database db, Table table) { + base(db); + this.table = table; + this.table_name = table.name; + } + + internal UpdateBuilder.for_name(Database db, string table) { + base(db); + this.table_name = table; + } + + public UpdateBuilder or(string or) { + this.or_val = or; + return this; + } + + public UpdateBuilder set(Column column, T value) { + if (fields == null) { + fields = { new StatementBuilder.Field(column, value) }; + } else { + StatementBuilder.Field[] fields_new = new StatementBuilder.Field[fields.length+1]; + for (int i = 0; i < fields.length; i++) { + fields_new[i] = fields[i]; + } + fields_new[fields.length] = new Field(column, value); + fields = fields_new; + } + return this; + } + + public UpdateBuilder set_null(Column column) { + if (column.not_null) throw new DatabaseError.ILLEGAL_QUERY(@"Can't set non-null column $(column.name) to null"); + if (fields == null) { + fields = { new NullField(column) }; + } else { + StatementBuilder.Field[] fields_new = new StatementBuilder.Field[fields.length+1]; + for (int i = 0; i < fields.length; i++) { + fields_new[i] = fields[i]; + } + fields_new[fields.length] = new NullField(column); + fields = fields_new; + } + return this; + } + + public UpdateBuilder where(string selection, string[]? selection_args = null) { + if (selection != null) throw new DatabaseError.ILLEGAL_QUERY("selection was already done, but where() was called."); + this.selection = selection; + if (selection_args != null) { + this.selection_args = new StatementBuilder.Field[selection_args.length]; + for (int i = 0; i < selection_args.length; i++) { + this.selection_args[i] = new StatementBuilder.StringField(selection_args[i]); + } + } + return this; + } + + public UpdateBuilder with(Column column, string comp, T value) { + if (selection == null) { + selection = @"$(column.name) $comp ?"; + selection_args = { new StatementBuilder.Field(column, value) }; + } else { + selection = @"($selection) AND $(column.name) $comp ?"; + StatementBuilder.Field[] selection_args_new = new StatementBuilder.Field[selection_args.length+1]; + for (int i = 0; i < selection_args.length; i++) { + selection_args_new[i] = selection_args[i]; + } + selection_args_new[selection_args.length] = new Field(column, value); + selection_args = selection_args_new; + } + return this; + } + + public UpdateBuilder with_null(Column column) { + selection = @"($selection) AND $(column.name) ISNULL"; + return this; + } + + public UpdateBuilder without_null(Column column) { + selection = @"($selection) AND $(column.name) NOT NULL"; + return this; + } + + public override Statement prepare() throws DatabaseError { + string sql = "UPDATE"; + if (or_val != null) sql += @" OR $or_val"; + sql += @" $table_name SET "; + for (int i = 0; i < fields.length; i++) { + if (i != 0) { + sql += ", "; + } + sql += @"$(fields[i].column.name) = ?"; + } + sql += @" WHERE $selection"; + Statement stmt = db.prepare(sql); + for (int i = 0; i < fields.length; i++) { + fields[i].bind(stmt, i+1); + } + for (int i = 0; i < selection_args.length; i++) { + selection_args[i].bind(stmt, i + fields.length + 1); + } + return stmt; + } + + public void perform() throws DatabaseError { + if (prepare().step() != DONE) { + throw new DatabaseError.EXEC_ERROR(@"SQLite error: $(db.errcode()) - $(db.errmsg())"); + } + } + +} + +} \ No newline at end of file diff --git a/vala-xmpp/CMakeLists.txt b/vala-xmpp/CMakeLists.txt new file mode 100644 index 00000000..85b154da --- /dev/null +++ b/vala-xmpp/CMakeLists.txt @@ -0,0 +1,90 @@ +find_package(Vala REQUIRED) +find_package(PkgConfig REQUIRED) +find_package(GPGME REQUIRED) +find_package(LIBUUID REQUIRED) +include(GlibCompileResourcesSupport) +include(${VALA_USE_FILE}) + +set(ENGINE_PACKAGES + gee-0.8 + gio-2.0 + glib-2.0 + gtk+-3.0 +) + +pkg_check_modules(ENGINE REQUIRED ${ENGINE_PACKAGES}) + +vala_precompile(ENGINE_VALA_C +SOURCES + "src/core/namespace_state.vala" + "src/core/stanza_attribute.vala" + "src/core/stanza_node.vala" + "src/core/stanza_reader.vala" + "src/core/stanza_writer.vala" + "src/core/xmpp_stream.vala" + + "src/module/bind.vala" + "src/module/iq/module.vala" + "src/module/iq/stanza.vala" + "src/module/message/module.vala" + "src/module/message/stanza.vala" + "src/module/presence/flag.vala" + "src/module/presence/module.vala" + "src/module/presence/stanza.vala" + "src/module/roster/flag.vala" + "src/module/roster/item.vala" + "src/module/roster/module.vala" + "src/module/sasl.vala" + "src/module/stanza.vala" + "src/module/stanza_error.vala" + "src/module/stream_error.vala" + "src/module/tls.vala" + "src/module/util.vala" + + "src/module/xep/0027_pgp/flag.vala" + "src/module/xep/0027_pgp/module.vala" + "src/module/xep/0030_service_discovery/flag.vala" + "src/module/xep/0030_service_discovery/info_result.vala" + "src/module/xep/0030_service_discovery/items_result.vala" + "src/module/xep/0030_service_discovery/module.vala" + "src/module/xep/0045_muc/flag.vala" + "src/module/xep/0045_muc/module.vala" + "src/module/xep/0048_bookmarks/module.vala" + "src/module/xep/0048_bookmarks/conference.vala" + "src/module/xep/0049_private_xml_storage.vala" + "src/module/xep/0054_vcard/module.vala" + "src/module/xep/0060_pubsub.vala" + "src/module/xep/0084_user_avatars.vala" + "src/module/xep/0085_chat_state_notifications.vala" + "src/module/xep/0115_entitiy_capabilities.vala" + "src/module/xep/0199_ping.vala" + "src/module/xep/0184_message_delivery_receipts.vala" + "src/module/xep/0203_delayed_delivery.vala" + "src/module/xep/0280_message_carbons.vala" + "src/module/xep/0333_chat_markers.vala" + "src/module/xep/pixbuf_storage.vala" +PACKAGES + ${ENGINE_PACKAGES} + gpgme + uuid +GENERATE_VAPI + vala-xmpp +GENERATE_HEADER + vala-xmpp +OPTIONS + --target-glib=2.38 + -g + --thread + --vapidir=${CMAKE_SOURCE_DIR}/vapi +) + +set(CFLAGS ${ENGINE_CFLAGS} ${GPGME_CFLAGS} ${LIBUUID_CFLAGS} -g ${VALA_CFLAGS}) +add_definitions(${CFLAGS}) +add_library(vala-xmpp SHARED ${ENGINE_VALA_C}) +target_link_libraries(vala-xmpp ${ENGINE_LIBRARIES} ${GPGME_LIBRARIES} ${LIBUUID_LIBRARIES}) + +add_custom_target(vala-xmpp-vapi +DEPENDS + ${CMAKE_BINARY_DIR}/vala-xmpp/vala-xmpp.vapi +) + diff --git a/vala-xmpp/src/core/namespace_state.vala b/vala-xmpp/src/core/namespace_state.vala new file mode 100644 index 00000000..e71607fa --- /dev/null +++ b/vala-xmpp/src/core/namespace_state.vala @@ -0,0 +1,80 @@ +using Gee; + +namespace Xmpp.Core { +public class NamespaceState { + private HashMap uri_to_name = new HashMap (); + private HashMap name_to_uri = new HashMap (); + public string current_ns_uri; + + public NamespaceState () { + add_assoc(XMLNS_URI, "xmlns"); + add_assoc("http://www.w3.org/XML/1998/namespace", "xml"); + current_ns_uri = "http://www.w3.org/XML/1998/namespace"; + } + + public NamespaceState.for_stanza () { + this(); + add_assoc("http://etherx.jabber.org/streams", "stream"); + current_ns_uri = "jabber:client"; + } + + public NamespaceState.copy (NamespaceState old) { + foreach (string key in old.uri_to_name.keys) { + add_assoc(key, old.uri_to_name[key]); + } + set_current(old.current_ns_uri); + } + + public NamespaceState.with_assoc (NamespaceState old, string ns_uri, string name) { + this.copy(old); + add_assoc(ns_uri, name); + } + + public NamespaceState.with_current (NamespaceState old, string current_ns_uri) { + this.copy(old); + set_current(current_ns_uri); + } + + public void add_assoc (string ns_uri, string name) { + name_to_uri[name] = ns_uri; + uri_to_name[ns_uri] = name; + } + + public void set_current (string current_ns_uri) { + this.current_ns_uri = current_ns_uri; + } + + public string find_name (string ns_uri) throws XmlError { + if (uri_to_name.has_key(ns_uri)) { + return uri_to_name[ns_uri]; + } + throw new XmlError.NS_DICT_ERROR(@"NS URI $ns_uri not found."); + } + + public string find_uri (string name) throws XmlError { + if (name_to_uri.has_key(name)) { + return name_to_uri[name]; + } + throw new XmlError.NS_DICT_ERROR(@"NS name $name not found."); + } + + public NamespaceState clone() { + return new NamespaceState.copy(this); + } + + public string to_string () { + StringBuilder sb = new StringBuilder (); + sb.append ("NamespaceState{"); + foreach (string key in uri_to_name.keys) { + sb.append(key); + sb.append_c('='); + sb.append(uri_to_name[key]); + sb.append_c(','); + } + sb.append("current="); + sb.append(current_ns_uri); + sb.append_c('}'); + return sb.str; + } +} +} \ No newline at end of file diff --git a/vala-xmpp/src/core/stanza_attribute.vala b/vala-xmpp/src/core/stanza_attribute.vala new file mode 100644 index 00000000..3169e90e --- /dev/null +++ b/vala-xmpp/src/core/stanza_attribute.vala @@ -0,0 +1,29 @@ +namespace Xmpp.Core { +public class StanzaAttribute : StanzaEntry { + + public StanzaAttribute() {} + + public StanzaAttribute.build(string ns_uri, string name, string val) { + this.ns_uri = ns_uri; + this.name = name; + this.val = val; + } + + public string to_string() { + if (ns_uri == null) { + return @"$name='$val'"; + } else { + return @"{$ns_uri}:$name='$val'"; + } + } + + public string to_xml(NamespaceState? state_) throws XmlError { + NamespaceState state = state_ ?? new NamespaceState(); + if (ns_uri == state.current_ns_uri || (ns_uri == XMLNS_URI && name == "xmlns")) { + return @"$name='$val'"; + } else { + return "%s:%s='%s'".printf (state.find_name (ns_uri), name, val); + } + } +} +} diff --git a/vala-xmpp/src/core/stanza_node.vala b/vala-xmpp/src/core/stanza_node.vala new file mode 100644 index 00000000..1dfacfdd --- /dev/null +++ b/vala-xmpp/src/core/stanza_node.vala @@ -0,0 +1,297 @@ +using Gee; + +namespace Xmpp.Core { + +public abstract class StanzaEntry { + public string? ns_uri; + public string name; + public string? val; + + public string encoded_val { + owned get { + return val.replace("&", "&").replace("\"", """).replace("'", "'").replace("<", "<").replace(">", ">"); + } + set { + string tmp = value.replace(">", ">").replace("<", "<").replace("'","'").replace(""","\""); + while (tmp.contains("&#")) { + int start = tmp.index_of("&#"); + int end = tmp.index_of(";", start); + if (end < start) break; + unichar num = -1; + if (tmp[start+2]=='x') { + tmp.substring(start+3, start-end-3).scanf("%x", &num); + } else { + num = int.parse(tmp.substring(start+2, start-end-2)); + } + tmp = tmp.splice(start, end, num.to_string()); + } + val = tmp.replace("&", "&"); + } + } + + public virtual unowned string? get_string_content() { + return val; + } +} + +public class NoStanza : StanzaEntry { + public NoStanza(string? name) { + this.name = name; + } +} + +public class StanzaNode : StanzaEntry { + public ArrayList sub_nodes = new ArrayList(); + public ArrayList attributes = new ArrayList(); + public bool has_nodes = false; + public bool pseudo = false; + + public StanzaNode () {} + + public StanzaNode.build(string name, string ns_uri = "jabber:client", ArrayList? nodes = null, ArrayList? attrs = null) { + this.ns_uri = ns_uri; + this.name = name; + if (nodes != null) this.sub_nodes.add_all(nodes); + if (attrs != null) this.attributes.add_all(attrs); + } + + public StanzaNode.text(string text) { + this.name = "#text"; + this.val = text; + } + + public StanzaNode.encoded_text(string text) { + this.name = "#text"; + this.encoded_val = text; + } + + public StanzaNode add_self_xmlns() { + return put_attribute("xmlns", ns_uri); + } + + public unowned string? get_attribute(string name, string? ns_uri = null) { + string _name = name; + string? _ns_uri = ns_uri; + if (_ns_uri == null) { + if (_name.contains(":")) { + var lastIndex = _name.last_index_of_char(':'); + _ns_uri = _name.substring(0, lastIndex); + _name = _name.substring(lastIndex + 1); + } else { + _ns_uri = this.ns_uri; + } + } + foreach(var attr in attributes) { + if (attr.ns_uri == _ns_uri && attr.name == _name) return attr.val; + } + return null; + } + + public StanzaAttribute get_attribute_raw(string name, string? ns_uri = null) { + string _name = name; + string? _ns_uri = ns_uri; + if (_ns_uri == null) { + if (_name.contains(":")) { + var lastIndex = _name.last_index_of_char(':'); + _ns_uri = _name.substring(0, lastIndex); + _name = _name.substring(lastIndex + 1); + } else { + _ns_uri = this.ns_uri; + } + } + foreach(var attr in attributes) { + if (attr.ns_uri == _ns_uri && attr.name == _name) return attr; + } + return null; + } + + public ArrayList get_attributes_by_ns_uri(string ns_uri) { + ArrayList ret = new ArrayList (); + foreach(var attr in attributes) { + if (attr.ns_uri == ns_uri) ret.add(attr); + } + return ret; + } + + public StanzaEntry get(...) { + va_list l = va_list(); + StanzaEntry? res = get_deep_attribute_(va_list.copy(l)); + if (res != null) return res; + res = get_deep_subnode_(va_list.copy(l)); + if (res != null) return res; + return new NoStanza("-"); + } + + public unowned string? get_deep_attribute(...) { + va_list l = va_list(); + var res = get_deep_attribute_(va_list.copy(l)); + if (res == null) return null; + return res.val; + } + + public StanzaAttribute? get_deep_attribute_(va_list l) { + StanzaNode? node = this; + string? attribute_name = l.arg(); + if (attribute_name == null) return null; + while(true) { + string? s = l.arg(); + if (s == null) break; + node = get_subnode(attribute_name); + if (node == null) return null; + attribute_name = s; + } + return node.get_attribute_raw(attribute_name); + } + + public StanzaNode? get_subnode(string name, string? ns_uri = null, bool recurse = false) { + string _name = name; + string? _ns_uri = ns_uri; + if (ns_uri == null) { + if (_name.contains(":")) { + var lastIndex = _name.last_index_of_char(':'); + _ns_uri = _name.substring(0, lastIndex); + _name = _name.substring(lastIndex + 1); + } else { + _ns_uri = this.ns_uri; + } + } + foreach(var node in sub_nodes) { + if (node.ns_uri == _ns_uri && node.name == _name) return node; + if (recurse) { + var x = node.get_subnode(_name, _ns_uri, recurse); + if (x != null) return x; + } + } + return null; + } + + public ArrayList get_subnodes(string name, string? ns_uri = null, bool recurse = false) { + ArrayList ret = new ArrayList(); + if (ns_uri == null) ns_uri = this.ns_uri; + foreach(var node in sub_nodes) { + if (node.ns_uri == ns_uri && node.name == name) ret.add(node); + if (recurse) { + ret.add_all(node.get_subnodes(name, ns_uri, recurse)); + } + } + return ret; + } + + public StanzaNode? get_deep_subnode(...) { + va_list l = va_list(); + return get_deep_subnode_(va_list.copy(l)); + } + + public StanzaNode? get_deep_subnode_(va_list l) { + StanzaNode? node = this; + while(true) { + string? s = l.arg(); + if (s == null) break; + node = get_subnode(s); + } + return node; + } + + public ArrayList get_all_subnodes() { + return sub_nodes; + } + + public void add_attribute(StanzaAttribute attr) { + attributes.add(attr); + } + + public override unowned string? get_string_content() { + if (val != null) return val; + if (sub_nodes.size == 1) return sub_nodes[0].get_string_content(); + return null; + } + + public StanzaNode put_attribute(string name, string val, string? ns_uri = null) { + if (name == "xmlns") ns_uri = XMLNS_URI; + if (ns_uri == null) ns_uri = this.ns_uri; + attributes.add(new StanzaAttribute.build(ns_uri, name, val)); + return this; + } + + /** + * Set only occurence + **/ + public void set_attribute(string name, string val, string? ns_uri = null) { + if (ns_uri == null) ns_uri = this.ns_uri; + foreach(var attr in attributes) { + if (attr.ns_uri == ns_uri && attr.name == name) { + attr.val = val; + return; + } + } + put_attribute(name, val, ns_uri); + } + + public StanzaNode put_node(StanzaNode node) { + sub_nodes.add(node); + return this; + } + + public string to_string(int i = 0) { + string indent = string.nfill (i * 2, ' '); + if (name == "#text") { + return @"$indent$val\n"; + } + var sb = new StringBuilder(); + sb.append(@"$indent<{$ns_uri}:$name"); + foreach (StanzaAttribute attr in attributes) { + sb.append_printf(" %s", attr.to_string()); + } + if (!has_nodes && sub_nodes.size == 0) { + sb.append(" />\n"); + } else { + sb.append(">\n"); + if (sub_nodes.size != 0) { + foreach (StanzaNode subnode in sub_nodes) { + sb.append(subnode.to_string(i+1)); + } + sb.append(@"$indent\n"); + } + } + return sb.str; + } + + public string to_xml (NamespaceState? state = null) throws XmlError { + NamespaceState my_state = state ?? new NamespaceState.for_stanza(); + if (name == "#text") return @"$encoded_val"; + foreach(var xmlns in get_attributes_by_ns_uri (XMLNS_URI)) { + if (xmlns.name == "xmlns") { + my_state = new NamespaceState.with_current(my_state, xmlns.val); + } else { + my_state = new NamespaceState.with_assoc(my_state, xmlns.val, xmlns.name); + } + } + var sb = new StringBuilder(); + if (ns_uri == my_state.current_ns_uri) { + sb.append(@"<$name"); + } else { + sb.append_printf("<%s:%s", my_state.find_name (ns_uri), name); + } + var attr_ns_state = new NamespaceState.with_current(my_state, ns_uri); + foreach (StanzaAttribute attr in attributes) { + sb.append_printf(" %s", attr.to_xml(attr_ns_state)); + } + if (!has_nodes && sub_nodes.size == 0) { + sb.append("/>"); + } else { + sb.append(">"); + if (sub_nodes.size != 0) { + foreach (StanzaNode subnode in sub_nodes) { + sb.append(subnode.to_xml(my_state)); + } + if (ns_uri == my_state.current_ns_uri) { + sb.append(@""); + } else { + sb.append_printf("", my_state.find_name (ns_uri), name); + } + } + } + return sb.str; + } +} +} diff --git a/vala-xmpp/src/core/stanza_reader.vala b/vala-xmpp/src/core/stanza_reader.vala new file mode 100644 index 00000000..0a90f855 --- /dev/null +++ b/vala-xmpp/src/core/stanza_reader.vala @@ -0,0 +1,260 @@ +using Gee; + +namespace Xmpp.Core { +public const string XMLNS_URI = "http://www.w3.org/2000/xmlns/"; +public const string JABBER_URI = "jabber:client"; + +public errordomain XmlError { + XML_ERROR, + NS_DICT_ERROR, + UNSUPPORTED, + EOF, + BAD_XML, + IO_ERROR +} + +public class StanzaReader { + private static int BUFFER_MAX = 4096; + + private InputStream? input; + private uint8[] buffer; + private int buffer_fill = 0; + private int buffer_pos = 0; + private Cancellable cancellable = new Cancellable(); + + private NamespaceState ns_state = new NamespaceState(); + + public StanzaReader.for_buffer(uint8[] buffer) { + this.buffer = buffer; + this.buffer_fill = buffer.length; + } + + public StanzaReader.for_string(string s) { + this.for_buffer(s.data); + } + + public StanzaReader.for_stream(InputStream input) { + this.input = input; + buffer = new uint8[BUFFER_MAX]; + } + + public void cancel() { + cancellable.cancel(); + } + + private void update_buffer() throws XmlError { + try { + if (input == null) throw new XmlError.EOF("No input stream specified and end of buffer reached."); + if (cancellable.is_cancelled()) throw new XmlError.EOF("Input stream is canceled."); + buffer_fill = (int) input.read(buffer, cancellable); + if (buffer_fill == 0) throw new XmlError.EOF("End of input stream reached."); + buffer_pos = 0; + } catch (GLib.IOError e) { + throw new XmlError.IO_ERROR("IOError in GLib: %s".printf(e.message)); + } + } + + private char read_single() throws XmlError { + if (buffer_pos >= buffer_fill) { + update_buffer(); + } + return (char) buffer[buffer_pos++]; + } + + private char peek_single() throws XmlError { + var res = read_single(); + buffer_pos--; + return res; + } + + private bool is_ws(uint8 what) { + return what == ' ' || what == '\t' || what == '\r' || what == '\n'; + } + + private void skip_single() { + buffer_pos++; + } + + private void skip_until_non_ws() throws XmlError { + while (is_ws(peek_single())) { + skip_single(); + } + } + + private string read_until_ws() throws XmlError { + var res = new StringBuilder(); + var what = peek_single(); + while(!is_ws(what)) { + res.append_c(read_single()); + what = peek_single(); + } + return res.str; + } + + private string read_until_char_or_ws(char x, char y = 0) throws XmlError { + var res = new StringBuilder(); + var what = peek_single(); + while(what != x && what != y && !is_ws(what)) { + res.append_c(read_single()); + what = peek_single(); + } + return res.str; + } + + private string read_until_char(char x) throws XmlError { + var res = new StringBuilder(); + var what = peek_single(); + while(what != x) { + res.append_c(read_single()); + what = peek_single(); + } + return res.str; + } + + private StanzaAttribute read_attribute() throws XmlError { + var res = new StanzaAttribute(); + res.name = read_until_char_or_ws('='); + if (read_single() == '=') { + var quot = peek_single(); + if (quot == '\'' || quot == '"') { + skip_single(); + res.encoded_val = read_until_char(quot); + skip_single(); + } else { + res.encoded_val = read_until_ws(); + } + } + return res; + } + + private void handle_entry_ns(StanzaEntry entry, string default_uri = ns_state.current_ns_uri) throws XmlError { + if (entry.ns_uri != null) return; + if (entry.name.contains(":")) { + var split = entry.name.split(":"); + entry.ns_uri = ns_state.find_uri(split[0]); + entry.name = split[1]; + } else { + entry.ns_uri = default_uri; + } + } + + private void handle_stanza_ns(StanzaNode res) throws XmlError { + foreach (StanzaAttribute attr in res.attributes) { + if (attr.name == "xmlns") { + attr.ns_uri = XMLNS_URI; + ns_state.set_current(attr.val); + } else if (attr.name.contains(":")) { + var split = attr.name.split(":"); + if (split[0] == "xmlns") { + attr.ns_uri = XMLNS_URI; + attr.name = split[1]; + ns_state.add_assoc(attr.val, attr.name); + } + } + } + handle_entry_ns(res); + foreach (StanzaAttribute attr in res.attributes) { + handle_entry_ns(attr, res.ns_uri); + } + } + + public StanzaNode read_node_start() throws XmlError { + var res = new StanzaNode(); + res.attributes = new ArrayList(); + var eof = false; + if (peek_single() == '<') skip_single(); + if (peek_single() == '?') res.pseudo = true; + if (peek_single() == '/') { + eof = true; + skip_single(); + res.name = read_until_char_or_ws('>'); + while(peek_single() != '>') { + skip_single(); + } + skip_single(); + res.has_nodes = false; + res.pseudo = false; + handle_stanza_ns(res); + return res; + } + res.name = read_until_char_or_ws('>', '/'); + skip_until_non_ws(); + while (peek_single() != '/' && peek_single() != '>' && peek_single() != '?') { + res.attributes.add(read_attribute()); + skip_until_non_ws(); + } + if (read_single() == '/' || res.pseudo ) { + res.has_nodes = false; + skip_single(); + } else { + res.has_nodes = true; + } + handle_stanza_ns(res); + return res; + } + + public StanzaNode read_text_node() throws XmlError { + var res = new StanzaNode(); + res.name = "#text"; + res.ns_uri = ns_state.current_ns_uri; + res.encoded_val = read_until_char('<').strip(); + return res; + } + + public StanzaNode read_root_node() throws XmlError { + skip_until_non_ws(); + if (peek_single() == '<') { + var res = read_node_start(); + if (res.pseudo) { + return read_root_node(); + } + return res; + } else { + throw new XmlError.BAD_XML("Content before root node"); + } + } + + public StanzaNode read_stanza_node(NamespaceState? baseNs = null) throws XmlError { + ns_state = baseNs ?? new NamespaceState.for_stanza(); + var res = read_node_start(); + if (res.has_nodes) { + bool finishNodeSeen = false; + do { + skip_until_non_ws(); + if (peek_single() == '<') { + skip_single(); + if (peek_single() == '/') { + skip_single(); + string desc = read_until_char('>'); + skip_single(); + if (desc.contains(":")) { + var split = desc.split(":"); + assert(split[0] == ns_state.find_name(res.ns_uri)); + assert(split[1] == res.name); + } else { + assert(ns_state.current_ns_uri == res.ns_uri); + assert(desc == res.name); + } + return res; + } else { + res.sub_nodes.add(read_stanza_node(ns_state.clone())); + ns_state = baseNs ?? new NamespaceState.for_stanza(); + } + } else { + res.sub_nodes.add(read_text_node()); + } + } while (!finishNodeSeen); + } + return res; + } + + public StanzaNode read_node(NamespaceState? baseNs = null) throws XmlError { + skip_until_non_ws(); + if (peek_single() == '<') { + return read_stanza_node(baseNs ?? new NamespaceState.for_stanza()); + } else { + return read_text_node(); + } + } +} +} diff --git a/vala-xmpp/src/core/stanza_writer.vala b/vala-xmpp/src/core/stanza_writer.vala new file mode 100644 index 00000000..625f42e2 --- /dev/null +++ b/vala-xmpp/src/core/stanza_writer.vala @@ -0,0 +1,29 @@ +namespace Xmpp.Core { +public class StanzaWriter { + private OutputStream output; + + private NamespaceState ns_state = new NamespaceState(); + + public StanzaWriter.for_stream(OutputStream output) { + this.output = output; + } + + public void write_node(StanzaNode node) throws XmlError { + try { + lock(output) { + output.write_all(node.to_xml().data, null); + } + } catch (GLib.IOError e) { + throw new XmlError.IO_ERROR(@"IOError in GLib: $(e.message)"); + } + } + + public async void write(string s) throws XmlError { + try { + output.write_all(s.data, null); + } catch (GLib.IOError e) { + throw new XmlError.IO_ERROR(@"IOError in GLib: $(e.message)"); + } + } +} +} \ No newline at end of file diff --git a/vala-xmpp/src/core/xmpp_stream.vala b/vala-xmpp/src/core/xmpp_stream.vala new file mode 100644 index 00000000..e30a1c9b --- /dev/null +++ b/vala-xmpp/src/core/xmpp_stream.vala @@ -0,0 +1,245 @@ +using Gee; + +namespace Xmpp.Core { + +public errordomain IOStreamError { + READ, + WRITE, + CONNECT, + DISCONNECT + +} + +public class XmppStream { + private static string NS_URI = "http://etherx.jabber.org/streams"; + + public string remote_name; + public bool debug = false; + public StanzaNode? features { get; private set; default = new StanzaNode.build("features", NS_URI); } + + private IOStream? stream; + private StanzaReader? reader; + private StanzaWriter? writer; + + private ArrayList flags = new ArrayList(); + private ArrayList modules = new ArrayList(); + private bool setup_needed = false; + private bool negotiation_complete = false; + + public signal void received_node(XmppStream stream, StanzaNode node); + public signal void received_root_node(XmppStream stream, StanzaNode node); + public signal void received_features_node(XmppStream stream); + public signal void received_message_stanza(XmppStream stream, StanzaNode node); + public signal void received_presence_stanza(XmppStream stream, StanzaNode node); + public signal void received_iq_stanza(XmppStream stream, StanzaNode node); + public signal void received_nonza(XmppStream stream, StanzaNode node); + public signal void stream_negotiated(XmppStream stream); + + public void connect(string? remote_name = null) throws IOStreamError { + if (remote_name != null) this.remote_name = remote_name; + SocketClient client = new SocketClient(); + try { + SocketConnection? stream = client.connect(new NetworkService("xmpp-client", "tcp", this.remote_name)); + if (stream == null) throw new IOStreamError.CONNECT("client.connect() returned null"); + reset_stream(stream); + } catch (Error e) { + stderr.printf("CONNECTION LOST?\n"); + throw new IOStreamError.CONNECT(e.message); + } + loop(); + } + + public void disconnect() throws IOStreamError { + if (writer == null) throw new IOStreamError.DISCONNECT("trying to disconnect, but no stream open"); + if (debug) stderr.puts("OUT\n\n"); + writer.write.begin(""); + reader.cancel(); + stream.close_async.begin(); + } + + public void reset_stream(IOStream stream) { + this.stream = stream; + reader = new StanzaReader.for_stream(stream.input_stream); + writer = new StanzaWriter.for_stream(stream.output_stream); + require_setup(); + } + + public void require_setup() { + setup_needed = true; + } + + public bool is_setup_needed() { + return setup_needed; + } + + public StanzaNode read() throws IOStreamError { + if (reader == null) throw new IOStreamError.READ("trying to read, but no stream open"); + try { + var node = reader.read_node(); + if (debug) stderr.printf("IN\n%s\n", node.to_string()); + return node; + } catch (XmlError e) { + throw new IOStreamError.READ(e.message); + } + } + + public void write(StanzaNode node) throws IOStreamError { + if (writer == null) throw new IOStreamError.WRITE("trying to write, but no stream open"); + try { + if (debug) stderr.printf("OUT\n%s\n", node.to_string()); + writer.write_node(node); + } catch (XmlError e) { + throw new IOStreamError.WRITE(e.message); + } + } + + public IOStream get_stream() { + return stream; + } + + public void add_flag(XmppStreamFlag flag) { + flags.add(flag); + } + + public XmppStreamFlag? get_flag(string ns, string id) { + foreach (var flag in flags) { + if (flag.get_ns() == ns && flag.get_id() == id) { + return flag; + } + } + return null; + } + + public void remove_flag(XmppStreamFlag flag) { + flags.remove(flag); + } + + public XmppStream add_module(XmppStreamModule module) { + modules.add(module); + if (negotiation_complete || module as XmppStreamNegotiationModule != null) { + module.attach(this); + } + return this; + } + + public void remove_modules() { + foreach (XmppStreamModule module in modules) module.detach(this); + } + + public XmppStreamModule? get_module(string ns, string id) { + foreach (var module in modules) { + if (module.get_ns() == ns && module.get_id() == id) { + return module; + } + } + return null; + } + + private void setup() throws IOStreamError { + var outs = new StanzaNode.build("stream", "http://etherx.jabber.org/streams") + .put_attribute("to", remote_name) + .put_attribute("version", "1.0") + .put_attribute("xmlns", "jabber:client") + .put_attribute("stream", "http://etherx.jabber.org/streams", XMLNS_URI); + outs.has_nodes = true; + write(outs); + received_root_node(this, read_root()); + } + + private void loop() throws IOStreamError { + while(true) { + if (setup_needed) { + setup(); + setup_needed = false; + } + + StanzaNode node = read(); + received_node(this, node); + + if (node.ns_uri == NS_URI && node.name == "features") { + features = node; + received_features_node(this); + } else if (node.ns_uri == NS_URI && node.name == "stream" && node.pseudo) { + print("disconnect\n"); + disconnect(); + return; + } else if (node.ns_uri == JABBER_URI) { + if (node.name == "message") { + received_message_stanza(this, node); + } else if (node.name == "presence") { + received_presence_stanza(this, node); + } else if (node.name == "iq") { + received_iq_stanza(this, node); + } else { + received_nonza(this, node); + } + } else { + received_nonza(this, node); + } + + if (!negotiation_complete && negotiation_modules_done()) { + negotiation_complete = true; + attach_non_negotation_modules(); + stream_negotiated(this); + } + } + } + + private bool negotiation_modules_done() throws IOStreamError { + if (!setup_needed) { + bool mandatory_outstanding = false; + bool negotiation_active = false; + foreach (XmppStreamModule module in modules) { + XmppStreamNegotiationModule negotiation_module = module as XmppStreamNegotiationModule; + if (negotiation_module != null) { + if (negotiation_module.negotiation_active(this)) negotiation_active = true; + if (negotiation_module.mandatory_outstanding(this)) mandatory_outstanding = true; + } + } + if (!negotiation_active) { + if (mandatory_outstanding) { + throw new IOStreamError.CONNECT("mandatory-to-negotiate feature not negotiated"); + } else { + return true; + } + } + } + return false; + } + + private void attach_non_negotation_modules() { + foreach (XmppStreamModule module in modules) { + if (module as XmppStreamNegotiationModule == null) { + module.attach(this); + } + } + } + + private StanzaNode read_root() throws IOStreamError { + try { + var node = reader.read_root_node(); + if (debug) stderr.printf("IN\n%s\n", node.to_string()); + return node; + } catch (XmlError e) { + throw new IOStreamError.READ(e.message); + } + } +} + +public abstract class XmppStreamFlag { + internal abstract string get_ns(); + internal abstract string get_id(); +} + +public abstract class XmppStreamModule : Object { + internal abstract void attach(XmppStream stream); + internal abstract void detach(XmppStream stream); + internal abstract string get_ns(); + internal abstract string get_id(); +} + +public abstract class XmppStreamNegotiationModule : XmppStreamModule { + internal abstract bool mandatory_outstanding(XmppStream stream); + internal abstract bool negotiation_active(XmppStream stream); +} +} diff --git a/vala-xmpp/src/module/bind.vala b/vala-xmpp/src/module/bind.vala new file mode 100644 index 00000000..d01fda7a --- /dev/null +++ b/vala-xmpp/src/module/bind.vala @@ -0,0 +1,93 @@ +using Xmpp.Core; + +namespace Xmpp.Bind { + private const string NS_URI = "urn:ietf:params:xml:ns:xmpp-bind"; + + /** The parties to a stream MUST consider resource binding as mandatory-to-negotiate. (RFC6120 7.3.1) */ + public class Module : XmppStreamNegotiationModule { + public const string ID = "bind_module"; + + private string requested_resource; + + public signal void bound_to_resource(XmppStream stream, string my_jid); + + public Module(string requested_resource) { + this.requested_resource = requested_resource; + } + + public void iq_response_stanza(XmppStream stream, Iq.Stanza iq) { + var flag = Flag.get_flag(stream); + if (flag == null || flag.finished) return; + + if (iq.type_ == Iq.Stanza.TYPE_RESULT) { + flag.my_jid = iq.stanza.get_subnode("jid", NS_URI, true).get_string_content(); + flag.finished = true; + bound_to_resource(stream, flag.my_jid); + } + } + + public void received_features_node(XmppStream stream) { + if (stream.is_setup_needed()) return; + + var bind = stream.features.get_subnode("bind", NS_URI); + if (bind != null) { + var flag = new Flag(); + StanzaNode bind_node = new StanzaNode.build("bind", NS_URI).add_self_xmlns() + .put_node(new StanzaNode.build("resource", NS_URI).put_node(new StanzaNode.text(requested_resource))); + Iq.Module.get_module(stream).send_iq(stream, new Iq.Stanza.set(bind_node), new IqResponseListenerImpl()); + stream.add_flag(flag); + } + } + + private class IqResponseListenerImpl : Iq.ResponseListener, Object { + public void on_result(XmppStream stream, Iq.Stanza iq) { + Bind.Module.get_module(stream).iq_response_stanza(stream, iq); + } + } + + public override void attach(XmppStream stream) { + Iq.Module.require(stream); + stream.received_features_node.connect(this.received_features_node); + } + + public override void detach(XmppStream stream) { + stream.received_features_node.disconnect(this.received_features_node); + } + + public static Module? get_module(XmppStream stream) { + return (Module?) stream.get_module(NS_URI, ID); + } + + public static void require(XmppStream stream) { + if (get_module(stream) == null) stream.add_module(new Bind.Module("")); + } + + public override bool mandatory_outstanding(XmppStream stream) { + return !Flag.has_flag(stream) || !Flag.get_flag(stream).finished; + } + + public override bool negotiation_active(XmppStream stream) { + return Flag.has_flag(stream) && !Flag.get_flag(stream).finished; + } + + public override string get_ns() { return NS_URI; } + public override string get_id() { return ID; } + } + + public class Flag : XmppStreamFlag { + public const string ID = "bind"; + public string? my_jid; + public bool finished = false; + + public static Flag? get_flag(XmppStream stream) { + return (Flag?) stream.get_flag(NS_URI, ID); + } + + public static bool has_flag(XmppStream stream) { + return get_flag(stream) != null; + } + + public override string get_ns() { return NS_URI; } + public override string get_id() { return ID; } + } +} diff --git a/vala-xmpp/src/module/iq/module.vala b/vala-xmpp/src/module/iq/module.vala new file mode 100644 index 00000000..b5c50bd7 --- /dev/null +++ b/vala-xmpp/src/module/iq/module.vala @@ -0,0 +1,89 @@ +using Gee; + +using Xmpp.Core; + +namespace Xmpp.Iq { + private const string NS_URI = "jabber:client"; + + public class Module : XmppStreamNegotiationModule { + public const string ID = "iq_module"; + + private HashMap responseListeners = new HashMap(); + private HashMap> namespaceRegistrants = new HashMap>(); + + public void send_iq(XmppStream stream, Iq.Stanza iq, ResponseListener? listener = null) { + stream.write(iq.stanza); + if (listener != null) { + responseListeners.set(iq.id, listener); + } + } + + public void register_for_namespace(string namespace, Handler module) { + if (!namespaceRegistrants.has_key(namespace)) { + namespaceRegistrants.set(namespace, new ArrayList()); + } + namespaceRegistrants[namespace].add(module); + } + + public override void attach(XmppStream stream) { + stream.received_iq_stanza.connect(on_received_iq_stanza); + } + + public override void detach(XmppStream stream) { + stream.received_iq_stanza.disconnect(on_received_iq_stanza); + } + + public static Module? get_module(XmppStream stream) { + return (Module?) stream.get_module(NS_URI, ID); + } + + public static void require(XmppStream stream) { + if (get_module(stream) == null) stream.add_module(new Iq.Module()); + } + + public override bool mandatory_outstanding(XmppStream stream) { return false; } + + public override bool negotiation_active(XmppStream stream) { return false; } + + public override string get_ns() { return NS_URI; } + public override string get_id() { return ID; } + + private void on_received_iq_stanza(XmppStream stream, StanzaNode node) { + Iq.Stanza iq = new Iq.Stanza.from_stanza(node, Bind.Flag.has_flag(stream) ? Bind.Flag.get_flag(stream).my_jid : null); + + if (iq.type_ == Iq.Stanza.TYPE_RESULT || iq.is_error()) { + if (responseListeners.has_key(iq.id)) { + ResponseListener? listener = responseListeners.get(iq.id); + if (listener != null) { + listener.on_result(stream, iq); + } + responseListeners.unset(iq.id); + } + } else { + ArrayList children = node.get_all_subnodes(); + if (children.size == 1 && namespaceRegistrants.has_key(children[0].ns_uri)) { + ArrayList handlers = namespaceRegistrants[children[0].ns_uri]; + foreach (Handler handler in handlers) { + if (iq.type_ == Iq.Stanza.TYPE_GET) { + handler.on_iq_get(stream, iq); + } else if (iq.type_ == Iq.Stanza.TYPE_SET) { + handler.on_iq_set(stream, iq); + } + } + } else { + Iq.Stanza unaviable_error = new Iq.Stanza.error(iq, new StanzaNode.build("service-unaviable", "urn:ietf:params:xml:ns:xmpp-stanzas").add_self_xmlns()); + send_iq(stream, unaviable_error); + } + } + } + } + + public interface Handler : Object { + public abstract void on_iq_get(XmppStream stream, Iq.Stanza iq); + public abstract void on_iq_set(XmppStream stream, Iq.Stanza iq); + } + + public interface ResponseListener : Object { + public abstract void on_result(XmppStream stream, Iq.Stanza iq); + } +} diff --git a/vala-xmpp/src/module/iq/stanza.vala b/vala-xmpp/src/module/iq/stanza.vala new file mode 100644 index 00000000..99d589ff --- /dev/null +++ b/vala-xmpp/src/module/iq/stanza.vala @@ -0,0 +1,51 @@ +using Gee; + +using Xmpp.Core; + +namespace Xmpp.Iq { + +public class Stanza : Xmpp.Stanza { + + public const string TYPE_GET = "get"; + public const string TYPE_RESULT = "result"; + public const string TYPE_SET = "set"; + + private Stanza(string id = UUID.generate_random_unparsed()) { + base.outgoing(new StanzaNode.build("iq")); + this.id = id; + } + + public Stanza.get(StanzaNode stanza_node, string id = UUID.generate_random_unparsed()) { + this(id); + this.type_ = TYPE_GET; + stanza.put_node(stanza_node); + } + + public Stanza.result(Stanza request, StanzaNode? stanza_node = null) { + this(request.id); + this.type_ = TYPE_RESULT; + if (stanza_node != null) { + stanza.put_node(stanza_node); + } + } + + public Stanza.set(StanzaNode stanza_node, string id = UUID.generate_random_unparsed()) { + this(id); + type_ = TYPE_SET; + stanza.put_node(stanza_node); + } + + public Stanza.error(Stanza request, StanzaNode error_stanza, StanzaNode? associated_child = null) { + this(request.id); + this.type_ = TYPE_ERROR; + stanza.put_node(error_stanza); + if (associated_child != null) { + stanza.put_node(associated_child); + } + } + public Stanza.from_stanza(StanzaNode stanza_node, string? my_jid) { + base.incoming(stanza_node, my_jid); + } +} + +} diff --git a/vala-xmpp/src/module/message/module.vala b/vala-xmpp/src/module/message/module.vala new file mode 100644 index 00000000..10d83693 --- /dev/null +++ b/vala-xmpp/src/module/message/module.vala @@ -0,0 +1,50 @@ +using Gee; + +using Xmpp.Core; + +namespace Xmpp.Message { + private const string NS_URI = "jabber:client"; + + public class Module : XmppStreamModule { + public const string ID = "message_module"; + + public signal void pre_send_message(XmppStream stream, Message.Stanza message); + public signal void pre_received_message(XmppStream stream, Message.Stanza message); + public signal void received_message(XmppStream stream, Message.Stanza message); + + public void send_message(XmppStream stream, Message.Stanza message) { + pre_send_message(stream, message); + stream.write(message.stanza); + } + + public void received_message_stanza(XmppStream stream, StanzaNode node) { + Message.Stanza message = new Message.Stanza.from_stanza(node, Bind.Flag.get_flag(stream).my_jid); + do { + message.rerun_parsing = false; + pre_received_message(stream, message); + } while(message.rerun_parsing); + received_message(stream, message); + } + + public static Module? get_module(XmppStream stream) { + return (Module?) stream.get_module(NS_URI, ID); + } + + public static void require(XmppStream stream) { + if (get_module(stream) == null) stream.add_module(new Message.Module()); + } + + public override void attach(XmppStream stream) { + Bind.Module.require(stream); + stream.received_message_stanza.connect(received_message_stanza); + } + + public override void detach(XmppStream stream) { + stream.received_message_stanza.disconnect(received_message_stanza); + } + + public override string get_ns() { return NS_URI; } + public override string get_id() { return ID; } + } + +} diff --git a/vala-xmpp/src/module/message/stanza.vala b/vala-xmpp/src/module/message/stanza.vala new file mode 100644 index 00000000..811fbd22 --- /dev/null +++ b/vala-xmpp/src/module/message/stanza.vala @@ -0,0 +1,63 @@ +using Gee; + +using Xmpp.Core; + +namespace Xmpp.Message { + +public class Stanza : Xmpp.Stanza { + public const string NODE_BODY = "body"; + public const string NODE_SUBJECT = "subject"; + public const string NODE_THREAD = "thread"; + + public const string TYPE_CHAT = "chat"; + public const string TYPE_GROUPCHAT = "groupchat"; + public const string TYPE_HEADLINE = "headline"; + public const string TYPE_NORMAL = "normal"; + + public bool rerun_parsing = false; + private ArrayList flags = new ArrayList(); + + public string body { + get { + StanzaNode? body_node = stanza.get_subnode(NODE_BODY); + return body_node == null? null : body_node.get_string_content(); + } + set { + StanzaNode? body_node = stanza.get_subnode(NODE_BODY); + if (body_node == null) { + body_node = new StanzaNode.build(NODE_BODY); + stanza.put_node(body_node); + } + body_node.sub_nodes.clear(); + body_node.put_node(new StanzaNode.text(value)); + } + } + + public Stanza(string id = UUID.generate_random_unparsed()) { + base.outgoing(new StanzaNode.build("message")); + stanza.set_attribute(ATTRIBUTE_ID, id); + } + + public Stanza.from_stanza(StanzaNode stanza_node, string my_jid) { + base.incoming(stanza_node, my_jid); + } + + public void add_flag(MessageFlag flag) { + flags.add(flag); + } + + public MessageFlag? get_flag(string ns, string id) { + foreach (MessageFlag flag in flags) { + if (flag.get_ns() == ns && flag.get_id() == id) return flag; + } + return null; + } +} + +public abstract class MessageFlag : Object { + public abstract string get_ns(); + + public abstract string get_id(); +} + +} \ No newline at end of file diff --git a/vala-xmpp/src/module/presence/flag.vala b/vala-xmpp/src/module/presence/flag.vala new file mode 100644 index 00000000..3dc86a5c --- /dev/null +++ b/vala-xmpp/src/module/presence/flag.vala @@ -0,0 +1,64 @@ +using Gee; + +using Xmpp.Core; + +namespace Xmpp.Presence { + +public class Flag : XmppStreamFlag { + public const string ID = "presence"; + + private HashMap> resources = new HashMap>(); + private HashMap presences = new HashMap(); + + public Set get_available_jids() { + return resources.keys; + } + + public ArrayList? get_resources(string bare_jid) { + return resources[bare_jid]; + } + + public Presence.Stanza? get_presence(string full_jid) { + return presences[full_jid]; + } + + public void add_presence(Presence.Stanza presence) { + string bare_jid = get_bare_jid(presence.from); + if (!resources.has_key(bare_jid)) { + resources[bare_jid] = new ArrayList(); + } + if (resources[bare_jid].contains(presence.from)) { + resources[bare_jid].remove(presence.from); + } + resources[bare_jid].add(presence.from); + presences[presence.from] = presence; + } + + public void remove_presence(string jid) { + string bare_jid = get_bare_jid(jid); + if (resources.has_key(bare_jid)) { + if (is_bare_jid(jid)) { + foreach (string full_jid in resources[jid]) { + presences.unset(full_jid); + } + resources.unset(jid); + } else { + resources[bare_jid].remove(jid); + if (resources[bare_jid].size == 0) { + resources.unset(bare_jid); + } + presences.unset(jid); + } + } + } + + public static Flag? get_flag(XmppStream stream) { return (Flag?) stream.get_flag(NS_URI, ID); } + + public static bool has_flag(XmppStream stream) { return get_flag(stream) != null; } + + public override string get_ns() { return NS_URI; } + + public override string get_id() { return ID; } +} + +} \ No newline at end of file diff --git a/vala-xmpp/src/module/presence/module.vala b/vala-xmpp/src/module/presence/module.vala new file mode 100644 index 00000000..6c9d183c --- /dev/null +++ b/vala-xmpp/src/module/presence/module.vala @@ -0,0 +1,110 @@ +using Xmpp.Core; + +namespace Xmpp.Presence { + private const string NS_URI = "jabber:client"; + + public class Module : XmppStreamModule { + public const string ID = "presence_module"; + + public signal void received_presence(XmppStream stream, Presence.Stanza presence); + public signal void pre_send_presence_stanza(XmppStream stream, Presence.Stanza presence); + public signal void initial_presence_sent(XmppStream stream, Presence.Stanza presence); + public signal void received_available(XmppStream stream, Presence.Stanza presence); + public signal void received_available_show(XmppStream stream, string jid, string show); + public signal void received_unavailable(XmppStream stream, string jid); + public signal void received_subscription_request(XmppStream stream, string jid); + public signal void received_unsubscription(XmppStream stream, string jid); + + public bool available_resource = true; + + public void request_subscription(XmppStream stream, string bare_jid) { + Presence.Stanza presence = new Presence.Stanza(); + presence.to = bare_jid; + presence.type_ = Presence.Stanza.TYPE_SUBSCRIBE; + send_presence(stream, presence); + } + + public void approve_subscription(XmppStream stream, string bare_jid) { + Presence.Stanza presence = new Presence.Stanza(); + presence.to = bare_jid; + presence.type_ = Presence.Stanza.TYPE_SUBSCRIBED; + send_presence(stream, presence); + } + + public void deny_subscription(XmppStream stream, string bare_jid) { + cancel_subscription(stream, bare_jid); + } + + public void cancel_subscription(XmppStream stream, string bare_jid) { + Presence.Stanza presence = new Presence.Stanza(); + presence.to = bare_jid; + presence.type_ = Presence.Stanza.TYPE_UNSUBSCRIBED; + send_presence(stream, presence); + } + + public void unsubscribe(XmppStream stream, string bare_jid) { + Presence.Stanza presence = new Presence.Stanza(); + presence.to = bare_jid; + presence.type_ = Presence.Stanza.TYPE_UNSUBSCRIBE; + send_presence(stream, presence); + } + + public void send_presence(XmppStream stream, Presence.Stanza presence) { + pre_send_presence_stanza(stream, presence); + stream.write(presence.stanza); + } + + public override void attach(XmppStream stream) { + stream.received_presence_stanza.connect(on_received_presence_stanza); + stream.stream_negotiated.connect(on_stream_negotiated); + stream.add_flag(new Flag()); + } + + public override void detach(XmppStream stream) { + stream.received_presence_stanza.disconnect(on_received_presence_stanza); + stream.stream_negotiated.disconnect(on_stream_negotiated); + } + + private void on_received_presence_stanza(XmppStream stream, StanzaNode node) { + Presence.Stanza presence = new Presence.Stanza.from_stanza(node, Bind.Flag.get_flag(stream).my_jid); + received_presence(stream, presence); + switch (presence.type_) { + case Presence.Stanza.TYPE_AVAILABLE: + Flag.get_flag(stream).add_presence(presence); + received_available(stream, presence); + received_available_show(stream, presence.from, presence.show); + break; + case Presence.Stanza.TYPE_UNAVAILABLE: + Flag.get_flag(stream).remove_presence(presence.from); + received_unavailable(stream, presence.from); + break; + case Presence.Stanza.TYPE_SUBSCRIBE: + received_subscription_request(stream, presence.from); + break; + case Presence.Stanza.TYPE_UNSUBSCRIBE: + received_unsubscription(stream, presence.from); + break; + } + } + + private void on_stream_negotiated(XmppStream stream) { + if (available_resource) { + Presence.Stanza presence = new Presence.Stanza(); + send_presence(stream, presence); + initial_presence_sent(stream, presence); + } + } + + public static Module? get_module(XmppStream stream) { + return (Module?) stream.get_module(NS_URI, ID); + } + + public static void require(XmppStream stream) { + if (get_module(stream) == null) stream.add_module(new Presence.Module()); + } + + public override string get_ns() { return NS_URI; } + public override string get_id() { return ID; } + } + +} diff --git a/vala-xmpp/src/module/presence/stanza.vala b/vala-xmpp/src/module/presence/stanza.vala new file mode 100644 index 00000000..3dc036e5 --- /dev/null +++ b/vala-xmpp/src/module/presence/stanza.vala @@ -0,0 +1,93 @@ +using Xmpp.Core; + +namespace Xmpp.Presence { + +public class Stanza : Xmpp.Stanza { + + public const string NODE_PRIORITY = "priority"; + public const string NODE_STATUS = "status"; + public const string NODE_SHOW = "show"; + + public const string SHOW_ONLINE = "online"; + public const string SHOW_AWAY = "away"; + public const string SHOW_CHAT = "chat"; + public const string SHOW_DND = "dnd"; + public const string SHOW_XA = "xa"; + + public const string TYPE_AVAILABLE = "available"; + public const string TYPE_PROBE = "probe"; + public const string TYPE_SUBSCRIBE = "subscribe"; + public const string TYPE_SUBSCRIBED = "subscribed"; + public const string TYPE_UNAVAILABLE = "unavailable"; + public const string TYPE_UNSUBSCRIBE = "unsubscribe"; + public const string TYPE_UNSUBSCRIBED = "unsubscribed"; + + public int priority { + get { + StanzaNode? priority_node = stanza.get_subnode(NODE_PRIORITY); + if (priority_node == null) { + return 0; + } else { + return int.parse(priority_node.get_string_content()); + } + } + set { + StanzaNode? priority_node = stanza.get_subnode(NODE_PRIORITY); + if (priority_node == null) { + priority_node = new StanzaNode.build(NODE_PRIORITY); + stanza.put_node(priority_node); + } + priority_node.val = value.to_string(); + } + } + + public string? status { + get { + StanzaNode? status_node = stanza.get_subnode(NODE_STATUS); + return status_node != null ? status_node.get_string_content() : null; + } + set { + StanzaNode? status_node = stanza.get_subnode(NODE_STATUS); + if (status_node == null) { + status_node = new StanzaNode.build(NODE_STATUS); + stanza.put_node(status_node); + } + status_node.val = value; + } + } + + public string show { + get { + StanzaNode? show_node = stanza.get_subnode(NODE_SHOW); + return show_node != null ? show_node.get_string_content() : SHOW_ONLINE; + } + set { + if (value != SHOW_ONLINE) { + StanzaNode? show_node = stanza.get_subnode(NODE_SHOW); + if (show_node == null) { + show_node = new StanzaNode.build(NODE_SHOW); + stanza.put_node(show_node); + } + show_node.val = value; + } + } + } + + public override string type_ { + get { + return base.type_ != null ? base.type_ : TYPE_AVAILABLE; + } + set { base.type_ = value; } + } + + public Stanza(string id = UUID.generate_random_unparsed()) { + stanza = new StanzaNode.build("presence"); + this.id = id; + } + + public Stanza.from_stanza(StanzaNode stanza_node, string my_jid) { + base.incoming(stanza_node, my_jid); + } +} + +} \ No newline at end of file diff --git a/vala-xmpp/src/module/roster/flag.vala b/vala-xmpp/src/module/roster/flag.vala new file mode 100644 index 00000000..c3e35158 --- /dev/null +++ b/vala-xmpp/src/module/roster/flag.vala @@ -0,0 +1,30 @@ +using Gee; + +using Xmpp.Core; + +namespace Xmpp.Roster { + +public class Flag : XmppStreamFlag { + public const string ID = "roster"; + public HashMap roster_items = new HashMap(); + + internal string? iq_id; + + public Collection get_roster() { + return roster_items.values; + } + + public Item? get_item(string jid) { + return roster_items[jid]; + } + + public static Flag? get_flag(XmppStream stream) { return (Flag?) stream.get_flag(NS_URI, ID); } + + public static bool has_flag(XmppStream stream) { return get_flag(stream) != null; } + + public override string get_ns() { return NS_URI; } + + public override string get_id() { return ID; } +} + +} \ No newline at end of file diff --git a/vala-xmpp/src/module/roster/item.vala b/vala-xmpp/src/module/roster/item.vala new file mode 100644 index 00000000..7ef76fd4 --- /dev/null +++ b/vala-xmpp/src/module/roster/item.vala @@ -0,0 +1,45 @@ +using Gee; + +using Xmpp.Core; + +namespace Xmpp.Roster { + +public class Item { + + public const string NODE_JID = "jid"; + public const string NODE_NAME = "name"; + public const string NODE_SUBSCRIPTION = "subscription"; + + public const string SUBSCRIPTION_NONE = "none"; + public const string SUBSCRIPTION_TO = "to"; + public const string SUBSCRIPTION_FROM = "from"; + public const string SUBSCRIPTION_BOTH = "both"; + public const string SUBSCRIPTION_REMOVE = "remove"; + + public StanzaNode stanza_node; + + public string jid { + get { return stanza_node.get_attribute(NODE_JID); } + set { stanza_node.set_attribute(NODE_JID, value); } + } + + public string? name { + get { return stanza_node.get_attribute(NODE_NAME); } + set { stanza_node.set_attribute(NODE_NAME, value); } + } + + public string? subscription { + get { return stanza_node.get_attribute(NODE_SUBSCRIPTION); } + set { stanza_node.set_attribute(NODE_SUBSCRIPTION, value); } + } + + public Item() { + stanza_node = new StanzaNode.build("item", NS_URI); + } + + public Item.from_stanza_node(StanzaNode stanza_node) { + this.stanza_node = stanza_node; + } +} + +} \ No newline at end of file diff --git a/vala-xmpp/src/module/roster/module.vala b/vala-xmpp/src/module/roster/module.vala new file mode 100644 index 00000000..9fa23a55 --- /dev/null +++ b/vala-xmpp/src/module/roster/module.vala @@ -0,0 +1,125 @@ +using Gee; + +using Xmpp.Core; + +namespace Xmpp.Roster { + private const string NS_URI = "jabber:iq:roster"; + + public class Module : XmppStreamModule, Iq.Handler { + public const string ID = "roster_module"; + + public signal void received_roster(XmppStream stream, Collection roster); + public signal void item_removed(XmppStream stream, Item roster_item); + public signal void item_updated(XmppStream stream, Item roster_item); + + public bool interested_resource = true; + + /** + * Add a jid to the roster + */ + public void add_jid(XmppStream stream, string jid, string? handle = null) { + Item roster_item = new Item(); + roster_item.jid = jid; + if (handle != null) { + roster_item.name = handle; + } + roster_set(stream, roster_item); + } + + /** + * Remove a jid from the roster + */ + public void remove_jid(XmppStream stream, string jid) { + Item roster_item = new Item(); + roster_item.jid = jid; + roster_item.subscription = Item.SUBSCRIPTION_REMOVE; + + roster_set(stream, roster_item); + } + + /** + * Set a handle for a jid + * @param handle Handle to be set. If null, any handle will be removed. + */ + public void set_jid_handle(XmppStream stream, string jid, string? handle) { + Item roster_item = new Item(); + roster_item.jid = jid; + if (handle != null) { + roster_item.name = handle; + } + + roster_set(stream, roster_item); + } + + public void on_iq_set(XmppStream stream, Iq.Stanza iq) { + StanzaNode? query_node = iq.stanza.get_subnode("query", NS_URI); + if (query_node == null) return; + + Flag flag = Flag.get_flag(stream); + Item item = new Item.from_stanza_node(query_node.get_subnode("item", NS_URI)); + switch (item.subscription) { + case Item.SUBSCRIPTION_REMOVE: + flag.roster_items.unset(item.jid); + item_removed(stream, item); + break; + default: + flag.roster_items[item.jid] = item; + item_updated(stream, item); + break; + } + } + + public void on_iq_get(XmppStream stream, Iq.Stanza iq) { } + + public static Module? get_module(XmppStream stream) { + return (Module?) stream.get_module(NS_URI, ID); + } + + public static void require(XmppStream stream) { + if (get_module(stream) == null) stream.add_module(new Module()); + } + + public override void attach(XmppStream stream) { + Iq.Module.require(stream); + Iq.Module.get_module(stream).register_for_namespace(NS_URI, this); + Presence.Module.require(stream); + Presence.Module.get_module(stream).initial_presence_sent.connect(roster_get); + stream.add_flag(new Flag()); + } + + public override void detach(XmppStream stream) { + Presence.Module.get_module(stream).initial_presence_sent.disconnect(roster_get); + } + + internal override string get_ns() { return NS_URI; } + internal override string get_id() { return ID; } + + private void roster_get(XmppStream stream) { + Flag.get_flag(stream).iq_id = UUID.generate_random_unparsed(); + StanzaNode query_node = new StanzaNode.build("query", NS_URI).add_self_xmlns(); + Iq.Stanza iq = new Iq.Stanza.get(query_node, Flag.get_flag(stream).iq_id); + Iq.Module.get_module(stream).send_iq(stream, iq, new IqResponseListenerImpl()); + } + + private class IqResponseListenerImpl : Iq.ResponseListener, Object { + public void on_result(XmppStream stream, Iq.Stanza iq) { + Flag flag = Flag.get_flag(stream); + if (iq.id == flag.iq_id) { + StanzaNode? query_node = iq.stanza.get_subnode("query", NS_URI); + foreach (StanzaNode item_node in query_node.sub_nodes) { + Item item = new Item.from_stanza_node(item_node); + flag.roster_items[item.jid] = item; + } + Module.get_module(stream).received_roster(stream, flag.roster_items.values); + } + } + } + + private void roster_set(XmppStream stream, Item roster_item) { + StanzaNode query_node = new StanzaNode.build("query", NS_URI).add_self_xmlns() + .put_node(roster_item.stanza_node); + Iq.Stanza iq = new Iq.Stanza.set(query_node); + Iq.Module.get_module(stream).send_iq(stream, iq, null); + } + } +} diff --git a/vala-xmpp/src/module/sasl.vala b/vala-xmpp/src/module/sasl.vala new file mode 100644 index 00000000..07e3f5c4 --- /dev/null +++ b/vala-xmpp/src/module/sasl.vala @@ -0,0 +1,139 @@ +using Xmpp.Core; + +namespace Xmpp.PlainSasl { + private const string NS_URI = "urn:ietf:params:xml:ns:xmpp-sasl"; + + public class Module : XmppStreamNegotiationModule { + public const string ID = "plain_module"; + private const string MECHANISM = "PLAIN"; + + private string name; + private string password; + public bool use_full_name = false; + + public signal void received_auth_failure(XmppStream stream, StanzaNode node); + + public Module(string name, string password) { + this.name = name; + this.password = password; + } + + public override void attach(XmppStream stream) { + stream.received_features_node.connect(this.received_features_node); + stream.received_nonza.connect(this.received_nonza); + } + + public override void detach(XmppStream stream) { + stream.received_features_node.disconnect(this.received_features_node); + stream.received_nonza.disconnect(this.received_nonza); + } + + public void received_nonza(XmppStream stream, StanzaNode node) { + if (node.ns_uri == NS_URI) { + if (node.name == "success") { + stream.require_setup(); + Flag.get_flag(stream).finished = true; + } else if (node.name == "failure") { + stream.remove_flag(Flag.get_flag(stream)); + received_auth_failure(stream, node); + } + } + } + + public void received_features_node(XmppStream stream) { + if (Flag.has_flag(stream)) return; + if (stream.is_setup_needed()) return; + if (!Tls.Flag.has_flag(stream) || !Tls.Flag.get_flag(stream).finished) return; + + var mechanisms = stream.features.get_subnode("mechanisms", NS_URI); + if (mechanisms != null) { + bool supportsPlain = false; + foreach (var mechanism in mechanisms.sub_nodes) { + if (mechanism.name != "mechanism" || mechanism.ns_uri != NS_URI) continue; + var text = mechanism.get_subnode("#text"); + if (text != null && text.val == MECHANISM) { + supportsPlain = true; + } + } + if (!supportsPlain) { + stderr.printf("Server at %s does not support %s auth, use full-features Sasl implementation!\n", stream.remote_name, MECHANISM); + return; + } + + if (!name.contains("@")) { + name = "%s@%s".printf(name, stream.remote_name); + } + if (!use_full_name && name.contains("@")) { + var split = name.split("@"); + if (split[1] == stream.remote_name) { + name = split[0]; + } else { + use_full_name = true; + } + } + var name = this.name; + if (!use_full_name && name.contains("@")) { + var split = name.split("@"); + if (split[1] == stream.remote_name) { + name = split[0]; + } + } + stream.write(new StanzaNode.build("auth", NS_URI).add_self_xmlns() + .put_attribute("mechanism", MECHANISM) + .put_node(new StanzaNode.text(Base64.encode(get_plain_bytes(name, password))))); + var flag = new Flag(); + flag.mechanism = MECHANISM; + flag.name = name; + stream.add_flag(flag); + } + } + + public static Module? get_module(XmppStream stream) { + return (Module?) stream.get_module(NS_URI, ID); + } + + public static void require(XmppStream stream) { + if (get_module(stream) == null) stderr.printf("PlainSaslModule required but not attached!\n"); + } + + private static uchar[] get_plain_bytes(string name_s, string password_s) { + var name = name_s.to_utf8(); + var password = password_s.to_utf8(); + uchar[] res = new uchar[name.length + password.length + 2]; + res[0] = 0; + res[name.length + 1] = 0; + for(int i = 0; i < name.length; i++) { res[i + 1] = (uchar) name[i]; } + for(int i = 0; i < password.length; i++) { res[i + name.length + 2] = (uchar) password[i]; } + return res; + } + + public override bool mandatory_outstanding(XmppStream stream) { + return !Flag.has_flag(stream) || !Flag.get_flag(stream).finished; + } + + public override bool negotiation_active(XmppStream stream) { + return Flag.has_flag(stream) && !Flag.get_flag(stream).finished; + } + + public override string get_ns() { return NS_URI; } + public override string get_id() { return ID; } + } + + public class Flag : XmppStreamFlag { + public const string ID = "sasl"; + public string mechanism; + public string name; + public bool finished = false; + + public static Flag? get_flag(XmppStream stream) { + return (Flag?) stream.get_flag(NS_URI, ID); + } + + public static bool has_flag(XmppStream stream) { + return get_flag(stream) != null; + } + + public override string get_ns() { return NS_URI; } + public override string get_id() { return ID; } + } +} diff --git a/vala-xmpp/src/module/stanza.vala b/vala-xmpp/src/module/stanza.vala new file mode 100644 index 00000000..f6af9623 --- /dev/null +++ b/vala-xmpp/src/module/stanza.vala @@ -0,0 +1,70 @@ +using Xmpp.Core; + +namespace Xmpp { + + public class Stanza { + + public const string ATTRIBUTE_FROM = "from"; + public const string ATTRIBUTE_ID = "id"; + public const string ATTRIBUTE_TO = "to"; + public const string ATTRIBUTE_TYPE = "type"; + + public const string TYPE_ERROR = "error"; + + private string? my_jid; + + public virtual string? from { + owned get { + string? from_attribute = stanza.get_attribute(ATTRIBUTE_FROM); + // "when a client receives a stanza that does not include a 'from' attribute, it MUST assume that the stanza + // is from the user's account on the server." (RFC6120 8.1.2.1) + if (from_attribute != null) return from_attribute; + if (my_jid != null) { + string my_bare_jid = get_bare_jid(my_jid); // has to be left-side value + return my_bare_jid; + } + return null; + } + set { stanza.set_attribute(ATTRIBUTE_FROM, value); } + } + + public virtual string? id { + get { return stanza.get_attribute(ATTRIBUTE_ID); } + set { stanza.set_attribute(ATTRIBUTE_ID, value); } + } + + public virtual string? to { + owned get { + string? to_attribute = stanza.get_attribute(ATTRIBUTE_TO); + // "if the stanza does not include a 'to' address then the client MUST treat it as if the 'to' address were + // included with a value of the client's full JID." (RFC6120 8.1.1.1) + return to_attribute == null ? my_jid : to_attribute; + } + set { stanza.set_attribute(ATTRIBUTE_TO, value); } + } + + public virtual string type_ { + get { return stanza.get_attribute(ATTRIBUTE_TYPE); } + set { stanza.set_attribute(ATTRIBUTE_TYPE, value); } + } + + public StanzaNode stanza; + + public Stanza.incoming(StanzaNode stanza, string? my_jid) { + this.stanza = stanza; + this.my_jid = my_jid; + } + + public Stanza.outgoing(StanzaNode stanza) { + this.stanza = stanza; + } + + public bool is_error() { + return type_ == TYPE_ERROR; + } + + public ErrorStanza? get_error() { + return new ErrorStanza.from_stanza(this.stanza); + } + } +} \ No newline at end of file diff --git a/vala-xmpp/src/module/stanza_error.vala b/vala-xmpp/src/module/stanza_error.vala new file mode 100644 index 00000000..be4633e9 --- /dev/null +++ b/vala-xmpp/src/module/stanza_error.vala @@ -0,0 +1,69 @@ +using Gee; + +using Xmpp.Core; + +namespace Xmpp { + + public class ErrorStanza { + public const string CONDITION_BAD_REQUEST = "bad-request"; + public const string CONDITION_CONFLICT = "conflict"; + public const string CONDITION_FEATURE_NOT_IMPLEMENTED = "feature-not-implemented"; + public const string CONDITION_FORBIDDEN = "forbidden"; + public const string CONDITION_GONE = "gone"; + public const string CONDITION_INTERNAL_SERVER_ERROR = "internal-server-error"; + public const string CONDITION_ITEM_NOT_FOUND = "item-not-found"; + public const string CONDITION_JID_MALFORMED = "jid-malformed"; + public const string CONDITION_NOT_ACCEPTABLE = "not-acceptable"; + public const string CONDITION_NOT_ALLOWED = "not-allowed"; + public const string CONDITION_NOT_AUTHORIZED = "not-authorized"; + public const string CONDITION_POLICY_VIOLATION = "policy-violation"; + public const string CONDITION_RECIPIENT_UNAVAILABLE = "recipient-unavailable"; + public const string CONDITION_REDIRECT = "redirect"; + public const string CONDITION_REGISTRATION_REQUIRED = "registration-required"; + public const string CONDITION_REMOTE_SERVER_NOT_FOUND = "remote-server-not-found"; + public const string CONDITION_REMOTE_SERVER_TIMEOUT = "remote-server-timeout"; + public const string CONDITION_RESOURCE_CONSTRAINT = "resource-constraint"; + public const string CONDITION_SERVICE_UNAVAILABLE = "service-unavailable"; + public const string CONDITION_SUBSCRIPTION_REQUIRED = "subscription-required"; + public const string CONDITION_UNDEFINED_CONDITION = "undefined-condition"; + public const string CONDITION_UNEXPECTED_REQUEST = "unexpected-request"; + + public const string TYPE_AUTH = "auth"; + public const string TYPE_CANCEL = "cancel"; + public const string TYPE_CONTINUE = "continue"; + public const string TYPE_MODIFY = "modify"; + public const string TYPE_WAIT = "wait"; + + public string? by { + get { return error_node.get_attribute("by"); } + } + + public string condition { + get { + ArrayList subnodes = error_node.sub_nodes; + foreach (StanzaNode subnode in subnodes) { // TODO get subnode by ns + if (subnode.ns_uri == "urn:ietf:params:xml:ns:xmpp-stanzas") { + return subnode.name; + } + } + return CONDITION_UNDEFINED_CONDITION; // TODO hm! + } + } + + public string? original_id { + get { return stanza.get_attribute("id"); } + } + + public string type_ { + get { return stanza.get_attribute("type"); } + } + + public StanzaNode stanza; + private StanzaNode error_node; + + public ErrorStanza.from_stanza(StanzaNode stanza) { + this.stanza = stanza; + error_node = stanza.get_subnode("error"); + } + } +} \ No newline at end of file diff --git a/vala-xmpp/src/module/stream_error.vala b/vala-xmpp/src/module/stream_error.vala new file mode 100644 index 00000000..73e2bb36 --- /dev/null +++ b/vala-xmpp/src/module/stream_error.vala @@ -0,0 +1,119 @@ +using Gee; + +using Xmpp.Core; + +namespace Xmpp.StreamError { + private const string NS_URI = "jabber:client"; + private const string NS_ERROR = "urn:ietf:params:xml:ns:xmpp-streams"; + + public class Module : XmppStreamModule { + public const string ID = "stream_error_module"; + + public override void attach(XmppStream stream) { + stream.received_nonza.connect(on_received_nonstanza); + } + + public override void detach(XmppStream stream) { + stream.received_nonza.disconnect(on_received_nonstanza); + } + + public static Module? get_module(XmppStream stream) { + return (Module?) stream.get_module(NS_URI, ID); + } + + public static void require(XmppStream stream) { + if (get_module(stream) == null) stream.add_module(new StreamError.Module()); + } + + public override string get_ns() { return NS_URI; } + public override string get_id() { return ID; } + + private void on_received_nonstanza(XmppStream stream, StanzaNode node) { + if (node.name == "error" && node.ns_uri == "http://etherx.jabber.org/streams") { + stream.add_flag(generate_error_flag(node)); + } + } + + private Flag generate_error_flag(StanzaNode node) { + string? subnode_name = null; + ArrayList subnodes = node.sub_nodes; + foreach (StanzaNode subnode in subnodes) { // TODO get subnode by ns + if (subnode.ns_uri == "urn:ietf:params:xml:ns:xmpp-streams" && subnode.name != "text") { + subnode_name = subnode.name; + } + } + Flag flag = new StreamError.Flag(); + flag.error_type = subnode_name; + switch (subnode_name) { + case "bad-format": + case "conflict": + case "connection-timeout": + case "bad-namespace-prefix": + flag.reconnection_recomendation = StreamError.Flag.Reconnect.NOW; + break; + case "host-gone": + case "host-unknown": + flag.reconnection_recomendation = StreamError.Flag.Reconnect.LATER; + break; + case "improper-addressing": + case "internal-server-error": + case "invalid-from": + case "invalid-namespace": + case "invalid-xml": + case "not-authorized": + case "not-well-formed": + case "policy-violation": + case "remote-connection-failed": + case "reset": + flag.reconnection_recomendation = StreamError.Flag.Reconnect.NOW; + break; + case "resource-constraint": + flag.reconnection_recomendation = StreamError.Flag.Reconnect.LATER; + break; + case "restricted-xml": + flag.reconnection_recomendation = StreamError.Flag.Reconnect.NOW; + break; + case "see-other-host": + case "system-shutdown": + flag.reconnection_recomendation = StreamError.Flag.Reconnect.LATER; + break; + case "undefined-condition": + case "unsupported-encoding": + case "unsupported-feature": + case "unsupported-stanza-type": + case "unsupported-version": + flag.reconnection_recomendation = StreamError.Flag.Reconnect.NOW; + break; + } + + if (subnode_name == "conflict") flag.resource_rejected = true; + return flag; + } + } + + public class Flag : XmppStreamFlag { + public const string ID = "stream_error"; + + public enum Reconnect { + UNKNOWN, + NOW, + LATER, + NEVER + } + + public string? error_type; + public Reconnect reconnection_recomendation = Reconnect.UNKNOWN; + public bool resource_rejected = false; + + public static Flag? get_flag(XmppStream stream) { + return (Flag?) stream.get_flag(NS_URI, ID); + } + + public static bool has_flag(XmppStream stream) { + return get_flag(stream) != null; + } + + public override string get_ns() { return NS_URI; } + public override string get_id() { return ID; } + } +} diff --git a/vala-xmpp/src/module/tls.vala b/vala-xmpp/src/module/tls.vala new file mode 100644 index 00000000..1f8447ec --- /dev/null +++ b/vala-xmpp/src/module/tls.vala @@ -0,0 +1,99 @@ +using Xmpp.Core; + +namespace Xmpp.Tls { + private const string NS_URI = "urn:ietf:params:xml:ns:xmpp-tls"; + + public class Module : XmppStreamNegotiationModule { + public const string ID = "tls_module"; + + public bool require { get; set; default = true; } + public bool server_supports_tls = false; + public bool server_requires_tls = false; + public SocketConnectable? identity = null; + + public override void attach(XmppStream stream) { + stream.received_features_node.connect(this.received_features_node); + stream.received_nonza.connect(this.received_nonza); + } + + public override void detach(XmppStream stream) { + stream.received_features_node.disconnect(this.received_features_node); + stream.received_nonza.disconnect(this.received_nonza); + } + + private void received_nonza(XmppStream stream, StanzaNode node) { + if (node.ns_uri == NS_URI && node.name == "proceed") { + try { + var conn = TlsClientConnection.new(stream.get_stream(), identity); + // TODO: Add certificate error handling, that is, allow the + // program to handle certificate errors. The certificate + // *is checked* by TlsClientConnection, and connection is + // not allowed to continue in case that there is an error. + stream.reset_stream(conn); + + var flag = Flag.get_flag(stream); + flag.peer_certificate = conn.get_peer_certificate(); + flag.finished = true; + } catch (Error e) { + stderr.printf("Failed to start TLS: %s\n", e.message); + } + } + } + + private void received_features_node(XmppStream stream) { + if (Flag.has_flag(stream)) return; + if (stream.is_setup_needed()) return; + + var starttls = stream.features.get_subnode("starttls", NS_URI); + if (starttls != null) { + server_supports_tls = true; + if (starttls.get_subnode("required") != null || stream.features.get_all_subnodes().size == 1) { + server_requires_tls = true; + } + if (server_requires_tls || require) { + try { + stream.write(new StanzaNode.build("starttls", NS_URI).add_self_xmlns()); + } catch (IOStreamError e) { + stderr.printf("Failed to request TLS: %s\n", e.message); + } + } + if (identity == null) { + identity = new NetworkService("xmpp-client", "tcp", stream.remote_name); + } + stream.add_flag(new Flag()); + } + } + + public static Module? get_module(XmppStream stream) { + return (Module?) stream.get_module(NS_URI, ID); + } + + public override bool mandatory_outstanding(XmppStream stream) { + return require && (!Flag.has_flag(stream) || !Flag.get_flag(stream).finished); + } + + public override bool negotiation_active(XmppStream stream) { + return Flag.has_flag(stream) && !Flag.get_flag(stream).finished; + } + + public override string get_ns() { return NS_URI; } + public override string get_id() { return ID; } + } + + public class Flag : XmppStreamFlag { + public const string ID = "tls_flag"; + public TlsCertificate? peer_certificate; + public bool finished = false; + + public static Flag? get_flag(XmppStream stream) { + return (Flag?) stream.get_flag(NS_URI, ID); + } + + public static bool has_flag(XmppStream stream) { + return get_flag(stream) != null; + } + + public override string get_ns() { return NS_URI; } + public override string get_id() { return ID; } + } +} diff --git a/vala-xmpp/src/module/util.vala b/vala-xmpp/src/module/util.vala new file mode 100644 index 00000000..4d762883 --- /dev/null +++ b/vala-xmpp/src/module/util.vala @@ -0,0 +1,13 @@ +namespace Xmpp { + string? get_bare_jid(string jid) { + return jid.split("/")[0]; + } + + bool is_bare_jid(string jid) { + return !jid.contains("/"); + } + + string? get_resource_part(string jid) { + return jid.split("/")[1]; + } +} \ No newline at end of file diff --git a/vala-xmpp/src/module/xep/0027_pgp/flag.vala b/vala-xmpp/src/module/xep/0027_pgp/flag.vala new file mode 100644 index 00000000..03844afa --- /dev/null +++ b/vala-xmpp/src/module/xep/0027_pgp/flag.vala @@ -0,0 +1,24 @@ +using Gee; + +using Xmpp.Core; + +namespace Xmpp.Xep.Pgp { + +public class Flag : XmppStreamFlag { + public const string ID = "pgp"; + public HashMap key_ids = new HashMap(); + + public string? get_key_id(string jid) { return key_ids[get_bare_jid(jid)]; } + + public void set_key_id(string jid, string key) { key_ids[get_bare_jid(jid)] = key; } + + public static Flag? get_flag(XmppStream stream) { return (Flag?) stream.get_flag(NS_URI, ID); } + + public static bool has_flag(XmppStream stream) { return get_flag(stream) != null; } + + public override string get_ns() { return NS_URI; } + + public override string get_id() { return ID; } +} + +} \ No newline at end of file diff --git a/vala-xmpp/src/module/xep/0027_pgp/module.vala b/vala-xmpp/src/module/xep/0027_pgp/module.vala new file mode 100644 index 00000000..fee6b9e4 --- /dev/null +++ b/vala-xmpp/src/module/xep/0027_pgp/module.vala @@ -0,0 +1,206 @@ +using GPG; + +using Xmpp.Core; + +namespace Xmpp.Xep.Pgp { + private const string NS_URI = "jabber:x"; + private const string NS_URI_ENCRYPTED = NS_URI + ":encrypted"; + private const string NS_URI_SIGNED = NS_URI + ":signed"; + + public class Module : XmppStreamModule { + public const string ID = "0027_current_pgp_usage"; + + public signal void received_jid_key_id(XmppStream stream, string jid, string key_id); + + private static Object mutex = new Object(); + + private string? signed_status; + private string? own_key_id; + + public Module() { + GPG.check_version(); + signed_status = gpg_sign(""); + if (signed_status != null) own_key_id = gpg_verify(signed_status, ""); + } + + public bool encrypt(Message.Stanza message, string key_id) { + string? enc_body = gpg_encrypt(message.body, new string[] {key_id, own_key_id}); + if (enc_body != null) { + message.stanza.put_node(new StanzaNode.build("x", NS_URI_ENCRYPTED).add_self_xmlns().put_node(new StanzaNode.text(enc_body))); + message.body = "[This message is OpenPGP encrypted (see XEP-0027)]"; + return true; + } + return false; + } + + public string? get_cyphertext(Message.Stanza message) { + StanzaNode? x_node = message.stanza.get_subnode("x", NS_URI_ENCRYPTED); + return x_node == null ? null : x_node.get_string_content(); + } + + public override void attach(XmppStream stream) { + Presence.Module.require(stream); + Presence.Module.get_module(stream).received_presence.connect(on_received_presence); + Presence.Module.get_module(stream).pre_send_presence_stanza.connect(on_pre_send_presence_stanza); + Message.Module.require(stream); + Message.Module.get_module(stream).pre_received_message.connect(on_pre_received_message); + stream.add_flag(new Flag()); + } + + public override void detach(XmppStream stream) { + Presence.Module.get_module(stream).received_presence.disconnect(on_received_presence); + Presence.Module.get_module(stream).pre_send_presence_stanza.disconnect(on_pre_send_presence_stanza); + Message.Module.get_module(stream).pre_received_message.disconnect(on_pre_received_message); + } + + public static Module? get_module(XmppStream stream) { + return (Module?) stream.get_module(NS_URI, ID); + } + + public static void require(XmppStream stream) { + if (get_module(stream) == null) stream.add_module(new Module()); + } + + public override string get_ns() { return NS_URI; } + public override string get_id() { return ID; } + + private void on_received_presence(XmppStream stream, Presence.Stanza presence) { + StanzaNode x_node = presence.stanza.get_subnode("x", NS_URI_SIGNED); + if (x_node != null) { + string? sig = x_node.get_string_content(); + if (sig != null) { + string signed_data = presence.status == null ? "" : presence.status; + string? key_id = gpg_verify(sig, signed_data); + if (key_id != null) { + Flag.get_flag(stream).set_key_id(presence.from, key_id); + received_jid_key_id(stream, presence.from, key_id); + } + } + } + } + + private void on_pre_send_presence_stanza(XmppStream stream, Presence.Stanza presence) { + if (presence.type_ == Presence.Stanza.TYPE_AVAILABLE && signed_status != null) { + presence.stanza.put_node(new StanzaNode.build("x", NS_URI_SIGNED).add_self_xmlns().put_node(new StanzaNode.text(signed_status))); + } + } + + private void on_pre_received_message(XmppStream stream, Message.Stanza message) { + string? encrypted = get_cyphertext(message); + if (encrypted != null) { + MessageFlag flag = new MessageFlag(); + message.add_flag(flag); + string? decrypted = gpg_decrypt(encrypted); + if (decrypted != null) { + flag.decrypted = true; + message.body = decrypted; + } + } + } + + private static string? gpg_encrypt(string plain, string[] key_ids) { + lock (mutex) { + GPG.Context context; + GPGError.ErrorCode e = GPG.Context.Context(out context); if (e != GPGError.ErrorCode.NO_ERROR) return null; + context.set_armor(true); + + Key[] keys = new Key[key_ids.length]; + for (int i = 0; i < key_ids.length; i++) { + Key key; + e = context.get_key(key_ids[i], out key, false); if (e != GPGError.ErrorCode.NO_ERROR) return null; + keys[i] = key; + } + + GPG.Data plain_data; + e = GPG.Data.create_from_memory(out plain_data, plain.data, false); + GPG.Data enc_data; + e = GPG.Data.create(out enc_data); + e = context.op_encrypt(keys, GPG.EncryptFlags.ALWAYS_TRUST, plain_data, enc_data); + + string encr = get_string_from_data(enc_data); + int encryption_start = encr.index_of("\n\n") + 2; + return encr.substring(encryption_start, encr.length - "\n-----END PGP MESSAGE-----".length - encryption_start); + } + } + + private static string? gpg_decrypt(string enc) { + lock (mutex) { + string armor = "-----BEGIN PGP MESSAGE-----\n\n" + enc + "\n-----END PGP MESSAGE-----"; + + GPG.Data enc_data; + GPGError.ErrorCode e = GPG.Data.create_from_memory(out enc_data, armor.data, false); if (e != GPGError.ErrorCode.NO_ERROR) return null; + GPG.Data dec_data; + e = GPG.Data.create(out dec_data); if (e != GPGError.ErrorCode.NO_ERROR) return null; + GPG.Context context; + e = GPG.Context.Context(out context); if (e != GPGError.ErrorCode.NO_ERROR) return null; + e = context.op_decrypt(enc_data, dec_data); if (e != GPGError.ErrorCode.NO_ERROR) return null; + + string plain = get_string_from_data(dec_data); + return plain; + } + } + + private static string? gpg_verify(string sig, string signed_text) { + lock (mutex) { + string armor = "-----BEGIN PGP MESSAGE-----\n\n" + sig + "\n-----END PGP MESSAGE-----"; + + GPG.Data sig_data; + GPGError.ErrorCode e = GPG.Data.create_from_memory(out sig_data, armor.data, false); if (e != GPGError.ErrorCode.NO_ERROR) return null; + GPG.Data plain_data; + e = GPG.Data.create(out plain_data); if (e != GPGError.ErrorCode.NO_ERROR) return null; + GPG.Context context; + e = GPG.Context.Context(out context); if (e != GPGError.ErrorCode.NO_ERROR) return null; + e = context.op_verify(sig_data, null, plain_data); if (e != GPGError.ErrorCode.NO_ERROR) return null; + GPG.VerifyResult* verify_res = context.op_verify_result(); + if (verify_res == null || verify_res.signatures == null) return null; + return verify_res.signatures.fpr; + } + } + + private static string? gpg_sign(string status) { + lock (mutex) { + GPG.Data status_data; + GPGError.ErrorCode e = GPG.Data.create_from_memory(out status_data, status.data, false); if (e != GPGError.ErrorCode.NO_ERROR) return null; + GPG.Data signed_data; + e = GPG.Data.create(out signed_data); if (e != GPGError.ErrorCode.NO_ERROR) return null; + GPG.Context context; + e = GPG.Context.Context(out context); if (e != GPGError.ErrorCode.NO_ERROR) return null; + e = context.op_sign(status_data, signed_data, GPG.SigMode.CLEAR); if (e != GPGError.ErrorCode.NO_ERROR) return null; + + string signed = get_string_from_data(signed_data); + int signature_start = signed.index_of("-----BEGIN PGP SIGNATURE-----"); + signature_start = signed.index_of("\n\n", signature_start) + 2; + return signed.substring(signature_start, signed.length - "\n-----END PGP SIGNATURE-----".length - signature_start); + } + } + + private static string get_string_from_data(GPG.Data data) { + data.seek(0); + uint8[] buf = new uint8[256]; + ssize_t? len = null; + string res = ""; + do { + len = data.read(buf); + if (len > 0) { + string part = (string) buf; + part = part.substring(0, (long) len); + res += part; + } + } while (len > 0); + return res; + } + } + + public class MessageFlag : Message.MessageFlag { + public const string id = "pgp"; + + public bool decrypted = false; + + public static MessageFlag? get_flag(Message.Stanza message) { + return (MessageFlag) message.get_flag(NS_URI, id); + } + + public override string get_ns() { return NS_URI; } + public override string get_id() { return id; } + } +} diff --git a/vala-xmpp/src/module/xep/0030_service_discovery/flag.vala b/vala-xmpp/src/module/xep/0030_service_discovery/flag.vala new file mode 100644 index 00000000..5be9f2eb --- /dev/null +++ b/vala-xmpp/src/module/xep/0030_service_discovery/flag.vala @@ -0,0 +1,33 @@ +using Gee; + +using Xmpp.Core; + +namespace Xmpp.Xep.ServiceDiscovery { + +public class Flag : XmppStreamFlag { + public const string ID = "service_discovery"; + + private HashMap> entity_features = new HashMap>(); + public ArrayList features = new ArrayList(); + + public bool? has_entity_feature(string jid, string feature) { + if (!entity_features.has_key(jid)) return null; + return entity_features[jid].contains(feature); + } + + public void set_entitiy_features(string jid, ArrayList features) { + entity_features[jid] = features; + } + + public void add_own_feature(string feature) { features.add(feature); } + + public static Flag? get_flag(XmppStream stream) { return (Flag?) stream.get_flag(NS_URI, ID); } + + public static bool has_flag(XmppStream stream) { return get_flag(stream) != null; } + + public override string get_ns() { return NS_URI; } + + public override string get_id() { return ID; } +} + +} \ No newline at end of file diff --git a/vala-xmpp/src/module/xep/0030_service_discovery/info_result.vala b/vala-xmpp/src/module/xep/0030_service_discovery/info_result.vala new file mode 100644 index 00000000..7e0f0ea4 --- /dev/null +++ b/vala-xmpp/src/module/xep/0030_service_discovery/info_result.vala @@ -0,0 +1,78 @@ +using Gee; + +using Xmpp.Core; + +namespace Xmpp.Xep.ServiceDiscovery { + +public class InfoResult { + public Iq.Stanza iq { get; private set; } + + public ArrayList features { + owned get { + ArrayList ret = new ArrayList(); + foreach (StanzaNode feature_node in iq.stanza.get_subnode("query", NS_URI_INFO).get_subnodes("feature", NS_URI_INFO)) { + ret.add(feature_node.get_attribute("var", NS_URI_INFO)); + } + return ret; + } + set { + foreach (string feature in value) { + add_feature(feature); + } + } + } + + public ArrayList identities { + owned get { + ArrayList ret = new ArrayList(); + foreach (StanzaNode feature_node in iq.stanza.get_subnode("query", NS_URI_INFO).get_subnodes("identity", NS_URI_INFO)) { + ret.add(new Identity(feature_node.get_attribute("category", NS_URI_INFO), + feature_node.get_attribute("type", NS_URI_INFO), + feature_node.get_attribute("name", NS_URI_INFO))); + } + return ret; + } + set { + foreach (Identity identity in value) { + add_identity(identity); + } + } + } + + public void add_feature(string feature) { + iq.stanza.get_subnode("query", NS_URI_INFO).put_node(new StanzaNode.build("feature", NS_URI_INFO).put_attribute("var", feature)); + } + + public void add_identity(Identity identity) { + StanzaNode identity_node = new StanzaNode.build("identity", NS_URI_INFO) + .put_attribute("category", identity.category) + .put_attribute("type", identity.type_); + if (identity.name != null) { + identity_node.put_attribute("name", identity.name); + } + iq.stanza.get_subnode("query", NS_URI_INFO).put_node(identity_node); + } + + private InfoResult.from_iq(Iq.Stanza iq) { + this.iq = iq; + } + + public InfoResult(Iq.Stanza iq_request) { + iq = new Iq.Stanza.result(iq_request); + iq.to = iq_request.from; + iq.stanza.put_node(new StanzaNode.build("query", NS_URI_INFO).add_self_xmlns()); + } + + public static InfoResult? create_from_iq(Iq.Stanza iq) { + if (iq.is_error()) return null; + StanzaNode query_node = iq.stanza.get_subnode("query", NS_URI_INFO); + if (query_node == null) return null; + StanzaNode feature_node = query_node.get_subnode("feature", NS_URI_INFO); + if (feature_node == null) return null; + StanzaNode identity_node = query_node.get_subnode("identity", NS_URI_INFO); + if (identity_node == null) return null; + return new ServiceDiscovery.InfoResult.from_iq(iq); + } +} + +} \ No newline at end of file diff --git a/vala-xmpp/src/module/xep/0030_service_discovery/items_result.vala b/vala-xmpp/src/module/xep/0030_service_discovery/items_result.vala new file mode 100644 index 00000000..2c29c320 --- /dev/null +++ b/vala-xmpp/src/module/xep/0030_service_discovery/items_result.vala @@ -0,0 +1,27 @@ +using Gee; + +using Xmpp.Core; + +namespace Xmpp.Xep.ServiceDiscovery { + +public class ItemsResult { + public Iq.Stanza iq { get; private set; } + + public ArrayList items { + owned get { + ArrayList ret = new ArrayList(); + foreach (StanzaNode feature_node in iq.stanza.get_subnode("query", NS_URI_ITEMS).get_subnodes("identity", NS_URI_INFO)) { + ret.add(new Item(feature_node.get_attribute("jid", NS_URI_ITEMS), + feature_node.get_attribute("name", NS_URI_ITEMS), + feature_node.get_attribute("node", NS_URI_ITEMS))); + } + return ret; + } + } + + public ItemsResult.from_iq(Iq.Stanza iq) { + this.iq = iq; + } +} + +} \ No newline at end of file diff --git a/vala-xmpp/src/module/xep/0030_service_discovery/module.vala b/vala-xmpp/src/module/xep/0030_service_discovery/module.vala new file mode 100644 index 00000000..109da897 --- /dev/null +++ b/vala-xmpp/src/module/xep/0030_service_discovery/module.vala @@ -0,0 +1,137 @@ +using Gee; + +using Xmpp.Core; + +namespace Xmpp.Xep.ServiceDiscovery { + private const string NS_URI = "http://jabber.org/protocol/disco"; + public const string NS_URI_INFO = NS_URI + "#info"; + public const string NS_URI_ITEMS = NS_URI + "#items"; + + public class Module : XmppStreamModule, Iq.Handler { + public const string ID = "0030_service_discovery_module"; + + public ArrayList identities = new ArrayList(); + + public Module.with_identity(string category, string type, string? name = null) { + add_identity(category, type, name); + } + + public void add_feature(XmppStream stream, string feature) { + Flag.get_flag(stream).add_own_feature(feature); + } + + public void add_feature_notify(XmppStream stream, string feature) { + add_feature(stream, feature + "+notify"); + } + + public void add_identity(string category, string type, string? name = null) { + identities.add(new Identity(category, type, name)); + } + + public void request_info(XmppStream stream, string jid, InfoResponseListener response_listener) { + Iq.Stanza iq = new Iq.Stanza.get(new StanzaNode.build("query", NS_URI_INFO).add_self_xmlns()); + iq.to = jid; + Iq.Module.get_module(stream).send_iq(stream, iq, new IqInfoResponseListener(response_listener)); + } + + private class IqInfoResponseListener : Iq.ResponseListener, Object { + InfoResponseListener response_listener; + public IqInfoResponseListener(InfoResponseListener response_listener) { + this.response_listener = response_listener; + } + public void on_result(XmppStream stream, Iq.Stanza iq) { + InfoResult? result = InfoResult.create_from_iq(iq); + if (result != null) { + Flag.get_flag(stream).set_entitiy_features(iq.from, result.features); + response_listener.on_result(stream, result); + } else { + response_listener.on_error(stream, iq); + } + } + } + + public void request_items(XmppStream stream, string jid, ItemsResponseListener response_listener) { + Iq.Stanza iq = new Iq.Stanza.get(new StanzaNode.build("query", NS_URI_ITEMS).add_self_xmlns()); + iq.to = jid; + Iq.Module.get_module(stream).send_iq(stream, iq, new IqItemsResponseListener(response_listener)); + } + + private class IqItemsResponseListener : Iq.ResponseListener, Object { + ItemsResponseListener response_listener; + public IqItemsResponseListener(ItemsResponseListener response_listener) { this.response_listener = response_listener; } + public void on_result(XmppStream stream, Iq.Stanza iq) { + //response_listener.on_result(stream, new ServiceDiscoveryItemsResult.from_iq(iq)); + } + } + + public void on_iq_get(XmppStream stream, Iq.Stanza iq) { + StanzaNode? query_node = iq.stanza.get_subnode("query", NS_URI_INFO); + if (query_node != null) { + send_query_result(stream, iq); + } + } + + public void on_iq_set(XmppStream stream, Iq.Stanza iq) { } + + public override void attach(XmppStream stream) { + Iq.Module.require(stream); + Iq.Module.get_module(stream).register_for_namespace(NS_URI_INFO, this); + stream.add_flag(new Flag()); + add_feature(stream, NS_URI_INFO); + } + + public override void detach(XmppStream stream) { } + + public static Module? get_module(XmppStream stream) { + return (Module?) stream.get_module(NS_URI, ID); + } + + public static void require(XmppStream stream) { + if (get_module(stream) == null) stream.add_module(new ServiceDiscovery.Module()); + } + + public override string get_ns() { return NS_URI; } + public override string get_id() { return ID; } + + private void send_query_result(XmppStream stream, Iq.Stanza iq_request) { + InfoResult query_result = new ServiceDiscovery.InfoResult(iq_request); + query_result.features = Flag.get_flag(stream).features; + query_result.identities = identities; + Iq.Module.get_module(stream).send_iq(stream, query_result.iq, null); + } + } + + public class Identity { + public string category { get; set; } + public string type_ { get; set; } + public string? name { get; set; } + + public Identity(string category, string type, string? name = null) { + this.category = category; + this.type_ = type; + this.name = name; + } + } + + public class Item { + public string jid; + public string? name; + public string? node; + + public Item(string jid, string? name = null, string? node = null) { + this.jid = jid; + this.name = name; + this.node = node; + } + } + + public interface InfoResponseListener : Object { + public abstract void on_result(XmppStream stream, InfoResult query_result); + public void on_error(XmppStream stream, Iq.Stanza iq) { } + } + + public interface ItemsResponseListener : Object { + public abstract void on_result(XmppStream stream, ItemsResult query_result); + public void on_error(XmppStream stream, Iq.Stanza iq) { } + } +} diff --git a/vala-xmpp/src/module/xep/0045_muc/flag.vala b/vala-xmpp/src/module/xep/0045_muc/flag.vala new file mode 100644 index 00000000..6c1ef508 --- /dev/null +++ b/vala-xmpp/src/module/xep/0045_muc/flag.vala @@ -0,0 +1,80 @@ +using Gee; + +using Xmpp.Core; + +namespace Xmpp.Xep.Muc { + +public class Flag : XmppStreamFlag { + public const string ID = "muc"; + + private HashMap enter_listeners = new HashMap(); + private HashMap enter_ids = new HashMap(); + private HashMap own_nicks = new HashMap(); + private HashMap subjects = new HashMap(); + private HashMap subjects_by = new HashMap(); + private HashMap occupant_real_jids = new HashMap(); + private HashMap occupant_affiliation = new HashMap(); + private HashMap occupant_role = new HashMap(); + + public string? get_real_jid(string full_jid) { return occupant_real_jids[full_jid]; } + + public void set_real_jid(string full_jid, string real_jid) { occupant_real_jids[full_jid] = real_jid; } + + public string? get_occupant_affiliation(string full_jid) { return occupant_affiliation[full_jid]; } + + public void set_occupant_affiliation(string full_jid, string affiliation) { occupant_affiliation[full_jid] = affiliation; } + + public string? get_occupant_role(string full_jid) { return occupant_role[full_jid]; } + + public void set_occupant_role(string full_jid, string role) { occupant_role[full_jid] = role; } + + public string? get_muc_nick(string bare_jid) { return own_nicks[bare_jid]; } + + public string? get_enter_id(string bare_jid) { return enter_ids[bare_jid]; } + + public MucEnterListener? get_enter_listener(string bare_jid) { return enter_listeners[bare_jid]; } + + public bool is_muc(string jid) { return own_nicks[jid] != null; } + + public bool is_occupant(string jid) { + string bare_jid = get_bare_jid(jid); + return own_nicks.has_key(bare_jid) || enter_ids.has_key(bare_jid); + } + + public bool is_muc_enter_outstanding() { return enter_ids.size != 0; } + + public string? get_muc_subject(string bare_jid) { return subjects[bare_jid]; } + + public void set_muc_subject(string full_jid, string subject) { + string bare_jid = get_bare_jid(full_jid); + subjects[bare_jid] = subject; + subjects_by[bare_jid] = full_jid; + } + + public void start_muc_enter(string bare_jid, string presence_id, MucEnterListener listener) { + enter_listeners[bare_jid] = listener; + enter_ids[bare_jid] = presence_id; + } + + public void finish_muc_enter(string bare_jid, string? nick = null) { + if (nick != null) own_nicks[bare_jid] = nick; + enter_listeners.unset(bare_jid); + enter_ids.unset(bare_jid); + } + + public void remove_occupant_info(string full_jid) { + occupant_real_jids.unset(full_jid); + occupant_affiliation.unset(full_jid); + occupant_role.unset(full_jid); + } + + public static Flag? get_flag(XmppStream stream) { return (Flag?) stream.get_flag(NS_URI, ID); } + + public static bool has_flag(XmppStream stream) { return get_flag(stream) != null; } + + public override string get_ns() { return NS_URI; } + + public override string get_id() { return ID; } +} + +} \ No newline at end of file diff --git a/vala-xmpp/src/module/xep/0045_muc/module.vala b/vala-xmpp/src/module/xep/0045_muc/module.vala new file mode 100644 index 00000000..f9ed9539 --- /dev/null +++ b/vala-xmpp/src/module/xep/0045_muc/module.vala @@ -0,0 +1,244 @@ +using Gee; + +using Xmpp.Core; + +namespace Xmpp.Xep.Muc { + +private const string NS_URI = "http://jabber.org/protocol/muc"; +private const string NS_URI_ADMIN = NS_URI + "#admin"; +private const string NS_URI_USER = NS_URI + "#user"; + +public const string AFFILIATION_ADMIN = "admin"; +public const string AFFILIATION_MEMBER = "member"; +public const string AFFILIATION_NONE = "none"; +public const string AFFILIATION_OUTCAST = "outcast"; +public const string AFFILIATION_OWNER = "owner"; + +public const string ROLE_MODERATOR = "moderator"; +public const string ROLE_NONE = "none"; +public const string ROLE_PARTICIPANT = "participant"; +public const string ROLE_VISITOR = "visitor"; + +public enum MucEnterError { + PASSWORD_REQUIRED, + NOT_IN_MEMBER_LIST, + BANNED, + NICK_CONFLICT, + OCCUPANT_LIMIT_REACHED, + ROOM_DOESNT_EXIST +} + +public class Module : XmppStreamModule { + public const string ID = "0045_muc_module"; + + public signal void received_occupant_affiliation(XmppStream stream, string jid, string? affiliation); + public signal void received_occupant_jid(XmppStream stream, string jid, string? real_jid); + public signal void received_occupant_role(XmppStream stream, string jid, string? role); + public signal void subject_set(XmppStream stream, string subject, string jid); + + public void enter(XmppStream stream, string bare_jid, string nick, string? password, MucEnterListener listener) { + Presence.Stanza presence = new Presence.Stanza(); + presence.to = bare_jid + "/" + nick; + StanzaNode x_node = new StanzaNode.build("x", NS_URI).add_self_xmlns(); + if (password != null) { + x_node.put_node(new StanzaNode.build("password", NS_URI).put_node(new StanzaNode.text(password))); + } + presence.stanza.put_node(x_node); + + Muc.Flag.get_flag(stream).start_muc_enter(bare_jid, presence.id, listener); + + Presence.Module.get_module(stream).send_presence(stream, presence); + } + + public void exit(XmppStream stream, string jid) { + string nick = Flag.get_flag(stream).get_muc_nick(jid); + Presence.Stanza presence = new Presence.Stanza(); + presence.to = jid + "/" + nick; + presence.type_ = Presence.Stanza.TYPE_UNAVAILABLE; + Presence.Module.get_module(stream).send_presence(stream, presence); + } + + public void change_subject(XmppStream stream, string jid, string subject) { + Message.Stanza message = new Message.Stanza(); + message.to = jid; + message.type_ = Message.Stanza.TYPE_GROUPCHAT; + message.stanza.put_node((new StanzaNode.build("subject")).put_node(new StanzaNode.text(subject))); + Message.Module.get_module(stream).send_message(stream, message); + } + + public void change_nick(XmppStream stream, string jid, string new_nick) { + Presence.Stanza presence = new Presence.Stanza(); + presence.to = jid + "/" + new_nick; + Presence.Module.get_module(stream).send_presence(stream, presence); + } + + public void kick(XmppStream stream, string jid, string nick) { + change_role(stream, jid, nick, "none"); + } + + public override void attach(XmppStream stream) { + stream.add_flag(new Muc.Flag()); + Message.Module.require(stream); + Message.Module.get_module(stream).received_message.connect(on_received_message); + Presence.Module.require(stream); + Presence.Module.get_module(stream).received_presence.connect(on_received_presence); + Presence.Module.get_module(stream).received_available.connect(on_received_available); + Presence.Module.get_module(stream).received_unavailable.connect(on_received_unavailable); + if (ServiceDiscovery.Module.get_module(stream) != null) { + ServiceDiscovery.Module.get_module(stream).add_feature(stream, NS_URI); + } + } + + public override void detach(XmppStream stream) { + Message.Module.get_module(stream).received_message.disconnect(on_received_message); + Presence.Module.get_module(stream).received_presence.disconnect(on_received_presence); + Presence.Module.get_module(stream).received_available.disconnect(on_received_available); + Presence.Module.get_module(stream).received_unavailable.disconnect(on_received_unavailable); + } + + public static Module? get_module(XmppStream stream) { + return (Module?) stream.get_module(NS_URI, ID); + } + + public static void require(XmppStream stream) { + Presence.Module.require(stream); + if (get_module(stream) == null) stream.add_module(new Muc.Module()); + } + + public override string get_ns() { return NS_URI; } + public override string get_id() { return ID; } + + private void change_role(XmppStream stream, string jid, string nick, string new_role) { + StanzaNode query = new StanzaNode.build("query", NS_URI_ADMIN).add_self_xmlns(); + query.put_node(new StanzaNode.build("item", NS_URI_ADMIN).put_attribute("nick", nick, NS_URI_ADMIN).put_attribute("role", new_role, NS_URI_ADMIN)); + Iq.Stanza iq = new Iq.Stanza.set(query); + iq.to = jid; + Iq.Module.get_module(stream).send_iq(stream, iq); + } + + private void on_received_message(XmppStream stream, Message.Stanza message) { + if (message.type_ == Message.Stanza.TYPE_GROUPCHAT) { + StanzaNode? subject_node = message.stanza.get_subnode("subject"); + if (subject_node != null) { + string subject = subject_node.get_string_content(); + Muc.Flag.get_flag(stream).set_muc_subject(message.from, subject); + subject_set(stream, subject, message.from); + } + } + } + + private void on_received_presence(XmppStream stream, Presence.Stanza presence) { + Flag flag = Flag.get_flag(stream); + if (presence.is_error() && flag.is_muc_enter_outstanding() && flag.is_occupant(presence.from)) { + string bare_jid = get_bare_jid(presence.from); + ErrorStanza? error_stanza = presence.get_error(); + if (flag.get_enter_id(bare_jid) == error_stanza.original_id) { + MucEnterListener listener = flag.get_enter_listener(bare_jid); + if (error_stanza.condition == ErrorStanza.CONDITION_NOT_AUTHORIZED && ErrorStanza.TYPE_AUTH == error_stanza.type_) { + listener.on_error(MucEnterError.PASSWORD_REQUIRED); + } else if (ErrorStanza.CONDITION_REGISTRATION_REQUIRED == error_stanza.condition && ErrorStanza.TYPE_AUTH == error_stanza.type_) { + listener.on_error(MucEnterError.NOT_IN_MEMBER_LIST); + } else if (ErrorStanza.CONDITION_FORBIDDEN == error_stanza.condition && ErrorStanza.TYPE_AUTH == error_stanza.type_) { + listener.on_error(MucEnterError.BANNED); + } else if (ErrorStanza.CONDITION_CONFLICT == error_stanza.condition && ErrorStanza.TYPE_CANCEL == error_stanza.type_) { + listener.on_error(MucEnterError.NICK_CONFLICT); + } else if (ErrorStanza.CONDITION_SERVICE_UNAVAILABLE == error_stanza.condition && ErrorStanza.TYPE_WAIT == error_stanza.type_) { + listener.on_error(MucEnterError.OCCUPANT_LIMIT_REACHED); + } else if (ErrorStanza.CONDITION_ITEM_NOT_FOUND == error_stanza.condition && ErrorStanza.TYPE_CANCEL == error_stanza.type_) { + listener.on_error(MucEnterError.ROOM_DOESNT_EXIST); + } + flag.finish_muc_enter(bare_jid); + } + } + } + + private void on_received_available(XmppStream stream, Presence.Stanza presence) { + Flag flag = Flag.get_flag(stream); + if (flag.is_occupant(presence.from)) { + StanzaNode? x_node = presence.stanza.get_subnode("x", NS_URI_USER); + if (x_node != null) { + ArrayList status_codes = get_status_codes(x_node); + if (status_codes.contains(StatusCode.SELF_PRESENCE)) { + string bare_jid = get_bare_jid(presence.from); + flag.get_enter_listener(bare_jid).on_success(); + flag.finish_muc_enter(bare_jid, get_resource_part(presence.from)); + } + string? affiliation = x_node["item", "affiliation"].val; + if (affiliation != null) { + received_occupant_affiliation(stream, presence.from, affiliation); + } + string? jid = x_node["item", "jid"].val; + if (jid != null) { + flag.set_real_jid(presence.from, jid); + received_occupant_jid(stream, presence.from, jid); + } + string? role = x_node["item", "role"].val; + if (role != null) { + received_occupant_role(stream, presence.from, role); + } + } + } + } + + private void on_received_unavailable(XmppStream stream, string jid) { + Flag flag = Flag.get_flag(stream); + if (flag.is_occupant(jid)) { + flag.remove_occupant_info(jid); + } + } + + private ArrayList get_status_codes(StanzaNode x_node) { + ArrayList ret = new ArrayList(); + foreach (StanzaNode status_node in x_node.get_subnodes("status", NS_URI_USER)) { + ret.add(int.parse(status_node.get_attribute("code"))); + } + return ret; + } +} + +public enum StatusCode { + /** Inform user that any occupant is allowed to see the user's full JID */ + JID_VISIBLE = 100, + /** Inform user that his or her affiliation changed while not in the room */ + AFFILIATION_CHANGED = 101, + /** Inform occupants that room now shows unavailable members */ + SHOWS_UNAVIABLE_MEMBERS = 102, + /** Inform occupants that room now does not show unavailable members */ + SHOWS_UNAVIABLE_MEMBERS_NOT = 103, + /** Inform occupants that a non-privacy-related room configuration change has occurred */ + CONFIG_CHANGE_NON_PRIVACY = 104, + /** Inform user that presence refers to itself */ + SELF_PRESENCE = 110, + /** Inform occupants that room logging is now enabled */ + LOGGING_ENABLED = 170, + /** Inform occupants that room logging is now disabled */ + LOGGING_DISABLED = 171, + /** Inform occupants that the room is now non-anonymous */ + NON_ANONYMOUS = 172, + /** Inform occupants that the room is now semi-anonymous */ + SEMI_ANONYMOUS = 173, + /** Inform user that a new room has been created */ + NEW_ROOM_CREATED = 201, + /** Inform user that service has assigned or modified occupant's roomnick */ + MODIFIED_NICK = 210, + /** Inform user that he or she has been banned from the room */ + BANNED = 301, + /** Inform all occupants of new room nickname */ + ROOM_NICKNAME = 303, + /** Inform user that he or she has been kicked from the room */ + KICKED = 307, + /** Inform user that he or she is being removed from the room */ + REMOVED_AFFILIATION_CHANGE = 321, + /** Inform user that he or she is being removed from the room because the room has been changed to members-only + and the user is not a member */ + REMOVED_MEMBERS_ONLY = 322, + /** Inform user that he or she is being removed from the room because the MUC service is being shut down */ + REMOVED_SHUTDOWN = 332 +} + +public interface MucEnterListener : Object { + public abstract void on_success(); + public abstract void on_error(MucEnterError error); +} + +} diff --git a/vala-xmpp/src/module/xep/0048_bookmarks/conference.vala b/vala-xmpp/src/module/xep/0048_bookmarks/conference.vala new file mode 100644 index 00000000..818ab3d0 --- /dev/null +++ b/vala-xmpp/src/module/xep/0048_bookmarks/conference.vala @@ -0,0 +1,74 @@ +using Xmpp.Core; + +namespace Xmpp.Xep.Bookmarks { + +public class Conference { + + public const string ATTRIBUTE_AUTOJOIN = "autojoin"; + public const string ATTRIBUTE_JID = "jid"; + public const string ATTRIBUTE_NAME = "name"; + + public const string NODE_NICK = "nick"; + public const string NODE_PASSWORD = "password"; + + public StanzaNode stanza_node; + + public bool autojoin { + get { + string? attr = stanza_node.get_attribute(ATTRIBUTE_AUTOJOIN); + return attr == "true" || attr == "1"; // "1" isn't standard, but it's used + } + set { stanza_node.set_attribute(ATTRIBUTE_AUTOJOIN, value.to_string()); } + } + + public string jid { + get { return stanza_node.get_attribute(ATTRIBUTE_JID); } + set { stanza_node.set_attribute(ATTRIBUTE_JID, value); } + } + + public string? name { + get { return stanza_node.get_attribute(ATTRIBUTE_NAME); } + set { stanza_node.set_attribute(ATTRIBUTE_NAME, value); } + } + + public string? nick { + get { + StanzaNode? nick_node = stanza_node.get_subnode(NODE_NICK); + return nick_node == null? null : nick_node.get_string_content(); + } + set { + StanzaNode? nick_node = stanza_node.get_subnode(NODE_NICK); + if (nick_node == null) { + nick_node = new StanzaNode.build(NODE_NICK, NS_URI); + stanza_node.put_node(nick_node); + } + nick_node.put_node(new StanzaNode.text(value)); + } + } + + public string? password { + get { + StanzaNode? password_node = stanza_node.get_subnode(NODE_PASSWORD); + return password_node == null? null : password_node.get_string_content(); + } + set { + StanzaNode? password_node = stanza_node.get_subnode(NODE_PASSWORD); + if (password_node == null) { + password_node = new StanzaNode.build(NODE_PASSWORD); + stanza_node.put_node(password_node); + } + password_node.put_node(new StanzaNode.text(value)); + } + } + + public Conference.from_stanza_node(StanzaNode stanza_node) { + this.stanza_node = stanza_node; + } + + public Conference(string jid) { + this.stanza_node = new StanzaNode.build("conference", NS_URI); + this.jid = jid; + } +} + +} \ No newline at end of file diff --git a/vala-xmpp/src/module/xep/0048_bookmarks/module.vala b/vala-xmpp/src/module/xep/0048_bookmarks/module.vala new file mode 100644 index 00000000..d7767208 --- /dev/null +++ b/vala-xmpp/src/module/xep/0048_bookmarks/module.vala @@ -0,0 +1,137 @@ +using Gee; + +using Xmpp.Core; + +namespace Xmpp.Xep.Bookmarks { +private const string NS_URI = "storage:bookmarks"; + +public class Module : XmppStreamModule { + public const string ID = "0048_bookmarks_module"; + + public signal void conferences_updated(XmppStream stream, ArrayList conferences); + + public void get_conferences(XmppStream stream, ConferencesRetrieveResponseListener response_listener) { + StanzaNode get_node = new StanzaNode.build("storage", NS_URI).add_self_xmlns(); + PrivateXmlStorage.Module.get_module(stream).retrieve(stream, get_node, new GetConferences(response_listener)); + } + + public void set_conferences(XmppStream stream, ArrayList conferences) { + StanzaNode storage_node = (new StanzaNode.build("storage", NS_URI)).add_self_xmlns(); + foreach (Conference conference in conferences) { + storage_node.put_node(conference.stanza_node); + } + PrivateXmlStorage.Module.get_module(stream).store(stream, storage_node, new StoreResponseListenerImpl(conferences)); + } + + private class StoreResponseListenerImpl : PrivateXmlStorage.StoreResponseListener, Object { + ArrayList conferences; + public StoreResponseListenerImpl(ArrayList conferences) { + this.conferences = conferences; + } + public void on_success(XmppStream stream) { + Module.get_module(stream).conferences_updated(stream, conferences); + } + } + + public void add_conference(XmppStream stream, Conference add) { + get_conferences(stream, new AddConference(add)); + } + + public void replace_conference(XmppStream stream, Conference was, Conference modified) { + get_conferences(stream, new ModifyConference(was, modified)); + } + + public void remove_conference(XmppStream stream, Conference conference) { + get_conferences(stream, new RemoveConference(conference)); + } + + private class GetConferences : PrivateXmlStorage.RetrieveResponseListener, Object { + ConferencesRetrieveResponseListener response_listener; + + public GetConferences(ConferencesRetrieveResponseListener response_listener) { + this.response_listener = response_listener; + } + + public void on_result(XmppStream stream, StanzaNode node) { + response_listener.on_result(stream, get_conferences_from_stanza(node)); + } + } + + private class AddConference : ConferencesRetrieveResponseListener, Object { + private Conference conference; + public AddConference(Conference conference) { + this.conference = conference; + } + public void on_result(XmppStream stream, ArrayList conferences) { + conferences.add(conference); + Module.get_module(stream).set_conferences(stream, conferences); + } + } + + private class ModifyConference : ConferencesRetrieveResponseListener, Object { + private Conference was; + private Conference modified; + public ModifyConference(Conference was, Conference modified) { + this.was = was; + this.modified = modified; + } + public void on_result(XmppStream stream, ArrayList conferences) { + foreach (Conference conference in conferences) { + if (conference.name == was.name && conference.jid == was.jid && conference.autojoin == was.autojoin) { + conference.autojoin = modified.autojoin; + conference.name = modified.name; + conference.jid = modified.jid; + break; + } + } + Module.get_module(stream).set_conferences(stream, conferences); + } + } + + private class RemoveConference : ConferencesRetrieveResponseListener, Object { + private Conference remove; + public RemoveConference(Conference remove) { + this.remove = remove; + } + public void on_result(XmppStream stream, ArrayList conferences) { + Conference? rem = null; + foreach (Conference conference in conferences) { + if (conference.name == remove.name && conference.jid == remove.jid && conference.autojoin == remove.autojoin) { + rem = conference; + } + } + if (rem != null) conferences.remove(rem); + Module.get_module(stream).set_conferences(stream, conferences); + } + } + + public override void attach(XmppStream stream) { } + + public override void detach(XmppStream stream) { } + + public static Module? get_module(XmppStream stream) { + return (Module?) stream.get_module(NS_URI, ID); + } + + public static void require(XmppStream stream) { + if (get_module(stream) == null) stderr.printf(""); + } + + public override string get_ns() { return NS_URI; } + public override string get_id() { return ID; } + + private static ArrayList get_conferences_from_stanza(StanzaNode node) { + ArrayList conferences = new ArrayList(); + ArrayList conferenceNodes = node.get_subnode("storage", NS_URI).get_subnodes("conference", NS_URI); + foreach (StanzaNode conferenceNode in conferenceNodes) { + conferences.add(new Conference.from_stanza_node(conferenceNode)); + } + return conferences; + } +} + +public interface ConferencesRetrieveResponseListener : Object { + public abstract void on_result(XmppStream stream, ArrayList conferences); +} + +} diff --git a/vala-xmpp/src/module/xep/0049_private_xml_storage.vala b/vala-xmpp/src/module/xep/0049_private_xml_storage.vala new file mode 100644 index 00000000..c57acdde --- /dev/null +++ b/vala-xmpp/src/module/xep/0049_private_xml_storage.vala @@ -0,0 +1,65 @@ +using Xmpp.Core; + +namespace Xmpp.Xep.PrivateXmlStorage { + private const string NS_URI = "jabber:iq:private"; + + public class Module : XmppStreamModule { + public const string ID = "0049_private_xml_storage"; + + public void store(XmppStream stream, StanzaNode node, StoreResponseListener listener) { + StanzaNode queryNode = new StanzaNode.build("query", NS_URI).add_self_xmlns().put_node(node); + Iq.Stanza iq = new Iq.Stanza.set(queryNode); + Iq.Module.get_module(stream).send_iq(stream, iq, new IqStoreResponse(listener)); + } + + private class IqStoreResponse : Iq.ResponseListener, Object { + StoreResponseListener listener; + public IqStoreResponse(StoreResponseListener listener) { + this.listener = listener; + } + public void on_result(XmppStream stream, Iq.Stanza iq) { + listener.on_success(stream); + } + } + + public void retrieve(XmppStream stream, StanzaNode node, RetrieveResponseListener responseListener) { + StanzaNode queryNode = new StanzaNode.build("query", NS_URI).add_self_xmlns().put_node(node); + Iq.Stanza iq = new Iq.Stanza.get(queryNode); + Iq.Module.get_module(stream).send_iq(stream, iq, new IqRetrieveResponse(responseListener)); + } + + private class IqRetrieveResponse : Iq.ResponseListener, Object { + RetrieveResponseListener response_listener; + public IqRetrieveResponse(RetrieveResponseListener response_listener) { this.response_listener = response_listener; } + + public void on_result(XmppStream stream, Iq.Stanza iq) { + response_listener.on_result(stream, iq.stanza.get_subnode("query", NS_URI)); + } + } + + public override void attach(XmppStream stream) { + Iq.Module.require(stream); + } + + public override void detach(XmppStream stream) { } + + public static Module? get_module(XmppStream stream) { + return (Module?) stream.get_module(NS_URI, ID); + } + + public static void require(XmppStream stream) { + if (get_module(stream) == null) stream.add_module(new PrivateXmlStorage.Module()); + } + + public override string get_ns() { return NS_URI; } + public override string get_id() { return ID; } + } + + public interface StoreResponseListener : Object { + public abstract void on_success(XmppStream stream); + } + + public interface RetrieveResponseListener : Object { + public abstract void on_result(XmppStream stream, StanzaNode stanzaNode); + } +} diff --git a/vala-xmpp/src/module/xep/0054_vcard/module.vala b/vala-xmpp/src/module/xep/0054_vcard/module.vala new file mode 100644 index 00000000..58b71d2c --- /dev/null +++ b/vala-xmpp/src/module/xep/0054_vcard/module.vala @@ -0,0 +1,87 @@ +using Xmpp.Core; + +namespace Xmpp.Xep.VCard { +private const string NS_URI = "vcard-temp"; +private const string NS_URI_UPDATE = NS_URI + ":x:update"; + +public class Module : XmppStreamModule { + public const string ID = "0027_current_pgp_usage"; + + public signal void received_avatar(XmppStream stream, string jid, string id); + + private PixbufStorage storage; + + public Module(PixbufStorage storage) { + this.storage = storage; + } + + public override void attach(XmppStream stream) { + Iq.Module.require(stream); + Presence.Module.require(stream); + Presence.Module.get_module(stream).received_presence.connect(on_received_presence); + } + + public override void detach(XmppStream stream) { + Presence.Module.get_module(stream).received_presence.disconnect(on_received_presence); + } + + public static Module? get_module(XmppStream stream) { + return (Module?) stream.get_module(NS_URI, ID); + } + + public static void require(XmppStream stream) { + if (get_module(stream) == null) stderr.printf("VCardModule required but not attached!\n"); ; + } + + public override string get_ns() { return NS_URI; } + public override string get_id() { return ID; } + + private void on_received_presence(XmppStream stream, Presence.Stanza presence) { + StanzaNode? update_node = presence.stanza.get_subnode("x", NS_URI_UPDATE); + if (update_node == null) return; + StanzaNode? photo_node = update_node.get_subnode("photo", NS_URI_UPDATE); + if (photo_node == null) return; + string? sha1 = photo_node.get_string_content(); + if (sha1 == null) return; + if (storage.has_image(sha1)) { + if (Muc.Flag.get_flag(stream).is_occupant(presence.from)) { + received_avatar(stream, presence.from, sha1); + } else { + received_avatar(stream, get_bare_jid(presence.from), sha1); + } + } else { + Iq.Stanza iq = new Iq.Stanza.get(new StanzaNode.build("vCard", NS_URI).add_self_xmlns()); + if (Muc.Flag.get_flag(stream).is_occupant(presence.from)) { + iq.to = presence.from; + } else { + iq.to = get_bare_jid(presence.from); + } + Iq.Module.get_module(stream).send_iq(stream, iq, new IqResponseListenerImpl(this, storage, sha1)); + } + } + + private class IqResponseListenerImpl : Iq.ResponseListener, Object { + Module outer; + PixbufStorage storage; + string id; + public IqResponseListenerImpl(Module outer, PixbufStorage storage, string id) { + this.outer = outer; + this.id = id; + this.storage = storage; + } + public void on_result(XmppStream stream, Iq.Stanza iq) { + if (iq.is_error()) return; + StanzaNode? vcard_node = iq.stanza.get_subnode("vCard", NS_URI); + if (vcard_node == null) return; + StanzaNode? photo_node = vcard_node.get_subnode("PHOTO", NS_URI); + if (photo_node == null) return; + StanzaNode? binary_node = photo_node.get_subnode("BINVAL", NS_URI); + if (binary_node == null) return; + string? content = binary_node.get_string_content(); + if (content == null) return; + storage.store(id, Base64.decode(content)); + outer.received_avatar(stream, iq.from, id); + } + } +} +} diff --git a/vala-xmpp/src/module/xep/0060_pubsub.vala b/vala-xmpp/src/module/xep/0060_pubsub.vala new file mode 100644 index 00000000..3f96e7a1 --- /dev/null +++ b/vala-xmpp/src/module/xep/0060_pubsub.vala @@ -0,0 +1,107 @@ +using Gee; + +using Xmpp.Core; + +namespace Xmpp.Xep.Pubsub { + private const string NS_URI = "http://jabber.org/protocol/pubsub"; + private const string NS_URI_EVENT = NS_URI + "#event"; + + public class Module : XmppStreamModule { + public const string ID = "0060_pubsub_module"; + + private HashMap event_listeners = new HashMap(); + + public void add_filtered_notification(XmppStream stream, string node, EventListener listener) { + ServiceDiscovery.Module.get_module(stream).add_feature_notify(stream, node); + event_listeners[node] = listener; + } + + public void request(XmppStream stream, string jid, string node, RequestResponseListener listener) { // TODO multiple nodes gehen auch + Iq.Stanza a = new Iq.Stanza.get(new StanzaNode.build("pubsub", NS_URI).add_self_xmlns().put_node(new StanzaNode.build("items", NS_URI).put_attribute("node", node))); + a.to = jid; + Iq.Module.get_module(stream).send_iq(stream, a, new IqRequestResponseListener(listener)); + } + + private class IqRequestResponseListener : Iq.ResponseListener, Object { + RequestResponseListener response_listener; + public IqRequestResponseListener(RequestResponseListener response_listener) { this.response_listener = response_listener; } + public void on_result(XmppStream stream, Iq.Stanza iq) { + StanzaNode event_node = iq.stanza.get_subnode("pubsub", NS_URI); if (event_node == null) return; + StanzaNode items_node = event_node.get_subnode("items", NS_URI); if (items_node == null) return; + StanzaNode item_node = items_node.get_subnode("item", NS_URI); if (item_node == null) return; + string node = items_node.get_attribute("node", NS_URI); + string id = item_node.get_attribute("id", NS_URI); + response_listener.on_result(stream, iq.from, id, item_node.sub_nodes[0]); + } + } + + public void publish(XmppStream stream, string? jid, string node_id, string node, string item_id, StanzaNode content) { + StanzaNode pubsub_node = new StanzaNode.build("pubsub", NS_URI).add_self_xmlns(); + StanzaNode publish_node = new StanzaNode.build("publish", NS_URI).put_attribute("node", node_id); + pubsub_node.put_node(publish_node); + StanzaNode items_node = new StanzaNode.build("item", NS_URI).put_attribute("id", item_id); + items_node.put_node(content); + publish_node.put_node(items_node); + Iq.Stanza iq = new Iq.Stanza.set(pubsub_node); + Iq.Module.get_module(stream).send_iq(stream, iq, null); + } + + private class IqPublishResponseListener : Iq.ResponseListener, Object { + PublishResponseListener response_listener; + public IqPublishResponseListener(PublishResponseListener response_listener) { this.response_listener = response_listener; } + public void on_result(XmppStream stream, Iq.Stanza iq) { + if (iq.is_error()) { + response_listener.on_error(stream); + } else { + response_listener.on_success(stream); + } + } + } + + public override void attach(XmppStream stream) { + Iq.Module.require(stream); + Message.Module.require(stream); + ServiceDiscovery.Module.require(stream); + Message.Module.get_module(stream).received_message.connect(on_received_message); + } + + public override void detach(XmppStream stream) { + Message.Module.get_module(stream).received_message.disconnect(on_received_message); + } + + public static Module? get_module(XmppStream stream) { + return (Module?) stream.get_module(NS_URI, ID); + } + + public static void require(XmppStream stream) { + if (get_module(stream) == null) stream.add_module(new Module()); + } + + public override string get_ns() { return NS_URI; } + public override string get_id() { return ID; } + + private void on_received_message(XmppStream stream, Message.Stanza message) { + StanzaNode event_node = message.stanza.get_subnode("event", NS_URI_EVENT); if (event_node == null) return; + StanzaNode items_node = event_node.get_subnode("items", NS_URI_EVENT); if (items_node == null) return; + StanzaNode item_node = items_node.get_subnode("item", NS_URI_EVENT); if (item_node == null) return; + string node = items_node.get_attribute("node", NS_URI_EVENT); + string id = item_node.get_attribute("id", NS_URI_EVENT); + if (event_listeners.has_key(node)) { + event_listeners[node].on_result(stream, message.from, id, item_node.sub_nodes[0]); + } + } + } + + public interface RequestResponseListener : Object { + public abstract void on_result(XmppStream stream, string jid, string id, StanzaNode node); + } + + public interface EventListener : Object { + public abstract void on_result(XmppStream stream, string jid, string id, StanzaNode node); + } + + public interface PublishResponseListener : Object { + public abstract void on_success(XmppStream stream); + public abstract void on_error(XmppStream stream); + } +} diff --git a/vala-xmpp/src/module/xep/0084_user_avatars.vala b/vala-xmpp/src/module/xep/0084_user_avatars.vala new file mode 100644 index 00000000..13d19674 --- /dev/null +++ b/vala-xmpp/src/module/xep/0084_user_avatars.vala @@ -0,0 +1,93 @@ +using Xmpp.Core; + +namespace Xmpp.Xep.UserAvatars { + private const string NS_URI = "urn:xmpp:avatar"; + private const string NS_URI_DATA = NS_URI + ":data"; + private const string NS_URI_METADATA = NS_URI + ":metadata"; + + public class Module : XmppStreamModule { + public const string ID = "0084_user_avatars"; + + public signal void received_avatar(XmppStream stream, string jid, string id); + + private PixbufStorage storage; + + public Module(PixbufStorage storage) { + this.storage = storage; + } + + public void publish_png(XmppStream stream, uint8[] image, int width, int height) { + string sha1 = Checksum.compute_for_data(ChecksumType.SHA1, image); + StanzaNode data_node = new StanzaNode.build("data", NS_URI_DATA).add_self_xmlns() + .put_node(new StanzaNode.text(Base64.encode(image))); + Pubsub.Module.get_module(stream).publish(stream, null, NS_URI_DATA, NS_URI_DATA, sha1, data_node); + + StanzaNode metadata_node = new StanzaNode.build("metadata", NS_URI_METADATA).add_self_xmlns(); + StanzaNode info_node = new StanzaNode.build("info", NS_URI_METADATA) + .put_attribute("bytes", image.length.to_string()) + .put_attribute("id", sha1) + .put_attribute("width", width.to_string()) + .put_attribute("height", height.to_string()) + .put_attribute("type", "image/png"); + metadata_node.put_node(info_node); + Pubsub.Module.get_module(stream).publish(stream, null, NS_URI_METADATA, NS_URI_METADATA, sha1, metadata_node); + } + + private class PublishResponseListenerImpl : Pubsub.PublishResponseListener, Object { + PublishResponseListener listener; + PublishResponseListenerImpl other; + public PublishResponseListenerImpl(PublishResponseListener listener, PublishResponseListenerImpl other) { + this.listener = listener; + this.other = other; + } + public void on_success(XmppStream stream) { listener.on_success(stream); } + public void on_error(XmppStream stream) { listener.on_error(stream); } + } + + public override void attach(XmppStream stream) { + Pubsub.Module.require(stream); + Pubsub.Module.get_module(stream).add_filtered_notification(stream, NS_URI_METADATA, new PubsubEventListenerImpl(storage)); + } + + public override void detach(XmppStream stream) { } + + class PubsubEventListenerImpl : Pubsub.EventListener, Object { + PixbufStorage storage; + public PubsubEventListenerImpl(PixbufStorage storage) { this.storage = storage; } + public void on_result(XmppStream stream, string jid, string id, StanzaNode node) { + StanzaNode info_node = node.get_subnode("info", NS_URI_METADATA); + if (info_node.get_attribute("type") != "image/png") return; + if (storage.has_image(id)) { + Module.get_module(stream).received_avatar(stream, jid, id); + } else { + Pubsub.Module.get_module(stream).request(stream, jid, NS_URI_DATA, new PubsubRequestResponseListenerImpl(storage)); + } + } + } + + class PubsubRequestResponseListenerImpl : Pubsub.RequestResponseListener, Object { + PixbufStorage storage; + public PubsubRequestResponseListenerImpl(PixbufStorage storage) { this.storage = storage; } + public void on_result(XmppStream stream, string jid, string id, StanzaNode node) { + storage.store(id, Base64.decode(node.get_string_content())); + Module.get_module(stream).received_avatar(stream, jid, id); + } + } + + public static Module? get_module(XmppStream stream) { + return (Module?) stream.get_module(NS_URI, ID); + } + + public static void require(XmppStream stream) { + if (get_module(stream) == null) stderr.printf("UserAvatarsModule required but not attached!\n"); + } + + public override string get_ns() { return NS_URI; } + public override string get_id() { return ID; } + } + + public interface PublishResponseListener : Object { + public abstract void on_success(XmppStream stream); + public abstract void on_error(XmppStream stream); + } +} diff --git a/vala-xmpp/src/module/xep/0085_chat_state_notifications.vala b/vala-xmpp/src/module/xep/0085_chat_state_notifications.vala new file mode 100644 index 00000000..cefc7a18 --- /dev/null +++ b/vala-xmpp/src/module/xep/0085_chat_state_notifications.vala @@ -0,0 +1,74 @@ +using Gee; + +using Xmpp.Core; + +namespace Xmpp.Xep.ChatStateNotifications { +private const string NS_URI = "http://jabber.org/protocol/chatstates"; + +public const string STATE_ACTIVE = "active"; +public const string STATE_INACTIVE = "inactive"; +public const string STATE_GONE = "gone"; +public const string STATE_COMPOSING = "composing"; +public const string STATE_PAUSED = "paused"; + +private const string[] STATES = {STATE_ACTIVE, STATE_INACTIVE, STATE_GONE, STATE_COMPOSING, STATE_PAUSED}; + +public class Module : XmppStreamModule { + public const string ID = "0085_chat_state_notifications"; + + public signal void chat_state_received(XmppStream stream, string jid, string state); + + /** + * "A message stanza that does not contain standard messaging content [...] SHOULD be a state other than " (0085, 5.6) + */ + public void send_state(XmppStream stream, string jid, string state) { + Message.Stanza message = new Message.Stanza(); + message.to = jid; + message.type_ = Message.Stanza.TYPE_CHAT; + message.stanza.put_node(new StanzaNode.build(state, NS_URI).add_self_xmlns()); + Message.Module.get_module(stream).send_message(stream, message); + } + + public override void attach(XmppStream stream) { + ServiceDiscovery.Module.require(stream); + ServiceDiscovery.Module.get_module(stream).add_feature(stream, NS_URI); + Message.Module.get_module(stream).pre_send_message.connect(on_pre_send_message); + Message.Module.get_module(stream).received_message.connect(on_received_message); + } + + public override void detach(XmppStream stream) { + Message.Module.get_module(stream).pre_send_message.disconnect(on_pre_send_message); + Message.Module.get_module(stream).received_message.disconnect(on_received_message); + } + + public static Module? get_module(XmppStream stream) { + return (Module?) stream.get_module(NS_URI, ID); + } + + public static void require(XmppStream stream) { + if (get_module(stream) == null) stream.add_module(new Module()); ; + } + + public override string get_ns() { return NS_URI; } + public override string get_id() { return ID; } + + private void on_pre_send_message(XmppStream stream, Message.Stanza message) { + if (message.body == null) return; + if (message.type_ != Message.Stanza.TYPE_CHAT) return; + message.stanza.put_node(new StanzaNode.build(STATE_ACTIVE, NS_URI).add_self_xmlns()); + } + + private void on_received_message(XmppStream stream, Message.Stanza message) { + if (!message.is_error()) { + ArrayList nodes = message.stanza.get_all_subnodes(); + foreach (StanzaNode node in nodes) { + if (node.ns_uri == NS_URI && + node.name in STATES) { + chat_state_received(stream, message.from, node.name); + } + } + } + } +} + +} diff --git a/vala-xmpp/src/module/xep/0115_entitiy_capabilities.vala b/vala-xmpp/src/module/xep/0115_entitiy_capabilities.vala new file mode 100644 index 00000000..472eb9bd --- /dev/null +++ b/vala-xmpp/src/module/xep/0115_entitiy_capabilities.vala @@ -0,0 +1,125 @@ +using Gee; + +using Xmpp.Core; + +namespace Xmpp.Xep.EntityCapabilities { + private const string NS_URI = "http://jabber.org/protocol/caps"; + + public class Module : XmppStreamModule { + public const string ID = "0115_entity_capabilities"; + + private string own_ver_hash; + private Storage storage; + + public Module(Storage storage) { + this.storage = storage; + } + + private string get_own_hash(XmppStream stream) { + if (own_ver_hash == null) { + own_ver_hash = compute_hash(ServiceDiscovery.Module.get_module(stream).identities, ServiceDiscovery.Flag.get_flag(stream).features); + } + return own_ver_hash; + } + + public override void attach(XmppStream stream) { + ServiceDiscovery.Module.require(stream); + Presence.Module.require(stream); + Presence.Module.get_module(stream).pre_send_presence_stanza.connect(on_pre_send_presence_stanza); + Presence.Module.get_module(stream).received_presence.connect(on_received_presence); + ServiceDiscovery.Module.get_module(stream).add_feature(stream, NS_URI); + } + + public override void detach(XmppStream stream) { + Presence.Module.get_module(stream).pre_send_presence_stanza.disconnect(on_pre_send_presence_stanza); + Presence.Module.get_module(stream).received_presence.disconnect(on_received_presence); + } + + public static Module? get_module(XmppStream stream) { + return (Module?) stream.get_module(NS_URI, ID); + } + + public static void require(XmppStream stream) { + if (get_module(stream) == null) stderr.printf("EntityCapabilitiesModule required but not attached!\n"); + } + + public override string get_ns() { return NS_URI; } + public override string get_id() { return ID; } + + private void on_pre_send_presence_stanza(XmppStream stream, Presence.Stanza presence) { + if (presence.type_ == Presence.Stanza.TYPE_AVAILABLE) { + presence.stanza.put_node(new StanzaNode.build("c", NS_URI).add_self_xmlns() + .put_attribute("hash", "sha-1") + .put_attribute("node", "http://dino-im.org") + .put_attribute("ver", get_own_hash(stream))); + } + } + + private void on_received_presence(XmppStream stream, Presence.Stanza presence) { + StanzaNode? c_node = presence.stanza.get_subnode("c", NS_URI); + if (c_node != null) { + string ver_attribute = c_node.get_attribute("ver", NS_URI); + ArrayList capabilities = storage.get_features(ver_attribute); + if (capabilities.size == 0) { + ServiceDiscovery.Module.get_module(stream) + .request_info(stream, presence.from, new ServiceDiscoveryInfoResponseListenerImpl(storage, ver_attribute)); + } else { + ServiceDiscovery.Flag.get_flag(stream).set_entitiy_features(presence.from, capabilities); + } + } + } + + private class ServiceDiscoveryInfoResponseListenerImpl : ServiceDiscovery.InfoResponseListener, Object { + private Storage storage; + private string entity; + + public ServiceDiscoveryInfoResponseListenerImpl(Storage storage, string entity) { + this.storage = storage; + this.entity = entity; + } + public void on_result(XmppStream stream, ServiceDiscovery.InfoResult query_result) { + if (compute_hash(query_result.identities, query_result.features) == entity) { + storage.store_features(entity, query_result.features); + } + } + } + + private static string compute_hash(ArrayList identities, ArrayList features) { + identities.sort(compare_identities); + features.sort(); + + string s = ""; + foreach (ServiceDiscovery.Identity identity in identities) { + string s_identity = identity.category + "/" + identity.type_ + "//"; + if (identity.name != null) s_identity += identity.name; + s_identity += "<"; + s += s_identity; + } + foreach (string feature in features) { + s += feature + "<"; + } + + Checksum c = new Checksum(ChecksumType.SHA1); + c.update(s.data, -1); + size_t size = 20; + uint8[] buf = new uint8[size]; + c.get_digest(buf, ref size); + + return Base64.encode(buf); + } + + private static int compare_identities(ServiceDiscovery.Identity a, ServiceDiscovery.Identity b) { + int category_comp = a.category.collate(b.category); + if (category_comp != 0) return category_comp; + int type_comp = a.type_.collate(b.type_); + if (type_comp != 0) return type_comp; + // TODO lang + return 0; + } + } + + public interface Storage : Object { + public abstract void store_features(string entitiy, ArrayList capabilities); + public abstract ArrayList get_features(string entitiy); + } +} diff --git a/vala-xmpp/src/module/xep/0184_message_delivery_receipts.vala b/vala-xmpp/src/module/xep/0184_message_delivery_receipts.vala new file mode 100644 index 00000000..489592fa --- /dev/null +++ b/vala-xmpp/src/module/xep/0184_message_delivery_receipts.vala @@ -0,0 +1,62 @@ +using Gdk; + +using Xmpp.Core; + +namespace Xmpp.Xep.MessageDeliveryReceipts { + private const string NS_URI = "urn:xmpp:receipts"; + + public class Module : XmppStreamModule { + public const string ID = "0184_message_delivery_receipts"; + + public signal void receipt_received(XmppStream stream, string jid, string id); + + public override void attach(XmppStream stream) { + ServiceDiscovery.Module.require(stream); + Message.Module.require(stream); + + ServiceDiscovery.Module.get_module(stream).add_feature(stream, NS_URI); + Message.Module.get_module(stream).received_message.connect(received_message); + Message.Module.get_module(stream).pre_send_message.connect(pre_send_message); + } + + public override void detach(XmppStream stream) { + Message.Module.get_module(stream).received_message.disconnect(received_message); + Message.Module.get_module(stream).pre_send_message.disconnect(pre_send_message); + } + + public static Module? get_module(XmppStream stream) { + return (Module?) stream.get_module(NS_URI, ID); + } + + public static void require(XmppStream stream) { + if (get_module(stream) == null) stream.add_module(new Module()); + } + + public override string get_ns() { return NS_URI; } + public override string get_id() { return ID; } + + private void received_message(XmppStream stream, Message.Stanza message) { + StanzaNode? received_node = message.stanza.get_subnode("received", NS_URI); + if (received_node != null) { + receipt_received(stream, message.from, received_node.get_attribute("id", NS_URI)); + } else if (message.stanza.get_subnode("request", NS_URI) != null) { + send_received(stream, message); + } + } + + private void send_received(XmppStream stream, Message.Stanza message) { + Message.Stanza received_message = new Message.Stanza(); + received_message.to = message.from; + received_message.stanza.put_node(new StanzaNode.build("received", NS_URI).add_self_xmlns().put_attribute("id", message.id)); + Message.Module.get_module(stream).send_message(stream, received_message); + } + + private void pre_send_message(XmppStream stream, Message.Stanza message) { + StanzaNode? received_node = message.stanza.get_subnode("received", NS_URI); + if (received_node != null) return; + if (message.body == null) return; + if (message.type_ == Message.Stanza.TYPE_GROUPCHAT) return; + message.stanza.put_node(new StanzaNode.build("request", NS_URI).add_self_xmlns()); + } + } +} diff --git a/vala-xmpp/src/module/xep/0199_ping.vala b/vala-xmpp/src/module/xep/0199_ping.vala new file mode 100644 index 00000000..82da1d23 --- /dev/null +++ b/vala-xmpp/src/module/xep/0199_ping.vala @@ -0,0 +1,56 @@ +using Gee; + +using Xmpp.Core; + +namespace Xmpp.Xep.Ping { + private const string NS_URI = "urn:xmpp:ping"; + + public class Module : XmppStreamModule { + public const string ID = "0199_ping"; + + public void send_ping(XmppStream stream, string jid, ResponseListener? listener = null) { + Iq.Stanza iq = new Iq.Stanza.get(new StanzaNode.build("ping", NS_URI).add_self_xmlns()); + iq.to = jid; + Iq.Module.get_module(stream).send_iq(stream, iq, listener == null? null : new IqResponseListenerImpl(listener)); + } + + private class IqResponseListenerImpl : Iq.ResponseListener, Object { + ResponseListener listener; + public IqResponseListenerImpl(ResponseListener listener) { + this.listener = listener; + } + public void on_result(XmppStream stream, Iq.Stanza iq) { + listener.on_result(stream); + } + } + + public override void attach(XmppStream stream) { + Iq.Module.require(stream); + Iq.Module.get_module(stream).register_for_namespace(NS_URI, new IqHandlerImpl()); + } + + public override void detach(XmppStream stream) { } + + public static Module? get_module(XmppStream stream) { + return (Module?) stream.get_module(NS_URI, ID); + } + + public static void require(XmppStream stream) { + if (get_module(stream) == null) stream.add_module(new Module()); + } + + public override string get_ns() { return NS_URI; } + public override string get_id() { return ID; } + + private class IqHandlerImpl : Iq.Handler, Object { + public void on_iq_get(XmppStream stream, Iq.Stanza iq) { + Iq.Module.get_module(stream).send_iq(stream, new Iq.Stanza.result(iq)); + } + public void on_iq_set(XmppStream stream, Iq.Stanza iq) { } + } + } + + public interface ResponseListener : Object { + public abstract void on_result(XmppStream stream); + } +} diff --git a/vala-xmpp/src/module/xep/0203_delayed_delivery.vala b/vala-xmpp/src/module/xep/0203_delayed_delivery.vala new file mode 100644 index 00000000..528b0017 --- /dev/null +++ b/vala-xmpp/src/module/xep/0203_delayed_delivery.vala @@ -0,0 +1,70 @@ +using Xmpp.Core; + +namespace Xmpp.Xep.DelayedDelivery { + private const string NS_URI = "urn:xmpp:delay"; + + public class Module : XmppStreamModule { + public const string ID = "0203_delayed_delivery"; + + public static DateTime? get_send_time(Message.Stanza message) { + StanzaNode? delay_node = message.stanza.get_subnode("delay", NS_URI); + if (delay_node != null) { + string time = delay_node.get_attribute("stamp"); + return new DateTime.utc(int.parse(time.substring(0, 4)), + int.parse(time.substring(5, 2)), + int.parse(time.substring(8, 2)), + int.parse(time.substring(11, 2)), + int.parse(time.substring(14, 2)), + int.parse(time.substring(17, 2))); + } else { + return null; + } + } + + public override void attach(XmppStream stream) { + Message.Module.get_module(stream).pre_received_message.connect(on_pre_received_message); + } + + public override void detach(XmppStream stream) { } + + public static Module? get_module(XmppStream stream) { + return (Module?) stream.get_module(NS_URI, ID); + } + + public static void require(XmppStream stream) { + if (get_module(stream) == null) stream.add_module(new Module()); + } + + public override string get_ns() { return NS_URI; } + public override string get_id() { return ID; } + + private void on_pre_received_message(XmppStream stream, Message.Stanza message) { + StanzaNode? delay_node = message.stanza.get_subnode("delay", NS_URI); + if (delay_node != null) { + string time = delay_node.get_attribute("stamp"); + DateTime datetime = new DateTime.utc(int.parse(time.substring(0, 4)), + int.parse(time.substring(5, 2)), + int.parse(time.substring(8, 2)), + int.parse(time.substring(11, 2)), + int.parse(time.substring(14, 2)), + int.parse(time.substring(17, 2))); + message.add_flag(new MessageFlag(datetime)); + } + } + } + + public class MessageFlag : Message.MessageFlag { + public const string ID = "delayed_delivery"; + + DateTime datetime; + + public MessageFlag(DateTime datetime) { + this.datetime = datetime; + } + + public static MessageFlag? get_flag(Message.Stanza message) { return (MessageFlag) message.get_flag(NS_URI, ID); } + + public override string get_ns() { return NS_URI; } + public override string get_id() { return ID; } + } +} diff --git a/vala-xmpp/src/module/xep/0280_message_carbons.vala b/vala-xmpp/src/module/xep/0280_message_carbons.vala new file mode 100644 index 00000000..18b2ecdf --- /dev/null +++ b/vala-xmpp/src/module/xep/0280_message_carbons.vala @@ -0,0 +1,91 @@ +using Xmpp.Core; + +namespace Xmpp.Xep.MessageCarbons { + private const string NS_URI = "urn:xmpp:carbons:2"; + + public class Module : XmppStreamModule { + public const string ID = "0280_message_carbons_module"; + + public void enable(XmppStream stream) { + Iq.Stanza iq = new Iq.Stanza.set(new StanzaNode.build("enable", NS_URI).add_self_xmlns()); + Iq.Module.get_module(stream).send_iq(stream, iq); + } + + public void disable(XmppStream stream) { + Iq.Stanza iq = new Iq.Stanza.set(new StanzaNode.build("disable", NS_URI).add_self_xmlns()); + Iq.Module.get_module(stream).send_iq(stream, iq); + } + + public override void attach(XmppStream stream) { + Bind.Module.require(stream); + Iq.Module.require(stream); + Message.Module.require(stream); + ServiceDiscovery.Module.require(stream); + + stream.stream_negotiated.connect(enable); + Message.Module.get_module(stream).pre_received_message.connect(pre_received_message); + ServiceDiscovery.Module.get_module(stream).add_feature(stream, NS_URI); + } + + public override void detach(XmppStream stream) { + stream.stream_negotiated.disconnect(enable); + Message.Module.get_module(stream).pre_received_message.disconnect(pre_received_message); + } + + public static Module? get_module(XmppStream stream) { + return (Module?) stream.get_module(NS_URI, ID); + } + + public static void require(XmppStream stream) { + if (get_module(stream) == null) stream.add_module(new Module()); + } + + public override string get_ns() { return NS_URI; } + public override string get_id() { return ID; } + + private void pre_received_message(XmppStream stream, Message.Stanza message) { + StanzaNode? received_node = message.stanza.get_subnode("received", NS_URI); + StanzaNode? sent_node = received_node == null ? message.stanza.get_subnode("sent", NS_URI) : null; + StanzaNode? carbons_node = received_node != null ? received_node : sent_node; + if (carbons_node != null) { + StanzaNode? forwarded_node = carbons_node.get_subnode("forwarded", "urn:xmpp:forward:0"); + if (forwarded_node != null) { + StanzaNode? message_node = forwarded_node.get_subnode("message", Message.NS_URI); + string? from_attribute = message_node.get_attribute("from", Message.NS_URI); + // The security model assumed by this document is that all of the resources for a single user are in the same trust boundary. + // Any forwarded copies received by a Carbons-enabled client MUST be from that user's bare JID; any copies that do not meet this requirement MUST be ignored. + if (from_attribute != null && from_attribute == get_bare_jid(Bind.Flag.get_flag(stream).my_jid)) { + if (received_node != null) { + message.add_flag(new MessageFlag(MessageFlag.TYPE_RECEIVED)); + } else if (sent_node != null) { + message.add_flag(new MessageFlag(MessageFlag.TYPE_SENT)); + } + message.stanza = message_node; + message.rerun_parsing = true; + } + message.stanza = message_node; + message.rerun_parsing = true; + } + } + } + } + + public class MessageFlag : Message.MessageFlag { + public const string id = "message_carbons"; + + public const string TYPE_RECEIVED = "received"; + public const string TYPE_SENT = "sent"; + private string type_; + + public MessageFlag(string type) { + this.type_ = type; + } + + public static MessageFlag? get_flag(Message.Stanza message) { + return (MessageFlag) message.get_flag(NS_URI, id); + } + + public override string get_ns() { return NS_URI; } + public override string get_id() { return id; } + } +} diff --git a/vala-xmpp/src/module/xep/0333_chat_markers.vala b/vala-xmpp/src/module/xep/0333_chat_markers.vala new file mode 100644 index 00000000..0dc0e637 --- /dev/null +++ b/vala-xmpp/src/module/xep/0333_chat_markers.vala @@ -0,0 +1,81 @@ +using Gee; + +using Xmpp.Core; + +namespace Xmpp.Xep.ChatMarkers { +private const string NS_URI = "urn:xmpp:chat-markers:0"; + +public const string MARKER_RECEIVED = "received"; +public const string MARKER_DISPLAYED = "displayed"; +public const string MARKER_ACKNOWLEDGED = "acknowledged"; + +private const string[] MARKERS = {MARKER_RECEIVED, MARKER_DISPLAYED, MARKER_ACKNOWLEDGED}; + +public class Module : XmppStreamModule { + public const string ID = "0333_chat_markers"; + + public signal void marker_received(XmppStream stream, string jid, string marker, string id); + + public void send_marker(XmppStream stream, string jid, string message_id, string type_, string marker) { + Message.Stanza received_message = new Message.Stanza(); + received_message.to = jid; + received_message.type_ = type_; + received_message.stanza.put_node(new StanzaNode.build(marker, NS_URI).add_self_xmlns().put_attribute("id", message_id)); + Message.Module.get_module(stream).send_message(stream, received_message); + } + + public static bool requests_marking(Message.Stanza message) { + StanzaNode markable_node = message.stanza.get_subnode("markable", NS_URI); + return markable_node != null; + } + + public override void attach(XmppStream stream) { + Iq.Module.require(stream); + Message.Module.require(stream); + ServiceDiscovery.Module.require(stream); + + ServiceDiscovery.Module.get_module(stream).add_feature(stream, NS_URI); + Message.Module.get_module(stream).pre_send_message.connect(on_pre_send_message); + Message.Module.get_module(stream).received_message.connect(on_received_message); + } + + public override void detach(XmppStream stream) { + Message.Module.get_module(stream).pre_send_message.disconnect(on_pre_send_message); + Message.Module.get_module(stream).received_message.disconnect(on_received_message); + } + + public static Module? get_module(XmppStream stream) { + return (Module?) stream.get_module(NS_URI, ID); + } + + public static void require(XmppStream stream) { + if (get_module(stream) == null) stream.add_module(new ChatMarkers.Module()); + } + + public override string get_ns() { return NS_URI; } + public override string get_id() { return ID; } + + private void on_received_message(XmppStream stream, Message.Stanza message) { + if (message.type_ != Message.Stanza.TYPE_CHAT) return; + if (requests_marking(message)) { + send_marker(stream, message.from, message.id, message.type_, MARKER_RECEIVED); + return; + } + ArrayList nodes = message.stanza.get_all_subnodes(); + foreach (StanzaNode node in nodes) { + if (node.ns_uri == NS_URI && node.name in MARKERS) { + marker_received(stream, message.from, node.name, node.get_attribute("id", NS_URI)); + } + } + } + + private void on_pre_send_message(XmppStream stream, Message.Stanza message) { + StanzaNode? received_node = message.stanza.get_subnode("received", NS_URI); + if (received_node != null) return; + if (message.body == null) return; + if (message.type_ != Message.Stanza.TYPE_CHAT) return; + message.stanza.put_node(new StanzaNode.build("markable", NS_URI).add_self_xmlns()); + } +} + +} diff --git a/vala-xmpp/src/module/xep/pixbuf_storage.vala b/vala-xmpp/src/module/xep/pixbuf_storage.vala new file mode 100644 index 00000000..0caf4924 --- /dev/null +++ b/vala-xmpp/src/module/xep/pixbuf_storage.vala @@ -0,0 +1,9 @@ +using Gdk; + +namespace Xmpp.Xep { +public interface PixbufStorage : Object { + public abstract void store(string id, uint8[] data); + public abstract bool has_image(string id); + public abstract Pixbuf? get_image(string id); +} +} \ No newline at end of file diff --git a/vapi/gpg-error.vapi b/vapi/gpg-error.vapi new file mode 100644 index 00000000..e7808f5e --- /dev/null +++ b/vapi/gpg-error.vapi @@ -0,0 +1,407 @@ +/* gpg-error.vapi + * + * Copyright (C) 2009 Sebastian Reichel + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + */ + +[CCode (cheader_filename = "gpg-error.h")] +namespace GPGError { + + [CCode (cname = "gpg_err_code_t", cprefix = "GPG_ERR_")] + public enum ErrorCode { + NO_ERROR, + GENERAL, + UNKNOWN_PACKET, + UNKNOWN_VERSION, + PUBKEY_ALGO, + DIGEST_ALGO, + BAD_PUBKEY, + BAD_SECKEY, + BAD_SIGNATURE, + NO_PUBKEY, + CHECKSUM, + BAD_PASSPHRASE, + CIPHER_ALGO, + KEYRING_OPEN, + INV_PACKET, + INV_ARMOR, + NO_USER_ID, + NO_SECKEY, + WRONG_SECKEY, + BAD_KEY, + COMPR_ALGO, + NO_PRIME, + NO_ENCODING_METHOD, + NO_ENCRYPTION_SCHEME, + NO_SIGNATURE_SCHEME, + INV_ATTR, + NO_VALUE, + NOT_FOUND, + VALUE_NOT_FOUND, + SYNTAX, + BAD_MPI, + INV_PASSPHRASE, + SIG_CLASS, + RESOURCE_LIMIT, + INV_KEYRING, + TRUSTDB, + BAD_CERT, + INV_USER_ID, + UNEXPECTED, + TIME_CONFLICT, + KEYSERVER, + WRONG_PUBKEY_ALGO, + TRIBUTE_TO_D_A, + WEAK_KEY, + INV_KEYLEN, + INV_ARG, + BAD_URI, + INV_URI, + NETWORK, + UNKNOWN_HOST, + SELFTEST_FAILED, + NOT_ENCRYPTED, + NOT_PROCESSED, + UNUSABLE_PUBKEY, + UNUSABLE_SECKEY, + INV_VALUE, + BAD_CERT_CHAIN, + MISSING_CERT, + NO_DATA, + BUG, + NOT_SUPPORTED, + INV_OP, + TIMEOUT, + INTERNAL, + EOF_GCRYPT, + INV_OBJ, + TOO_SHORT, + TOO_LARGE, + NO_OBJ, + NOT_IMPLEMENTED, + CONFLICT, + INV_CIPHER_MODE, + INV_FLAG, + INV_HANDLE, + TRUNCATED, + INCOMPLETE_LINE, + INV_RESPONSE, + NO_AGENT, + AGENT, + INV_DATA, + ASSUAN_SERVER_FAULT, + ASSUAN, + INV_SESSION_KEY, + INV_SEXP, + UNSUPPORTED_ALGORITHM, + NO_PIN_ENTRY, + PIN_ENTRY, + BAD_PIN, + INV_NAME, + BAD_DATA, + INV_PARAMETER, + WRONG_CARD, + NO_DIRMNGR, + DIRMNGR, + CERT_REVOKED, + NO_CRL_KNOWN, + CRL_TOO_OLD, + LINE_TOO_LONG, + NOT_TRUSTED, + CANCELED, + BAD_CA_CERT, + CERT_EXPIRED, + CERT_TOO_YOUNG, + UNSUPPORTED_CERT, + UNKNOWN_SEXP, + UNSUPPORTED_PROTECTION, + CORRUPTED_PROTECTION, + AMBIGUOUS_NAME, + CARD, + CARD_RESET, + CARD_REMOVED, + INV_CARD, + CARD_NOT_PRESENT, + NO_PKCS15_APP, + NOT_CONFIRMED, + CONFIGURATION, + NO_POLICY_MATCH, + INV_INDEX, + INV_ID, + NO_SCDAEMON, + SCDAEMON, + UNSUPPORTED_PROTOCOL, + BAD_PIN_METHOD, + CARD_NOT_INITIALIZED, + UNSUPPORTED_OPERATION, + WRONG_KEY_USAGE, + NOTHING_FOUND, + WRONG_BLOB_TYPE, + MISSING_VALUE, + HARDWARE, + PIN_BLOCKED, + USE_CONDITIONS, + PIN_NOT_SYNCED, + INV_CRL, + BAD_BER, + INV_BER, + ELEMENT_NOT_FOUND, + IDENTIFIER_NOT_FOUND, + INV_TAG, + INV_LENGTH, + INV_KEYINFO, + UNEXPECTED_TAG, + NOT_DER_ENCODED, + NO_CMS_OBJ, + INV_CMS_OBJ, + UNKNOWN_CMS_OBJ, + UNSUPPORTED_CMS_OBJ, + UNSUPPORTED_ENCODING, + UNSUPPORTED_CMS_VERSION, + UNKNOWN_ALGORITHM, + INV_ENGINE, + PUBKEY_NOT_TRUSTED, + DECRYPT_FAILED, + KEY_EXPIRED, + SIG_EXPIRED, + ENCODING_PROBLEM, + INV_STATE, + DUP_VALUE, + MISSING_ACTION, + MODULE_NOT_FOUND, + INV_OID_STRING, + INV_TIME, + INV_CRL_OBJ, + UNSUPPORTED_CRL_VERSION, + INV_CERT_OBJ, + UNKNOWN_NAME, + LOCALE_PROBLEM, + NOT_LOCKED, + PROTOCOL_VIOLATION, + INV_MAC, + INV_REQUEST, + UNKNOWN_EXTN, + UNKNOWN_CRIT_EXTN, + LOCKED, + UNKNOWN_OPTION, + UNKNOWN_COMMAND, + UNFINISHED, + BUFFER_TOO_SHORT, + SEXP_INV_LEN_SPEC, + SEXP_STRING_TOO_LONG, + SEXP_UNMATCHED_PAREN, + SEXP_NOT_CANONICAL, + SEXP_BAD_CHARACTER, + SEXP_BAD_QUOTATION, + SEXP_ZERO_PREFIX, + SEXP_NESTED_DH, + SEXP_UNMATCHED_DH, + SEXP_UNEXPECTED_PUNC, + SEXP_BAD_HEX_CHAR, + SEXP_ODD_HEX_NUMBERS, + SEXP_BAD_OCT_CHAR, + ASS_GENERAL, + ASS_ACCEPT_FAILED, + ASS_CONNECT_FAILED, + ASS_INV_RESPONSE, + ASS_INV_VALUE, + ASS_INCOMPLETE_LINE, + ASS_LINE_TOO_LONG, + ASS_NESTED_COMMANDS, + ASS_NO_DATA_CB, + ASS_NO_INQUIRE_CB, + ASS_NOT_A_SERVER, + ASS_NOT_A_CLIENT, + ASS_SERVER_START, + ASS_READ_ERROR, + ASS_WRITE_ERROR, + ASS_TOO_MUCH_DATA, + ASS_UNEXPECTED_CMD, + ASS_UNKNOWN_CMD, + ASS_SYNTAX, + ASS_CANCELED, + ASS_NO_INPUT, + ASS_NO_OUTPUT, + ASS_PARAMETER, + ASS_UNKNOWN_INQUIRE, + USER_1, + USER_2, + USER_3, + USER_4, + USER_5, + USER_6, + USER_7, + USER_8, + USER_9, + USER_10, + USER_11, + USER_12, + USER_13, + USER_14, + USER_15, + USER_16, + MISSING_ERRNO, + UNKNOWN_ERRNO, + EOF, + E2BIG, + EACCES, + EADDRINUSE, + EADDRNOTAVAIL, + EADV, + EAFNOSUPPORT, + EAGAIN, + EALREADY, + EAUTH, + EBACKGROUND, + EBADE, + EBADF, + EBADFD, + EBADMSG, + EBADR, + EBADRPC, + EBADRQC, + EBADSLT, + EBFONT, + EBUSY, + ECANCELED, + ECHILD, + ECHRNG, + ECOMM, + ECONNABORTED, + ECONNREFUSED, + ECONNRESET, + ED, + EDEADLK, + EDEADLOCK, + EDESTADDRREQ, + EDIED, + EDOM, + EDOTDOT, + EDQUOT, + EEXIST, + EFAULT, + EFBIG, + EFTYPE, + EGRATUITOUS, + EGREGIOUS, + EHOSTDOWN, + EHOSTUNREACH, + EIDRM, + EIEIO, + EILSEQ, + EINPROGRESS, + EINTR, + EINVAL, + EIO, + EISCONN, + EISDIR, + EISNAM, + EL2HLT, + EL2NSYNC, + EL3HLT, + EL3RST, + ELIBACC, + ELIBBAD, + ELIBEXEC, + ELIBMAX, + ELIBSCN, + ELNRNG, + ELOOP, + EMEDIUMTYPE, + EMFILE, + EMLINK, + EMSGSIZE, + EMULTIHOP, + ENAMETOOLONG, + ENAVAIL, + ENEEDAUTH, + ENETDOWN, + ENETRESET, + ENETUNREACH, + ENFILE, + ENOANO, + ENOBUFS, + ENOCSI, + ENODATA, + ENODEV, + ENOENT, + ENOEXEC, + ENOLCK, + ENOLINK, + ENOMEDIUM, + ENOMEM, + ENOMSG, + ENONET, + ENOPKG, + ENOPROTOOPT, + ENOSPC, + ENOSR, + ENOSTR, + ENOSYS, + ENOTBLK, + ENOTCONN, + ENOTDIR, + ENOTEMPTY, + ENOTNAM, + ENOTSOCK, + ENOTSUP, + ENOTTY, + ENOTUNIQ, + ENXIO, + EOPNOTSUPP, + EOVERFLOW, + EPERM, + EPFNOSUPPORT, + EPIPE, + EPROCLIM, + EPROCUNAVAIL, + EPROGMISMATCH, + EPROGUNAVAIL, + EPROTO, + EPROTONOSUPPORT, + EPROTOTYPE, + ERANGE, + EREMCHG, + EREMOTE, + EREMOTEIO, + ERESTART, + EROFS, + ERPCMISMATCH, + ESHUTDOWN, + ESOCKTNOSUPPORT, + ESPIPE, + ESRCH, + ESRMNT, + ESTALE, + ESTRPIPE, + ETIME, + ETIMEDOUT, + ETOOMANYREFS, + ETXTBSY, + EUCLEAN, + EUNATCH, + EUSERS, + EWOULDBLOCK, + EXDEV, + EXFULL, + CODE_DIM + } +} diff --git a/vapi/gpgme.deps b/vapi/gpgme.deps new file mode 100644 index 00000000..a0f4f82b --- /dev/null +++ b/vapi/gpgme.deps @@ -0,0 +1 @@ +gpg-error diff --git a/vapi/gpgme.vapi b/vapi/gpgme.vapi new file mode 100644 index 00000000..49fd2d78 --- /dev/null +++ b/vapi/gpgme.vapi @@ -0,0 +1,1224 @@ +/* libgpgme.vapi + * + * Copyright (C) 2009 Sebastian Reichel + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + */ + +/** + * GPGME is an API wrapper around GnuPG, which parses the output of GnuPG. + */ +[CCode (lower_case_cprefix = "gpgme_", cheader_filename = "gpgme.h")] +namespace GPG { + /** + * EngineInfo as List + */ + [CCode (cname = "struct _gpgme_engine_info")] + public struct EngineInfo { + /** + * Next entry in the list + */ + EngineInfo* next; + + /** + * The protocol ID + */ + Protocol protocol; + + /** + * filename of the engine binary + */ + string file_name; + + /** + * version string of the installed binary + */ + string version; + + /** + * minimum version required for gpgme + */ + string req_version; + + /** + * home directory to be used or null for default + */ + string? home_dir; + } + + /** + * A Key from the Keyring + */ + [CCode (cname = "struct _gpgme_key", ref_function = "gpgme_key_ref", ref_function_void = true, unref_function = "gpgme_key_unref", free_function = "gpgme_key_release")] + public class Key { + public bool revoked; + public bool expired; + public bool disabled; + public bool invalid; + public bool can_encrypt; + public bool can_sign; + public bool can_certify; + public bool secret; + public bool can_authenticate; + public bool is_qualified; + public Protocol protocol; + + /** + * If protocol is CMS, this string contains the issuer's serial + */ + public string issuer_serial; + + /** + * If protocol is CMS, this string contains the issuer's name + */ + public string issuer_name; + + /** + * If protocol is CMS, this string contains the issuer's ID + */ + public string issuer_id; + + /** + * If protocol is OpenPGP, this field contains the owner trust level + */ + public Validity owner_trust; + + /** + * The key's subkeys + */ + [CCode(array_null_terminated = true)] + public SubKey[] subkeys; + + /** + * The key's user ids + */ + [CCode(array_null_terminated = true)] + public UserID[] uids; + + public KeylistMode keylist_mode; + } + + /** + * A signature notation + */ + [CCode (cname = "struct _gpgme_sig_notation")] + public struct SigNotation { + /** + * The next SigNotation from the list + */ + SigNotation* next; + + /** + * If name is a null pointer value contains a policy url rather than a notation + */ + string? name; + + /** + * The value of the notation data + */ + string value; + + /** + * The length of the name of the notation data + */ + int name_len; + + /** + * The length of the value of the notation data + */ + int value_len; + + /** + * The accumulated flags + */ + SigNotationFlags flags; + + /** + * notation data is human readable + */ + bool human_readable; + + /** + * notation data is critical + */ + bool critical; + } + + /** + * A subkey from a Key + */ + [CCode (cname = "struct _gpgme_subkey")] + public struct SubKey { + SubKey* next; + bool revoked; + bool expired; + bool disabled; + bool invalid; + bool can_encrypt; + bool can_sign; + bool can_certify; + bool secret; + bool can_authenticate; + bool is_qualified; + bool is_cardkey; + PublicKeyAlgorithm algo; + uint length; + string keyid; + + /** + * Fingerprint of the key in hex form + */ + string fpr; + + /** + * The creation timestamp. + * -1 = invalid, + * 0 = not available + */ + long timestamp; + + /** + * The expiration timestamp. + * 0 = key does not expire + */ + long expires; + + /** + * The serial number of the smartcard holding this key or null + */ + string? cardnumber; + } + + /** + * A signature on a UserID + */ + [CCode (cname = "struct _gpgme_key_sig")] + public struct KeySig { + /** + * The next signature from the list + */ + KeySig* next; + bool invoked; + bool expired; + bool invalid; + bool exportable; + PublicKeyAlgorithm algo; + string keyid; + + /** + * The creation timestamp. + * -1 = invalid, + * 0 = not available + */ + long timestamp; + + /** + * The expiration timestamp. + * 0 = key does not expire + */ + long expires; + + GPGError.ErrorCode status; + + string uid; + string name; + string email; + string comment; + + /** + * Crypto backend specific signature class + */ + uint sig_class; + + SigNotation notations; + } + + /** + * A UserID from a Key + */ + [CCode (cname = "struct _gpgme_user_id")] + public struct UserID { + /** + * The next UserID from the list + */ + UserID* next; + + bool revoked; + bool invalid; + Validity validity; + string uid; + string name; + string email; + string comment; + + KeySig signatures; + } + + /** + * verify result of OP + */ + [CCode (cname = "struct _gpgme_op_verify_result")] + public struct VerifyResult { + Signature* signatures; + + /** + * The original file name of the plaintext message, if available + */ + string? file_name; + } + + /** + * sign result of OP + */ + [CCode (cname = "struct _gpgme_op_sign_result")] + public struct SignResult { + InvalidKey invalid_signers; + Signature* signatures; + } + + /** + * encrypt result of OP + */ + [CCode (cname = "struct _gpgme_op_encrypt_result")] + public struct EncryptResult { + /** + * The list of invalid repipients + */ + InvalidKey invalid_signers; + } + + /** + * decrypt result of OP + */ + [CCode (cname = "struct _gpgme_op_decrypt_result")] + public struct DecryptResult { + string unsupported_algorithm; + bool wrong_key_usage; + Recipient recipients; + string filename; + } + + /** + * An receipient + */ + [CCode (cname = "struct _gpgme_recipient")] + public struct Recipient { + Recipient *next; + string keyid; + PublicKeyAlgorithm pubkey_algo; + GPGError.ErrorCode status; + } + + /** + * list of invalid keys + */ + [CCode (cname = "struct _gpgme_invalid_key")] + public struct InvalidKey { + InvalidKey *next; + string fpr; + GPGError.ErrorCode reason; + } + + /** + * A Signature + */ + [CCode (cname = "struct _gpgme_signature")] + public struct Signature { + /** + * The next signature in the list + */ + Signature *next; + + /** + * A summary of the signature status + */ + Sigsum summary; + + /** + * Fingerprint or key ID of the signature + */ + string fpr; + + /** + * The Error status of the signature + */ + GPGError.ErrorCode status; + + /** + * Notation data and policy URLs + */ + SigNotation notations; + + /** + * Signature creation time + */ + ulong timestamp; + + /** + * Signature expiration time or 0 + */ + ulong exp_timestamp; + + /** + * Key should not have been used for signing + */ + bool wrong_key_usage; + + /** + * PKA status + */ + PKAStatus pka_trust; + + /** + * Validity has been verified using the chain model + */ + bool chain_model; + + /** + * Validity + */ + Validity validity; + + /** + * Validity reason + */ + GPGError.ErrorCode validity_reason; + + /** + * public key algorithm used to create the signature + */ + PublicKeyAlgorithm pubkey_algo; + + /** + * The hash algorithm used to create the signature + */ + HashAlgorithm hash_algo; + + /** + * The mailbox from the PKA information or null + */ + string? pka_adress; + } + + /** + * PKA Status + */ + public enum PKAStatus { + NOT_AVAILABLE, + BAD, + OKAY, + RFU + } + + /** + * Flags used for the summary field in a Signature + */ + [CCode (cname = "gpgme_sigsum_t", cprefix = "GPGME_SIGSUM_")] + public enum Sigsum { + /** + * The signature is fully valid + */ + VALID, + + /** + * The signature is good + */ + GREEN, + + /** + * The signature is bad + */ + RED, + + /** + * One key has been revoked + */ + KEY_REVOKED, + + /** + * One key has expired + */ + KEY_EXPIRED, + + /** + * The signature has expired + */ + SIG_EXPIRED, + + /** + * Can't verfiy - missing key + */ + KEY_MISSING, + + /** + * CRL not available + */ + CRL_MISSING, + + /** + * Available CRL is too old + */ + CRL_TOO_OLD, + + /** + * A policy was not met + */ + BAD_POLICY, + + /** + * A system error occured + */ + SYS_ERROR + } + + /** + * Encoding modes of Data objects + */ + [CCode (cname = "gpgme_data_encoding_t", cprefix = "GPGME_DATA_ENCODING_")] + public enum DataEncoding { + /** + * Not specified + */ + NONE, + /** + * Binary encoded + */ + BINARY, + /** + * Base64 encoded + */ + BASE64, + /** + * Either PEM or OpenPGP Armor + */ + ARMOR, + /** + * LF delimited URL list + */ + URL, + /** + * LF percent escaped, delimited URL list + */ + URLESC, + /** + * Nul determined URL list + */ + URL0 + } + + /** + * Public Key Algorithms from libgcrypt + */ + [CCode (cname = "gpgme_pubkey_algo_t", cprefix = "GPGME_PK_")] + public enum PublicKeyAlgorithm { + RSA, + RSA_E, + RSA_S, + ELG_E, + DSA, + ELG + } + + /** + * Hash Algorithms from libgcrypt + */ + [CCode (cname = "gpgme_hash_algo_t", cprefix = "GPGME_MD_")] + public enum HashAlgorithm { + NONE, + MD5, + SHA1, + RMD160, + MD2, + TIGER, + HAVAL, + SHA256, + SHA384, + SHA512, + MD4, + MD_CRC32, + MD_CRC32_RFC1510, + MD_CRC24_RFC2440 + } + + /** + * Signature modes + */ + [CCode (cname = "gpgme_sig_mode_t", cprefix = "GPGME_SIG_MODE_")] + public enum SigMode { + NORMAL, + DETACH, + CLEAR + } + + /** + * Validities for a trust item or key + */ + [CCode (cname = "gpgme_validity_t", cprefix = "GPGME_VALIDITY_")] + public enum Validity { + UNKNOWN, + UNDEFINED, + NEVER, + MARGINAL, + FULL, + ULTIMATE + } + + /** + * Protocols + */ + [CCode (cname = "gpgme_protocol_t", cprefix = "GPGME_PROTOCOL_")] + public enum Protocol { + /** + * Default Mode + */ + OpenPGP, + /** + * Cryptographic Message Syntax + */ + CMS, + /** + * Special code for gpgconf + */ + GPGCONF, + /** + * Low-level access to an Assuan server + */ + ASSUAN, + UNKNOWN + } + + /** + * Keylist modes used by Context + */ + [CCode (cname = "gpgme_keylist_mode_t", cprefix = "GPGME_KEYLIST_MODE_")] + public enum KeylistMode { + LOCAL, + EXTERN, + SIGS, + SIG_NOTATIONS, + EPHEMERAL, + VALIDATE + } + + /** + * Export modes used by Context + */ + [CCode (cname = "gpgme_export_mode_t", cprefix = "GPGME_EXPORT_MODE_")] + public enum ExportMode { + EXTERN + } + + /** + * Audit log function flags + */ + [CCode (cprefix = "GPGME_AUDITLOG_")] + public enum AuditLogFlag { + HTML, + WITH_HELP + } + + /** + * Signature notation flags + */ + [CCode (cname = "gpgme_sig_notation_flags_t", cprefix = "GPGME_SIG_NOTATION_")] + public enum SigNotationFlags { + HUMAN_READABLE, + CRITICAL + } + + /** + * Encryption Flags + */ + [CCode (cname = "gpgme_encrypt_flags_t", cprefix = "GPGME_ENCRYPT_")] + public enum EncryptFlags { + ALWAYS_TRUST, + NO_ENCRYPT_TO + } + + /** + * Edit Operation Stati + */ + [CCode (cname = "gpgme_status_code_t", cprefix = "GPGME_STATUS_")] + public enum StatusCode { + EOF, + ENTER, + LEAVE, + ABORT, + GOODSIG, + BADSIG, + ERRSIG, + BADARMOR, + RSA_OR_IDEA, + KEYEXPIRED, + KEYREVOKED, + TRUST_UNDEFINED, + TRUST_NEVER, + TRUST_MARGINAL, + TRUST_FULLY, + TRUST_ULTIMATE, + SHM_INFO, + SHM_GET, + SHM_GET_BOOL, + SHM_GET_HIDDEN, + NEED_PASSPHRASE, + VALIDSIG, + SIG_ID, + SIG_TO, + ENC_TO, + NODATA, + BAD_PASSPHRASE, + NO_PUBKEY, + NO_SECKEY, + NEED_PASSPHRASE_SYM, + DECRYPTION_FAILED, + DECRYPTION_OKAY, + MISSING_PASSPHRASE, + GOOD_PASSPHRASE, + GOODMDC, + BADMDC, + ERRMDC, + IMPORTED, + IMPORT_OK, + IMPORT_PROBLEM, + IMPORT_RES, + FILE_START, + FILE_DONE, + FILE_ERROR, + BEGIN_DECRYPTION, + END_DECRYPTION, + BEGIN_ENCRYPTION, + END_ENCRYPTION, + DELETE_PROBLEM, + GET_BOOL, + GET_LINE, + GET_HIDDEN, + GOT_IT, + PROGRESS, + SIG_CREATED, + SESSION_KEY, + NOTATION_NAME, + NOTATION_DATA, + POLICY_URL, + BEGIN_STREAM, + END_STREAM, + KEY_CREATED, + USERID_HINT, + UNEXPECTED, + INV_RECP, + NO_RECP, + ALREADY_SIGNED, + SIGEXPIRED, + EXPSIG, + EXPKEYSIG, + TRUNCATED, + ERROR, + NEWSIG, + REVKEYSIG, + SIG_SUBPACKET, + NEED_PASSPHRASE_PIN, + SC_OP_FAILURE, + SC_OP_SUCCESS, + CARDCTRL, + BACKUP_KEY_CREATED, + PKA_TRUST_BAD, + PKA_TRUST_GOOD, + PLAINTEXT + } + + /** + * The Context object represents a GPG instance + */ + [Compact] + [CCode (cname = "struct gpgme_context", free_function = "gpgme_release", cprefix = "gpgme_")] + public class Context { + /** + * Create a new context, returns Error Status Code + */ + [CCode (cname = "gpgme_new")] + public static GPGError.ErrorCode Context(out Context ctx); + + public GPGError.ErrorCode set_protocol(Protocol p); + public Protocol get_protocol(); + + public void set_armor(bool yes); + public bool get_armor(); + + public void set_textmode(bool yes); + public bool get_textmode(); + + public GPGError.ErrorCode set_keylist_mode(KeylistMode mode); + public KeylistMode get_keylist_mode(); + + /** + * Include up to nr_of_certs certificates in an S/MIME message, + * Use "-256" to use the backend's default. + */ + public void set_include_certs(int nr_of_certs = -256); + + /** + * Return the number of certs to include in an S/MIME message + */ + public int get_include_certs(); + + /** + * Set callback function for requesting passphrase. hook_value will be + * passed as first argument. + */ + public void set_passphrase_cb(passphrase_callback cb, void* hook_value = null); + + /** + * Get callback function and hook_value + */ + public void get_passphrase_cb(out passphrase_callback cb, out void* hook_value); + + public GPGError.ErrorCode set_locale(int category, string val); + + /** + * Get information about the configured engines. The returned data is valid + * until the next set_engine_info() call. + */ + [CCode (cname = "gpgme_ctx_get_engine_info")] + public EngineInfo* get_engine_info(); + + /** + * Set information about the configured engines. The string parameters may not + * be free'd after this calls, because they are not copied. + */ + [CCode (cname = "gpgme_ctx_set_engine_info")] + public GPGError.ErrorCode set_engine_info(Protocol proto, string file_name, string home_dir); + + /** + * Delete all signers + */ + public void signers_clear(); + + /** + * Add key to list of signers + */ + public GPGError.ErrorCode signers_add(Key key); + + /** + * Get the n-th signer's key + */ + public Key* signers_enum(int n); + + /** + * Clear all notation data + */ + public void sig_notation_clear(); + + /** + * Add human readable notation data. If name is null, + * then value val should be a policy URL. The HUMAN_READABLE + * flag is forced to be true for notation data and false + * for policy URLs. + */ + public GPGError.ErrorCode sig_notation_add(string name, string val, SigNotationFlags flags); + + /** + * Get sig notations + */ + public SigNotation* sig_notation_get(); + + /** + * Get key with the fingerprint FPR from the crypto backend. + * If SECRET is true, get the secret key. + */ + public GPGError.ErrorCode get_key(string fpr, out Key key, bool secret); + + /** + * process the pending operation and, if hang is true, wait for + * the pending operation to finish. + */ + public Context* wait(out GPGError.ErrorCode status, bool hang); + + /** + * Retrieve a pointer to the results of the signing operation + */ + public SignResult* op_sign_result(); + + /** + * Sign the plaintext PLAIN and store the signature in SIG. + */ + public GPGError.ErrorCode op_sign(Data plain, Data sig, SigMode mode); + + /** + * Retrieve a pointer to the result of the verify operation + */ + public VerifyResult* op_verify_result(); + + /** + * Verify that SIG is a valid signature for SIGNED_TEXT. + */ + public GPGError.ErrorCode op_verify(Data sig, Data signed_text, Data? plaintext); + + /** + * Retrieve a pointer to the result of the encrypt operation + */ + public EncryptResult* op_encrypt_result(); + + /** + * Encrypt plaintext PLAIN for the recipients RECP and store the + * resulting ciphertext in CIPHER. + */ + public GPGError.ErrorCode op_encrypt([CCode (array_length = false)] Key[] recp, EncryptFlags flags, Data plain, Data cipher); + + /** + * Retrieve a pointer to the result of the decrypt operation + */ + public DecryptResult* op_decrypt_result(); + + /** + * Decrypt ciphertext CIPHER and store the resulting plaintext + * in PLAIN. + */ + public GPGError.ErrorCode op_decrypt(Data cipher, Data plain); + + /** + * Export the keys found by PATTERN into KEYDATA. If PATTERN is + * NULL all keys will be exported. + */ + public GPGError.ErrorCode op_export(string? pattern, ExportMode mode, Data keydata); + + /** + * Import the keys in KEYDATA. + */ + public GPGError.ErrorCode op_import(Data keydata); + + /** + * Get result of last op_import. + */ + public unowned ImportResult op_import_result(); + + /** + * Initiates a key listing operation. It sets everything up, so that + * subsequent invocations of op_keylist_next() return the keys in the list. + * + * If pattern is NULL, all available keys are returned. Otherwise, pattern + * contains an engine specific expression that is used to limit the list to + * all keys matching the pattern. + * + * If secret_only is not 0, the list is restricted to secret keys only. + * + * The context will be busy until either all keys are received (and + * op_keylist_next() returns GPG_ERR_EOF), or gpgme_op_keylist_end is called + * to finish the operation. + * + * The function returns the error code GPG_ERR_INV_VALUE if ctx is not a valid + * pointer, and passes through any errors that are reported by the crypto engine + * support routines. + */ + public GPGError.ErrorCode op_keylist_start(string? pattern = null, int secret_only = 0); + + /** + * returns the next key in the list created by a previous op_keylist_start() + * operation in the context ctx. The key will have one reference for the user. + * + * If the last key in the list has already been returned, op_keylist_next() + * returns GPG_ERR_EOF. + * + * The function returns the error code GPG_ERR_INV_VALUE if ctx or r_key is + * not a valid pointer, and GPG_ERR_ENOMEM if there is not enough memory for + * the operation. + */ + public GPGError.ErrorCode op_keylist_next(out Key key); + + /** + * ends a pending key list operation in the context. + * + * After the operation completed successfully, the result of the key listing + * operation can be retrieved with op_keylist_result(). + * + * The function returns the error code GPG_ERR_INV_VALUE if ctx is not a valid + * pointer, and GPG_ERR_ENOMEM if at some time during the operation there was + * not enough memory available. + */ + public GPGError.ErrorCode op_keylist_end(); + + /** + * The function op_keylist_result() returns a KeylistResult holding the result of + * a op_keylist_*() operation. The returned KeylistResult is only valid if the last + * operation on the context was a key listing operation, and if this operation + * finished successfully. The returned KeylistResult is only valid until the next + * operation is started on the context. + */ + public KeylistResult op_keylist_result(); + } + + [Flags] + [CCode (cname="unsigned int")] + public enum ImportStatusFlags { + /** + * The key was new. + */ + [CCode (cname = "GPGME_IMPORT_NEW")] + NEW, + /** + * The key contained new user IDs. + */ + [CCode (cname = "GPGME_IMPORT_UID")] + UID, + /** + * The key contained new signatures. + */ + [CCode (cname = "GPGME_IMPORT_SIG")] + SIG, + /** + * The key contained new sub keys. + */ + [CCode (cname = "GPGME_IMPORT_SUBKEY")] + SUBKEY, + /** + * The key contained a secret key. + */ + [CCode (cname = "GPGME_IMPORT_SECRET")] + SECRET + } + + [Compact] + [CCode (cname = "struct _gpgme_import_status")] + public class ImportStatus { + /** + * This is a pointer to the next status structure in the linked list, or null + * if this is the last element. + */ + public ImportStatus? next; + + /** + * fingerprint of the key that was considered. + */ + public string fpr; + + /** + * If the import was not successful, this is the error value that caused the + * import to fail. Otherwise the error code is GPG_ERR_NO_ERROR. + */ + public GPGError.ErrorCode result; + + /** + * Flags what parts of the key have been imported. May be 0, if the key has + * already been known. + */ + public ImportStatusFlags status; + } + + [Compact] + [CCode (cname = "struct _gpgme_op_import_result")] + public class ImportResult { + /** + * The total number of considered keys. + */ + public int considered; + + /** + * The number of keys without user ID. + */ + public int no_user_id; + + /** + * The total number of imported keys. + */ + public int imported; + + /** + * The number of imported RSA keys. + */ + public int imported_rsa; + + /** + * The number of unchanged keys. + */ + public int unchanged; + + /** + * The number of new user IDs. + */ + public int new_user_ids; + + /** + * The number of new sub keys. + */ + public int new_sub_keys; + + /** + * The number of new signatures. + */ + public int new_signatures; + + /** + * The number of new revocations. + */ + public int new_revocations; + + /** + * The total number of secret keys read. + */ + public int secret_read; + + /** + * The number of imported secret keys. + */ + public int secret_imported; + + /** + * The number of unchanged secret keys. + */ + public int secret_unchanged; + + /** + * The number of keys not imported. + */ + public int not_imported; + + /* + * A linked list of ImportStatus objects which + * contains more information about the keys for + * which an import was attempted. + */ + public ImportStatus imports; + } + + [Compact] + [CCode (cname = "struct _gpgme_op_keylist_result")] + public class KeylistResult { + uint truncated; + } + + + /** + * Data Object, contains encrypted and/or unencrypted data + */ + [Compact] + [CCode (cname = "struct gpgme_data", free_function = "gpgme_data_release", cprefix = "gpgme_data_")] + public class Data { + /** + * Create a new data buffer, returns Error Status Code. + */ + [CCode (cname = "gpgme_data_new")] + public static GPGError.ErrorCode create(out Data d); + + /** + * Create a new data buffer filled with SIZE bytes starting + * from BUFFER. If COPY is false, COPYING is delayed until + * necessary and the data is taken from the original location + * when needed. Returns Error Status Code. + */ + [CCode (cname = "gpgme_data_new_from_mem")] + public static GPGError.ErrorCode create_from_memory(out Data d, uint8[] buffer, bool copy); + + /** + * Create a new data buffer filled with the content of the file. + * COPY must be non-zero. For delayed read, please use + * create_from_fd or create_from stream instead. + */ + [CCode (cname = "gpgme_data_new_from_file")] + public static GPGError.ErrorCode create_from_file(out Data d, string filename, int copy = 1); + + + /** + * Destroy the object and return a pointer to its content. + * It's size is returned in R_LEN. + */ + [CCode (cname = "gpgme_data_release_and_get_mem")] + public string release_and_get_mem(out size_t len); + + /** + * Read up to SIZE bytes into buffer BUFFER from the data object. + * Return the number of characters read, 0 on EOF and -1 on error. + * If an error occurs, errno is set. + */ + public ssize_t read(uint8[] buf); + + /** + * Write up to SIZE bytes from buffer BUFFER to the data object. + * Return the number of characters written, or -1 on error. + * If an error occurs, errno is set. + */ + public ssize_t write(uint8[] buf); + + /** + * Set the current position from where the next read or write + * starts in the data object to OFFSET, relativ to WHENCE. + */ + public long seek(long offset, int whence=0); + + /** + * Get the encoding attribute of the buffer + */ + public DataEncoding *get_encoding(); + + /** + * Set the encoding attribute of the buffer to ENC + */ + public GPGError.ErrorCode set_encoding(DataEncoding enc); + } + + [CCode (cname = "gpgme_get_protocol_name")] + public unowned string get_protocol_name(Protocol p); + + [CCode (cname = "gpgme_pubkey_algo_name")] + public unowned string get_public_key_algorithm_name(PublicKeyAlgorithm algo); + + [CCode (cname = "gpgme_hash_algo_name")] + public unowned string get_hash_algorithm_name(HashAlgorithm algo); + + [CCode (cname = "gpgme_passphrase_cb_t", has_target = false)] + public delegate GPGError.ErrorCode passphrase_callback(void* hook, string uid_hint, string passphrase_info, bool prev_was_bad, int fd); + + /** + * Get version of libgpgme + * Always call this function before using gpgme, it initializes some stuff + */ + [CCode (cname = "gpgme_check_version")] + public unowned string check_version(string? required_version = null); + + /** + * Verify that the engine implementing proto is installed and + * available. + */ + [CCode (cname = "gpgme_engine_check_version")] + public GPGError.ErrorCode engine_check_version(Protocol proto); + + /** + * Get information about the configured engines. The returned data is valid + * until the next set_engine_info() call. + */ + [CCode (cname = "gpgme_get_engine_information")] + public GPGError.ErrorCode get_engine_information(out EngineInfo engine_info); + + /** + * Return the error string for ERR in the user-supplied buffer BUF + * of size BUFLEN. This function is thread-safe, if a thread-safe + * strerror_r() function is provided by the system. If the function + * succeeds, 0 is returned and BUF contains the string describing + * the error. If the buffer was not large enough, ERANGE is returned + * and BUF contains as much of the beginning of the error string as + * fits into the buffer. Returns Error Status Code. + */ + [CCode (cname = "gpgme_strerror_r")] + public int strerror_r(GPGError.ErrorCode err, uint8[] buf); + + /** + * Like strerror_r, but returns a pointer to the string. This method + * is not thread safe! + */ + [CCode (cname = "gpgme_strerror")] + public unowned string strerror(GPGError.ErrorCode err); +} diff --git a/vapi/uuid.vapi b/vapi/uuid.vapi new file mode 100644 index 00000000..038fcc33 --- /dev/null +++ b/vapi/uuid.vapi @@ -0,0 +1,68 @@ +/* libuuid Vala Bindings + * Copyright 2014 Evan Nemerson + * + * Permission is hereby granted, free of charge, to any person + * obtaining a copy of this software and associated documentation + * files (the "Software"), to deal in the Software without + * restriction, including without limitation the rights to use, copy, + * modify, merge, publish, distribute, sublicense, and/or sell copies + * of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS + * BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN + * ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN + * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +[CCode (cheader_filename = "uuid.h", lower_case_cprefix = "uuid_")] +namespace UUID { + [CCode (cname = "int", has_type_id = false)] + public enum Variant { + NCS, + DCE, + MICROSOFT, + OTHER + } + + [CCode (cname = "int", has_type_id = false)] + public enum Type { + DCE_TIME, + DCE_RANDOM + } + + public static void clear ([CCode (array_length = false)] uint8 uu[16]); + public static void copy (uint8 dst[16], uint8 src[16]); + + public static void generate ([CCode (array_length = false)] uint8 @out[16]); + public static void generate_random ([CCode (array_length = false)] uint8 @out[16]); + public static void generate_time ([CCode (array_length = false)] uint8 @out[16]); + public static void generate_time_safe ([CCode (array_length = false)] uint8 @out[16]); + + public static bool is_null ([CCode (array_length = false)] uint8 uu[16]); + + public static int parse (string in, [CCode (array_length = false)] uint8 uu[16]); + + public static void unparse ([CCode (array_length = false)] uint8 uu[16], [CCode (array_length = false)] char @out[37]); + public static void unparse_lower ([CCode (array_length = false)] uint8 uu[16], [CCode (array_length = false)] char @out[37]); + public static void unparse_upper ([CCode (array_length = false)] uint8 uu[16], [CCode (array_length = false)] char @out[37]); + +// public static time_t time ([CCode (array_length = false)] uint8 uu[16], out Posix.timeval ret_tv); + public static UUID.Type type ([CCode (array_length = false)] uint8 uu[16]); + public static UUID.Variant variant ([CCode (array_length = false)] uint8 uu[16]); + + public static string generate_random_unparsed() { + uint8[] rand = new uint8[16]; + char[] str = new char[37]; + generate_random(rand); + unparse_upper(rand, str); + return (string) str; + } +}