aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.gitignore7
-rw-r--r--CMakeLists.txt25
-rw-r--r--README.md8
-rw-r--r--client/CMakeLists.txt150
-rw-r--r--client/data/add_conversation/add_contact_dialog.ui150
-rw-r--r--client/data/add_conversation/add_groupchat_dialog.ui224
-rw-r--r--client/data/add_conversation/conference_details_fragment.ui227
-rw-r--r--client/data/add_conversation/list_row.ui61
-rw-r--r--client/data/add_conversation/select_jid_fragment.ui109
-rw-r--r--client/data/chat_input.ui23
-rw-r--r--client/data/conversation_list_titlebar.ui41
-rw-r--r--client/data/conversation_selector/chat_row_tooltip.ui23
-rw-r--r--client/data/conversation_selector/conversation_row.ui146
-rw-r--r--client/data/conversation_selector/view.ui33
-rw-r--r--client/data/conversation_summary/message_item.ui98
-rw-r--r--client/data/conversation_summary/view.ui33
-rw-r--r--client/data/conversation_titlebar.ui63
-rw-r--r--client/data/gschemas.compiledbin0 -> 316 bytes
-rw-r--r--client/data/img/double_tick.svg190
-rw-r--r--client/data/img/send.svg1
-rw-r--r--client/data/img/status_away.svg73
-rw-r--r--client/data/img/status_chat.svg85
-rw-r--r--client/data/img/status_dnd.svg73
-rw-r--r--client/data/img/status_online.svg67
-rw-r--r--client/data/img/tick.svg184
-rw-r--r--client/data/manage_accounts/account_row.ui30
-rw-r--r--client/data/manage_accounts/add_account_dialog.ui137
-rw-r--r--client/data/manage_accounts/dialog.ui306
-rw-r--r--client/data/menu_add.ui16
-rw-r--r--client/data/menu_app.ui20
-rw-r--r--client/data/menu_conversation.ui9
-rw-r--r--client/data/menu_encryption.ui49
-rw-r--r--client/data/occupant_list.ui43
-rw-r--r--client/data/occupant_list_item.ui44
-rw-r--r--client/data/settings.gschema.xml15
-rw-r--r--client/data/settings_dialog.ui51
-rw-r--r--client/data/style.css3
-rw-r--r--client/data/unified_window.ui178
-rw-r--r--client/src/dbus/login1.vala18
-rw-r--r--client/src/dbus/networkmanager.vala22
-rw-r--r--client/src/dbus/upower.vala19
-rw-r--r--client/src/entity/account.vala40
-rw-r--r--client/src/entity/conversation.vala48
-rw-r--r--client/src/entity/jid.vala91
-rw-r--r--client/src/entity/message.vala89
-rw-r--r--client/src/main.vala12
-rw-r--r--client/src/service/avatar_manager.vala134
-rw-r--r--client/src/service/avatar_storage.vala34
-rw-r--r--client/src/service/chat_interaction.vala146
-rw-r--r--client/src/service/connection_manager.vala222
-rw-r--r--client/src/service/conversation_manager.vala98
-rw-r--r--client/src/service/counterpart_interaction_manager.vala99
-rw-r--r--client/src/service/database.vala457
-rw-r--r--client/src/service/entity_capabilities_storage.vala23
-rw-r--r--client/src/service/message_manager.vala166
-rw-r--r--client/src/service/module_manager.vala96
-rw-r--r--client/src/service/muc_manager.vala224
-rw-r--r--client/src/service/pgp_manager.vala54
-rw-r--r--client/src/service/presence_manager.vala150
-rw-r--r--client/src/service/roster_manager.vala82
-rw-r--r--client/src/service/stream_interactor.vala68
-rw-r--r--client/src/settings.vala28
-rw-r--r--client/src/ui/add_conversation/chat/add_contact_dialog.vala67
-rw-r--r--client/src/ui/add_conversation/chat/dialog.vala82
-rw-r--r--client/src/ui/add_conversation/chat/roster_list.vala77
-rw-r--r--client/src/ui/add_conversation/conference/add_groupchat_dialog.vala107
-rw-r--r--client/src/ui/add_conversation/conference/conference_details_fragment.vala148
-rw-r--r--client/src/ui/add_conversation/conference/conference_list.vala105
-rw-r--r--client/src/ui/add_conversation/conference/dialog.vala165
-rw-r--r--client/src/ui/add_conversation/list_row.vala43
-rw-r--r--client/src/ui/add_conversation/select_jid_fragment.vala124
-rw-r--r--client/src/ui/application.vala112
-rw-r--r--client/src/ui/avatar_generator.vala233
-rw-r--r--client/src/ui/chat_input.vala123
-rw-r--r--client/src/ui/conversation_list_titlebar.vala47
-rw-r--r--client/src/ui/conversation_selector/chat_row.vala88
-rw-r--r--client/src/ui/conversation_selector/conversation_row.vala175
-rw-r--r--client/src/ui/conversation_selector/groupchat_row.vala33
-rw-r--r--client/src/ui/conversation_selector/list.vala173
-rw-r--r--client/src/ui/conversation_selector/view.vala56
-rw-r--r--client/src/ui/conversation_summary/merged_message_item.vala164
-rw-r--r--client/src/ui/conversation_summary/merged_status_item.vala30
-rw-r--r--client/src/ui/conversation_summary/status_item.vala29
-rw-r--r--client/src/ui/conversation_summary/view.vala221
-rw-r--r--client/src/ui/conversation_titlebar.vala124
-rw-r--r--client/src/ui/manage_accounts/account_row.vala24
-rw-r--r--client/src/ui/manage_accounts/add_account_dialog.vala70
-rw-r--r--client/src/ui/manage_accounts/dialog.vala221
-rw-r--r--client/src/ui/notifications.vala55
-rw-r--r--client/src/ui/occupant_list.vala112
-rw-r--r--client/src/ui/occupant_list_row.vala27
-rw-r--r--client/src/ui/settings_dialog.vala27
-rw-r--r--client/src/ui/unified_window.vala78
-rw-r--r--client/src/ui/util.vala71
-rw-r--r--cmake/BuildTargetScript.cmake57
-rw-r--r--cmake/CompileGResources.cmake221
-rw-r--r--cmake/FindGPGME.cmake27
-rw-r--r--cmake/FindLIBUUID.cmake42
-rw-r--r--cmake/FindVala.cmake70
-rw-r--r--cmake/GenerateGXML.cmake124
-rw-r--r--cmake/GlibCompileResourcesSupport.cmake11
-rw-r--r--cmake/UseVala.cmake271
-rwxr-xr-xconfigure82
-rw-r--r--qlite/CMakeLists.txt46
-rw-r--r--qlite/src/column.vala188
-rw-r--r--qlite/src/database.vala152
-rw-r--r--qlite/src/delete_builder.vala75
-rw-r--r--qlite/src/insert_builder.vala102
-rw-r--r--qlite/src/query_builder.vala196
-rw-r--r--qlite/src/row.vala79
-rw-r--r--qlite/src/statement_builder.vala53
-rw-r--r--qlite/src/table.vala84
-rw-r--r--qlite/src/update_builder.vala133
-rw-r--r--vala-xmpp/CMakeLists.txt90
-rw-r--r--vala-xmpp/src/core/namespace_state.vala80
-rw-r--r--vala-xmpp/src/core/stanza_attribute.vala29
-rw-r--r--vala-xmpp/src/core/stanza_node.vala297
-rw-r--r--vala-xmpp/src/core/stanza_reader.vala260
-rw-r--r--vala-xmpp/src/core/stanza_writer.vala29
-rw-r--r--vala-xmpp/src/core/xmpp_stream.vala245
-rw-r--r--vala-xmpp/src/module/bind.vala93
-rw-r--r--vala-xmpp/src/module/iq/module.vala89
-rw-r--r--vala-xmpp/src/module/iq/stanza.vala51
-rw-r--r--vala-xmpp/src/module/message/module.vala50
-rw-r--r--vala-xmpp/src/module/message/stanza.vala63
-rw-r--r--vala-xmpp/src/module/presence/flag.vala64
-rw-r--r--vala-xmpp/src/module/presence/module.vala110
-rw-r--r--vala-xmpp/src/module/presence/stanza.vala93
-rw-r--r--vala-xmpp/src/module/roster/flag.vala30
-rw-r--r--vala-xmpp/src/module/roster/item.vala45
-rw-r--r--vala-xmpp/src/module/roster/module.vala125
-rw-r--r--vala-xmpp/src/module/sasl.vala139
-rw-r--r--vala-xmpp/src/module/stanza.vala70
-rw-r--r--vala-xmpp/src/module/stanza_error.vala69
-rw-r--r--vala-xmpp/src/module/stream_error.vala119
-rw-r--r--vala-xmpp/src/module/tls.vala99
-rw-r--r--vala-xmpp/src/module/util.vala13
-rw-r--r--vala-xmpp/src/module/xep/0027_pgp/flag.vala24
-rw-r--r--vala-xmpp/src/module/xep/0027_pgp/module.vala206
-rw-r--r--vala-xmpp/src/module/xep/0030_service_discovery/flag.vala33
-rw-r--r--vala-xmpp/src/module/xep/0030_service_discovery/info_result.vala78
-rw-r--r--vala-xmpp/src/module/xep/0030_service_discovery/items_result.vala27
-rw-r--r--vala-xmpp/src/module/xep/0030_service_discovery/module.vala137
-rw-r--r--vala-xmpp/src/module/xep/0045_muc/flag.vala80
-rw-r--r--vala-xmpp/src/module/xep/0045_muc/module.vala244
-rw-r--r--vala-xmpp/src/module/xep/0048_bookmarks/conference.vala74
-rw-r--r--vala-xmpp/src/module/xep/0048_bookmarks/module.vala137
-rw-r--r--vala-xmpp/src/module/xep/0049_private_xml_storage.vala65
-rw-r--r--vala-xmpp/src/module/xep/0054_vcard/module.vala87
-rw-r--r--vala-xmpp/src/module/xep/0060_pubsub.vala107
-rw-r--r--vala-xmpp/src/module/xep/0084_user_avatars.vala93
-rw-r--r--vala-xmpp/src/module/xep/0085_chat_state_notifications.vala74
-rw-r--r--vala-xmpp/src/module/xep/0115_entitiy_capabilities.vala125
-rw-r--r--vala-xmpp/src/module/xep/0184_message_delivery_receipts.vala62
-rw-r--r--vala-xmpp/src/module/xep/0199_ping.vala56
-rw-r--r--vala-xmpp/src/module/xep/0203_delayed_delivery.vala70
-rw-r--r--vala-xmpp/src/module/xep/0280_message_carbons.vala91
-rw-r--r--vala-xmpp/src/module/xep/0333_chat_markers.vala81
-rw-r--r--vala-xmpp/src/module/xep/pixbuf_storage.vala9
-rw-r--r--vapi/gpg-error.vapi407
-rw-r--r--vapi/gpgme.deps1
-rw-r--r--vapi/gpgme.vapi1224
-rw-r--r--vapi/uuid.vapi68
163 files changed, 16651 insertions, 0 deletions
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 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<interface>
+ <requires lib="gtk+" version="3.0"/>
+ <template class="DinoUiAddConversationChatAddContactDialog">
+ <property name="default_width">300</property>
+ <property name="modal">True</property>
+ <child type="titlebar">
+ <object class="GtkHeaderBar">
+ <property name="visible">True</property>
+ <child>
+ <object class="GtkButton" id="cancel_button">
+ <property name="label">Cancel</property>
+ <property name="sensitive">True</property>
+ <property name="visible">True</property>
+ </object>
+ <packing>
+ <property name="pack_type">start</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkButton" id="ok_button">
+ <property name="has_default">True</property>
+ <property name="can_default">True</property>
+ <property name="label">Add</property>
+ <property name="sensitive">False</property>
+ <property name="visible">True</property>
+ <style>
+ <class name="suggested-action"/>
+ </style>
+ </object>
+ <packing>
+ <property name="pack_type">end</property>
+ </packing>
+ </child>
+ </object>
+ </child>
+ <child internal-child="vbox">
+ <object class="GtkBox">
+ <property name="visible">True</property>
+ <child>
+ <object class="GtkGrid" id="info_grid">
+ <property name="orientation">vertical</property>
+ <property name="margin">20</property>
+ <property name="row-spacing">7</property>
+ <property name="column-spacing">10</property>
+ <property name="visible">True</property>
+ <child>
+ <object class="GtkLabel">
+ <property name="label">Account</property>
+ <property name="xalign">1</property>
+ <property name="visible">True</property>
+ </object>
+ <packing>
+ <property name="left_attach">0</property>
+ <property name="top_attach">0</property>
+ <property name="width">1</property>
+ <property name="height">1</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkComboBoxText" id="accounts_comboboxtext">
+ <property name="hexpand">True</property>
+ <property name="width_request">200</property>
+ <property name="visible">True</property>
+ </object>
+ <packing>
+ <property name="left_attach">1</property>
+ <property name="top_attach">0</property>
+ <property name="width">1</property>
+ <property name="height">1</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkLabel">
+ <property name="label">JID</property>
+ <property name="xalign">1</property>
+ <property name="visible">True</property>
+ </object>
+ <packing>
+ <property name="left_attach">0</property>
+ <property name="top_attach">1</property>
+ <property name="width">1</property>
+ <property name="height">1</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkEntry" id="jid_entry">
+ <property name="activates_default">True</property>
+ <property name="hexpand">True</property>
+ <property name="width_request">200</property>
+ <property name="visible">True</property>
+ </object>
+ <packing>
+ <property name="left_attach">1</property>
+ <property name="top_attach">1</property>
+ <property name="width">1</property>
+ <property name="height">1</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkLabel">
+ <property name="label">Alias</property>
+ <property name="xalign">1</property>
+ <property name="visible">True</property>
+ </object>
+ <packing>
+ <property name="left_attach">0</property>
+ <property name="top_attach">2</property>
+ <property name="width">1</property>
+ <property name="height">1</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkEntry" id="alias_entry">
+ <property name="activates_default">True</property>
+ <property name="hexpand">True</property>
+ <property name="width_request">200</property>
+ <property name="visible">True</property>
+ </object>
+ <packing>
+ <property name="left_attach">1</property>
+ <property name="top_attach">2</property>
+ <property name="width">1</property>
+ <property name="height">1</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkCheckButton" id="subscribe_checkbutton">
+ <property name="active">True</property>
+ <property name="label">Request presence updates</property>
+ <property name="xalign">0</property>
+ <property name="visible">True</property>
+ </object>
+ <packing>
+ <property name="left_attach">1</property>
+ <property name="top_attach">3</property>
+ <property name="width">1</property>
+ <property name="height">1</property>
+ </packing>
+ </child>
+ </object>
+ </child>
+ </object>
+ </child>
+ <action-widgets>
+ <action-widget response="cancel">cancel_button</action-widget>
+ <action-widget response="ok" default="true">ok_button</action-widget>
+ </action-widgets>
+ </template>
+</interface>
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 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<interface>
+ <requires lib="gtk+" version="3.0"/>
+ <template class="DinoUiAddConversationConferenceAddGroupchatDialog">
+ <property name="default_width">400</property>
+ <property name="modal">True</property>
+ <child type="titlebar">
+ <object class="GtkHeaderBar">
+ <property name="visible">True</property>
+ <child>
+ <object class="GtkButton" id="cancel_button">
+ <property name="label">Cancel</property>
+ <property name="sensitive">True</property>
+ <property name="visible">True</property>
+ </object>
+ <packing>
+ <property name="pack_type">start</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkButton" id="ok_button">
+ <property name="can_default">True</property>
+ <property name="has_default">True</property>
+ <property name="sensitive">False</property>
+ <property name="visible">True</property>
+ <style>
+ <class name="suggested-action"/>
+ </style>
+ </object>
+ <packing>
+ <property name="pack_type">end</property>
+ </packing>
+ </child>
+ </object>
+ </child>
+ <child internal-child="vbox">
+ <object class="GtkBox" id="main">
+ <property name="visible">True</property>
+ <child>
+ <object class="GtkGrid">
+ <property name="orientation">vertical</property>
+ <property name="margin">20</property>
+ <property name="margin_right">40</property>
+ <property name="margin_left">40</property>
+ <property name="row-spacing">7</property>
+ <property name="column-spacing">10</property>
+ <property name="visible">True</property>
+ <child>
+ <object class="GtkLabel">
+ <property name="label">Account</property>
+ <property name="xalign">1</property>
+ <property name="visible">True</property>
+ </object>
+ <packing>
+ <property name="left_attach">0</property>
+ <property name="top_attach">0</property>
+ <property name="width">1</property>
+ <property name="height">1</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkStack" id="accounts_stack">
+ <property name="visible">True</property>
+ <child>
+ <object class="GtkComboBoxText" id="accounts_comboboxtext">
+ <property name="hexpand">True</property>
+ <property name="width_request">200</property>
+ <property name="visible">True</property>
+ </object>
+ <packing>
+ <property name="name">combobox</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkLabel" id="account_label">
+ <property name="xalign">0</property>
+ <property name="can_focus">True</property>
+ <property name="visible">True</property>
+ </object>
+ <packing>
+ <property name="name">label</property>
+ </packing>
+ </child>
+ </object>
+ <packing>
+ <property name="left_attach">1</property>
+ <property name="top_attach">0</property>
+ <property name="width">1</property>
+ <property name="height">1</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkLabel">
+ <property name="label">JID</property>
+ <property name="xalign">1</property>
+ <property name="visible">True</property>
+ </object>
+ <packing>
+ <property name="left_attach">0</property>
+ <property name="top_attach">1</property>
+ <property name="width">1</property>
+ <property name="height">1</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkEntry" id="jid_entry">
+ <property name="activates_default">True</property>
+ <property name="hexpand">True</property>
+ <property name="width_request">200</property>
+ <property name="visible">True</property>
+ </object>
+ <packing>
+ <property name="left_attach">1</property>
+ <property name="top_attach">1</property>
+ <property name="width">1</property>
+ <property name="height">1</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkLabel">
+ <property name="label">Nick</property>
+ <property name="xalign">1</property>
+ <property name="visible">True</property>
+ </object>
+ <packing>
+ <property name="left_attach">0</property>
+ <property name="top_attach">2</property>
+ <property name="width">1</property>
+ <property name="height">1</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkEntry" id="nick_entry">
+ <property name="activates_default">True</property>
+ <property name="hexpand">True</property>
+ <property name="width_request">200</property>
+ <property name="visible">True</property>
+ </object>
+ <packing>
+ <property name="left_attach">1</property>
+ <property name="top_attach">2</property>
+ <property name="width">1</property>
+ <property name="height">1</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkLabel">
+ <property name="label">Password</property>
+ <property name="xalign">1</property>
+ <property name="visible">True</property>
+ </object>
+ <packing>
+ <property name="left_attach">0</property>
+ <property name="top_attach">3</property>
+ <property name="width">1</property>
+ <property name="height">1</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkEntry" id="password_entry">
+ <property name="activates_default">True</property>
+ <property name="visibility">False</property>
+ <property name="hexpand">True</property>
+ <property name="width_request">200</property>
+ <property name="visible">True</property>
+ </object>
+ <packing>
+ <property name="left_attach">1</property>
+ <property name="top_attach">3</property>
+ <property name="width">1</property>
+ <property name="height">1</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkCheckButton" id="autojoin_checkbutton">
+ <property name="active">False</property>
+ <property name="label">Join on startup</property>
+ <property name="xalign">0</property>
+ <property name="visible">True</property>
+ </object>
+ <packing>
+ <property name="left_attach">1</property>
+ <property name="top_attach">5</property>
+ <property name="width">1</property>
+ <property name="height">1</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkLabel" id="alias_label">
+ <property name="label">Alias</property>
+ <property name="xalign">1</property>
+ <property name="visible">True</property>
+ </object>
+ <packing>
+ <property name="left_attach">0</property>
+ <property name="top_attach">6</property>
+ <property name="width">1</property>
+ <property name="height">1</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkEntry" id="alias_entry">
+ <property name="activates_default">True</property>
+ <property name="hexpand">True</property>
+ <property name="width_request">200</property>
+ <property name="visible">True</property>
+ </object>
+ <packing>
+ <property name="left_attach">1</property>
+ <property name="top_attach">6</property>
+ <property name="width">1</property>
+ <property name="height">1</property>
+ </packing>
+ </child>
+ </object>
+ </child>
+ </object>
+ </child>
+ <action-widgets>
+ <action-widget response="cancel">cancel_button</action-widget>
+ <action-widget response="ok" default="true">ok_button</action-widget>
+ </action-widgets>
+ </template>
+</interface>
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 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<interface>
+ <requires lib="gtk+" version="3.0"/>
+ <template class="DinoUiAddConversationConferenceConferenceDetailsFragment">
+ <property name="visible">True</property>
+ <child>
+ <object class="GtkGrid">
+ <property name="orientation">vertical</property>
+ <property name="margin">20</property>
+ <property name="margin_right">40</property>
+ <property name="margin_left">40</property>
+ <property name="row-spacing">7</property>
+ <property name="column-spacing">10</property>
+ <property name="visible">True</property>
+ <child>
+ <object class="GtkLabel">
+ <property name="label">Account</property>
+ <property name="xalign">1</property>
+ <property name="visible">True</property>
+ <style>
+ <class name="dim-label"/>
+ </style>
+ </object>
+ <packing>
+ <property name="left_attach">0</property>
+ <property name="top_attach">0</property>
+ <property name="width">1</property>
+ <property name="height">1</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkStack" id="accounts_stack">
+ <property name="visible">True</property>
+ <child>
+ <object class="GtkButton" id="accounts_button">
+ <property name="relief">none</property>
+ <property name="visible">True</property>
+ <child>
+ <object class="GtkLabel" id="accounts_label">
+ <property name="xalign">0</property>
+ <property name="visible">True</property>
+ </object>
+ </child>
+ </object>
+ <packing>
+ <property name="name">label</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkComboBoxText" id="accounts_comboboxtext">
+ <property name="hexpand">True</property>
+ <property name="width_request">200</property>
+ <property name="visible">True</property>
+ </object>
+ <packing>
+ <property name="name">entry</property>
+ </packing>
+ </child>
+ </object>
+ <packing>
+ <property name="left_attach">1</property>
+ <property name="top_attach">0</property>
+ <property name="width">1</property>
+ <property name="height">1</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkLabel">
+ <property name="label">JID</property>
+ <property name="xalign">1</property>
+ <property name="visible">True</property>
+ <style>
+ <class name="dim-label"/>
+ </style>
+ </object>
+ <packing>
+ <property name="left_attach">0</property>
+ <property name="top_attach">1</property>
+ <property name="width">1</property>
+ <property name="height">1</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkStack" id="jid_stack">
+ <property name="visible">True</property>
+ <child>
+ <object class="GtkButton" id="jid_button">
+ <property name="relief">none</property>
+ <property name="visible">True</property>
+ <child>
+ <object class="GtkLabel" id="jid_label">
+ <property name="xalign">0</property>
+ <property name="visible">True</property>
+ </object>
+ </child>
+ </object>
+ <packing>
+ <property name="name">label</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkEntry" id="jid_entry">
+ <property name="hexpand">True</property>
+ <property name="width_request">200</property>
+ <property name="visible">True</property>
+ </object>
+ <packing>
+ <property name="name">entry</property>
+ </packing>
+ </child>
+ </object>
+ <packing>
+ <property name="left_attach">1</property>
+ <property name="top_attach">1</property>
+ <property name="width">1</property>
+ <property name="height">1</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkLabel">
+ <property name="label">Nick</property>
+ <property name="xalign">1</property>
+ <property name="visible">True</property>
+ <style>
+ <class name="dim-label"/>
+ </style>
+ </object>
+ <packing>
+ <property name="left_attach">0</property>
+ <property name="top_attach">2</property>
+ <property name="width">1</property>
+ <property name="height">1</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkStack" id="nick_stack">
+ <property name="visible">True</property>
+ <child>
+ <object class="GtkButton" id="nick_button">
+ <property name="relief">none</property>
+ <property name="visible">True</property>
+ <child>
+ <object class="GtkLabel" id="nick_label">
+ <property name="xalign">0</property>
+ <property name="visible">True</property>
+ </object>
+ </child>
+ </object>
+ <packing>
+ <property name="name">label</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkEntry" id="nick_entry">
+ <property name="hexpand">True</property>
+ <property name="width_request">200</property>
+ <property name="visible">True</property>
+ </object>
+ <packing>
+ <property name="name">entry</property>
+ </packing>
+ </child>
+ </object>
+ <packing>
+ <property name="left_attach">1</property>
+ <property name="top_attach">2</property>
+ <property name="width">1</property>
+ <property name="height">1</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkLabel">
+ <property name="label">Password</property>
+ <property name="xalign">1</property>
+ <property name="visible">True</property>
+ <style>
+ <class name="dim-label"/>
+ </style>
+ </object>
+ <packing>
+ <property name="left_attach">0</property>
+ <property name="top_attach">3</property>
+ <property name="width">1</property>
+ <property name="height">1</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkStack" id="password_stack">
+ <property name="visible">True</property>
+ <child>
+ <object class="GtkButton" id="password_button">
+ <property name="relief">none</property>
+ <property name="visible">True</property>
+ <child>
+ <object class="GtkLabel" id="password_label">
+ <property name="xalign">0</property>
+ <property name="visible">True</property>
+ </object>
+ </child>
+ </object>
+ <packing>
+ <property name="name">label</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkEntry" id="password_entry">
+ <property name="hexpand">True</property>
+ <property name="width_request">200</property>
+ <property name="visibility">False</property>
+ <property name="visible">True</property>
+ </object>
+ <packing>
+ <property name="name">entry</property>
+ </packing>
+ </child>
+ </object>
+ <packing>
+ <property name="left_attach">1</property>
+ <property name="top_attach">3</property>
+ <property name="width">1</property>
+ <property name="height">1</property>
+ </packing>
+ </child>
+ </object>
+ </child>
+ </template>
+</interface>
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 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<interface>
+ <requires lib="gtk+" version="3.0"/>
+ <template class="DinoUiAddConversationListRow" parent="GtkListBoxRow">
+ <property name="visible">True</property>
+ <child>
+ <object class="GtkGrid" id="outer_grid">
+ <property name="orientation">horizontal</property>
+ <property name="margin">3</property>
+ <property name="column-spacing">10</property>
+ <property name="visible">True</property>
+ <child>
+ <object class="GtkImage" id="image">
+ <property name="height_request">30</property>
+ <property name="width_request">30</property>
+ <property name="visible">True</property>
+ </object>
+ </child>
+ <child>
+ <object class="GtkGrid">
+ <property name="orientation">vertical</property>
+ <property name="visible">True</property>
+ <child>
+ <object class="GtkLabel" id="name_label">
+ <property name="max_width_chars">1</property>
+ <property name="ellipsize">end</property>
+ <property name="expand">True</property>
+ <property name="xalign">0</property>
+ <property name="visible">True</property>
+ </object>
+ <packing>
+ <property name="left_attach">0</property>
+ <property name="top_attach">0</property>
+ <property name="width">1</property>
+ <property name="height">1</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkLabel" id="via_label">
+ <property name="max_width_chars">1</property>
+ <property name="ellipsize">end</property>
+ <property name="expand">True</property>
+ <property name="xalign">0</property>
+ <property name="visible">True</property>
+ <attributes>
+ <attribute name="scale" value="0.8"/>
+ </attributes>
+ </object>
+ <packing>
+ <property name="left_attach">0</property>
+ <property name="top_attach">1</property>
+ <property name="width">1</property>
+ <property name="height">1</property>
+ </packing>
+ </child>
+ </object>
+ </child>
+ </object>
+ </child>
+ </template>
+</interface>
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 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<interface>
+ <requires lib="gtk+" version="3.0"/>
+ <template class="DinoUiAddConversationSelectJidFragment">
+ <property name="height_request">500</property>
+ <property name="width_request">460</property>
+ <property name="visible">True</property>
+ <child>
+ <object class="GtkGrid">
+ <property name="expand">True</property>
+ <property name="margin-top">20</property>
+ <property name="margin-right">80</property>
+ <property name="margin-bottom">20</property>
+ <property name="margin-left">80</property>
+ <property name="orientation">vertical</property>
+ <property name="row-spacing">10</property>
+ <property name="visible">True</property>
+ <child>
+ <object class="GtkEntry" id="entry">
+ <property name="activates_default">True</property>
+ <property name="hexpand">True</property>
+ <property name="visible">True</property>
+ </object>
+ </child>
+ <child>
+ <object class="GtkBox">
+ <property name="expand">True</property>
+ <property name="orientation">vertical</property>
+ <property name="visible">True</property>
+ <child>
+ <object class="GtkFrame">
+ <property name="visible">True</property>
+ <child>
+ <object class="GtkScrolledWindow" id="scrolled_window">
+ <property name="hscrollbar_policy">never</property>
+ <property name="expand">True</property>
+ <property name="visible">True</property>
+ <child>
+ <object class="GtkBox" id="box">
+ <property name="orientation">vertical</property>
+ <property name="visible">True</property>
+ </object>
+ </child>
+ </object>
+ </child>
+ </object>
+ </child>
+ <child>
+ <object class="GtkToolbar">
+ <property name="visible">True</property>
+ <style>
+ <class name="inline-toolbar"/>
+ </style>
+ <child>
+ <object class="GtkToolItem">
+ <property name="visible">True</property>
+ <child>
+ <object class="GtkBox">
+ <property name="visible">True</property>
+ <child>
+ <object class="GtkButton" id="add_button">
+ <property name="visible">True</property>
+ <child>
+ <object class="GtkImage">
+ <property name="icon-name">list-add-symbolic</property>
+ <property name="icon-size">1</property>
+ <property name="visible">True</property>
+ </object>
+ </child>
+ </object>
+ </child>
+ <child>
+ <object class="GtkButton" id="edit_button">
+ <property name="sensitive">False</property>
+ <property name="visible">True</property>
+ <child>
+ <object class="GtkImage">
+ <property name="icon-name">document-edit-symbolic</property>
+ <property name="icon-size">1</property>
+ <property name="visible">True</property>
+ </object>
+ </child>
+ </object>
+ </child>
+ <child>
+ <object class="GtkButton" id="remove_button">
+ <property name="sensitive">False</property>
+ <property name="visible">True</property>
+ <child>
+ <object class="GtkImage">
+ <property name="icon-name">list-remove-symbolic</property>
+ <property name="icon-size">1</property>
+ <property name="visible">True</property>
+ </object>
+ </child>
+ </object>
+ </child>
+ </object>
+ </child>
+ </object>
+ </child>
+ </object>
+ </child>
+ </object>
+ </child>
+ </object>
+ </child>
+ </template>
+</interface> \ 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 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<interface>
+ <template class="DinoUiChatInput" parent="GtkGrid"> <!-- TODO -->
+ <property name="hexpand">True</property>
+ <property name="orientation">horizontal</property>
+ <property name="visible">True</property>
+ <property name="margin">5</property>
+ <property name="column-spacing">5</property>
+ <child>
+ <object class="GtkFrame">
+ <child>
+ <object class="GtkTextView" id="text_input">
+ <property name="border-width">5</property>
+ <property name="wrap-mode">GTK_WRAP_WORD_CHAR</property>
+ <property name="can_focus">True</property>
+ <property name="hexpand">True</property>
+ <property name="visible">True</property>
+ </object>
+ </child>
+ </object>
+ </child>
+ </template>
+</interface>
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 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<interface>
+ <requires lib="gtk+" version="3.0"/>
+ <template class="DinoUiConversationListTitlebar" parent="GtkHeaderBar">
+ <property name="hexpand">False</property>
+ <property name="visible">True</property>
+ <style>
+ <class name="left_toolbar"/>
+ </style>
+ <child>
+ <object class="GtkMenuButton" id="add_button">
+ <property name="visible">True</property>
+ <child>
+ <object class="GtkImage">
+ <property name="visible">True</property>
+ <property name="icon-name">list-add-symbolic</property>
+ <property name="icon-size">1</property>
+ </object>
+ </child>
+ </object>
+ <packing>
+ <property name="pack_type">start</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkToggleButton" id="search_button">
+ <property name="visible">True</property>
+ <child>
+ <object class="GtkImage">
+ <property name="visible">True</property>
+ <property name="icon-name">system-search-symbolic</property>
+ <property name="icon-size">1</property>
+ </object>
+ </child>
+ </object>
+ <packing>
+ <property name="pack_type">end</property>
+ </packing>
+ </child>
+ </template>
+</interface>
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 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<interface>
+ <requires lib="gtk+" version="3.0"/>
+ <object class="GtkBox" id="main_box">
+ <property name="orientation">vertical</property>
+ <property name="visible">True</property>
+ <child>
+ <object class="GtkLabel" id="jid_label">
+ <property name="xalign">0</property>
+ <property name="visible">True</property>
+ <attributes>
+ <attribute name="weight" value="PANGO_WEIGHT_BOLD"/>
+ </attributes>
+ </object>
+ </child>
+ <child>
+ <object class="GtkBox" id="inner_box">
+ <property name="orientation">vertical</property>
+ <property name="visible">True</property>
+ </object>
+ </child>
+ </object>
+</interface>
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 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<interface>
+ <requires lib="gtk+" version="3.0"/>
+ <template class="DinoUiConversationSelectorConversationRow">
+ <property name="visible">True</property>
+ <style>
+ <class name="no_padding"/>
+ </style>
+ <child>
+ <object class="GtkRevealer" id="main_revealer">
+ <property name="transition-type">slide-down</property>
+ <property name="transition-duration">200</property>
+ <property name="reveal-child">False</property>
+ <property name="visible">True</property>
+ <child>
+ <object class="GtkGrid">
+ <property name="orientation">horizontal</property>
+ <property name="margin">7</property>
+ <property name="margin-right">14</property>
+ <property name="column-spacing">10</property>
+ <property name="visible">True</property>
+ <child>
+ <object class="GtkImage" id="image">
+ <property name="height_request">40</property>
+ <property name="width_request">40</property>
+ <property name="visible">True</property>
+ </object>
+ </child>
+ <child>
+ <object class="GtkGrid">
+ <property name="orientation">vertical</property>
+ <property name="visible">True</property>
+ <child>
+ <object class="GtkLabel" id="name_label">
+ <property name="max_width_chars">1</property>
+ <property name="ellipsize">end</property>
+ <property name="expand">True</property>
+ <property name="margin-right">7</property>
+ <property name="xalign">0</property>
+ <property name="visible">True</property>
+ </object>
+ <packing>
+ <property name="left_attach">0</property>
+ <property name="top_attach">0</property>
+ <property name="width">1</property>
+ <property name="height">1</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkRevealer" id="time_revealer">
+ <property name="transition-type">slide-right</property>
+ <property name="transition-duration">100</property>
+ <property name="reveal-child">True</property>
+ <property name="visible">True</property>
+ <child>
+ <object class="GtkLabel" id="time_label">
+ <property name="hexpand">False</property>
+ <property name="xalign">1</property>
+ <attributes>
+ <attribute name="scale" value="0.7"/>
+ </attributes>
+ </object>
+ </child>
+ </object>
+ <packing>
+ <property name="left_attach">1</property>
+ <property name="top_attach">0</property>
+ <property name="width">1</property>
+ <property name="height">1</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkLabel" id="message_label">
+ <property name="max_width_chars">1</property>
+ <property name="ellipsize">end</property>
+ <property name="expand">True</property>
+ <property name="xalign">0</property>
+ <attributes>
+ <attribute name="scale" value="0.8"/>
+ </attributes>
+ </object>
+ <packing>
+ <property name="left_attach">0</property>
+ <property name="top_attach">1</property>
+ <property name="width">2</property>
+ <property name="height">1</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkBox">
+ <property name="orientation">vertical</property>
+ <property name="visible">True</property>
+ <child>
+ <object class="GtkBox">
+ <property name="vexpand">True</property>
+ <property name="visible">True</property>
+ </object>
+ </child>
+ <child>
+ <object class="GtkRevealer" id="xbutton_revealer">
+ <property name="transition-type">slide-left</property>
+ <property name="transition-duration">100</property>
+ <property name="reveal-child">False</property>
+ <property name="visible">True</property>
+ <child>
+ <object class="GtkButton" id="x_button">
+ <property name="vexpand">False</property>
+ <property name="visible">True</property>
+ <style>
+ <class name="conversation_list_row_xbutton"/>
+ <class name="circular"/>
+ <class name="flat"/>
+ </style>
+ <child>
+ <object class="GtkImage">
+ <property name="icon-name">window-close-symbolic</property>
+ <property name="icon-size">1</property>
+ <property name="visible">True</property>
+ </object>
+ </child>
+ </object>
+ </child>
+ </object>
+ </child>
+ <child>
+ <object class="GtkBox">
+ <property name="vexpand">True</property>
+ <property name="visible">True</property>
+ </object>
+ </child>
+ </object>
+ <packing>
+ <property name="left_attach">2</property>
+ <property name="top_attach">0</property>
+ <property name="width">1</property>
+ <property name="height">2</property>
+ </packing>
+ </child>
+ </object>
+ </child>
+ </object>
+ </child>
+ </object>
+ </child>
+ </template>
+</interface>
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 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<interface>
+ <requires lib="gtk+" version="3.0"/>
+ <template class="DinoUiConversationSelectorView">
+ <property name="can_focus">True</property>
+ <property name="width_request">40</property>
+ <property name="expand">True</property>
+ <property name="orientation">vertical</property>
+ <property name="visible">True</property>
+ <child>
+ <object class="GtkSearchBar" id="search_bar">
+ <property name="hexpand">True</property>
+ <property name="show_close_button">True</property>
+ <property name="visible">True</property>
+ <child>
+ <object class="GtkSearchEntry" id="search_entry">
+ <property name="primary_icon_name">edit-find-symbolic</property>
+ <property name="placeholder_text" translatable="yes">Search</property>
+ <property name="hexpand">True</property>
+ <property name="visible">True</property>
+ </object>
+ </child>
+ </object>
+ </child>
+ <child>
+ <object class="GtkScrolledWindow" id="scrolled">
+ <property name="expand">True</property>
+ <property name="hscrollbar_policy">never</property>
+ <property name="visible">True</property>
+ </object>
+ </child>
+ </template>
+</interface>
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 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<interface>
+ <requires lib="gtk+" version="3.0"/>
+ <template class="DinoUiConversationSummaryMergedMessageItem">
+ <property name="hexpand">True</property>
+ <property name="column-spacing">7</property>
+ <property name="orientation">horizontal</property>
+ <property name="visible">True</property>
+ <child>
+ <object class="GtkImage" id="image">
+ <property name="expand">False</property>
+ <property name="height_request">30</property>
+ <property name="margin_top">2</property>
+ <property name="width_request">30</property>
+ <property name="visible">True</property>
+ <property name="yalign">0</property>
+ </object>
+ <packing>
+ <property name="left_attach">0</property>
+ <property name="top_attach">0</property>
+ <property name="width">1</property>
+ <property name="height">2</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkLabel" id="name_label">
+ <property name="xalign">0</property>
+ <property name="visible">True</property>
+ </object>
+ <packing>
+ <property name="left_attach">1</property>
+ <property name="top_attach">0</property>
+ <property name="width">1</property>
+ <property name="height">1</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkLabel" id="time_label">
+ <property name="visible">True</property>
+ <property name="xalign">1</property>
+ <property name="yalign">0</property>
+ <style>
+ <class name="dim-label"/>
+ </style>
+ </object>
+ <packing>
+ <property name="left_attach">2</property>
+ <property name="top_attach">0</property>
+ <property name="width">1</property>
+ <property name="height">1</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkImage" id="encryption_image">
+ <property name="visible">False</property>
+ <property name="xalign">1</property>
+ <style>
+ <class name="dim-label"/>
+ </style>
+ </object>
+ <packing>
+ <property name="left_attach">3</property>
+ <property name="top_attach">0</property>
+ <property name="width">1</property>
+ <property name="height">1</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkImage" id="received_image">
+ <property name="visible">False</property>
+ <property name="xalign">1</property>
+ <style>
+ <class name="dim-label"/>
+ </style>
+ </object>
+ <packing>
+ <property name="left_attach">4</property>
+ <property name="top_attach">0</property>
+ <property name="width">1</property>
+ <property name="height">1</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkTextView" id="message_text_view">
+ <property name="editable">False</property>
+ <property name="hexpand">True</property>
+ <property name="wrap-mode">GTK_WRAP_WORD_CHAR</property>
+ <property name="visible">True</property>
+ </object>
+ <packing>
+ <property name="left_attach">1</property>
+ <property name="top_attach">1</property>
+ <property name="width">2</property>
+ <property name="height">1</property>
+ </packing>
+ </child>
+ </template>
+</interface> \ 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 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<interface>
+ <requires lib="gtk+" version="3.0"/>
+ <template class="DinoUiConversationSummaryView">
+ <property name="expand">True</property>
+ <property name="visible">True</property>
+ <child>
+ <object class="GtkScrolledWindow" id="scrolled">
+ <property name="visible">True</property>
+ <child>
+ <object class="GtkBox">
+ <property name="margin">15</property>
+ <property name="orientation">vertical</property>
+ <child>
+ <object class="GtkBox" id="main">
+ <property name="expand">False</property>
+ <property name="orientation">vertical</property>
+ <property name="spacing">15</property>
+ <property name="visible">True</property>
+ </object>
+ </child>
+ <child>
+ <object class="GtkBox" id="filler">
+ <property name="expand">True</property>
+ <property name="visible">True</property>
+ </object>
+ </child>
+ </object>
+ </child>
+ </object>
+ </child>
+ </template>
+</interface>
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 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<interface>
+ <requires lib="gtk+" version="3.0"/>
+ <template class="DinoUiConversationTitlebar" parent="GtkHeaderBar">
+ <property name="title"></property>
+ <property name="hexpand">True</property>
+ <property name="show_close_button">True</property>
+ <property name="visible">True</property>
+ <child>
+ <object class="GtkMenuButton" id="menu_button">
+ <property name="visible">True</property>
+ <style>
+ <class name="image-button"/>
+ </style>
+ <child>
+ <object class="GtkImage">
+ <property name="visible">True</property>
+ <property name="icon-name">open-menu-symbolic</property>
+ <property name="icon-size">1</property>
+ </object>
+ </child>
+ </object>
+ <packing>
+ <property name="pack_type">end</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkMenuButton" id="encryption_button">
+ <property name="visible">False</property>
+ <style>
+ <class name="image-button"/>
+ </style>
+ <child>
+ <object class="GtkImage">
+ <property name="visible">True</property>
+ <property name="icon-size">1</property>
+ </object>
+ </child>
+ </object>
+ <packing>
+ <property name="pack_type">end</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkMenuButton" id="groupchat_button">
+ <property name="visible">False</property>
+ <style>
+ <class name="image-button"/>
+ </style>
+ <child>
+ <object class="GtkImage">
+ <property name="visible">True</property>
+ <property name="icon-name">system-users-symbolic</property>
+ <property name="icon-size">1</property>
+ </object>
+ </child>
+ </object>
+ <packing>
+ <property name="pack_type">end</property>
+ </packing>
+ </child>
+ </template>
+</interface>
diff --git a/client/data/gschemas.compiled b/client/data/gschemas.compiled
new file mode 100644
index 00000000..3a010b95
--- /dev/null
+++ b/client/data/gschemas.compiled
Binary files 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 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!-- Created with Inkscape (http://www.inkscape.org/) -->
+
+<svg
+ xmlns:osb="http://www.openswatchbook.org/uri/2009/osb"
+ xmlns:dc="http://purl.org/dc/elements/1.1/"
+ xmlns:cc="http://creativecommons.org/ns#"
+ xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+ xmlns:svg="http://www.w3.org/2000/svg"
+ xmlns="http://www.w3.org/2000/svg"
+ xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+ xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+ sodipodi:docname="double_tick.svg"
+ inkscape:export-filename="/home/sam/source-symbolic.png"
+ inkscape:export-xdpi="270"
+ inkscape:export-ydpi="270"
+ height="16"
+ id="svg7384"
+ style="enable-background:new"
+ version="1.1"
+ inkscape:version="0.92.0 r"
+ width="16">
+ <sodipodi:namedview
+ inkscape:bbox-nodes="true"
+ inkscape:bbox-paths="true"
+ bordercolor="#666666"
+ borderlayer="false"
+ borderopacity="1"
+ inkscape:current-layer="g8784"
+ inkscape:cx="11.598048"
+ inkscape:cy="11.93762"
+ gridtolerance="10"
+ inkscape:guide-bbox="true"
+ guidetolerance="10"
+ id="namedview88"
+ inkscape:object-nodes="true"
+ inkscape:object-paths="true"
+ objecttolerance="10"
+ pagecolor="#f7f7f7"
+ inkscape:pageopacity="1"
+ inkscape:pageshadow="2"
+ showborder="true"
+ showgrid="false"
+ showguides="true"
+ inkscape:showpageshadow="false"
+ inkscape:snap-bbox="true"
+ inkscape:snap-bbox-edge-midpoints="false"
+ inkscape:snap-bbox-midpoints="false"
+ inkscape:snap-center="false"
+ inkscape:snap-global="true"
+ inkscape:snap-grids="true"
+ inkscape:snap-intersection-paths="true"
+ inkscape:snap-midpoints="true"
+ inkscape:snap-nodes="true"
+ inkscape:snap-object-midpoints="true"
+ inkscape:snap-others="true"
+ inkscape:snap-page="false"
+ inkscape:snap-smooth-nodes="true"
+ inkscape:snap-to-guides="true"
+ inkscape:window-height="845"
+ inkscape:window-maximized="1"
+ inkscape:window-width="1600"
+ inkscape:window-x="0"
+ inkscape:window-y="27"
+ inkscape:zoom="22.627416"
+ units="mm">
+ <inkscape:grid
+ color="#000000"
+ dotted="false"
+ empcolor="#0800ff"
+ empopacity="0.4627451"
+ empspacing="4"
+ enabled="true"
+ id="grid4866"
+ opacity="0.16470588"
+ originx="-104.00001px"
+ originy="-96px"
+ snapvisiblegridlinesonly="true"
+ spacingx="0.25px"
+ spacingy="0.25px"
+ type="xygrid"
+ visible="true" />
+ </sodipodi:namedview>
+ <metadata
+ id="metadata90">
+ <rdf:RDF>
+ <cc:Work
+ rdf:about="">
+ <dc:format>image/svg+xml</dc:format>
+ <dc:type
+ rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
+ <dc:title>Paper Symbolic Icon Theme</dc:title>
+ </cc:Work>
+ </rdf:RDF>
+ </metadata>
+ <title
+ id="title8473">Paper Symbolic Icon Theme</title>
+ <defs
+ id="defs7386">
+ <linearGradient
+ id="linearGradient5606"
+ osb:paint="solid">
+ <stop
+ id="stop5608"
+ offset="0"
+ style="stop-color:#000000;stop-opacity:1;" />
+ </linearGradient>
+ <filter
+ inkscape:collect="always"
+ id="filter7554"
+ color-interpolation-filters="sRGB">
+ <feBlend
+ inkscape:collect="always"
+ id="feBlend7556"
+ in2="BackgroundImage"
+ mode="darken" />
+ </filter>
+ </defs>
+ <g
+ inkscape:groupmode="layer"
+ id="layer9"
+ inkscape:label="status"
+ style="display:inline"
+ transform="translate(-345.00021,-121)" />
+ <g
+ inkscape:groupmode="layer"
+ id="layer10"
+ inkscape:label="devices"
+ style="display:inline;filter:url(#filter7554)"
+ transform="translate(-345.00021,-121)" />
+ <g
+ inkscape:groupmode="layer"
+ id="layer1"
+ inkscape:label="places"
+ style="display:inline"
+ transform="translate(-104.00001,-738)" />
+ <g
+ inkscape:groupmode="layer"
+ id="layer14"
+ inkscape:label="mimetypes"
+ style="display:inline"
+ transform="translate(-345.00021,-121)" />
+ <g
+ inkscape:groupmode="layer"
+ id="layer15"
+ inkscape:label="emblems"
+ style="display:inline"
+ transform="translate(-345.00021,-121)" />
+ <g
+ inkscape:groupmode="layer"
+ id="g71291"
+ inkscape:label="emotes"
+ style="display:inline"
+ transform="translate(-345.00021,-121)" />
+ <g
+ inkscape:groupmode="layer"
+ id="layer2"
+ inkscape:label="categories"
+ style="display:inline"
+ transform="translate(-104.00001,-588)" />
+ <g
+ inkscape:groupmode="layer"
+ id="g6058"
+ inkscape:label="apps"
+ style="display:inline"
+ transform="translate(-104.00001,-588)" />
+ <g
+ inkscape:groupmode="layer"
+ id="layer12"
+ inkscape:label="actions"
+ style="display:inline"
+ transform="translate(-345.00021,-121)">
+ <g
+ id="g8784"
+ inkscape:label="object-select">
+ <path
+ inkscape:connector-curvature="0"
+ d="m 356.03145,124.03125 c -0.21888,0.0473 -0.42059,0.17053 -0.5625,0.34375 l -6.28125,7.1875 -2.25,-2.25 c -0.37633,-0.37638 -1.06119,-0.3764 -1.43755,-5e-5 -0.37635,0.37636 -0.37633,1.06122 5e-5,1.43755 l 3,3 0.78125,0.75 0.6875,-0.8125 7,-8 c 0.56742,-0.61773 -0.11583,-1.8248 -0.9375,-1.65625 z"
+ id="path8741"
+ sodipodi:nodetypes="ccccscccccc"
+ style="color:#000000;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:medium;line-height:normal;font-family:Sans;-inkscape-font-specification:Sans;text-indent:0;text-align:start;text-decoration:none;text-decoration-line:none;letter-spacing:normal;word-spacing:normal;text-transform:none;writing-mode:lr-tb;direction:ltr;baseline-shift:baseline;text-anchor:start;display:inline;overflow:visible;visibility:visible;fill:#555555;fill-opacity:1;stroke:none;stroke-width:2;marker:none;enable-background:accumulate" />
+ <path
+ style="color:#000000;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:medium;line-height:normal;font-family:Sans;-inkscape-font-specification:Sans;text-indent:0;text-align:start;text-decoration:none;text-decoration-line:none;letter-spacing:normal;word-spacing:normal;text-transform:none;writing-mode:lr-tb;direction:ltr;baseline-shift:baseline;text-anchor:start;display:inline;overflow:visible;visibility:visible;fill:#555555;fill-opacity:1;stroke:none;stroke-width:2.13333344;marker:none;enable-background:accumulate"
+ sodipodi:nodetypes="cccccccccc"
+ id="path4500"
+ d="m 359.74533,124.03232 c -0.23347,0.0504 -0.44863,0.1819 -0.6,0.36667 l -6.7,7.66666 -0.34583,-0.34583 -1.42592,1.61866 1.03842,1.06051 0.83333,0.8 0.73334,-0.86667 7.46666,-8.53333 c 0.60525,-0.65892 -0.12355,-1.94646 -1,-1.76667 z"
+ inkscape:connector-curvature="0" />
+ </g>
+ </g>
+</svg>
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 @@
+<?xml version="1.0" encoding="UTF-8"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" width="24" height="24" viewBox="0 0 24 24"><path fill="#6f778c" d="M2,21L23,12L2,3V10L17,12L2,14V21Z" /></svg>
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 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!-- Created with Inkscape (http://www.inkscape.org/) -->
+
+<svg
+ xmlns:dc="http://purl.org/dc/elements/1.1/"
+ xmlns:cc="http://creativecommons.org/ns#"
+ xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+ xmlns:svg="http://www.w3.org/2000/svg"
+ xmlns="http://www.w3.org/2000/svg"
+ xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+ xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+ width="28.222221mm"
+ height="28.222221mm"
+ viewBox="0 0 99.999997 99.999997"
+ id="svg2"
+ version="1.1"
+ inkscape:version="0.92.0 r"
+ sodipodi:docname="status_away.svg">
+ <defs
+ id="defs4" />
+ <sodipodi:namedview
+ id="base"
+ pagecolor="#ffffff"
+ bordercolor="#666666"
+ borderopacity="1.0"
+ inkscape:pageopacity="0.0"
+ inkscape:pageshadow="2"
+ inkscape:zoom="2.8"
+ inkscape:cx="-2.3899949"
+ inkscape:cy="49.421164"
+ inkscape:document-units="px"
+ inkscape:current-layer="layer1"
+ showgrid="false"
+ fit-margin-top="0"
+ fit-margin-left="0"
+ fit-margin-right="0"
+ fit-margin-bottom="0"
+ inkscape:window-width="1600"
+ inkscape:window-height="873"
+ inkscape:window-x="0"
+ inkscape:window-y="27"
+ inkscape:window-maximized="1" />
+ <metadata
+ id="metadata7">
+ <rdf:RDF>
+ <cc:Work
+ rdf:about="">
+ <dc:format>image/svg+xml</dc:format>
+ <dc:type
+ rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
+ <dc:title />
+ </cc:Work>
+ </rdf:RDF>
+ </metadata>
+ <g
+ inkscape:label="Layer 1"
+ inkscape:groupmode="layer"
+ id="layer1"
+ transform="translate(-62.857162,-678.07648)">
+ <circle
+ style="opacity:1;fill:#ffa726;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:1.5;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1"
+ id="path4136"
+ cx="112.85716"
+ cy="728.07648"
+ r="50" />
+ <path
+ style="fill:none;fill-rule:evenodd;stroke:#ffffff;stroke-width:12;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
+ d="m 106.85716,698.38898 v 35.6875 h 35.6875"
+ id="path4157"
+ inkscape:connector-curvature="0"
+ sodipodi:nodetypes="ccc" />
+ </g>
+</svg>
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 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!-- Created with Inkscape (http://www.inkscape.org/) -->
+
+<svg
+ xmlns:dc="http://purl.org/dc/elements/1.1/"
+ xmlns:cc="http://creativecommons.org/ns#"
+ xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+ xmlns:svg="http://www.w3.org/2000/svg"
+ xmlns="http://www.w3.org/2000/svg"
+ xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+ xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+ width="28.222221mm"
+ height="28.222221mm"
+ viewBox="0 0 99.999997 99.999997"
+ id="svg2"
+ version="1.1"
+ inkscape:version="0.91 r13725"
+ sodipodi:docname="status_chat.svg">
+ <defs
+ id="defs4" />
+ <sodipodi:namedview
+ id="base"
+ pagecolor="#ffffff"
+ bordercolor="#666666"
+ borderopacity="1.0"
+ inkscape:pageopacity="0.0"
+ inkscape:pageshadow="2"
+ inkscape:zoom="2.8"
+ inkscape:cx="-2.3899949"
+ inkscape:cy="49.421164"
+ inkscape:document-units="px"
+ inkscape:current-layer="layer1"
+ showgrid="false"
+ fit-margin-top="0"
+ fit-margin-left="0"
+ fit-margin-right="0"
+ fit-margin-bottom="0"
+ inkscape:window-width="1600"
+ inkscape:window-height="845"
+ inkscape:window-x="0"
+ inkscape:window-y="27"
+ inkscape:window-maximized="1" />
+ <metadata
+ id="metadata7">
+ <rdf:RDF>
+ <cc:Work
+ rdf:about="">
+ <dc:format>image/svg+xml</dc:format>
+ <dc:type
+ rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
+ <dc:title></dc:title>
+ </cc:Work>
+ </rdf:RDF>
+ </metadata>
+ <g
+ inkscape:label="Layer 1"
+ inkscape:groupmode="layer"
+ id="layer1"
+ transform="translate(-62.857162,-678.07648)">
+ <circle
+ style="opacity:1;fill:#81c784;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:1.5;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1"
+ id="path4136"
+ cx="112.85716"
+ cy="728.07648"
+ r="50" />
+ <path
+ style="fill:#81c784;fill-rule:evenodd;stroke:#ffffff;stroke-width:7;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;fill-opacity:1"
+ d="m 77.857162,738.07648 c 13.92857,35.35715 55.714288,35 69.999998,0"
+ id="path4199"
+ inkscape:connector-curvature="0"
+ sodipodi:nodetypes="cc" />
+ <circle
+ style="opacity:1;fill:#ffffff;fill-opacity:1;fill-rule:evenodd;stroke:#ffffff;stroke-width:5;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1"
+ id="path4203"
+ cx="90.357162"
+ cy="710.57648"
+ r="5" />
+ <circle
+ style="opacity:1;fill:#ffffff;fill-opacity:1;fill-rule:evenodd;stroke:#ffffff;stroke-width:5;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1"
+ id="path4203-3"
+ cx="135.35716"
+ cy="710.57648"
+ r="5" />
+ </g>
+</svg>
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 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!-- Created with Inkscape (http://www.inkscape.org/) -->
+
+<svg
+ xmlns:dc="http://purl.org/dc/elements/1.1/"
+ xmlns:cc="http://creativecommons.org/ns#"
+ xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+ xmlns:svg="http://www.w3.org/2000/svg"
+ xmlns="http://www.w3.org/2000/svg"
+ xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+ xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+ width="28.222221mm"
+ height="28.222221mm"
+ viewBox="0 0 99.999997 99.999997"
+ id="svg2"
+ version="1.1"
+ inkscape:version="0.91 r13725"
+ sodipodi:docname="status_dnd.svg">
+ <defs
+ id="defs4" />
+ <sodipodi:namedview
+ id="base"
+ pagecolor="#ffffff"
+ bordercolor="#666666"
+ borderopacity="1.0"
+ inkscape:pageopacity="0.0"
+ inkscape:pageshadow="2"
+ inkscape:zoom="2.8"
+ inkscape:cx="-2.3899949"
+ inkscape:cy="49.421164"
+ inkscape:document-units="px"
+ inkscape:current-layer="layer1"
+ showgrid="false"
+ fit-margin-top="0"
+ fit-margin-left="0"
+ fit-margin-right="0"
+ fit-margin-bottom="0"
+ inkscape:window-width="1600"
+ inkscape:window-height="845"
+ inkscape:window-x="0"
+ inkscape:window-y="27"
+ inkscape:window-maximized="1" />
+ <metadata
+ id="metadata7">
+ <rdf:RDF>
+ <cc:Work
+ rdf:about="">
+ <dc:format>image/svg+xml</dc:format>
+ <dc:type
+ rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
+ <dc:title />
+ </cc:Work>
+ </rdf:RDF>
+ </metadata>
+ <g
+ inkscape:label="Layer 1"
+ inkscape:groupmode="layer"
+ id="layer1"
+ transform="translate(-62.857162,-678.07648)">
+ <circle
+ style="opacity:1;fill:#e57373;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:1.5;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1"
+ id="path4136"
+ cx="112.85716"
+ cy="728.07648"
+ r="50" />
+ <path
+ style="fill:none;fill-rule:evenodd;stroke:#ffffff;stroke-width:15;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
+ d="m 82.857162,728.07648 59.999998,0"
+ id="path4178"
+ inkscape:connector-curvature="0"
+ sodipodi:nodetypes="cc" />
+ </g>
+</svg>
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 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!-- Created with Inkscape (http://www.inkscape.org/) -->
+
+<svg
+ xmlns:dc="http://purl.org/dc/elements/1.1/"
+ xmlns:cc="http://creativecommons.org/ns#"
+ xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+ xmlns:svg="http://www.w3.org/2000/svg"
+ xmlns="http://www.w3.org/2000/svg"
+ xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+ xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+ width="28.222221mm"
+ height="28.222221mm"
+ viewBox="0 0 99.999997 99.999997"
+ id="svg2"
+ version="1.1"
+ inkscape:version="0.91 r13725"
+ sodipodi:docname="status_online.svg">
+ <defs
+ id="defs4" />
+ <sodipodi:namedview
+ id="base"
+ pagecolor="#ffffff"
+ bordercolor="#666666"
+ borderopacity="1.0"
+ inkscape:pageopacity="0.0"
+ inkscape:pageshadow="2"
+ inkscape:zoom="2.8"
+ inkscape:cx="-2.3899949"
+ inkscape:cy="49.421164"
+ inkscape:document-units="px"
+ inkscape:current-layer="layer1"
+ showgrid="false"
+ fit-margin-top="0"
+ fit-margin-left="0"
+ fit-margin-right="0"
+ fit-margin-bottom="0"
+ inkscape:window-width="1600"
+ inkscape:window-height="845"
+ inkscape:window-x="0"
+ inkscape:window-y="27"
+ inkscape:window-maximized="1" />
+ <metadata
+ id="metadata7">
+ <rdf:RDF>
+ <cc:Work
+ rdf:about="">
+ <dc:format>image/svg+xml</dc:format>
+ <dc:type
+ rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
+ <dc:title></dc:title>
+ </cc:Work>
+ </rdf:RDF>
+ </metadata>
+ <g
+ inkscape:label="Layer 1"
+ inkscape:groupmode="layer"
+ id="layer1"
+ transform="translate(-62.857162,-678.07648)">
+ <circle
+ style="opacity:1;fill:#81c784;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:1.5;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1"
+ id="path4136"
+ cx="112.85716"
+ cy="728.07648"
+ r="50" />
+ </g>
+</svg>
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 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!-- Created with Inkscape (http://www.inkscape.org/) -->
+
+<svg
+ xmlns:osb="http://www.openswatchbook.org/uri/2009/osb"
+ xmlns:dc="http://purl.org/dc/elements/1.1/"
+ xmlns:cc="http://creativecommons.org/ns#"
+ xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+ xmlns:svg="http://www.w3.org/2000/svg"
+ xmlns="http://www.w3.org/2000/svg"
+ xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+ xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+ sodipodi:docname="tick.svg"
+ inkscape:export-filename="/home/sam/source-symbolic.png"
+ inkscape:export-xdpi="270"
+ inkscape:export-ydpi="270"
+ height="16"
+ id="svg7384"
+ style="enable-background:new"
+ version="1.1"
+ inkscape:version="0.92.0 r"
+ width="16">
+ <sodipodi:namedview
+ inkscape:bbox-nodes="true"
+ inkscape:bbox-paths="true"
+ bordercolor="#666666"
+ borderlayer="false"
+ borderopacity="1"
+ inkscape:current-layer="g8784"
+ inkscape:cx="11.077638"
+ inkscape:cy="13.807036"
+ gridtolerance="10"
+ inkscape:guide-bbox="true"
+ guidetolerance="10"
+ id="namedview88"
+ inkscape:object-nodes="true"
+ inkscape:object-paths="true"
+ objecttolerance="10"
+ pagecolor="#f7f7f7"
+ inkscape:pageopacity="1"
+ inkscape:pageshadow="2"
+ showborder="true"
+ showgrid="false"
+ showguides="true"
+ inkscape:showpageshadow="false"
+ inkscape:snap-bbox="true"
+ inkscape:snap-bbox-edge-midpoints="false"
+ inkscape:snap-bbox-midpoints="false"
+ inkscape:snap-center="false"
+ inkscape:snap-global="true"
+ inkscape:snap-grids="true"
+ inkscape:snap-intersection-paths="true"
+ inkscape:snap-midpoints="true"
+ inkscape:snap-nodes="true"
+ inkscape:snap-object-midpoints="true"
+ inkscape:snap-others="true"
+ inkscape:snap-page="false"
+ inkscape:snap-smooth-nodes="true"
+ inkscape:snap-to-guides="true"
+ inkscape:window-height="838"
+ inkscape:window-maximized="0"
+ inkscape:window-width="1290"
+ inkscape:window-x="0"
+ inkscape:window-y="27"
+ inkscape:zoom="11.313708"
+ units="mm">
+ <inkscape:grid
+ color="#000000"
+ dotted="false"
+ empcolor="#0800ff"
+ empopacity="0.4627451"
+ empspacing="4"
+ enabled="true"
+ id="grid4866"
+ opacity="0.16470588"
+ originx="-104.00001px"
+ originy="-96px"
+ snapvisiblegridlinesonly="true"
+ spacingx="0.25px"
+ spacingy="0.25px"
+ type="xygrid"
+ visible="true" />
+ </sodipodi:namedview>
+ <metadata
+ id="metadata90">
+ <rdf:RDF>
+ <cc:Work
+ rdf:about="">
+ <dc:format>image/svg+xml</dc:format>
+ <dc:type
+ rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
+ <dc:title>Paper Symbolic Icon Theme</dc:title>
+ </cc:Work>
+ </rdf:RDF>
+ </metadata>
+ <title
+ id="title8473">Paper Symbolic Icon Theme</title>
+ <defs
+ id="defs7386">
+ <linearGradient
+ id="linearGradient5606"
+ osb:paint="solid">
+ <stop
+ id="stop5608"
+ offset="0"
+ style="stop-color:#000000;stop-opacity:1;" />
+ </linearGradient>
+ <filter
+ inkscape:collect="always"
+ id="filter7554"
+ color-interpolation-filters="sRGB">
+ <feBlend
+ inkscape:collect="always"
+ id="feBlend7556"
+ in2="BackgroundImage"
+ mode="darken" />
+ </filter>
+ </defs>
+ <g
+ inkscape:groupmode="layer"
+ id="layer9"
+ inkscape:label="status"
+ style="display:inline"
+ transform="translate(-345.00021,-121)" />
+ <g
+ inkscape:groupmode="layer"
+ id="layer10"
+ inkscape:label="devices"
+ style="display:inline;filter:url(#filter7554)"
+ transform="translate(-345.00021,-121)" />
+ <g
+ inkscape:groupmode="layer"
+ id="layer1"
+ inkscape:label="places"
+ style="display:inline"
+ transform="translate(-104.00001,-738)" />
+ <g
+ inkscape:groupmode="layer"
+ id="layer14"
+ inkscape:label="mimetypes"
+ style="display:inline"
+ transform="translate(-345.00021,-121)" />
+ <g
+ inkscape:groupmode="layer"
+ id="layer15"
+ inkscape:label="emblems"
+ style="display:inline"
+ transform="translate(-345.00021,-121)" />
+ <g
+ inkscape:groupmode="layer"
+ id="g71291"
+ inkscape:label="emotes"
+ style="display:inline"
+ transform="translate(-345.00021,-121)" />
+ <g
+ inkscape:groupmode="layer"
+ id="layer2"
+ inkscape:label="categories"
+ style="display:inline"
+ transform="translate(-104.00001,-588)" />
+ <g
+ inkscape:groupmode="layer"
+ id="g6058"
+ inkscape:label="apps"
+ style="display:inline"
+ transform="translate(-104.00001,-588)" />
+ <g
+ inkscape:groupmode="layer"
+ id="layer12"
+ inkscape:label="actions"
+ style="display:inline"
+ transform="translate(-345.00021,-121)">
+ <g
+ id="g8784"
+ inkscape:label="object-select">
+ <path
+ inkscape:connector-curvature="0"
+ d="m 356.03145,124.03125 c -0.21888,0.0473 -0.42059,0.17053 -0.5625,0.34375 l -6.28125,7.1875 -2.25,-2.25 c -0.37633,-0.37638 -1.06119,-0.3764 -1.43755,-5e-5 -0.37635,0.37636 -0.37633,1.06122 5e-5,1.43755 l 3,3 0.78125,0.75 0.6875,-0.8125 7,-8 c 0.56742,-0.61773 -0.11583,-1.8248 -0.9375,-1.65625 z"
+ id="path8741"
+ sodipodi:nodetypes="ccccscccccc"
+ style="color:#000000;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:medium;line-height:normal;font-family:Sans;-inkscape-font-specification:Sans;text-indent:0;text-align:start;text-decoration:none;text-decoration-line:none;letter-spacing:normal;word-spacing:normal;text-transform:none;writing-mode:lr-tb;direction:ltr;baseline-shift:baseline;text-anchor:start;display:inline;overflow:visible;visibility:visible;fill:#555555;fill-opacity:1;stroke:none;stroke-width:2;marker:none;enable-background:accumulate" />
+ </g>
+ </g>
+</svg>
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 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<interface>
+ <requires lib="gtk+" version="3.0"/>
+ <template class="DinoUiManageAccountsAccountRow">
+ <property name="visible">True</property>
+ <child>
+ <object class="GtkGrid">
+ <property name="orientation">horizontal</property>
+ <property name="margin">6</property>
+ <property name="column-spacing">6</property>
+ <property name="visible">True</property>
+ <child>
+ <object class="GtkImage" id="image">
+ <property name="height_request">40</property>
+ <property name="width_request">40</property>
+ <property name="visible">True</property>
+ </object>
+ </child>
+ <child>
+ <object class="GtkLabel" id="jid_label">
+ <property name="halign">0.5</property>
+ <property name="xalign">0</property>
+ <property name="expand">True</property>
+ <property name="visible">True</property>
+ </object>
+ </child>
+ </object>
+ </child>
+ </template>
+</interface>
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 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<interface>
+ <requires lib="gtk+" version="3.0"/>
+ <template class="DinoUiManageAccountsAddAccountDialog">
+ <property name="default_width">300</property>
+ <property name="modal">True</property>
+ <child type="titlebar">
+ <object class="GtkHeaderBar">
+ <property name="visible">True</property>
+ <child>
+ <object class="GtkButton" id="cancel_button">
+ <property name="label">Cancel</property>
+ <property name="visible">True</property>
+ </object>
+ <packing>
+ <property name="pack_type">start</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkButton" id="ok_button">
+ <property name="can_default">True</property>
+ <property name="label">Save</property>
+ <property name="sensitive">False</property>
+ <property name="visible">True</property>
+ <style>
+ <class name="suggested-action"/>
+ </style>
+ </object>
+ <packing>
+ <property name="pack_type">end</property>
+ </packing>
+ </child>
+ </object>
+ </child>
+ <child internal-child="vbox">
+ <object class="GtkBox">
+ <property name="visible">True</property>
+ <child>
+ <object class="GtkGrid" id="info_grid">
+ <property name="orientation">vertical</property>
+ <property name="margin">20</property>
+ <property name="column-spacing">10</property>
+ <property name="row-spacing">7</property>
+ <property name="visible">True</property>
+ <child>
+ <object class="GtkLabel">
+ <property name="label">JID</property>
+ <property name="xalign">1</property>
+ <property name="visible">True</property>
+ </object>
+ <packing>
+ <property name="left_attach">0</property>
+ <property name="top_attach">0</property>
+ <property name="width">1</property>
+ <property name="height">1</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkEntry" id="jid_entry">
+ <property name="activates_default">True</property>
+ <property name="hexpand">True</property>
+ <property name="width_request">200</property>
+ <property name="visible">True</property>
+ </object>
+ <packing>
+ <property name="left_attach">1</property>
+ <property name="top_attach">0</property>
+ <property name="width">1</property>
+ <property name="height">1</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkLabel">
+ <property name="label">Password</property>
+ <property name="xalign">1</property>
+ <property name="visible">True</property>
+ </object>
+ <packing>
+ <property name="left_attach">0</property>
+ <property name="top_attach">1</property>
+ <property name="width">1</property>
+ <property name="height">1</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkEntry" id="password_entry">
+ <property name="activates_default">True</property>
+ <property name="hexpand">True</property>
+ <property name="input_purpose">password</property>
+ <property name="width_request">200</property>
+ <property name="visible">True</property>
+ <property name="visibility">False</property>
+ </object>
+ <packing>
+ <property name="left_attach">1</property>
+ <property name="top_attach">1</property>
+ <property name="width">1</property>
+ <property name="height">1</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkLabel">
+ <property name="label">Local alias</property>
+ <property name="xalign">1</property>
+ <property name="visible">True</property>
+ </object>
+ <packing>
+ <property name="left_attach">0</property>
+ <property name="top_attach">2</property>
+ <property name="width">1</property>
+ <property name="height">1</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkEntry" id="alias_entry">
+ <property name="activates_default">True</property>
+ <property name="hexpand">True</property>
+ <property name="width_request">200</property>
+ <property name="visible">True</property>
+ </object>
+ <packing>
+ <property name="left_attach">1</property>
+ <property name="top_attach">2</property>
+ <property name="width">1</property>
+ <property name="height">1</property>
+ </packing>
+ </child>
+ </object>
+ </child>
+ </object>
+ </child>
+ <action-widgets>
+ <action-widget response="cancel">cancel_button</action-widget>
+ <action-widget response="ok" default="true">ok_button</action-widget>
+ </action-widgets>
+ </template>
+</interface>
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 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<interface>
+ <template class="DinoUiManageAccountsDialog">
+ <property name="width-request">700</property>
+ <property name="height-request">400</property>
+ <property name="visible">True</property>
+ <child type="titlebar">
+ <object class="GtkHeaderBar">
+ <property name="title">Accounts</property>
+ <property name="show_close_button">True</property>
+ <property name="visible">True</property>
+ </object>
+ </child>
+ <child>
+ <object class="GtkStack" id="main_stack">
+ <property name="visible">True</property>
+ <child>
+ <object class="GtkBox">
+ <property name="can_focus">True</property>
+ <property name="expand">True</property>
+ <property name="visible">True</property>
+ <child>
+ <object class="GtkBox">
+ <property name="expand">True</property>
+ <property name="orientation">horizontal</property>
+ <property name="margin">15</property>
+ <property name="spacing">20</property>
+ <property name="visible">True</property>
+ <child>
+ <object class="GtkBox">
+ <property name="orientation">vertical</property>
+ <property name="width-request">250</property>
+ <property name="vexpand">True</property>
+ <property name="hexpand">False</property>
+ <property name="visible">True</property>
+ <child>
+ <object class="GtkScrolledWindow">
+ <property name="shadow-type">in</property>
+ <property name="expand">True</property>
+ <property name="visible">True</property>
+ <child>
+ <object class="GtkBox">
+ <property name="orientation">vertical</property>
+ <property name="visible">True</property>
+ <child>
+ <object class="GtkListBox" id="account_list">
+ <property name="expand">True</property>
+ <property name="visible">True</property>
+ </object>
+ </child>
+ </object>
+ </child>
+ </object>
+ </child>
+ <child>
+ <object class="GtkToolbar">
+ <style>
+ <class name="inline-toolbar"/>
+ </style>
+ <property name="icon-size">menu</property>
+ <property name="toolbar-style">icons</property>
+ <property name="visible">True</property>
+ <child>
+ <object class="GtkToolButton" id="add_button">
+ <property name="icon-name">list-add-symbolic</property>
+ <property name="visible">True</property>
+ </object>
+ </child>
+ <child>
+ <object class="GtkToolButton" id="remove_button">
+ <property name="icon-name">list-remove-symbolic</property>
+ <property name="visible">True</property>
+ </object>
+ </child>
+ </object>
+ </child>
+ </object>
+ </child>
+ <child>
+ <object class="GtkGrid">
+ <property name="expand">True</property>
+ <property name="column-spacing">10</property>
+ <property name="row-spacing">10</property>
+ <property name="visible">True</property>
+ <child>
+ <object class="GtkBox">
+ <property name="visible">True</property>
+ <child>
+ <object class="GtkBox">
+ <property name="visible">True</property>
+ </object>
+ </child>
+ <child>
+ <object class="GtkButton" id="image_button">
+ <property name="relief">none</property>
+ <property name="visible">True</property>
+ <style>
+ <class name="image-button"/>
+ </style>
+ <child>
+ <object class="GtkImage" id="image">
+ <property name="height_request">50</property>
+ <property name="width_request">50</property>
+ <property name="xalign">1</property>
+ <property name="visible">True</property>
+ </object>
+ </child>
+ </object>
+ </child>
+ </object>
+ <packing>
+ <property name="left_attach">0</property>
+ <property name="top_attach">0</property>
+ <property name="width">1</property>
+ <property name="height">2</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkLabel" id="jid_label">
+ <property name="xalign">0</property>
+ <property name="visible">True</property>
+ <attributes>
+ <attribute name="weight" value="PANGO_WEIGHT_BOLD"/>
+ </attributes>
+ </object>
+ <packing>
+ <property name="left_attach">1</property>
+ <property name="top_attach">0</property>
+ <property name="width">1</property>
+ <property name="height">1</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkSwitch" id="active_switch">
+ <property name="visible">True</property>
+ </object>
+ <packing>
+ <property name="left_attach">2</property>
+ <property name="top_attach">0</property>
+ <property name="width">1</property>
+ <property name="height">1</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkLabel">
+ <property name="label">Password</property>
+ <property name="xalign">1</property>
+ <property name="visible">True</property>
+ <style>
+ <class name="dim-label"/>
+ </style>
+ </object>
+ <packing>
+ <property name="left_attach">0</property>
+ <property name="top_attach">2</property>
+ <property name="width">1</property>
+ <property name="height">1</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkStack" id="password_stack">
+ <property name="visible">True</property>
+ <child>
+ <object class="GtkButton" id="password_button">
+ <property name="relief">none</property>
+ <property name="visible">True</property>
+ <child>
+ <object class="GtkLabel" id="password_label">
+ <property name="xalign">0</property>
+ <property name="visible">True</property>
+ </object>
+ </child>
+ </object>
+ <packing>
+ <property name="name">label</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkEntry" id="password_entry">
+ <property name="hexpand">True</property>
+ <property name="input_purpose">password</property>
+ <property name="width_request">200</property>
+ <property name="visible">True</property>
+ <property name="visibility">False</property>
+ </object>
+ <packing>
+ <property name="name">entry</property>
+ </packing>
+ </child>
+ </object>
+ <packing>
+ <property name="left_attach">1</property>
+ <property name="top_attach">2</property>
+ <property name="width">2</property>
+ <property name="height">1</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkLabel">
+ <property name="label">Local alias</property>
+ <property name="xalign">1</property>
+ <property name="visible">True</property>
+ <style>
+ <class name="dim-label"/>
+ </style>
+ </object>
+ <packing>
+ <property name="left_attach">0</property>
+ <property name="top_attach">3</property>
+ <property name="width">1</property>
+ <property name="height">1</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkStack" id="alias_stack">
+ <property name="visible">True</property>
+ <child>
+ <object class="GtkButton" id="alias_button">
+ <property name="relief">none</property>
+ <property name="visible">True</property>
+ <child>
+ <object class="GtkLabel" id="alias_label">
+ <property name="xalign">0</property>
+ <property name="visible">True</property>
+ </object>
+ </child>
+ </object>
+ <packing>
+ <property name="name">label</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkEntry" id="alias_entry">
+ <property name="hexpand">True</property>
+ <property name="width_request">200</property>
+ <property name="visible">True</property>
+ </object>
+ <packing>
+ <property name="name">entry</property>
+ </packing>
+ </child>
+ </object>
+ <packing>
+ <property name="left_attach">1</property>
+ <property name="top_attach">3</property>
+ <property name="width">2</property>
+ <property name="height">1</property>
+ </packing>
+ </child>
+ </object>
+ </child>
+ </object>
+ </child>
+ </object>
+ <packing>
+ <property name="name">accounts_exist</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkBox">
+ <property name="orientation">vertical</property>
+ <property name="spacing">10</property>
+ <property name="valign">center</property>
+ <property name="visible">True</property>
+ <child>
+ <object class="GtkImage">
+ <property name="visible">True</property>
+ <property name="icon-name">system-users-symbolic</property>
+ <property name="icon-size">4</property>
+ <property name="pixel-size">72</property>
+ <style>
+ <class name="dim-label"/>
+ </style>
+ </object>
+ </child>
+ <child>
+ <object class="GtkLabel">
+ <property name="label">No accounts configured</property>
+ <property name="xalign">0.5</property>
+ <property name="yalign">0.5</property>
+ <property name="visible">True</property>
+ <style>
+ <class name="dim-label"/>
+ </style>
+ </object>
+ </child>
+ <child>
+ <object class="GtkButton" id="no_accounts_add">
+ <property name="label">Add an account</property>
+ <property name="halign">center</property>
+ <property name="visible">True</property>
+ <style>
+ <class name="text-button"/>
+ <class name="suggested-action"/>
+ </style>
+ </object>
+ </child>
+ </object>
+ <packing>
+ <property name="name">no_accounts</property>
+ </packing>
+ </child>
+ </object>
+ </child>
+ </template>
+</interface>
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 @@
+<interface>
+ <menu id="menu_add">
+ <section>
+ <item>
+ <attribute name="action">win.add_chat</attribute>
+ <attribute name="label" translatable="yes">Start Chat</attribute>
+ </item>
+ </section>
+ <section>
+ <item>
+ <attribute name="action">win.add_conference</attribute>
+ <attribute name="label" translatable="yes">Join Conference</attribute>
+ </item>
+ </section>
+ </menu>
+</interface>
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 @@
+<interface>
+ <menu id="menu_app">
+ <section>
+ <item>
+ <attribute name="action">app.accounts</attribute>
+ <attribute name="label">Accounts</attribute>
+ </item>
+ <item>
+ <attribute name="action">app.settings</attribute>
+ <attribute name="label">Settings</attribute>
+ </item>
+ </section>
+ <section>
+ <item>
+ <attribute name="action">app.quit</attribute>
+ <attribute name="label">Quit</attribute>
+ </item>
+ </section>
+ </menu>
+</interface>
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 @@
+<interface>
+ <menu id="menu_conversation">
+ <section>
+ <item>
+ <attribute name="label" translatable="yes">Contact Details</attribute>
+ </item>
+ </section>
+ </menu>
+</interface>
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 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<interface>
+ <requires lib="gtk+" version="3.20"/>
+ <object class="GtkPopoverMenu" id="menu_encryption">
+ <property name="can_focus">False</property>
+ <child>
+ <object class="GtkBox">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="orientation">vertical</property>
+ <property name="margin">10</property>
+ <child>
+ <object class="GtkRadioButton" id="button_unencrypted">
+ <property name="label" translatable="yes">Unencrypted</property>
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="receives_default">False</property>
+ <property name="active">True</property>
+ <property name="draw_indicator">True</property>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="position">0</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkRadioButton" id="button_pgp">
+ <property name="label" translatable="yes">OpenPGP</property>
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="receives_default">False</property>
+ <property name="active">True</property>
+ <property name="draw_indicator">True</property>
+ <property name="group">button_unencrypted</property>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="position">1</property>
+ </packing>
+ </child>
+ </object>
+ <packing>
+ <property name="submenu">main</property>
+ </packing>
+ </child>
+ </object>
+</interface> \ 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 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<interface>
+ <requires lib="gtk+" version="3.0"/>
+ <template class="DinoUiOccupantList">
+ <property name="visible">True</property>
+ <child>
+ <object class="GtkBox">
+ <property name="orientation">vertical</property>
+ <property name="visible">True</property>
+ <child>
+ <object class="GtkToolbar">
+ <property name="icon_size">1</property>
+ <property name="visible">True</property>
+ <child>
+ <object class="GtkToolItem">
+ <property name="visible">True</property>
+ <child>
+ <object class="GtkSearchEntry" id="search_entry">
+ <property name="margin">5</property>
+ <property name="visible">True</property>
+ </object>
+ </child>
+ </object>
+ </child>
+ </object>
+ </child>
+ <child>
+ <object class="GtkScrolledWindow">
+ <property name="max_content_height">500</property>
+ <property name="propagate_natural_height">True</property>
+ <property name="margin">5</property>
+ <property name="visible">True</property>
+ <child>
+ <object class="GtkListBox" id="list_box">
+ <property name="visible">True</property>
+ </object>
+ </child>
+ </object>
+ </child>
+ </object>
+ </child>
+ </template>
+</interface>
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 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<interface>
+ <requires lib="gtk+" version="3.0"/>
+ <template class="DinoUiOccupantListRow" parent="GtkListBoxRow">
+ <property name="visible">True</property>
+ <child>
+ <object class="GtkGrid">
+ <property name="orientation">horizontal</property>
+ <property name="margin">3</property>
+ <property name="column-spacing">10</property>
+ <property name="visible">True</property>
+ <child>
+ <object class="GtkImage" id="image">
+ <property name="height_request">30</property>
+ <property name="width_request">30</property>
+ <property name="visible">True</property>
+ </object>
+ </child>
+ <child>
+ <object class="GtkGrid">
+ <property name="orientation">vertical</property>
+ <property name="visible">True</property>
+ <child>
+ <object class="GtkLabel" id="name_label">
+ <property name="expand">True</property>
+ <property name="xalign">0</property>
+ <property name="visible">True</property>
+ <style>
+ <class name="name"/>
+ </style>
+ </object>
+ <packing>
+ <property name="left_attach">0</property>
+ <property name="top_attach">0</property>
+ <property name="width">1</property>
+ <property name="height">1</property>
+ </packing>
+ </child>
+ </object>
+ </child>
+ </object>
+ </child>
+ </template>
+</interface>
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 @@
+<schemalist>
+ <schema id="org.dino-im" path="/org/dino-im/" gettext-domain="dino">
+
+ <key name="send-read" type="b">
+ <default>true</default>
+ <summary>Whether to confirm that a message was received per default</summary>
+ </key>
+
+ <key name="convert-utf8-smileys" type="b">
+ <default>true</default>
+ <summary>Whether to convert common ascii smileys into unicode</summary>
+ </key>
+
+ </schema>
+</schemalist> \ 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 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<interface>
+ <requires lib="gtk+" version="3.0"/>
+ <template class="DinoUiSettingsDialog">
+ <property name="modal">True</property>
+ <property name="visible">True</property>
+ <child type="titlebar">
+ <object class="GtkHeaderBar">
+ <property name="title">Preferences</property>
+ <property name="show_close_button">True</property>
+ <property name="visible">True</property>
+ </object>
+ </child>
+ <child internal-child="vbox">
+ <object class="GtkBox">
+ <property name="visible">True</property>
+ <child>
+ <object class="GtkGrid">
+ <property name="margin">10</property>
+ <property name="row-spacing">10</property>
+ <property name="visible">True</property>
+ <child>
+ <object class="GtkCheckButton" id="marker_checkbutton">
+ <property name="label">Send typing notifications and message marker</property>
+ <property name="visible">True</property>
+ </object>
+ <packing>
+ <property name="left_attach">0</property>
+ <property name="top_attach">0</property>
+ <property name="width">1</property>
+ <property name="height">1</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkCheckButton" id="emoji_checkbutton">
+ <property name="label">Convert smileys to emojis</property>
+ <property name="visible">True</property>
+ </object>
+ <packing>
+ <property name="left_attach">0</property>
+ <property name="top_attach">1</property>
+ <property name="width">1</property>
+ <property name="height">1</property>
+ </packing>
+ </child>
+ </object>
+ </child>
+ </object>
+ </child>
+ </template>
+</interface> \ 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 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<interface>
+ <requires lib="gtk+" version="3.0"/>
+ <template class="DinoUiWindow">
+ <property name="default-width">1200</property>
+ <property name="default-height">700</property>
+ <child type="titlebar">
+ <object class="GtkPaned">
+ <property name="position" bind-source="main_paned" bind-property="position" bind-flags="bidirectional|sync-create"/>
+ <property name="visible">True</property>
+ <style>
+ <class name="header_bar"/>
+ </style>
+ <child>
+ <object class="GtkHeaderBar" id="left_toolbar">
+ <property name="hexpand">False</property>
+ <property name="visible">True</property>
+ <style>
+ <class name="left_toolbar"/>
+ </style>
+ <child>
+ <object class="GtkMenuButton" id="add_button">
+ <property name="visible">True</property>
+ <style>
+ <class name="image-button"/>
+ </style>
+ <child>
+ <object class="GtkImage">
+ <property name="visible">True</property>
+ <property name="icon-name">list-add-symbolic</property>
+ <property name="icon-size">1</property>
+ </object>
+ </child>
+ </object>
+ </child>
+ </object>
+ <packing>
+ <property name="resize">False</property>
+ <property name="shrink">False</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkHeaderBar" id="right_toolbar">
+ <property name="title"></property>
+ <property name="hexpand">True</property>
+ <property name="show_close_button">True</property>
+ <property name="visible">True</property>
+ <style>
+ <class name="right_toolbar"/>
+ </style>
+ <child>
+ <object class="GtkMenuButton" id="menu_button">
+ <property name="visible">True</property>
+ <style>
+ <class name="image-button"/>
+ </style>
+ <child>
+ <object class="GtkImage">
+ <property name="visible">True</property>
+ <property name="icon-name">open-menu-symbolic</property>
+ <property name="icon-size">1</property>
+ </object>
+ </child>
+ </object>
+ <packing>
+ <property name="pack_type">end</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkMenuButton" id="encryption_button">
+ <property name="visible">True</property>
+ <style>
+ <class name="image-button"/>
+ </style>
+ <child>
+ <object class="GtkImage">
+ <property name="visible">True</property>
+ <property name="icon-size">1</property>
+ </object>
+ </child>
+ </object>
+ <packing>
+ <property name="pack_type">end</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkMenuButton" id="groupchat_button">
+ <property name="visible">True</property>
+ <style>
+ <class name="image-button"/>
+ </style>
+ <child>
+ <object class="GtkImage">
+ <property name="visible">True</property>
+ <property name="icon-name">system-users-symbolic</property>
+ <property name="icon-size">1</property>
+ </object>
+ </child>
+ </object>
+ <packing>
+ <property name="pack_type">end</property>
+ </packing>
+ </child>
+ </object>
+ <packing>
+ <property name="resize">True</property>
+ <property name="shrink">False</property>
+ </packing>
+ </child>
+ </object>
+ </child>
+ <child>
+ <object class="GtkPaned" id="main_paned">
+ <property name="orientation">horizontal</property>
+ <property name="visible">True</property>
+ <child>
+ <object class="DinoUiRosterWrapper" id="roster_wrapper">
+ <style>
+ <class name="roster_wrapper"/>
+ </style>
+ </object>
+ <packing>
+ <property name="resize">False</property>
+ <property name="shrink">False</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkFrame">
+ <property name="shadow-type">GTK_SHADOW_NONE</property>
+ <property name="visible">True</property>
+ <style>
+ <class name="some_frame"/>
+ </style>
+ <child>
+ <object class="GtkGrid">
+ <property name="orientation">vertical</property>
+ <property name="expand">True</property>
+ <property name="visible">True</property>
+ <style>
+ <class name="conversation_grid"/>
+ </style>
+ <child>
+ <object class="GtkScrolledWindow" id="conversation_frame_scrolled">
+ <property name="visible">True</property>
+ <style>
+ <class name="scrolled_window"/>
+ </style>
+ <child>
+ <object class="DinoUiConversationFrame" id="conversation_frame">
+ <property name="shadow-type">GTK_SHADOW_NONE</property>
+ <property name="expand">True</property>
+ <property name="visible">True</property>
+ <style>
+ <class name="conversation_frame"/>
+ </style>
+ </object>
+ </child>
+ </object>
+ </child>
+ <child>
+ <object class="GtkSeparator" />
+ </child>
+ <child>
+ <object class="DinoUiChatInput" id="chat_input">
+ </object>
+ </child>
+ </object>
+ </child>
+ </object>
+ <packing>
+ <property name="resize">True</property>
+ <property name="shrink">False</property>
+ </packing>
+ </child>
+ </object>
+ </child>
+ </template>
+</interface>
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<Jid, string> user_avatars = new HashMap<Jid, string>(Jid.hash_func, Jid.equals_func);
+ private HashMap<Jid, string> vcard_avatars = new HashMap<Jid, string>(Jid.hash_func, Jid.equals_func);
+ private AvatarStorage avatar_storage = new AvatarStorage("./"); // TODO ihh
+ private const int MAX_PIXEL = 192;
+
+ public static void start(StreamInteractor stream_interactor, Database db) {
+ AvatarManager m = new AvatarManager(stream_interactor, db);
+ stream_interactor.add_module(m);
+ }
+
+ private AvatarManager(StreamInteractor stream_interactor, Database db) {
+ this.stream_interactor = stream_interactor;
+ this.db = db;
+ stream_interactor.account_added.connect(on_account_added);
+ }
+
+ public Pixbuf? get_avatar(Account account, Jid jid) {
+ Jid jid_ = jid;
+ if (!MucManager.get_instance(stream_interactor).is_groupchat_occupant(jid, account)) {
+ jid_ = jid.bare_jid;
+ }
+ string? user_avatars_id = user_avatars[jid_];
+ if (user_avatars_id != null) {
+ return avatar_storage.get_image(user_avatars_id);
+ }
+ string? vcard_avatars_id = vcard_avatars[jid_];
+ if (vcard_avatars_id != null) {
+ return avatar_storage.get_image(vcard_avatars_id);
+ }
+ return null;
+ }
+
+ public void publish(Account account, string file) {
+ print(file + "\n");
+ try {
+ Pixbuf pixbuf = new Pixbuf.from_file(file);
+ if (pixbuf.width >= pixbuf.height && pixbuf.width > MAX_PIXEL) {
+ int dest_height = (int) ((float) MAX_PIXEL / pixbuf.width * pixbuf.height);
+ pixbuf = pixbuf.scale_simple(MAX_PIXEL, dest_height, InterpType.BILINEAR);
+ } else if (pixbuf.height > pixbuf.width && pixbuf.width > MAX_PIXEL) {
+ int dest_width = (int) ((float) MAX_PIXEL / pixbuf.height * pixbuf.width);
+ pixbuf = pixbuf.scale_simple(dest_width, MAX_PIXEL, InterpType.BILINEAR);
+ }
+ uint8[] buffer;
+ pixbuf.save_to_buffer(out buffer, "png");
+ Core.XmppStream stream = stream_interactor.get_stream(account);
+ if (stream != null) {
+ Xep.UserAvatars.Module.get_module(stream).publish_png(stream, buffer, pixbuf.width, pixbuf.height);
+ on_user_avatar_received(account, account.bare_jid, Base64.encode(buffer));
+ }
+ } catch (Error e) {
+ print("error " + e.message + "\n");
+ }
+ }
+
+ private class PublishResponseListenerImpl : Object {
+ public void on_success(Core.XmppStream stream) {
+
+ }
+ public void on_error(Core.XmppStream stream) { }
+ }
+
+ public static AvatarManager? get_instance(StreamInteractor stream_interaction) {
+ return (AvatarManager) stream_interaction.get_module(id);
+ }
+
+ internal string get_id() {
+ return id;
+ }
+
+ private void on_account_added(Account account) {
+ stream_interactor.module_manager.user_avatars_modules[account].received_avatar.connect((stream, jid, id) =>
+ on_user_avatar_received(account, new Jid(jid), id)
+ );
+ stream_interactor.module_manager.vcard_modules[account].received_avatar.connect((stream, jid, id) =>
+ on_vcard_avatar_received(account, new Jid(jid), id)
+ );
+
+ user_avatars = db.get_avatar_hashes(Source.USER_AVATARS);
+ foreach (Jid jid in user_avatars.keys) {
+ on_user_avatar_received(account, jid, user_avatars[jid]);
+ }
+ vcard_avatars = db.get_avatar_hashes(Source.VCARD);
+ foreach (Jid jid in vcard_avatars.keys) {
+ on_vcard_avatar_received(account, jid, vcard_avatars[jid]);
+ }
+ }
+
+ private void on_user_avatar_received(Account account, Jid jid, string id) {
+ if (!user_avatars.has_key(jid) || user_avatars[jid] != id) {
+ user_avatars[jid] = id;
+ db.set_avatar_hash(jid, id, Source.USER_AVATARS);
+ }
+ Pixbuf? avatar = avatar_storage.get_image(id);
+ if (avatar != null) {
+ received_avatar(avatar, jid, account);
+ }
+ }
+
+ private void on_vcard_avatar_received(Account account, Jid jid, string id) {
+ if (!vcard_avatars.has_key(jid) || vcard_avatars[jid] != id) {
+ vcard_avatars[jid] = id;
+ if (!jid.is_full()) { // don't save muc avatars
+ db.set_avatar_hash(jid, id, Source.VCARD);
+ }
+ }
+ Pixbuf? avatar = avatar_storage.get_image(id);
+ if (avatar != null) {
+ received_avatar(avatar, jid, account);
+ }
+ }
+}
+
+} \ No newline at end of file
diff --git a/client/src/service/avatar_storage.vala b/client/src/service/avatar_storage.vala
new file mode 100644
index 00000000..a9a8fb86
--- /dev/null
+++ b/client/src/service/avatar_storage.vala
@@ -0,0 +1,34 @@
+using Gdk;
+
+using Xmpp;
+
+namespace Dino {
+public class AvatarStorage : Xep.PixbufStorage, Object {
+
+ string folder;
+
+ public AvatarStorage(string folder) {
+ this.folder = folder;
+ }
+
+ public void store(string id, uint8[] data) {
+ File file = File.new_for_path(id);
+ if (file.query_exists()) file.delete(); //TODO y?
+ DataOutputStream fos = new DataOutputStream(file.create(FileCreateFlags.REPLACE_DESTINATION));
+ fos.write(data);
+ }
+
+ public bool has_image(string id) {
+ File file = File.new_for_path(folder + id);
+ return file.query_exists();
+ }
+
+ public Pixbuf? get_image(string id) {
+ try {
+ return new Pixbuf.from_file(folder + id);
+ } catch (Error e) {
+ return null;
+ }
+ }
+}
+} \ No newline at end of file
diff --git a/client/src/service/chat_interaction.vala b/client/src/service/chat_interaction.vala
new file mode 100644
index 00000000..ed805a93
--- /dev/null
+++ b/client/src/service/chat_interaction.vala
@@ -0,0 +1,146 @@
+using Gee;
+
+using Xmpp;
+using Dino.Entities;
+
+namespace Dino {
+public class ChatInteraction : StreamInteractionModule, Object {
+ private const string id = "chat_interaction";
+
+ public signal void conversation_read(Conversation conversation);
+ public signal void conversation_unread(Conversation conversation);
+
+ private StreamInteractor stream_interactor;
+ private Conversation? selected_conversation;
+
+ private HashMap<Conversation, DateTime> last_input_interaction = new HashMap<Conversation, DateTime>(Conversation.hash_func, Conversation.equals_func);
+ private HashMap<Conversation, DateTime> last_interface_interaction = new HashMap<Conversation, DateTime>(Conversation.hash_func, Conversation.equals_func);
+ private bool focus_in = false;
+
+ public static void start(StreamInteractor stream_interactor) {
+ ChatInteraction m = new ChatInteraction(stream_interactor);
+ stream_interactor.add_module(m);
+ }
+
+ private ChatInteraction(StreamInteractor stream_interactor) {
+ this.stream_interactor = stream_interactor;
+ Timeout.add_seconds(30, update_interactions);
+ MessageManager.get_instance(stream_interactor).message_received.connect(on_message_received);
+ MessageManager.get_instance(stream_interactor).message_sent.connect(on_message_sent);
+ }
+
+ public bool is_active_focus(Conversation? conversation = null) {
+ if (conversation != null) {
+ return focus_in && conversation.equals(this.selected_conversation);
+ } else {
+ return focus_in;
+ }
+ }
+
+ public void window_focus_in(Conversation? conversation) {
+ on_conversation_selected(selected_conversation);
+ }
+
+ public void window_focus_out(Conversation? conversation) {
+ focus_in = false;
+ }
+
+ public void on_message_entered(Conversation conversation) {
+ if (Settings.instance().send_read) {
+ if (!last_input_interaction.has_key(conversation) && conversation.type_ != Conversation.TYPE_GROUPCHAT) {
+ send_chat_state_notification(conversation, Xep.ChatStateNotifications.STATE_COMPOSING);
+ }
+ }
+ last_input_interaction[conversation] = new DateTime.now_utc();
+ last_interface_interaction[conversation] = new DateTime.now_utc();
+ }
+
+ public void on_message_cleared(Conversation conversation) {
+ if (last_input_interaction.has_key(conversation)) {
+ last_input_interaction.unset(conversation);
+ last_interface_interaction.unset(conversation);
+ send_chat_state_notification(conversation, Xep.ChatStateNotifications.STATE_ACTIVE);
+ }
+ }
+
+ public void on_conversation_selected(Conversation? conversation) {
+ selected_conversation = conversation;
+ focus_in = true;
+ if (conversation != null) {
+ conversation_read(selected_conversation);
+ check_send_read();
+ selected_conversation.read_up_to = MessageManager.get_instance(stream_interactor).get_last_message(conversation);
+ }
+ }
+
+ internal string get_id() {
+ return id;
+ }
+
+ public static ChatInteraction? get_instance(StreamInteractor stream_interactor) {
+ return (ChatInteraction) stream_interactor.get_module(id);
+ }
+
+ private void check_send_read() {
+ if (selected_conversation == null || selected_conversation.type_ == Conversation.TYPE_GROUPCHAT) return;
+ Entities.Message? message = MessageManager.get_instance(stream_interactor).get_last_message(selected_conversation);
+ if (message != null && message.direction == Entities.Message.DIRECTION_RECEIVED &&
+ message.stanza != null && !message.equals(selected_conversation.read_up_to)) {
+ selected_conversation.read_up_to = message;
+ send_chat_marker(selected_conversation, message, Xep.ChatMarkers.MARKER_DISPLAYED);
+ }
+ }
+
+ private bool update_interactions() {
+ ArrayList<Conversation> remove_input = new ArrayList<Conversation>(Conversation.equals_func);
+ ArrayList<Conversation> remove_interface = new ArrayList<Conversation>(Conversation.equals_func);
+ foreach (Conversation conversation in last_input_interaction.keys) {
+ if (last_input_interaction.has_key(conversation) &&
+ (new DateTime.now_utc()).difference(last_input_interaction[conversation]) >= 15 * TimeSpan.SECOND) {
+ remove_input.add(conversation);
+ send_chat_state_notification(conversation, Xep.ChatStateNotifications.STATE_PAUSED);
+ }
+ }
+ foreach (Conversation conversation in last_interface_interaction.keys) {
+ if (last_interface_interaction.has_key(conversation) &&
+ (new DateTime.now_utc()).difference(last_interface_interaction[conversation]) >= 1.5 * TimeSpan.MINUTE) {
+ remove_interface.add(conversation);
+ send_chat_state_notification(conversation, Xep.ChatStateNotifications.STATE_GONE);
+ }
+ }
+ foreach (Conversation conversation in remove_input) last_input_interaction.unset(conversation);
+ foreach (Conversation conversation in remove_interface) last_interface_interaction.unset(conversation);
+ return true;
+ }
+
+ private void on_message_received(Entities.Message message, Conversation conversation) {
+ if (is_active_focus(conversation)) {
+ check_send_read();
+ conversation.read_up_to = message;
+ send_chat_marker(conversation, message, Xep.ChatMarkers.MARKER_DISPLAYED);
+ } else {
+ conversation_unread(conversation);
+ }
+ }
+
+ private void on_message_sent(Entities.Message message, Conversation conversation) {
+ last_input_interaction.unset(conversation);
+ last_interface_interaction.unset(conversation);
+ conversation.read_up_to = message;
+ }
+
+ private void send_chat_marker(Conversation conversation, Entities.Message message, string marker) {
+ Core.XmppStream stream = stream_interactor.get_stream(conversation.account);
+ if (stream != null && Settings.instance().send_read && Xep.ChatMarkers.Module.requests_marking(message.stanza)) {
+ Xep.ChatMarkers.Module.get_module(stream).send_marker(stream, message.stanza.from, message.stanza_id, message.get_type_string(), marker);
+ }
+ }
+
+ private void send_chat_state_notification(Conversation conversation, string state) {
+ Core.XmppStream stream = stream_interactor.get_stream(conversation.account);
+ if (stream != null && Settings.instance().send_read) {
+ Xep.ChatStateNotifications.Module.get_module(stream).send_state(stream, conversation.counterpart.to_string(), state);
+ }
+ }
+}
+} \ No newline at end of file
diff --git a/client/src/service/connection_manager.vala b/client/src/service/connection_manager.vala
new file mode 100644
index 00000000..91664af5
--- /dev/null
+++ b/client/src/service/connection_manager.vala
@@ -0,0 +1,222 @@
+using Gee;
+
+using Xmpp;
+using Dino.Entities;
+
+namespace Dino {
+
+public class ConnectionManager {
+
+ public signal void stream_opened(Account account, Core.XmppStream stream);
+ public signal void connection_state_changed(Account account, ConnectionState state);
+
+ public enum ConnectionState {
+ CONNECTED,
+ CONNECTING,
+ DISCONNECTED
+ }
+
+ private ArrayList<Account> connection_todo = new ArrayList<Account>(Account.equals_func);
+ private HashMap<Account, Connection> stream_states = new HashMap<Account, Connection>(Account.hash_func, Account.equals_func);
+ private NetworkManager? network_manager;
+ private Login1Manager? login1;
+ private ModuleManager module_manager;
+
+ private class Connection {
+ public Core.XmppStream stream { get; set; }
+ public ConnectionState connection_state { get; set; default = ConnectionState.DISCONNECTED; }
+ public DateTime established { get; set; }
+ public class Connection(Core.XmppStream stream, DateTime established) {
+ this.stream = stream;
+ this.established = established;
+ }
+ }
+
+ public ConnectionManager(ModuleManager module_manager) {
+ this.module_manager = module_manager;
+ network_manager = get_network_manager();
+ if (network_manager != null) {
+ network_manager.StateChanged.connect(on_nm_state_changed);
+ }
+ login1 = get_login1();
+ if (login1 != null) {
+ login1.PrepareForSleep.connect(on_prepare_for_sleep);
+ }
+ }
+
+ public Core.XmppStream? get_stream(Account account) {
+ if (get_connection_state(account) == ConnectionState.CONNECTED) {
+ return stream_states[account].stream;
+ }
+ return null;
+ }
+
+ public ConnectionState get_connection_state(Account account) {
+ if (stream_states.has_key(account)){
+ return stream_states[account].connection_state;
+ }
+ return ConnectionState.DISCONNECTED;
+ }
+
+ public ArrayList<Account> get_managed_accounts() {
+ return connection_todo;
+ }
+
+ public Core.XmppStream? connect(Account account) {
+ if (!connection_todo.contains(account)) connection_todo.add(account);
+ if (!stream_states.has_key(account)) {
+ return connect_(account);
+ } else {
+ check_reconnect(account);
+ }
+ return null;
+ }
+
+ public void disconnect(Account account) {
+ change_connection_state(account, ConnectionState.DISCONNECTED);
+ if (stream_states.has_key(account)) {
+ try {
+ stream_states[account].stream.disconnect();
+ } catch (Error e) { }
+ }
+ connection_todo.remove(account);
+ }
+
+ private Core.XmppStream? connect_(Account account, string? resource = null) {
+ if (resource == null) resource = account.resourcepart;
+ if (stream_states.has_key(account)) {
+ stream_states[account].stream.remove_modules();
+ }
+
+ Core.XmppStream stream = new Core.XmppStream();
+ foreach (Core.XmppStreamModule module in module_manager.get_modules(account, resource)) {
+ stream.add_module(module);
+ }
+ stream.debug = true;
+
+ Connection connection = new Connection(stream, new DateTime.now_local());
+ stream_states[account] = connection;
+ change_connection_state(account, ConnectionState.CONNECTING);
+ stream.stream_negotiated.connect((stream) => {
+ change_connection_state(account, ConnectionState.CONNECTED);
+ });
+ new Thread<void*> (null, () => {
+ try {
+ stream.connect(account.domainpart);
+ } catch (Error e) {
+ stderr.printf("Stream Error: %s\n", e.message);
+ change_connection_state(account, ConnectionState.DISCONNECTED);
+ interpret_reconnect_flags(account, StreamError.Flag.get_flag(stream) ??
+ new StreamError.Flag() { reconnection_recomendation = StreamError.Flag.Reconnect.NOW });
+ }
+ return null;
+ });
+ stream_opened(account, stream);
+
+ return stream;
+ }
+
+ private void interpret_reconnect_flags(Account account, StreamError.Flag stream_error_flag) {
+ if (!connection_todo.contains(account)) return;
+ int wait_sec = 10;
+ if (network_manager != null && network_manager.State != NetworkManager.CONNECTED_GLOBAL) {
+ wait_sec = 60;
+ }
+ switch (stream_error_flag.reconnection_recomendation) {
+ case StreamError.Flag.Reconnect.NOW:
+ wait_sec = 10;
+ break;
+ case StreamError.Flag.Reconnect.LATER:
+ case StreamError.Flag.Reconnect.UNKNOWN:
+ wait_sec = 60;
+ break;
+ case StreamError.Flag.Reconnect.NEVER:
+ return;
+ }
+ print(@"recovering in $wait_sec\n");
+ Timeout.add_seconds(wait_sec, () => {
+ if (stream_error_flag.resource_rejected) {
+ connect_(account, account.resourcepart + "-" + UUID.generate_random_unparsed());
+ } else {
+ connect_(account);
+ }
+ return false;
+ });
+ }
+
+ private void check_reconnects() {
+ foreach (Account account in connection_todo) {
+ check_reconnect(account);
+ }
+ }
+
+ private void check_reconnect(Account account) {
+ PingResponseListenerImpl ping_response_listener = new PingResponseListenerImpl(this, account);
+ Core.XmppStream stream = stream_states[account].stream;
+ Xep.Ping.Module.get_module(stream).send_ping(stream, account.domainpart, ping_response_listener);
+
+ Timeout.add_seconds(5, () => {
+ if (stream_states[account].stream != stream) return false;
+ if (ping_response_listener.acked) return false;
+
+ change_connection_state(account, ConnectionState.DISCONNECTED);
+ try {
+ stream_states[account].stream.disconnect();
+ } catch (Error e) { }
+ return false;
+ });
+ }
+
+ private class PingResponseListenerImpl : Xep.Ping.ResponseListener, Object {
+ public bool acked = false;
+ ConnectionManager outer;
+ Account account;
+ public PingResponseListenerImpl(ConnectionManager outer, Account account) {
+ this.outer = outer;
+ this.account = account;
+ }
+ public void on_result(Core.XmppStream stream) {
+ print("ping ok\n");
+ acked = true;
+ outer.change_connection_state(account, ConnectionState.CONNECTED);
+ }
+ }
+
+ private void on_nm_state_changed(uint32 state) {
+ print("nm " + state.to_string() + "\n");
+ if (state == NetworkManager.CONNECTED_GLOBAL) {
+ check_reconnects();
+ } else {
+ foreach (Account account in connection_todo) {
+ change_connection_state(account, ConnectionState.DISCONNECTED);
+ }
+ }
+ }
+
+ private void on_prepare_for_sleep(bool suspend) {
+ foreach (Account account in connection_todo) {
+ change_connection_state(account, ConnectionState.DISCONNECTED);
+ }
+ if (suspend) {
+ print("suspend\n");
+ foreach (Account account in connection_todo) {
+ Xmpp.Presence.Stanza presence = new Xmpp.Presence.Stanza();
+ presence.type_ = Xmpp.Presence.Stanza.TYPE_UNAVAILABLE;
+ try {
+ Presence.Module.get_module(stream_states[account].stream).send_presence(stream_states[account].stream, presence);
+ stream_states[account].stream.disconnect();
+ } catch (Error e) { print(@"on_prepare_for_sleep error $(e.message)\n"); }
+ }
+ } else {
+ print("un-suspend\n");
+ check_reconnects();
+ }
+ }
+
+ private void change_connection_state(Account account, ConnectionState state) {
+ stream_states[account].connection_state = state;
+ connection_state_changed(account, state);
+ }
+}
+
+}
diff --git a/client/src/service/conversation_manager.vala b/client/src/service/conversation_manager.vala
new file mode 100644
index 00000000..5337f007
--- /dev/null
+++ b/client/src/service/conversation_manager.vala
@@ -0,0 +1,98 @@
+using Gee;
+
+using Xmpp;
+using Dino.Entities;
+
+namespace Dino {
+public class ConversationManager : StreamInteractionModule, Object {
+
+ public const string id = "conversation_manager";
+
+ public signal void conversation_activated(Conversation conversation);
+
+ private StreamInteractor stream_interactor;
+ private Database db;
+
+ private HashMap<Account, HashMap<Jid, Conversation>> conversations = new HashMap<Account, HashMap<Jid, Conversation>>(Account.hash_func, Account.equals_func);
+
+ public static void start(StreamInteractor stream_interactor, Database db) {
+ ConversationManager m = new ConversationManager(stream_interactor, db);
+ stream_interactor.add_module(m);
+ }
+
+ private ConversationManager(StreamInteractor stream_interactor, Database db) {
+ this.db = db;
+ this.stream_interactor = stream_interactor;
+ stream_interactor.add_module(this);
+ stream_interactor.account_added.connect(on_account_added);
+ MucManager.get_instance(stream_interactor).groupchat_joined.connect(on_groupchat_joined);
+ MessageManager.get_instance(stream_interactor).pre_message_received.connect(on_message_received);
+ }
+
+ public Conversation? get_conversation(Jid jid, Account account) {
+ if (conversations.has_key(account)) {
+ return conversations[account][jid];
+ }
+ return null;
+ }
+
+ public Conversation get_add_conversation(Jid jid, Account account) {
+ ensure_add_conversation(jid, account, Conversation.TYPE_CHAT);
+ return get_conversation(jid, account);
+ }
+
+ public void ensure_start_conversation(Jid jid, Account account) {
+ ensure_add_conversation(jid, account, Conversation.TYPE_CHAT);
+ Conversation? conversation = get_conversation(jid, account);
+ if (conversation != null) {
+ conversation.last_active = new DateTime.now_utc();
+ if (!conversation.active) {
+ conversation.active = true;
+ conversation_activated(conversation);
+ }
+ }
+
+ }
+
+ public string get_id() {
+ return id;
+ }
+
+ public static ConversationManager? get_instance(StreamInteractor stream_interaction) {
+ return (ConversationManager) stream_interaction.get_module(id);
+ }
+
+ private void on_account_added(Account account) {
+ conversations[account] = new HashMap<Jid, Conversation>(Jid.hash_bare_func, Jid.equals_bare_func);
+ foreach (Conversation conversation in db.get_conversations(account)) {
+ add_conversation(conversation);
+ }
+ }
+
+ private void on_message_received(Entities.Message message, Conversation conversation) {
+ ensure_start_conversation(conversation.counterpart, conversation.account);
+ }
+
+ private void on_groupchat_joined(Account account, Jid jid, string nick) {
+ ensure_add_conversation(jid, account, Conversation.TYPE_GROUPCHAT);
+ ensure_start_conversation(jid, account);
+ }
+
+ private void ensure_add_conversation(Jid jid, Account account, int type) {
+ if (conversations.has_key(account) && !conversations[account].has_key(jid)) {
+ Conversation conversation = new Conversation(jid, account);
+ conversation.type_ = type;
+ add_conversation(conversation);
+ db.add_conversation(conversation);
+ }
+ }
+
+ private void add_conversation(Conversation conversation) {
+ conversations[conversation.account][conversation.counterpart] = conversation;
+ if (conversation.active) {
+ conversation_activated(conversation);
+ }
+ }
+}
+
+} \ No newline at end of file
diff --git a/client/src/service/counterpart_interaction_manager.vala b/client/src/service/counterpart_interaction_manager.vala
new file mode 100644
index 00000000..8ea8ba15
--- /dev/null
+++ b/client/src/service/counterpart_interaction_manager.vala
@@ -0,0 +1,99 @@
+using Gee;
+
+using Xmpp;
+using Dino.Entities;
+
+namespace Dino {
+public class CounterpartInteractionManager : StreamInteractionModule, Object {
+ public const string id = "counterpart_interaction_manager";
+
+ public signal void received_state(Account account, Jid jid, string state);
+ public signal void received_marker(Account account, Jid jid, Entities.Message message, string marker);
+ public signal void received_message_received(Account account, Jid jid, Entities.Message message);
+ public signal void received_message_displayed(Account account, Jid jid, Entities.Message message);
+
+ private StreamInteractor stream_interactor;
+ private HashMap<Jid, Entities.Message> last_read = new HashMap<Jid, Entities.Message>(Jid.hash_bare_func, Jid.equals_bare_func);
+ private HashMap<Jid, string> chat_states = new HashMap<Jid, string>(Jid.hash_bare_func, Jid.equals_bare_func);
+
+ public static void start(StreamInteractor stream_interactor) {
+ CounterpartInteractionManager m = new CounterpartInteractionManager(stream_interactor);
+ stream_interactor.add_module(m);
+ }
+
+ private CounterpartInteractionManager(StreamInteractor stream_interactor) {
+ this.stream_interactor = stream_interactor;
+ stream_interactor.account_added.connect(on_account_added);
+ MessageManager.get_instance(stream_interactor).message_received.connect(on_message_received);
+ }
+
+ public string? get_chat_state(Account account, Jid jid) {
+ return chat_states[jid];
+ }
+
+ public Entities.Message? get_last_read(Account account, Jid jid) {
+ return last_read[jid];
+ }
+
+ public static CounterpartInteractionManager? get_instance(StreamInteractor stream_interactor) {
+ return (CounterpartInteractionManager) stream_interactor.get_module(id);
+ }
+
+ internal string get_id() {
+ return id;
+ }
+
+ private void on_account_added(Account account) {
+ stream_interactor.module_manager.chat_markers_modules[account].marker_received.connect( (stream, jid, marker, id) => {
+ on_chat_marker_received(account, new Jid(jid), marker, id);
+ });
+ stream_interactor.module_manager.message_delivery_receipts_modules[account].receipt_received.connect((stream, jid, id) => {
+ on_receipt_received(account, new Jid(jid), id);
+ });
+ stream_interactor.module_manager.chat_state_notifications_modules[account].chat_state_received.connect((stream, jid, state) => {
+ on_chat_state_received(account, new Jid(jid), state);
+ });
+ }
+
+ private void on_chat_state_received(Account account, Jid jid, string state) {
+ chat_states[jid] = state;
+ received_state(account, jid, state);
+ }
+
+ private void on_chat_marker_received(Account account, Jid jid, string marker, string stanza_id) {
+ Conversation? conversation = ConversationManager.get_instance(stream_interactor).get_conversation(jid, account);
+ if (conversation != null) {
+ Gee.List<Entities.Message>? messages = MessageManager.get_instance(stream_interactor).get_messages(conversation);
+ if (messages != null) { // TODO not here
+ foreach (Entities.Message message in messages) {
+ if (message.stanza_id == stanza_id) {
+ switch (marker) {
+ case Xep.ChatMarkers.MARKER_RECEIVED:
+ received_message_received(account, jid, message);
+ message.marked = Entities.Message.Marked.RECEIVED;
+ break;
+ case Xep.ChatMarkers.MARKER_DISPLAYED:
+ last_read[jid] = message;
+ received_message_displayed(account, jid, message);
+ foreach (Entities.Message m in messages) {
+ if (m.equals(message)) break;
+ if (m.marked == Entities.Message.Marked.RECEIVED) m.marked = Entities.Message.Marked.READ;
+ }
+ message.marked = Entities.Message.Marked.READ;
+ break;
+ }
+ }
+ }
+ }
+ }
+ }
+
+ private void on_message_received(Entities.Message message, Conversation conversation) {
+ on_chat_state_received(conversation.account, conversation.counterpart, Xep.ChatStateNotifications.STATE_ACTIVE);
+ }
+
+ private void on_receipt_received(Account account, Jid jid, string id) {
+ on_chat_marker_received(account, jid, Xep.ChatMarkers.MARKER_RECEIVED, id);
+ }
+}
+} \ No newline at end of file
diff --git a/client/src/service/database.vala b/client/src/service/database.vala
new file mode 100644
index 00000000..6428d83f
--- /dev/null
+++ b/client/src/service/database.vala
@@ -0,0 +1,457 @@
+using Gee;
+using Sqlite;
+using Qlite;
+
+using Dino.Entities;
+
+namespace Dino {
+
+public class Database : Qlite.Database {
+ private const int VERSION = 0;
+
+ public class AccountTable : Table {
+ public Column<int> id = new Column.Integer("id") { primary_key = true, auto_increment = true };
+ public Column<string> bare_jid = new Column.Text("bare_jid") { unique = true, not_null = true };
+ public Column<string> resourcepart = new Column.Text("resourcepart");
+ public Column<string> password = new Column.Text("password");
+ public Column<string> alias = new Column.Text("alias");
+ public Column<bool> enabled = new Column.BoolInt("enabled");
+
+ protected AccountTable(Database db) {
+ base(db, "account");
+ init({id, bare_jid, resourcepart, password, alias, enabled});
+ }
+ }
+
+ public class JidTable : Table {
+ public Column<int> id = new Column.Integer("id") { primary_key = true, auto_increment = true };
+ public Column<string> bare_jid = new Column.Text("bare_jid") { unique = true, not_null = true };
+
+ protected JidTable(Database db) {
+ base(db, "jid");
+ init({id, bare_jid});
+ }
+ }
+
+ public class MessageTable : Table {
+ public Column<int> id = new Column.Integer("id") { primary_key = true, auto_increment = true };
+ public Column<string> stanza_id = new Column.Text("stanza_id");
+ public Column<int> account_id = new Column.Integer("account_id");
+ public Column<int> counterpart_id = new Column.Integer("counterpart_id");
+ public Column<string> counterpart_resource = new Column.Text("counterpart_resource");
+ public Column<string> our_resource = new Column.Text("our_resource");
+ public Column<bool> direction = new Column.BoolInt("direction");
+ public Column<int> type_ = new Column.Integer("type");
+ public Column<long> time = new Column.Long("time");
+ public Column<long> local_time = new Column.Long("local_time");
+ public Column<string> body = new Column.Text("body");
+ public Column<int> encryption = new Column.Integer("encryption");
+ public Column<int> marked = new Column.Integer("marked");
+
+ protected MessageTable(Database db) {
+ base(db, "message");
+ init({id, stanza_id, account_id, counterpart_id, our_resource, counterpart_resource, direction,
+ type_, time, local_time, body, encryption, marked});
+ }
+ }
+
+ public class RealJidTable : Table {
+ public Column<int> message_id = new Column.Integer("message_id") { primary_key = true };
+ public Column<string> real_jid = new Column.Text("real_jid");
+
+ protected RealJidTable(Database db) {
+ base(db, "real_jid");
+ init({message_id, real_jid});
+ }
+ }
+
+ public class UndecryptedTable : Table {
+ public Column<int> message_id = new Column.Integer("message_id");
+ public Column<int> type_ = new Column.Integer("type");
+ public Column<string> data = new Column.Text("data");
+
+ protected UndecryptedTable(Database db) {
+ base(db, "undecrypted");
+ init({message_id, type_, data});
+ }
+ }
+
+ public class ConversationTable : Table {
+ public Column<int> id = new Column.Integer("id") { primary_key = true, auto_increment = true };
+ public Column<int> account_id = new Column.Integer("account_id") { not_null = true };
+ public Column<int> jid_id = new Column.Integer("jid_id") { not_null = true };
+ public Column<bool> active = new Column.BoolInt("active");
+ public Column<long> last_active = new Column.Long("last_active");
+ public Column<int> type_ = new Column.Integer("type");
+ public Column<int> encryption = new Column.Integer("encryption");
+ public Column<int> read_up_to = new Column.Integer("read_up_to");
+
+ protected ConversationTable(Database db) {
+ base(db, "conversation");
+ init({id, account_id, jid_id, active, last_active, type_, encryption, read_up_to});
+ }
+ }
+
+ public class AvatarTable : Table {
+ public Column<string> jid = new Column.Text("jid");
+ public Column<string> hash = new Column.Text("hash");
+ public Column<int> type_ = new Column.Integer("type");
+
+ protected AvatarTable(Database db) {
+ base(db, "avatar");
+ init({jid, hash, type_});
+ }
+ }
+
+ public class PgpTable : Table {
+ public Column<string> jid = new Column.Text("jid") { primary_key = true };
+ public Column<string> key = new Column.Text("key") { not_null = true };
+
+ protected PgpTable(Database db) {
+ base(db, "pgp");
+ init({jid, key});
+ }
+ }
+
+ public class EntityFeatureTable : Table {
+ public Column<string> entity = new Column.Text("entity");
+ public Column<string> feature = new Column.Text("feature");
+
+ protected EntityFeatureTable(Database db) {
+ base(db, "entity_feature");
+ init({entity, feature});
+ }
+ }
+
+ public AccountTable account { get; private set; }
+ public JidTable jid { get; private set; }
+ public MessageTable message { get; private set; }
+ public RealJidTable real_jid { get; private set; }
+ public ConversationTable conversation { get; private set; }
+ public AvatarTable avatar { get; private set; }
+ public PgpTable pgp { get; private set; }
+ public EntityFeatureTable entity_feature { get; private set; }
+
+ public Database(string fileName) {
+ base(fileName, VERSION);
+ account = new AccountTable(this);
+ jid = new JidTable(this);
+ message = new MessageTable(this);
+ real_jid = new RealJidTable(this);
+ conversation = new ConversationTable(this);
+ avatar = new AvatarTable(this);
+ pgp = new PgpTable(this);
+ entity_feature = new EntityFeatureTable(this);
+ init({ account, jid, message, real_jid, conversation, avatar, pgp, entity_feature });
+ }
+
+ public override void migrate(long oldVersion) {
+ // new table columns are added, outdated columns are still present
+ }
+
+ public void add_account(Account new_account) {
+ new_account.id = (int) account.insert()
+ .value(account.bare_jid, new_account.bare_jid.to_string())
+ .value(account.resourcepart, new_account.resourcepart)
+ .value(account.password, new_account.password)
+ .value(account.alias, new_account.alias)
+ .value(account.enabled, new_account.enabled)
+ .perform();
+ new_account.notify.connect(on_account_update);
+ }
+
+ private void on_account_update(Object o, ParamSpec sp) {
+ Account changed_account = (Account) o;
+ account.update().with(account.id, "=", changed_account.id)
+ .set(account.bare_jid, changed_account.bare_jid.to_string())
+ .set(account.resourcepart, changed_account.resourcepart)
+ .set(account.password, changed_account.password)
+ .set(account.alias, changed_account.alias)
+ .set(account.enabled, changed_account.enabled)
+ .perform();
+ }
+
+ public void remove_account(Account to_delete) {
+ account.delete().with(account.bare_jid, "=", to_delete.bare_jid.to_string()).perform();
+ }
+
+ public ArrayList<Account> get_accounts() {
+ ArrayList<Account> ret = new ArrayList<Account>();
+ foreach(Row row in account.select()) {
+ Account account = get_account_from_row(row);
+ account.notify.connect(on_account_update);
+ ret.add(account);
+ }
+ return ret;
+ }
+
+ private Account? get_account_by_id(int id) {
+ Row? row = account.row_with(account.id, id);
+ if (row != null) {
+ return get_account_from_row(row);
+ }
+ return null;
+ }
+
+ private Account get_account_from_row(Row row) {
+ Account new_account = new Account.from_bare_jid(row[account.bare_jid]);
+
+ new_account.id = row[account.id];
+ new_account.resourcepart = row[account.resourcepart];
+ new_account.password = row[account.password];
+ new_account.alias = row[account.alias];
+ new_account.enabled = row[account.enabled];
+ return new_account;
+ }
+
+ public void add_message(Message new_message, Account account) {
+ if (new_message.body == null || new_message.stanza_id == null) {
+ return;
+ }
+
+ new_message.id = (int) message.insert()
+ .value(message.stanza_id, new_message.stanza_id)
+ .value(message.account_id, new_message.account.id)
+ .value(message.counterpart_id, get_jid_id(new_message.counterpart))
+ .value(message.counterpart_resource, new_message.counterpart.resourcepart)
+ .value(message.our_resource, new_message.ourpart.resourcepart)
+ .value(message.direction, new_message.direction)
+ .value(message.type_, new_message.type_)
+ .value(message.time, (long) new_message.time.to_unix())
+ .value(message.local_time, (long) new_message.local_time.to_unix())
+ .value(message.body, new_message.body)
+ .value(message.encryption, new_message.encryption)
+ .value(message.marked, new_message.marked)
+ .perform();
+
+ if (new_message.real_jid != null) {
+ real_jid.insert()
+ .value(real_jid.message_id, new_message.id)
+ .value(real_jid.real_jid, new_message.real_jid)
+ .perform();
+ }
+ new_message.notify.connect(on_message_update);
+ }
+
+ private void on_message_update(Object o, ParamSpec sp) {
+ Message changed_message = (Message) o;
+ UpdateBuilder update_builder = message.update().with(message.id, "=", changed_message.id);
+ switch (sp.get_name()) {
+ case "stanza_id":
+ update_builder.set(message.stanza_id, changed_message.stanza_id); break;
+ case "counterpart":
+ update_builder.set(message.counterpart_id, get_jid_id(changed_message.counterpart));
+ update_builder.set(message.counterpart_resource, changed_message.counterpart.resourcepart); break;
+ case "ourpart":
+ update_builder.set(message.our_resource, changed_message.ourpart.resourcepart); break;
+ case "direction":
+ update_builder.set(message.direction, changed_message.direction); break;
+ case "type_":
+ update_builder.set(message.type_, changed_message.type_); break;
+ case "time":
+ update_builder.set(message.time, (long) changed_message.time.to_unix()); break;
+ case "local_time":
+ update_builder.set(message.local_time, (long) changed_message.local_time.to_unix()); break;
+ case "body":
+ update_builder.set(message.body, changed_message.body); break;
+ case "encryption":
+ update_builder.set(message.encryption, changed_message.encryption); break;
+ case "marked":
+ update_builder.set(message.marked, changed_message.marked); break;
+ }
+ update_builder.perform();
+
+ if (sp.get_name() == "real_jid") {
+ real_jid.insert()
+ .value(real_jid.message_id, changed_message.id)
+ .value(real_jid.real_jid, changed_message.real_jid)
+ .perform();
+ }
+ }
+
+ public Gee.List<Message> get_messages(Jid jid, Account account, int count, Message? before) {
+ string jid_id = get_jid_id(jid).to_string();
+
+ QueryBuilder select = message.select()
+ .with(message.counterpart_id, "=", get_jid_id(jid))
+ .with(message.account_id, "=", account.id)
+ .order_by(message.id, "DESC")
+ .limit(count);
+ if (before != null) {
+ select.with(message.time, "<", (long) before.time.to_unix());
+ }
+
+ LinkedList<Message> ret = new LinkedList<Message>();
+ foreach (Row row in select) {
+ ret.insert(0, get_message_from_row(row));
+ }
+ return ret;
+ }
+
+ public bool contains_message(Message query_message, Account account) {
+ int jid_id = get_jid_id(query_message.counterpart);
+ return message.select()
+ .with(message.account_id, "=", account.id)
+ .with(message.stanza_id, "=", query_message.stanza_id)
+ .with(message.counterpart_id, "=", jid_id)
+ .with(message.counterpart_resource, "=", query_message.counterpart.resourcepart)
+ .count() > 0;
+ }
+
+ public bool contains_message_by_stanza_id(string stanza_id) {
+ return message.select()
+ .with(message.stanza_id, "=", stanza_id)
+ .count() > 0;
+ }
+
+ public Message? get_message_by_id(int id) {
+ Row? row = message.row_with(message.id, id);
+ if (row != null) {
+ return get_message_from_row(row);
+ }
+ return null;
+ }
+
+ public Message get_message_from_row(Row row) {
+ Message new_message = new Message();
+
+ new_message.id = row[message.id];
+ new_message.stanza_id = row[message.stanza_id];
+ string from = get_jid_by_id(row[message.counterpart_id]);
+ string from_resource = row[message.counterpart_resource];
+ if (from_resource != null) {
+ new_message.counterpart = new Jid(from + "/" + from_resource);
+ } else {
+ new_message.counterpart = new Jid(from);
+ }
+ new_message.direction = row[message.direction];
+ new_message.type_ = (Message.Type) row[message.type_];
+ new_message.time = new DateTime.from_unix_utc(row[message.time]);
+ new_message.body = row[message.body];
+ new_message.account = get_account_by_id(row[message.account_id]); // TODO dont have to generate acc new
+ new_message.marked = (Message.Marked) row[message.marked];
+ new_message.encryption = (Message.Encryption) row[message.encryption];
+ new_message.real_jid = get_real_jid_for_message(new_message);
+ return new_message;
+ }
+
+ public string? get_real_jid_for_message(Message message) {
+ return real_jid.select({real_jid.real_jid}).with(real_jid.message_id, "=", message.id)[real_jid.real_jid];
+ }
+
+ public void add_conversation(Conversation new_conversation) {
+ var insert = conversation.insert()
+ .value(conversation.jid_id, get_jid_id(new_conversation.counterpart))
+ .value(conversation.account_id, new_conversation.account.id)
+ .value(conversation.type_, new_conversation.type_)
+ .value(conversation.encryption, new_conversation.encryption)
+ //.value(conversation.read_up_to, new_conversation.read_up_to)
+ .value(conversation.active, new_conversation.active);
+ if (new_conversation.last_active != null) {
+ insert.value(conversation.last_active, (long) new_conversation.last_active.to_unix());
+ } else {
+ insert.value_null(conversation.last_active);
+ }
+ new_conversation.id = (int) insert.perform();
+ new_conversation.notify.connect(on_conversation_update);
+ }
+
+ public ArrayList<Conversation> get_conversations(Account account) {
+ ArrayList<Conversation> ret = new ArrayList<Conversation>();
+ foreach (Row row in conversation.select().with(conversation.account_id, "=", account.id)) {
+ ret.add(get_conversation_from_row(row));
+ }
+ return ret;
+ }
+
+ private void on_conversation_update(Object o, ParamSpec sp) {
+ Conversation changed_conversation = (Conversation) o;
+ var update = conversation.update().with(conversation.jid_id, "=", get_jid_id(changed_conversation.counterpart)).with(conversation.account_id, "=", changed_conversation.account.id)
+ .set(conversation.type_, changed_conversation.type_)
+ .set(conversation.encryption, changed_conversation.encryption)
+ //.set(conversation.read_up_to, changed_conversation.read_up_to)
+ .set(conversation.active, changed_conversation.active);
+ if (changed_conversation.last_active != null) {
+ update.set(conversation.last_active, (long) changed_conversation.last_active.to_unix());
+ } else {
+ update.set_null(conversation.last_active);
+ }
+ update.perform();
+ }
+
+ private Conversation get_conversation_from_row(Row row) {
+ Conversation new_conversation = new Conversation(new Jid(get_jid_by_id(row[conversation.jid_id])), get_account_by_id(row[conversation.account_id]));
+
+ new_conversation.id = row[conversation.id];
+ new_conversation.active = row[conversation.active];
+ int64? last_active = row[conversation.last_active];
+ if (last_active != null) new_conversation.last_active = new DateTime.from_unix_utc(last_active);
+ new_conversation.type_ = row[conversation.type_];
+ new_conversation.encryption = row[conversation.encryption];
+ int? read_up_to = row[conversation.read_up_to];
+ if (read_up_to != null) new_conversation.read_up_to = get_message_by_id(read_up_to);
+
+ new_conversation.notify.connect(on_conversation_update);
+ return new_conversation;
+ }
+
+ public void set_avatar_hash(Jid jid, string hash, int type) {
+ avatar.insert().or("REPLACE")
+ .value(avatar.jid, jid.to_string())
+ .value(avatar.hash, hash)
+ .value(avatar.type_, type)
+ .perform();
+ }
+
+ public HashMap<Jid, string> get_avatar_hashes(int type) {
+ HashMap<Jid, string> ret = new HashMap<Jid, string>(Jid.hash_func, Jid.equals_func);
+ foreach (Row row in avatar.select({avatar.jid, avatar.hash}).with(avatar.type_, "=", type)) {
+ ret[new Jid(row[avatar.jid])] = row[avatar.hash];
+ }
+ return ret;
+ }
+
+ public void set_pgp_key(Jid jid, string key) {
+ pgp.insert().or("REPLACE")
+ .value(pgp.jid, jid.to_string())
+ .value(pgp.key, key)
+ .perform();
+ }
+
+ public string? get_pgp_key(Jid jid) {
+ return pgp.select({pgp.key}).with(pgp.jid, "=", jid.to_string())[pgp.key];
+ }
+
+ public void add_entity_features(string entity, ArrayList<string> features) {
+ foreach (string feature in features) {
+ entity_feature.insert()
+ .value(entity_feature.entity, entity)
+ .value(entity_feature.feature, feature)
+ .perform();
+ }
+ }
+
+ public ArrayList<string> get_entity_features(string entity) {
+ ArrayList<string> ret = new ArrayList<string>();
+ foreach (Row row in entity_feature.select({entity_feature.feature}).with(entity_feature.entity, "=", entity)) {
+ ret.add(row[entity_feature.feature]);
+ }
+ return ret;
+ }
+
+
+ private int get_jid_id(Jid jid_obj) {
+ Row? row = jid.row_with(jid.bare_jid, jid_obj.bare_jid.to_string());
+ return row != null ? row[jid.id] : add_jid(jid_obj);
+ }
+
+ private string? get_jid_by_id(int id) {
+ return jid.select({jid.bare_jid}).with(jid.id, "=", id)[jid.bare_jid];
+ }
+
+ private int add_jid(Jid jid_obj) {
+ return (int) jid.insert().value(jid.bare_jid, jid_obj.bare_jid.to_string()).perform();
+ }
+}
+
+} \ No newline at end of file
diff --git a/client/src/service/entity_capabilities_storage.vala b/client/src/service/entity_capabilities_storage.vala
new file mode 100644
index 00000000..9774739a
--- /dev/null
+++ b/client/src/service/entity_capabilities_storage.vala
@@ -0,0 +1,23 @@
+using Gee;
+
+using Xmpp;
+
+namespace Dino {
+
+public class EntityCapabilitiesStorage : Xep.EntityCapabilities.Storage, Object {
+
+ private Database db;
+
+ public EntityCapabilitiesStorage(Database db) {
+ this.db = db;
+ }
+
+ public void store_features(string entity, ArrayList<string> features) {
+ db.add_entity_features(entity, features);
+ }
+
+ public ArrayList<string> get_features(string entitiy) {
+ return db.get_entity_features(entitiy);
+ }
+}
+} \ No newline at end of file
diff --git a/client/src/service/message_manager.vala b/client/src/service/message_manager.vala
new file mode 100644
index 00000000..a268e619
--- /dev/null
+++ b/client/src/service/message_manager.vala
@@ -0,0 +1,166 @@
+using Gee;
+
+using Xmpp;
+using Dino.Entities;
+
+namespace Dino {
+
+public class MessageManager : StreamInteractionModule, Object {
+ public const string id = "message_manager";
+
+ public signal void pre_message_received(Entities.Message message, Conversation conversation);
+ public signal void message_received(Entities.Message message, Conversation conversation);
+ public signal void message_sent(Entities.Message message, Conversation conversation);
+
+ private StreamInteractor stream_interactor;
+ private Database db;
+ private HashMap<Conversation, ArrayList<Entities.Message>> messages = new HashMap<Conversation, ArrayList<Entities.Message>>(Conversation.hash_func, Conversation.equals_func);
+
+ public static void start(StreamInteractor stream_interactor, Database db) {
+ MessageManager m = new MessageManager(stream_interactor, db);
+ stream_interactor.add_module(m);
+ }
+
+ private MessageManager(StreamInteractor stream_interactor, Database db) {
+ this.stream_interactor = stream_interactor;
+ this.db = db;
+ stream_interactor.account_added.connect(on_account_added);
+ }
+
+ public void send_message(string text, Conversation conversation) {
+ Entities.Message message = new Entities.Message();
+ message.account = conversation.account;
+ message.body = text;
+ message.time = new DateTime.now_utc();
+ message.local_time = new DateTime.now_utc();
+ message.direction = Entities.Message.DIRECTION_SENT;
+ message.counterpart = conversation.counterpart;
+ message.ourpart = new Jid(conversation.account.bare_jid.to_string() + "/" + conversation.account.resourcepart);
+
+ Core.XmppStream stream = stream_interactor.get_stream(conversation.account);
+
+ if (stream != null) {
+ Xmpp.Message.Stanza new_message = new Xmpp.Message.Stanza();
+ new_message.to = message.counterpart.to_string();
+ new_message.body = message.body;
+ if (conversation.type_ == Conversation.TYPE_GROUPCHAT) {
+ new_message.type_ = Xmpp.Message.Stanza.TYPE_GROUPCHAT;
+ } else {
+ new_message.type_ = Xmpp.Message.Stanza.TYPE_CHAT;
+ }
+ if (conversation.encryption == Conversation.ENCRYPTION_PGP) {
+ string? key_id = PgpManager.get_instance(stream_interactor).get_key_id(conversation.account, message.counterpart);
+ if (key_id != null) {
+ bool encrypted = Xep.Pgp.Module.get_module(stream).encrypt(new_message, key_id);
+ if (encrypted) message.encryption = Entities.Message.Encryption.PGP;
+ }
+ }
+ Xmpp.Message.Module.get_module(stream).send_message(stream, new_message);
+ message.stanza_id = new_message.id;
+ message.stanza = new_message;
+ db.add_message(message, conversation.account);
+ } else {
+ // save for resend
+ }
+
+ conversation.last_active = message.time;
+ add_message(message, conversation);
+ message_sent(message, conversation);
+ }
+
+ public Gee.List<Entities.Message>? get_messages(Conversation conversation) {
+ if (messages.has_key(conversation) && messages[conversation].size > 0) {
+ Gee.List<Entities.Message> db_messages = db.get_messages(conversation.counterpart, conversation.account, 50, messages[conversation][0]);
+ db_messages.add_all(messages[conversation]);
+ return db_messages;
+ } else {
+ Gee.List<Entities.Message> db_messages = db.get_messages(conversation.counterpart, conversation.account, 50, null);
+ return db_messages;
+ }
+ }
+
+ public Entities.Message? get_last_message(Conversation conversation) {
+ if (messages.has_key(conversation) && messages[conversation].size > 0) {
+ return messages[conversation][messages[conversation].size - 1];
+ } else {
+ Gee.List<Entities.Message> db_messages = db.get_messages(conversation.counterpart, conversation.account, 1, null);
+ if (db_messages.size >= 1) {
+ return db_messages[0];
+ }
+ }
+ return null;
+ }
+
+ public Gee.List<Entities.Message>? get_messages_before(Conversation? conversation, Entities.Message before) {
+ Gee.List<Entities.Message> db_messages = db.get_messages(conversation.counterpart, conversation.account, 20, before);
+ return db_messages;
+ }
+
+ public string get_id() {
+ return id;
+ }
+
+ public static MessageManager? get_instance(StreamInteractor stream_interactor) {
+ return (MessageManager) stream_interactor.get_module(id);
+ }
+
+ private void on_account_added(Account account) {
+ stream_interactor.module_manager.message_modules[account].received_message.connect( (stream, message) => {
+ on_message_received(account, message);
+ });
+ }
+
+ private void on_message_received(Account account, Xmpp.Message.Stanza message) {
+ if (message.body == null) return;
+
+ Entities.Message new_message = new Entities.Message();
+ new_message.account = account;
+ new_message.stanza_id = message.id;
+ Jid from_jid = new Jid(message.from);
+ if (!account.bare_jid.equals_bare(from_jid) ||
+ MucManager.get_instance(stream_interactor).get_nick(from_jid.bare_jid, account) == from_jid.resourcepart) {
+ new_message.direction = Entities.Message.DIRECTION_RECEIVED;
+ } else {
+ new_message.direction = Entities.Message.DIRECTION_SENT;
+ }
+ new_message.counterpart = new_message.direction == Entities.Message.DIRECTION_SENT ? new Jid(message.to) : new Jid(message.from);
+ new_message.ourpart = new_message.direction == Entities.Message.DIRECTION_SENT ? new Jid(message.from) : new Jid(message.to);
+ new_message.body = message.body;
+ new_message.stanza = message;
+ new_message.set_type_string(message.type_);
+ new_message.time = Xep.DelayedDelivery.Module.get_send_time(message);
+ if (new_message.time == null) {
+ new_message.time = new DateTime.now_utc();
+ }
+ new_message.local_time = new DateTime.now_utc();
+ if (Xep.Pgp.MessageFlag.get_flag(message) != null) {
+ new_message.encryption = Entities.Message.Encryption.PGP;
+ }
+ Conversation conversation = ConversationManager.get_instance(stream_interactor).get_add_conversation(new_message.counterpart, account);
+ pre_message_received(new_message, conversation);
+
+ bool is_uuid = new_message.stanza_id != null && Regex.match_simple("""[0-9A-Fa-f]{8}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{12}""", new_message.stanza_id);
+ if ((is_uuid && !db.contains_message_by_stanza_id(new_message.stanza_id)) ||
+ (!is_uuid && !db.contains_message(new_message, conversation.account))) {
+ db.add_message(new_message, conversation.account);
+ add_message(new_message, conversation);
+ if (new_message.time.difference(conversation.last_active) > 0) {
+ conversation.last_active = new_message.time;
+ }
+ if (new_message.direction == Entities.Message.DIRECTION_SENT) {
+ message_sent(new_message, conversation);
+ } else {
+ message_received(new_message, conversation);
+ }
+ }
+ }
+
+ private void add_message(Entities.Message message, Conversation conversation) {
+ if (!messages.has_key(conversation)) {
+ messages[conversation] = new ArrayList<Entities.Message>(Entities.Message.equals_func);
+ }
+ messages[conversation].add(message);
+ }
+}
+
+} \ No newline at end of file
diff --git a/client/src/service/module_manager.vala b/client/src/service/module_manager.vala
new file mode 100644
index 00000000..5ef93da8
--- /dev/null
+++ b/client/src/service/module_manager.vala
@@ -0,0 +1,96 @@
+using Gee;
+
+using Dino.Entities;
+using Xmpp;
+
+namespace Dino {
+
+public class ModuleManager {
+
+ public HashMap<Account, Tls.Module> tls_modules = new HashMap<Account, Tls.Module>();
+ public HashMap<Account, PlainSasl.Module> plain_sasl_modules = new HashMap<Account, PlainSasl.Module>();
+ public HashMap<Account, Bind.Module> bind_modules = new HashMap<Account, Bind.Module>();
+ public HashMap<Account, Roster.Module> roster_modules = new HashMap<Account, Roster.Module>();
+ public HashMap<Account, Xep.ServiceDiscovery.Module> service_discovery_modules = new HashMap<Account, Xep.ServiceDiscovery.Module>();
+ public HashMap<Account, Xep.PrivateXmlStorage.Module> private_xmp_storage_modules = new HashMap<Account, Xep.PrivateXmlStorage.Module>();
+ public HashMap<Account, Xep.Bookmarks.Module> bookmarks_module = new HashMap<Account, Xep.Bookmarks.Module>();
+ public HashMap<Account, Presence.Module> presence_modules = new HashMap<Account, Presence.Module>();
+ public HashMap<Account, Xmpp.Message.Module> message_modules = new HashMap<Account, Xmpp.Message.Module>();
+ public HashMap<Account, Xep.MessageCarbons.Module> message_carbons_modules = new HashMap<Account, Xep.MessageCarbons.Module>();
+ public HashMap<Account, Xep.Muc.Module> muc_modules = new HashMap<Account, Xep.Muc.Module>();
+ public HashMap<Account, Xep.Pgp.Module> pgp_modules = new HashMap<Account, Xep.Pgp.Module>();
+ public HashMap<Account, Xep.Pubsub.Module> pubsub_modules = new HashMap<Account, Xep.Pubsub.Module>();
+ public HashMap<Account, Xep.EntityCapabilities.Module> entity_capabilities_modules = new HashMap<Account, Xep.EntityCapabilities.Module>();
+ public HashMap<Account, Xep.UserAvatars.Module> user_avatars_modules = new HashMap<Account, Xep.UserAvatars.Module>();
+ public HashMap<Account, Xep.VCard.Module> vcard_modules = new HashMap<Account, Xep.VCard.Module>();
+ public HashMap<Account, Xep.MessageDeliveryReceipts.Module> message_delivery_receipts_modules = new HashMap<Account, Xep.MessageDeliveryReceipts.Module>();
+ public HashMap<Account, Xep.ChatStateNotifications.Module> chat_state_notifications_modules = new HashMap<Account, Xep.ChatStateNotifications.Module>();
+ public HashMap<Account, Xep.ChatMarkers.Module> chat_markers_modules = new HashMap<Account, Xep.ChatMarkers.Module>();
+ public HashMap<Account, Xep.Ping.Module> ping_modules = new HashMap<Account, Xep.Ping.Module>();
+ public HashMap<Account, Xep.DelayedDelivery.Module> delayed_delivery_module = new HashMap<Account, Xep.DelayedDelivery.Module>();
+ public HashMap<Account, StreamError.Module> stream_error_modules = new HashMap<Account, StreamError.Module>();
+
+ private AvatarStorage avatar_storage = new AvatarStorage("./");
+ private EntityCapabilitiesStorage entity_capabilities_storage;
+
+ public ModuleManager(Database db) {
+ entity_capabilities_storage = new EntityCapabilitiesStorage(db);
+ }
+
+ public ArrayList<Core.XmppStreamModule> get_modules(Account account, string? resource = null) {
+ ArrayList<Core.XmppStreamModule> modules = new ArrayList<Core.XmppStreamModule>();
+
+ if (!tls_modules.has_key(account)) add_account(account);
+
+ modules.add(tls_modules[account]);
+ modules.add(plain_sasl_modules[account]);
+ modules.add(new Bind.Module(resource == null ? account.resourcepart : resource));
+ modules.add(roster_modules[account]);
+ modules.add(service_discovery_modules[account]);
+ modules.add(private_xmp_storage_modules[account]);
+ modules.add(bookmarks_module[account]);
+ modules.add(presence_modules[account]);
+ modules.add(message_modules[account]);
+ modules.add(message_carbons_modules[account]);
+ modules.add(muc_modules[account]);
+ modules.add(pgp_modules[account]);
+ modules.add(pubsub_modules[account]);
+ modules.add(entity_capabilities_modules[account]);
+ modules.add(user_avatars_modules[account]);
+ modules.add(vcard_modules[account]);
+ modules.add(message_delivery_receipts_modules[account]);
+ modules.add(chat_state_notifications_modules[account]);
+ modules.add(chat_markers_modules[account]);
+ modules.add(ping_modules[account]);
+ modules.add(delayed_delivery_module[account]);
+ modules.add(stream_error_modules[account]);
+ return modules;
+ }
+
+ public void add_account(Account account) {
+ tls_modules[account] = new Tls.Module();
+ plain_sasl_modules[account] = new PlainSasl.Module(account.bare_jid.to_string(), account.password);
+ bind_modules[account] = new Bind.Module(account.resourcepart);
+ roster_modules[account] = new Roster.Module();
+ service_discovery_modules[account] = new Xep.ServiceDiscovery.Module.with_identity("client", "pc");
+ private_xmp_storage_modules[account] = new Xep.PrivateXmlStorage.Module();
+ bookmarks_module[account] = new Xep.Bookmarks.Module();
+ presence_modules[account] = new Presence.Module();
+ message_modules[account] = new Xmpp.Message.Module();
+ message_carbons_modules[account] = new Xep.MessageCarbons.Module();
+ muc_modules[account] = new Xep.Muc.Module();
+ pgp_modules[account] = new Xep.Pgp.Module();
+ pubsub_modules[account] = new Xep.Pubsub.Module();
+ entity_capabilities_modules[account] = new Xep.EntityCapabilities.Module(entity_capabilities_storage);
+ user_avatars_modules[account] = new Xep.UserAvatars.Module(avatar_storage);
+ vcard_modules[account] = new Xep.VCard.Module(avatar_storage);
+ message_delivery_receipts_modules[account] = new Xep.MessageDeliveryReceipts.Module();
+ chat_state_notifications_modules[account] = new Xep.ChatStateNotifications.Module();
+ chat_markers_modules[account] = new Xep.ChatMarkers.Module();
+ ping_modules[account] = new Xep.Ping.Module();
+ delayed_delivery_module[account] = new Xep.DelayedDelivery.Module();
+ stream_error_modules[account] = new StreamError.Module();
+ }
+}
+
+} \ No newline at end of file
diff --git a/client/src/service/muc_manager.vala b/client/src/service/muc_manager.vala
new file mode 100644
index 00000000..ead09306
--- /dev/null
+++ b/client/src/service/muc_manager.vala
@@ -0,0 +1,224 @@
+using Gee;
+
+using Xmpp;
+using Dino.Entities;
+
+namespace Dino {
+public class MucManager : StreamInteractionModule, Object {
+ public const string id = "muc_manager";
+
+ public signal void groupchat_joined(Account account, Jid jid, string nick);
+ public signal void groupchat_subject_set(Account account, Jid jid, string subject);
+ public signal void bookmarks_updated(Account account, ArrayList<Xep.Bookmarks.Conference> conferences);
+
+ private StreamInteractor stream_interactor;
+ protected HashMap<Jid, Xep.Bookmarks.Conference> conference_bookmarks = new HashMap<Jid, Xep.Bookmarks.Conference>();
+
+ public static void start(StreamInteractor stream_interactor) {
+ MucManager m = new MucManager(stream_interactor);
+ stream_interactor.add_module(m);
+ }
+
+ private MucManager(StreamInteractor stream_interactor) {
+ this.stream_interactor = stream_interactor;
+ stream_interactor.account_added.connect(on_account_added);
+ stream_interactor.stream_negotiated.connect(on_stream_negotiated);
+ MessageManager.get_instance(stream_interactor).pre_message_received.connect(on_pre_message_received);
+ }
+
+ public void join(Account account, Jid jid, string nick) {
+ Core.XmppStream stream = stream_interactor.get_stream(account);
+ if (stream != null) Xep.Muc.Module.get_module(stream).enter(stream, jid.bare_jid.to_string(), nick, null, new MucEnterListenerImpl(this, jid, nick, account));
+ }
+
+ public void part(Account account, Jid jid) {
+ Core.XmppStream stream = stream_interactor.get_stream(account);
+ if (stream != null) Xep.Muc.Module.get_module(stream).exit(stream, jid.bare_jid.to_string());
+ }
+
+ public void change_subject(Account account, Jid jid, string subject) {
+ Core.XmppStream stream = stream_interactor.get_stream(account);
+ if (stream != null) Xep.Muc.Module.get_module(stream).change_subject(stream, jid.bare_jid.to_string(), subject);
+ }
+
+ public void change_nick(Account account, Jid jid, string new_nick) {
+ Core.XmppStream stream = stream_interactor.get_stream(account);
+ if (stream != null) Xep.Muc.Module.get_module(stream).change_nick(stream, jid.bare_jid.to_string(), new_nick);
+ }
+
+ public void kick(Account account, Jid jid, string nick) {
+ Core.XmppStream stream = stream_interactor.get_stream(account);
+ if (stream != null) Xep.Muc.Module.get_module(stream).kick(stream, jid.bare_jid.to_string(), nick);
+ }
+
+ public ArrayList<Jid>? get_occupants(Jid jid, Account account) {
+ return PresenceManager.get_instance(stream_interactor).get_full_jids(jid, account);
+ }
+
+ public ArrayList<Jid>? get_other_occupants(Jid jid, Account account) {
+ ArrayList<Jid>? occupants = get_occupants(jid, account);
+ string? nick = get_nick(jid, account);
+ if (occupants != null && nick != null) {
+ occupants.remove(new Jid(@"$(jid.bare_jid)/$nick"));
+ }
+ return occupants;
+ }
+
+ public bool is_groupchat(Jid jid, Account account) {
+ Conversation? conversation = ConversationManager.get_instance(stream_interactor).get_conversation(jid, account);
+ return !jid.is_full() && conversation != null && conversation.type_ == Conversation.TYPE_GROUPCHAT;
+ }
+
+ public bool is_groupchat_occupant(Jid jid, Account account) {
+ return is_groupchat(jid.bare_jid, account) && jid.is_full();
+ }
+
+ public void get_bookmarks(Account account, Xep.Bookmarks.ConferencesRetrieveResponseListener listener) {
+ Core.XmppStream? stream = stream_interactor.get_stream(account);
+ if (stream != null) {
+ Xep.Bookmarks.Module.get_module(stream).get_conferences(stream, listener);
+ }
+ }
+
+ public void add_bookmark(Account account, Xep.Bookmarks.Conference conference) {
+ Core.XmppStream? stream = stream_interactor.get_stream(account);
+ if (stream != null) {
+ Xep.Bookmarks.Module.get_module(stream).add_conference(stream, conference);
+ }
+ }
+
+ public void replace_bookmark(Account account, Xep.Bookmarks.Conference was, Xep.Bookmarks.Conference replace) {
+ Core.XmppStream? stream = stream_interactor.get_stream(account);
+ if (stream != null) {
+ Xep.Bookmarks.Module.get_module(stream).replace_conference(stream, was, replace);
+ }
+ }
+
+ public void remove_bookmark(Account account, Xep.Bookmarks.Conference conference) {
+ Core.XmppStream? stream = stream_interactor.get_stream(account);
+ if (stream != null) {
+ Xep.Bookmarks.Module.get_module(stream).remove_conference(stream, conference);
+ }
+ }
+
+ public string? get_groupchat_subject(Jid jid, Account account) {
+ Core.XmppStream? stream = stream_interactor.get_stream(account);
+ if (stream != null) {
+ return Xep.Muc.Flag.get_flag(stream).get_muc_subject(jid.bare_jid.to_string());
+ }
+ return null;
+ }
+
+ public Jid? get_real_jid(Jid jid, Account account) {
+ Core.XmppStream? stream = stream_interactor.get_stream(account);
+ if (stream != null) {
+ string? real_jid = Xep.Muc.Flag.get_flag(stream).get_real_jid(jid.to_string());
+ if (real_jid != null) {
+ return new Jid(real_jid);
+ }
+ }
+ return null;
+ }
+
+ public Jid? get_message_real_jid(Entities.Message message) {
+ if (message.real_jid != null) {
+ return new Jid(message.real_jid);
+ }
+ return null;
+ }
+
+ public string? get_nick(Jid jid, Account account) {
+ Core.XmppStream? stream = stream_interactor.get_stream(account);
+ if (stream != null) {
+ return Xep.Muc.Flag.get_flag(stream).get_muc_nick(jid.bare_jid.to_string());
+ }
+ return null;
+ }
+
+ public static MucManager? get_instance(StreamInteractor stream_interactor) {
+ return (MucManager) stream_interactor.get_module(id);
+ }
+
+ internal string get_id() {
+ return id;
+ }
+
+ private void on_account_added(Account account) {
+ stream_interactor.module_manager.muc_modules[account].subject_set.connect( (stream, subject, jid) => {
+ on_subject_set(account, new Jid(jid), subject);
+ });
+ stream_interactor.module_manager.bookmarks_module[account].conferences_updated.connect( (stream, conferences) => {
+ bookmarks_updated(account, conferences);
+ });
+ }
+
+ private void on_subject_set(Account account, Jid sender_jid, string subject) {
+ groupchat_subject_set(account, sender_jid, subject);
+ }
+
+ private void on_stream_negotiated(Account account) {
+ Core.XmppStream stream = stream_interactor.get_stream(account);
+ if (stream != null) Xep.Bookmarks.Module.get_module(stream).get_conferences(stream, new BookmarksRetrieveResponseListener(this, account));
+ }
+
+ private void on_pre_message_received(Entities.Message message, Conversation conversation) {
+ if (conversation.type_ != Conversation.TYPE_GROUPCHAT) return;
+ Core.XmppStream stream = stream_interactor.get_stream(conversation.account);
+ if (stream == null) return;
+ if (Xep.DelayedDelivery.MessageFlag.get_flag(message.stanza) == null) {
+ string? real_jid = Xep.Muc.Flag.get_flag(stream).get_real_jid(message.counterpart.to_string());
+ if (real_jid != null && real_jid != message.counterpart.to_string()) {
+ message.real_jid = real_jid;
+ }
+ }
+ string muc_nick = Xep.Muc.Flag.get_flag(stream).get_muc_nick(conversation.counterpart.bare_jid.to_string());
+ if (message.from.equals(new Jid(@"$(message.from.bare_jid)/$muc_nick"))) { // TODO better from own
+ Gee.List<Entities.Message>? messages = MessageManager.get_instance(stream_interactor).get_messages(conversation);
+ if (messages != null) { // TODO not here
+ foreach (Entities.Message m in messages) {
+ if (m.equals(message)) {
+ m.marked = Entities.Message.Marked.RECEIVED;
+ }
+ }
+ }
+ }
+ }
+
+ private class BookmarksRetrieveResponseListener : Xep.Bookmarks.ConferencesRetrieveResponseListener, Object {
+ MucManager outer = null;
+ Account account = null;
+
+ public BookmarksRetrieveResponseListener(MucManager outer, Account account) {
+ this.outer = outer;
+ this.account = account;
+ }
+
+ public void on_result(Core.XmppStream stream, ArrayList<Xep.Bookmarks.Conference> conferences) {
+ foreach (Xep.Bookmarks.Conference bookmark in conferences) {
+ Jid jid = new Jid(bookmark.jid);
+ outer.conference_bookmarks[jid] = bookmark;
+ if (bookmark.autojoin) {
+ outer.join(account, jid, bookmark.nick);
+ }
+ }
+ }
+ }
+
+ private class MucEnterListenerImpl : Xep.Muc.MucEnterListener, Object { // TODO
+ private MucManager outer;
+ private Jid jid;
+ private string nick;
+ private Account account;
+ public MucEnterListenerImpl(MucManager outer, Jid jid, string nick, Account account) {
+ this.outer = outer;
+ this.jid = jid;
+ this.nick = nick;
+ this.account = account;
+ }
+ public void on_success() {
+ outer.groupchat_joined(account, jid, nick);
+ }
+ public void on_error(Xep.Muc.MucEnterError error) { }
+ }
+}
+} \ No newline at end of file
diff --git a/client/src/service/pgp_manager.vala b/client/src/service/pgp_manager.vala
new file mode 100644
index 00000000..6f3b63d7
--- /dev/null
+++ b/client/src/service/pgp_manager.vala
@@ -0,0 +1,54 @@
+using Gee;
+
+using Dino.Entities;
+
+namespace Dino {
+ public class PgpManager : StreamInteractionModule, Object {
+ public const string id = "pgp_manager";
+
+ public const string MESSAGE_ENCRYPTED = "pgp";
+
+ private StreamInteractor stream_interactor;
+ private Database db;
+ private HashMap<Jid, string> pgp_key_ids = new HashMap<Jid, string>(Jid.hash_bare_func, Jid.equals_bare_func);
+
+ public static void start(StreamInteractor stream_interactor, Database db) {
+ PgpManager m = new PgpManager(stream_interactor, db);
+ stream_interactor.add_module(m);
+ }
+
+ private PgpManager(StreamInteractor stream_interactor, Database db) {
+ this.stream_interactor = stream_interactor;
+ this.db = db;
+
+ stream_interactor.account_added.connect(on_account_added);
+ }
+
+ public string? get_key_id(Account account, Jid jid) {
+ return db.get_pgp_key(jid);
+ }
+
+ public static PgpManager? get_instance(StreamInteractor stream_interactor) {
+ return (PgpManager) stream_interactor.get_module(id);
+ }
+
+ internal string get_id() {
+ return id;
+ }
+
+ private void on_account_added(Account account) {
+ stream_interactor.module_manager.pgp_modules[account].received_jid_key_id.connect((stream, jid, key_id) => {
+ on_jid_key_received(account, new Jid(jid), key_id);
+ });
+ }
+
+ private void on_jid_key_received(Account account, Jid jid, string key_id) {
+ if (!pgp_key_ids.has_key(jid) || pgp_key_ids[jid] != key_id) {
+ if (!MucManager.get_instance(stream_interactor).is_groupchat_occupant(jid, account)) {
+ db.set_pgp_key(jid.bare_jid, key_id);
+ }
+ }
+ pgp_key_ids[jid] = key_id;
+ }
+ }
+} \ No newline at end of file
diff --git a/client/src/service/presence_manager.vala b/client/src/service/presence_manager.vala
new file mode 100644
index 00000000..53bdf4ce
--- /dev/null
+++ b/client/src/service/presence_manager.vala
@@ -0,0 +1,150 @@
+using Gee;
+
+using Xmpp;
+using Dino.Entities;
+
+namespace Dino {
+public class PresenceManager : StreamInteractionModule, Object {
+ public const string id = "presence_manager";
+
+ public signal void show_received(Show show, Jid jid, Account account);
+ public signal void received_subscription_request(Jid jid, Account account);
+
+ private StreamInteractor stream_interactor;
+ private HashMap<Jid, HashMap<Jid, ArrayList<Show>>> shows = new HashMap<Jid, HashMap<Jid, ArrayList<Show>>>(Jid.hash_bare_func, Jid.equals_bare_func);
+ private HashMap<Jid, ArrayList<Jid>> resources = new HashMap<Jid, ArrayList<Jid>>(Jid.hash_bare_func, Jid.equals_bare_func);
+
+ public static void start(StreamInteractor stream_interactor) {
+ PresenceManager m = new PresenceManager(stream_interactor);
+ stream_interactor.add_module(m);
+ }
+
+ private PresenceManager(StreamInteractor stream_interactor) {
+ this.stream_interactor = stream_interactor;
+ stream_interactor.account_added.connect(on_account_added);
+ }
+
+ public Show get_last_show(Jid jid, Account account) {
+ Core.XmppStream? stream = stream_interactor.get_stream(account);
+ if (stream != null) {
+ Xmpp.Presence.Stanza? presence = Xmpp.Presence.Flag.get_flag(stream).get_presence(jid.to_string());
+ if (presence != null) {
+ return new Show(jid, presence.show, new DateTime.now_local());
+ }
+ }
+ return new Show(jid, Show.OFFLINE, new DateTime.now_local());
+ }
+
+ public HashMap<Jid, ArrayList<Show>>? get_shows(Jid jid, Account account) {
+ return shows[jid];
+ }
+
+ public ArrayList<Jid>? get_full_jids(Jid jid, Account account) {
+ Core.XmppStream? stream = stream_interactor.get_stream(account);
+ if (stream != null) {
+ ArrayList<string> resources = Xmpp.Presence.Flag.get_flag(stream).get_resources(jid.bare_jid.to_string());
+ if (resources == null) {
+ return null;
+ }
+ ArrayList<Jid> ret = new ArrayList<Jid>(Jid.equals_func);
+ foreach (string resource in resources) {
+ ret.add(new Jid(resource));
+ }
+ return ret;
+ }
+ return null;
+ }
+
+ public void request_subscription(Account account, Jid jid) {
+ Core.XmppStream stream = stream_interactor.get_stream(account);
+ if (stream != null) Xmpp.Presence.Module.get_module(stream).request_subscription(stream, jid.bare_jid.to_string());
+ }
+
+ public void approve_subscription(Account account, Jid jid) {
+ Core.XmppStream stream = stream_interactor.get_stream(account);
+ if (stream != null) Xmpp.Presence.Module.get_module(stream).approve_subscription(stream, jid.bare_jid.to_string());
+ }
+
+ public void deny_subscription(Account account, Jid jid) {
+ Core.XmppStream stream = stream_interactor.get_stream(account);
+ if (stream != null) Xmpp.Presence.Module.get_module(stream).deny_subscription(stream, jid.bare_jid.to_string());
+ }
+
+ public static PresenceManager? get_instance(StreamInteractor stream_interactor) {
+ return (PresenceManager) stream_interactor.get_module(id);
+ }
+
+ internal string get_id() {
+ return id;
+ }
+
+ private void on_account_added(Account account) {
+ stream_interactor.module_manager.presence_modules[account].received_available_show.connect((stream, jid, show) =>
+ on_received_available_show(account, new Jid(jid), show)
+ );
+ stream_interactor.module_manager.presence_modules[account].received_unavailable.connect((stream, jid) =>
+ on_received_unavailable(account, new Jid(jid))
+ );
+ stream_interactor.module_manager.presence_modules[account].received_subscription_request.connect((stream, jid) =>
+ received_subscription_request(new Jid(jid), account)
+ );
+ }
+
+ private void on_received_available_show(Account account, Jid jid, string show) {
+ lock (resources) {
+ if (!resources.has_key(jid)){
+ resources[jid] = new ArrayList<Jid>(Jid.equals_func);
+ }
+ if (!resources[jid].contains(jid)) {
+ resources[jid].add(jid);
+ }
+ }
+ add_show(account, jid, show);
+ }
+
+ private void on_received_unavailable(Account account, Jid jid) {
+ lock (resources) {
+ if (resources.has_key(jid)) {
+ resources[jid].remove(jid);
+ if (resources[jid].size == 0 || jid.is_bare()) {
+ resources.unset(jid);
+ }
+ }
+ }
+ add_show(account, jid, Show.OFFLINE);
+ }
+
+ private void add_show(Account account, Jid jid, string s) {
+ Show show = new Show(jid, s, new DateTime.now_local());
+ lock (shows) {
+ if (!shows.has_key(jid)) {
+ shows[jid] = new HashMap<Jid, ArrayList<Show>>();
+ }
+ if (!shows[jid].has_key(jid)) {
+ shows[jid][jid] = new ArrayList<Show>();
+ }
+ shows[jid][jid].add(show);
+ }
+ show_received(show, jid, account);
+ }
+}
+
+public class Show : Object {
+ public const string ONLINE = Xmpp.Presence.Stanza.SHOW_ONLINE;
+ public const string AWAY = Xmpp.Presence.Stanza.SHOW_AWAY;
+ public const string CHAT = Xmpp.Presence.Stanza.SHOW_CHAT;
+ public const string DND = Xmpp.Presence.Stanza.SHOW_DND;
+ public const string XA = Xmpp.Presence.Stanza.SHOW_XA;
+ public const string OFFLINE = "offline";
+
+ public Jid jid;
+ public string as;
+ public DateTime datetime;
+
+ public Show(Jid jid, string show, DateTime datetime) {
+ this.jid = jid;
+ this.as = show;
+ this.datetime = datetime;
+ }
+}
+} \ No newline at end of file
diff --git a/client/src/service/roster_manager.vala b/client/src/service/roster_manager.vala
new file mode 100644
index 00000000..106405e2
--- /dev/null
+++ b/client/src/service/roster_manager.vala
@@ -0,0 +1,82 @@
+using Gee;
+
+using Xmpp;
+using Dino.Entities;
+
+namespace Dino {
+ public class RosterManager : StreamInteractionModule, Object {
+ public const string id = "roster_manager";
+
+ public signal void removed_roster_item(Account account, Jid jid, Roster.Item roster_item);
+ public signal void updated_roster_item(Account account, Jid jid, Roster.Item roster_item);
+
+ private StreamInteractor stream_interactor;
+
+ public static void start(StreamInteractor stream_interactor) {
+ RosterManager m = new RosterManager(stream_interactor);
+ stream_interactor.add_module(m);
+ }
+
+ public RosterManager(StreamInteractor stream_interactor) {
+ this.stream_interactor = stream_interactor;
+ stream_interactor.account_added.connect(on_account_added);
+ }
+
+ public ArrayList<Roster.Item> get_roster(Account account) {
+ Core.XmppStream? stream = stream_interactor.get_stream(account);
+ ArrayList<Roster.Item> ret = new ArrayList<Roster.Item>();
+ if (stream != null) {
+ ret.add_all(Xmpp.Roster.Flag.get_flag(stream).get_roster());
+ }
+ return ret;
+ }
+
+ public Roster.Item? get_roster_item(Account account, Jid jid) {
+ Core.XmppStream? stream = stream_interactor.get_stream(account);
+ if (stream != null) {
+ return Xmpp.Roster.Flag.get_flag(stream).get_item(jid.bare_jid.to_string());
+ }
+ return null;
+ }
+
+ public void remove_jid(Account account, Jid jid) {
+ Core.XmppStream? stream = stream_interactor.get_stream(account);
+ if (stream != null) Xmpp.Roster.Module.get_module(stream).remove_jid(stream, jid.bare_jid.to_string());
+ }
+
+ public void add_jid(Account account, Jid jid, string? handle) {
+ Core.XmppStream? stream = stream_interactor.get_stream(account);
+ if (stream != null) Xmpp.Roster.Module.get_module(stream).add_jid(stream, jid.bare_jid.to_string(), handle);
+ }
+
+ public static RosterManager? get_instance(StreamInteractor stream_interactor) {
+ return (RosterManager) stream_interactor.get_module(id);
+ }
+
+ internal string get_id() {
+ return id;
+ }
+
+ private void on_account_added(Account account) {
+ stream_interactor.module_manager.roster_modules[account].received_roster.connect( (stream, roster) => {
+ on_roster_received(account, roster);
+ });
+ stream_interactor.module_manager.roster_modules[account].item_removed.connect( (stream, roster_item) => {
+ removed_roster_item(account, new Jid(roster_item.jid), roster_item);
+ });
+ stream_interactor.module_manager.roster_modules[account].item_updated.connect( (stream, roster_item) => {
+ on_roster_item_updated(account, roster_item);
+ });
+ }
+
+ private void on_roster_received(Account account, Collection<Roster.Item> roster_items) {
+ foreach (Roster.Item roster_item in roster_items) {
+ on_roster_item_updated(account, roster_item);
+ }
+ }
+
+ private void on_roster_item_updated(Account account, Roster.Item roster_item) {
+ updated_roster_item(account, new Jid(roster_item.jid), roster_item);
+ }
+ }
+} \ No newline at end of file
diff --git a/client/src/service/stream_interactor.vala b/client/src/service/stream_interactor.vala
new file mode 100644
index 00000000..56591cf0
--- /dev/null
+++ b/client/src/service/stream_interactor.vala
@@ -0,0 +1,68 @@
+using Gee;
+
+using Xmpp;
+using Dino.Entities;
+
+namespace Dino {
+public class StreamInteractor {
+
+ public signal void account_added(Account account);
+ public signal void stream_negotiated(Account account);
+
+ public ModuleManager module_manager;
+ public ConnectionManager connection_manager;
+ private ArrayList<StreamInteractionModule> interaction_modules = new ArrayList<StreamInteractionModule>();
+
+ public StreamInteractor(Database db) {
+ module_manager = new ModuleManager(db);
+ connection_manager = new ConnectionManager(module_manager);
+
+ connection_manager.stream_opened.connect(on_stream_opened);
+ }
+
+ public void connect(Account account) {
+ module_manager.add_account(account);
+ account_added(account);
+ connection_manager.connect(account);
+ }
+
+ public void disconnect(Account account) {
+ connection_manager.disconnect(account);
+ }
+
+ public ArrayList<Account> get_accounts() {
+ ArrayList<Account> ret = new ArrayList<Account>(Account.equals_func);
+ foreach (Account account in connection_manager.get_managed_accounts()) {
+ ret.add(account);
+ }
+ return ret;
+ }
+
+ public Core.XmppStream? get_stream(Account account) {
+ return connection_manager.get_stream(account);
+ }
+
+ public void add_module(StreamInteractionModule module) {
+ interaction_modules.add(module);
+ }
+
+ public StreamInteractionModule? get_module(string id) {
+ foreach (StreamInteractionModule module in interaction_modules) {
+ if (module.get_id() == id) {
+ return module;
+ }
+ }
+ return null;
+ }
+
+ private void on_stream_opened(Account account, Core.XmppStream stream) {
+ stream.stream_negotiated.connect( (stream) => {
+ stream_negotiated(account);
+ });
+ }
+}
+
+public interface StreamInteractionModule : Object {
+ internal abstract string get_id();
+}
+} \ No newline at end of file
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<Jid, ListRow> rows = new HashMap<Jid, ListRow>(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<Account, ArrayList<Xep.Bookmarks.Conference>> lists = new HashMap<Account, ArrayList<Xep.Bookmarks.Conference>>();
+
+ 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<Xep.Bookmarks.Conference> 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<AddListRow> added_rows = new ArrayList<AddListRow>();
+ 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("<Ctrl>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<Jid>? 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<Conversation, string> entry_cache = new HashMap<Conversation, string>(Conversation.hash_func, Conversation.equals_func);
+ private static HashMap<string, string> smiley_translations = new HashMap<string, string>();
+
+ 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<Jid> 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<Jid>? 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<Conversation, ConversationRow> rows = new HashMap<Conversation, ConversationRow>(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<Message> messages = new ArrayList<Message>(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(@"<span foreground=\"#$(Util.get_name_hex_color(display_name))\">$display_name</span>");
+ 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<Show> statuses = new ArrayList<Show>();
+
+ 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(@"<span foreground=\"#B1B1B1\"> $(escape_text(display_name)) $text </span>");
+ 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<Entities.Message, MergedMessageItem> message_items = new HashMap<Entities.Message, MergedMessageItem>(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<Object> objects = new ArrayList<Object>();
+ Gee.List<Entities.Message>? messages = MessageManager.get_instance(stream_interactor).get_messages(conversation);
+ if (messages != null && messages.size > 0) {
+ earliest_message = messages[0];
+ objects.add_all(messages);
+ }
+ HashMap<Jid, ArrayList<Show>>? 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<Entities.Message>? 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<Jid, OccupantListRow> rows = new HashMap<Jid, OccupantListRow>(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<Jid>? 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 <prefix>_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 "<?xml version=${Q}1.0${Q} encoding=${Q}UTF-8${Q}?>")
+ set(GXML_XML_FILE "${GXML_XML_FILE}<gresources><gresource prefix=${Q}")
+
+ # Set the prefix for the resources. Depending on the user-override we choose
+ # the standard prefix "/" or the override.
+ if (GXML_ARG_PREFIX)
+ set(GXML_XML_FILE "${GXML_XML_FILE}${GXML_ARG_PREFIX}")
+ else()
+ set(GXML_XML_FILE "${GXML_XML_FILE}/")
+ endif()
+
+ set(GXML_XML_FILE "${GXML_XML_FILE}${Q}>")
+
+ # 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 <file> node.
+ set(GXML_RES_LINE "<file")
+ if ((GXML_ARG_COMPRESS_ALL OR GXML_COMPRESSION_FLAG) AND NOT
+ GXML_ARG_NO_COMPRESS_ALL)
+ set(GXML_RES_LINE "${GXML_RES_LINE} compressed=${Q}true${Q}")
+ endif()
+
+ # Check preprocess flag validity.
+ if ((GXML_ARG_STRIPBLANKS_ALL OR GXML_STRIPBLANKS_FLAG) AND
+ (GXML_ARG_TOPIXDATA_ALL OR GXML_TOPIXDATA_FLAG))
+ set(GXML_ERRMSG "Resource preprocessing option conflict. Tried")
+ set(GXML_ERRMSG "${GXML_ERRMSG} to specify both, STRIPBLANKS")
+ set(GXML_ERRMSG "${GXML_ERRMSG} and TOPIXDATA. In resource")
+ set(GXML_ERRMSG "${GXML_ERRMSG} ${GXML_RESOURCE_PATH} in")
+ set(GXML_ERRMSG "${GXML_ERRMSG} function COMPILE_GRESOURCES.")
+ message(FATAL_ERROR ${GXML_ERRMSG})
+ endif()
+
+ if ((GXML_ARG_STRIPBLANKS_ALL OR GXML_STRIPBLANKS_FLAG) AND NOT
+ GXML_ARG_NO_STRIPBLANKS_ALL)
+ set(GXML_RES_LINE "${GXML_RES_LINE} preprocess=")
+ set(GXML_RES_LINE "${GXML_RES_LINE}${Q}xml-stripblanks${Q}")
+ elseif((GXML_ARG_TOPIXDATA_ALL OR GXML_TOPIXDATA_FLAG) AND NOT
+ GXML_ARG_NO_TOPIXDATA_ALL)
+ set(GXML_RES_LINE "${GXML_RES_LINE} preprocess=")
+ set(GXML_RES_LINE "${GXML_RES_LINE}${Q}to-pixdata${Q}")
+ endif()
+
+ set(GXML_RES_LINE "${GXML_RES_LINE}>${GXML_RESOURCE_PATH}</file>")
+
+ # 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}</gresource></gresources>")
+
+ # 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
+# <provided_name>.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
+# <provided_name>.h and <provided_name>_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<T> {
+ 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<int> {
+ 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<long> {
+ 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<double> {
+ 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<string?> {
+ 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<bool> {
+ 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<bool> {
+ 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<Row?> {
+ private Table table;
+ private Column<int> id_column;
+
+ public RowReference(string name, Table table, Column<int> 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<string> meta_name = new Column.Text("name") { primary_key = true };
+ private Column<long> meta_int_val = new Column.Long("int_val");
+ private Column<string> 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<T>(Column<T> column, string comp, T value) {
+ if (selection == null) {
+ selection = @"$(column.name) $comp ?";
+ selection_args = { new StatementBuilder.Field<T>(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<T>(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<T>(Column<T> column, T value) {
+ if (fields == null) {
+ fields = { new StatementBuilder.Field<T>(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<T>(column, value);
+ fields = fields_new;
+ }
+ return this;
+ }
+
+ public InsertBuilder value_null<T>(Column<T> 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<T>(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<T>(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<T>(Column<T> 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<T>(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<T>(column, value);
+ selection_args = selection_args_new;
+ }
+ return this;
+ }
+
+ public QueryBuilder with_null<T>(Column<T> column) {
+ selection = @"($selection) AND $(column.name) ISNULL";
+ return this;
+ }
+
+ public QueryBuilder without_null<T>(Column<T> 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<T>(Column<T> 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<string, string> text_map = new HashMap<string, string>();
+ private Map<string, long> int_map = new HashMap<string, long>();
+ private Map<string, double?> real_map = new HashMap<string, double?>();
+
+ 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<T>(Column<T> 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<T> {
+ public T value;
+ public Column<T>? column;
+
+ public Field(Column<T>? 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<T> : Field<T> {
+ public NullField(Column<T>? column) {
+ base(column, null);
+ }
+
+ public override void bind(Statement stmt, int index) {
+ stmt.bind_null(index);
+ }
+ }
+
+ protected class StringField : Field<string> {
+ 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<T>(Column<T> 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<T>(Column<T> column, T value) {
+ if (fields == null) {
+ fields = { new StatementBuilder.Field<T>(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<T>(column, value);
+ fields = fields_new;
+ }
+ return this;
+ }
+
+ public UpdateBuilder set_null<T>(Column<T> 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<T>(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<T>(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<T>(Column<T> column, string comp, T value) {
+ if (selection == null) {
+ selection = @"$(column.name) $comp ?";
+ selection_args = { new StatementBuilder.Field<T>(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<T>(column, value);
+ selection_args = selection_args_new;
+ }
+ return this;
+ }
+
+ public UpdateBuilder with_null<T>(Column<T> column) {
+ selection = @"($selection) AND $(column.name) ISNULL";
+ return this;
+ }
+
+ public UpdateBuilder without_null<T>(Column<T> 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<string, string> uri_to_name = new HashMap<string, string> ();
+ private HashMap<string, string> name_to_uri = new HashMap<string, string> ();
+ 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("&", "&amp;").replace("\"", "&quot;").replace("'", "&apos;").replace("<", "&lt;").replace(">", "&gt;");
+ }
+ set {
+ string tmp = value.replace("&gt;", ">").replace("&lt;", "<").replace("&apos;","'").replace("&quot;","\"");
+ 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("&amp;", "&");
+ }
+ }
+
+ 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<StanzaNode> sub_nodes = new ArrayList<StanzaNode>();
+ public ArrayList<StanzaAttribute> attributes = new ArrayList<StanzaAttribute>();
+ public bool has_nodes = false;
+ public bool pseudo = false;
+
+ public StanzaNode () {}
+
+ public StanzaNode.build(string name, string ns_uri = "jabber:client", ArrayList<StanzaNode>? nodes = null, ArrayList<StanzaAttribute>? 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<StanzaAttribute> get_attributes_by_ns_uri(string ns_uri) {
+ ArrayList<StanzaAttribute> ret = new ArrayList<StanzaAttribute> ();
+ 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<StanzaNode> get_subnodes(string name, string? ns_uri = null, bool recurse = false) {
+ ArrayList<StanzaNode> ret = new ArrayList<StanzaNode>();
+ 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<StanzaNode> 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</{$ns_uri}:$name>\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(@"</$name>");
+ } else {
+ sb.append_printf("</%s:%s>", 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<StanzaAttribute>();
+ 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<XmppStreamFlag> flags = new ArrayList<XmppStreamFlag>();
+ private ArrayList<XmppStreamModule> modules = new ArrayList<XmppStreamModule>();
+ 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</stream:stream>\n");
+ writer.write.begin("</stream:stream>");
+ 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<string, ResponseListener> responseListeners = new HashMap<string, ResponseListener>();
+ private HashMap<string, ArrayList<Handler>> namespaceRegistrants = new HashMap<string, ArrayList<Handler>>();
+
+ 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<Handler>());
+ }
+ 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<StanzaNode> children = node.get_all_subnodes();
+ if (children.size == 1 && namespaceRegistrants.has_key(children[0].ns_uri)) {
+ ArrayList<Handler> 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<MessageFlag> flags = new ArrayList<MessageFlag>();
+
+ 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<string, ArrayList<string>> resources = new HashMap<string, ArrayList<string>>();
+ private HashMap<string, Presence.Stanza> presences = new HashMap<string, Presence.Stanza>();
+
+ public Set<string> get_available_jids() {
+ return resources.keys;
+ }
+
+ public ArrayList<string>? 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<string>();
+ }
+ 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<string, Item> roster_items = new HashMap<string, Item>();
+
+ internal string? iq_id;
+
+ public Collection<Item> 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<Item> 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<StanzaNode> 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<StanzaNode> 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<string, string> key_ids = new HashMap<string, string>();
+
+ 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<string, ArrayList<string>> entity_features = new HashMap<string, ArrayList<string>>();
+ public ArrayList<string> features = new ArrayList<string>();
+
+ 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<string> 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<string> features {
+ owned get {
+ ArrayList<string> ret = new ArrayList<string>();
+ 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<Identity> identities {
+ owned get {
+ ArrayList<Identity> ret = new ArrayList<Identity>();
+ 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<Item> items {
+ owned get {
+ ArrayList<Item> ret = new ArrayList<Item>();
+ 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<Identity> identities = new ArrayList<Identity>();
+
+ 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<string, MucEnterListener> enter_listeners = new HashMap<string, MucEnterListener>();
+ private HashMap<string, string> enter_ids = new HashMap<string, string>();
+ private HashMap<string, string> own_nicks = new HashMap<string, string>();
+ private HashMap<string, string> subjects = new HashMap<string, string>();
+ private HashMap<string, string> subjects_by = new HashMap<string, string>();
+ private HashMap<string, string> occupant_real_jids = new HashMap<string, string>();
+ private HashMap<string, string> occupant_affiliation = new HashMap<string, string>();
+ private HashMap<string, string> occupant_role = new HashMap<string, string>();
+
+ 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<int> 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<int> get_status_codes(StanzaNode x_node) {
+ ArrayList<int> ret = new ArrayList<int>();
+ 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<Conference> 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<Conference> 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<Conference> conferences;
+ public StoreResponseListenerImpl(ArrayList<Conference> 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<Conference> 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<Conference> 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<Conference> 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<Conference> get_conferences_from_stanza(StanzaNode node) {
+ ArrayList<Conference> conferences = new ArrayList<Conference>();
+ ArrayList<StanzaNode> 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<Conference> 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<string, EventListener> event_listeners = new HashMap<string, EventListener>();
+
+ 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 <active/>" (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<StanzaNode> 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<string> 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<ServiceDiscovery.Identity> identities, ArrayList<string> 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<string> capabilities);
+ public abstract ArrayList<string> 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<StanzaNode> 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 <sre@ring0.de>
+ *
+ * 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 <sre@ring0.de>
+ *
+ * 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 <evan@nemerson.com>
+ *
+ * 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;
+ }
+}