diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 2af1724a6eeae9f6171b27ccb4595f42b76a1995..4c0193b940f2c455ea2380d10ddc65a6b55c2174 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -25,6 +25,7 @@ build-pureos-gcc-byzantium-no-purple: stage: build script: - apt-get -y build-dep . + - apt-get -y install git - export LC_ALL=C.UTF-8 - meson . _build -Dpurple=disabled - ninja -C _build @@ -35,7 +36,7 @@ build-pureos-gcc-byzantium: stage: build script: - apt-get -y build-dep . - - apt-get -y install libgtk-3-bin xvfb + - apt-get -y install libgtk-3-bin xvfb git - export LC_ALL=C.UTF-8 - meson . _build -Db_coverage=true --werror - ninja -C _build @@ -51,7 +52,7 @@ test:debian-gcc: - build-pureos-gcc-byzantium script: - apt-get -y build-dep . - - apt-get -y install libgtk-3-bin xvfb lcov + - apt-get -y install libgtk-3-bin xvfb lcov git - export G_DEBUG=fatal-warnings - export LC_ALL=C.UTF-8 - xvfb-run -a -s "-screen 0 1024x768x24" ninja -C _build test @@ -66,7 +67,7 @@ check-po: - build-pureos-gcc-byzantium before_script: - apt-get -y update - - apt-get -y install intltool + - apt-get -y install intltool git script: # barf on untranslated C files. Seems intltool # can't be told to exit with non-zero exit status diff --git a/.gitmodules b/.gitmodules index fa136361fc689a5cf44a753279a18c5d524de098..6c4ab7bc691c7567f4d89900fa827606be8301a0 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,3 +1,6 @@ [submodule "subprojects/libgd"] path = subprojects/libgd url = https://gitlab.gnome.org/GNOME/libgd.git +[submodule "subprojects/libcmatrix"] + path = subprojects/libcmatrix + url = https://source.puri.sm/Librem5/libcmatrix.git diff --git a/NEWS b/NEWS index b62c97c16fef1c3382540a7d64137279438e0f5f..132a08daa9cd9f3e96d00a24c703c9911c1f39ac 100644 --- a/NEWS +++ b/NEWS @@ -1,3 +1,15 @@ +Overview of Changes in Chats 0.7.0~rc0. Mon, 17 October 2022 +============================================================ +* Move all matrix code to libcmatrix +* Matrix support is no longer experimental +* Fix build with GNOME desktop 43 +* Update Translations + - Swedish + - Ukrainian + - Hebrew + - Romanian + - Spanish + Overview of Changes in Chats 0.6.7. Tue, 25 June 2022 ===================================================== * Scale down user avatar to speed up UI diff --git a/data/sm.puri.Chatty.desktop.in b/data/sm.puri.Chatty.desktop.in index bc0cb41fc957023fe9e7f9c93695912932b8726e..1e3f7aecdcf6b2a2f1cb28474a65b45af2469064 100644 --- a/data/sm.puri.Chatty.desktop.in +++ b/data/sm.puri.Chatty.desktop.in @@ -11,3 +11,4 @@ StartupNotify=true MimeType=x-scheme-handler/sms # Translators: Do NOT translate or transliterate this text (these are enum types)! X-Purism-FormFactor=Workstation;Mobile; +X-Phosh-UsesFeedback=true diff --git a/debian/changelog b/debian/changelog index c38abc14775396e9491c661173da9bd2cad10f2c..d20f91ff4ae243e961ca621d35f79361d8d26234 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,3 +1,128 @@ +chatty (0.7.0~rc0) byzantium; urgency=medium + + [ Mohammed Sadiq ] + * tests: Use memory backend for gsettings + * matrix: Port to use libcmatrix + * mm-chat-info: Fix possible invalid free + * log: Sync with upstream + * application: Fix nologin commandline argument + * build: Bump meson version to 0.56 + * build: Fix using deprecated meson options + * build: Improve meson build summary + * log: Fix a thinko + * build: Fix validating ui files + * po: Add Hebrew translation + * mm: Fix removing duplicate items from array + * log: Anonymize unicode strings properly + * message: Add support for CmEvent messages + * tests: Remove matrix-utils test + * matrix: Update libcmatrix and adapt to changes + * image-item: Don't update image on dispose + * tests: Create matrix account from cm_client + * matrix: Remove unused code + * ma-chat: Remove unused code + * secret-store: Remove unused code + * matrix-utils: Remove unused code + * ma-account: Remove unused code + * subprojects: Update cmatrix + * Update cmatrix + * subprojects: Update cmatrix + * build: Point libcmatrix URL to official source + * ma-account: Fix a crash when accessing avatar url + * message: Use the same API to get file for all protocols + * Update cmatrix + * ma-chat: Fix unread count + * tests: Remove chat history tests + * history: Remove matrix support + * ma-chat: Remove no longer used code + * ma-account: Remove no longer used code + * ma-chat: Fix setting room id + * application: Add an action to show about dialog + * Add chatty-header-bar + * window: Use chatty-header-bar for header bar + * Add chatty-main-view + * window: Use chatty-main-view + * chat-view: Remove empty view + * enums: Add ChattyChatState + * Update cmatrix + * chat: Add API to get chat state + * ma-chat: Implement get_state() + * chat: Add API to accept/reject invites + * ma-chat: Implement accept/reject invites + * manager: List invited rooms in the top of chat list + * ma-account: Add invited chats to chat list + * list-row: Mark invited room as such + * Add chatty-invite-view + * main-view: Use chatty-invite-view + * history: Use sqlite transactions + * history: Improve setting user version + * history: More sqlite transactions + * message: Change status atomically + * chat-view: Create a new draft to save in history + * invite-view: Add a translator comment + * Update cmatrix + * enums: Add CHATTY_CHAT_VERIFICATION + * Add chatty-ma-key-chat + * ma-account: Add chat verifications to chat list + * avatar: Use system-lock icon for matrix verification + * list-row: Hide chat details for key verifications + * manager: Place chat verifications at the top of chat list + * Add chatty-verification-view + * main-view: Use verification-view for verifications + * log: Improve abort() on fatal errors + * main: Log backtrace on fatal errors + + [ Chris Talbot ] + * Add function to call out transmit or receieve issue for MMS + + [ Anders Jonsson ] + * po: Update Swedish translation + * po: Update Swedish translation + + [ Yuri Chornoivan ] + * po: Update Ukrainian translation + * po: Update Ukrainian translation + * po: Update Ukrainian translation + * po: Update Ukrainian translation + + [ Vittorio Monti ] + * Update Italian translation + * Update Italian translation + + [ Guido Günther ] + * data: Indicate that chatty provides LED/haptic feedback + + [ Jeremy Bicha ] + * utils: update for GNOME Desktop 43 thumbnail API changes + + [ Марко Ðœ. КоÑтић (Marko M. Kostić) ] + * Update Serbian translation + + [ Rafael Fontenelle ] + * Update Brazilian Portuguese translation + + [ Emin Tufan Çetin ] + * Update Turkish translation + + [ Evangelos Ribeiro Tzaras ] + * submodule: Point libcmatrix URL to official sources + * settings-dialog: Mark "add" button in account page as suggested action + * settings-dialog: Default to https if no scheme given in homeserver entry + + [ Daniel Șerbănescu ] + * po: Update Romanian translation + + [ Pablo Correa Gómez ] + * po: Update Spanish translation + + [ carlosgonz ] + * po: Update Spanish translation + + [ Yosef Or Boczko ] + * po: Update Hebrew Translation + + -- Mohammed Sadiq <sadiq@sadiqpk.org> Mon, 17 Oct 2022 11:16:33 +0530 + chatty (0.6.7) byzantium; urgency=medium [ Andrey Skvortsov ] diff --git a/meson.build b/meson.build index 66df1e80087f8fe6b41d8fb7e568e66b0a27ba64..9d3a35cbef7de2fee2f84055d34a4ebdac766be6 100644 --- a/meson.build +++ b/meson.build @@ -1,7 +1,7 @@ project( 'chatty', 'c', 'cpp', - version: '0.6.7', - meson_version: '>= 0.53.0', + version: '0.7.0.rc0', + meson_version: '>= 0.56.0', ) i18n = import('i18n') @@ -21,27 +21,12 @@ config_h.set_quoted('LOCALEDIR', join_paths(get_option('prefix'), get_option('lo config_h.set_quoted('PACKAGE_NAME', meson.project_name()) config_h.set_quoted('PACKAGE_VERSION', meson.project_version()) -# TODO: Use has_headers: 'olm/olm.h' when we bump meson requirement -libolm_dep = cc.find_library('olm', required: true) - -if (cc.has_function('olm_account_unpublished_fallback_key', dependencies: libolm_dep)) - config_h.set('OLM_ACCOUNT_PICKLE_V4', true) -else - config_h.set('OLM_ACCOUNT_PICKLE_V4', false) -endif - -if (cc.has_function('olm_pk_key_from_private', dependencies: libolm_dep)) - config_h.set('HAVE_OLM3', true) -else - config_h.set('HAVE_OLM2', true) -endif - configure_file( output: 'config.h', configuration: config_h, ) add_project_arguments([ - '-I' + meson.build_root(), + '-I' + meson.project_build_root(), '-DHAVE_CONFIG_H', '-DGLIB_DISABLE_DEPRECATION_WARNINGS', '-DG_LOG_USE_STRUCTURED', @@ -118,6 +103,13 @@ libgd = subproject('libgd', libgd_dep = libgd.get_variable('libgd_dep') +libcmatrix = subproject('libcmatrix', + default_options: [ + 'build-examples=false', + ]) +libcmatrix_dep = libcmatrix.get_variable('libcmatrix_dep') + + subdir('completion') subdir('data') subdir('help') @@ -125,8 +117,20 @@ subdir('src') subdir('tests') subdir('po') -summary({ - 'Enable libpurple': purple_dep.found(), -}) +system = target_machine.system() +if system == 'linux' + system = 'GNU/Linux' +endif + +summary({'Target': system, + 'Target arch': target_machine.cpu(), + 'Compiler': cc.get_id(), + 'Version': cc.version(), + 'Linker': cc.get_linker_id(), + }, section: 'Toolchain') + +summary({'Build type': get_option('buildtype'), + 'libpurple': purple_dep.found(), + }, section: 'Configuration') meson.add_install_script('build-aux/meson/postinstall.py') diff --git a/po/LINGUAS b/po/LINGUAS index 4b483e709be4904885352a3453b11c1fcb8963f4..601220786174957ccff8d236374a93f65afea73f 100644 --- a/po/LINGUAS +++ b/po/LINGUAS @@ -7,6 +7,7 @@ es fi fr fur +he ht hu id diff --git a/po/POTFILES.in b/po/POTFILES.in index 62abc7088239b58e50a778dec9a153000e946328..3a4bf969fb33a385d8cd8070202b76d050bd0617 100644 --- a/po/POTFILES.in +++ b/po/POTFILES.in @@ -13,10 +13,12 @@ src/chatty-contact-list.c src/chatty-clock.c src/chatty-contact-provider.c src/chatty-contact-provider.h +src/chatty-header-bar.c src/chatty-history.c src/chatty-history.h src/chatty-fp-row.c src/chatty-fp-row.h +src/chatty-invite-view.c src/chatty-list-row.c src/chatty-list-row.h src/chatty-manager.c @@ -35,6 +37,7 @@ src/chatty-secret-store.c src/chatty-secret-store.h src/chatty-settings.c src/chatty-settings.h +src/chatty-verification-view.c src/chatty-window.c src/chatty-window.h src/dialogs/chatty-info-dialog.c @@ -60,6 +63,7 @@ src/matrix/chatty-ma-account.c src/matrix/chatty-ma-account.h src/matrix/chatty-ma-chat.c src/matrix/chatty-ma-chat.h +src/matrix/chatty-ma-key-chat.c src/matrix/matrix-utils.c src/matrix/matrix-utils.h src/mm/chatty-mmsd.c @@ -69,7 +73,9 @@ src/ui/chatty-contact-row.ui src/ui/chatty-dialog-join-muc.ui src/ui/chatty-dialog-new-chat.ui src/ui/chatty-file-item.ui +src/ui/chatty-header-bar.ui src/ui/chatty-info-dialog.ui +src/ui/chatty-invite-view.ui src/ui/chatty-ma-chat-info.ui src/ui/chatty-pp-chat-info.ui src/ui/chatty-mm-chat-info.ui @@ -78,6 +84,7 @@ src/ui/chatty-list-row.ui src/ui/chatty-ma-account-details.ui src/ui/chatty-pp-account-details.ui src/ui/chatty-settings-dialog.ui +src/ui/chatty-verification-view.ui src/ui/chatty-window.ui src/ui/help-overlay.ui src/users/chatty-account.c diff --git a/po/es.po b/po/es.po index c943c4089c98225493bdc93857c8ee8a5ade9bf6..2fc9d099046f8e2696e077cca798db7856205539 100644 --- a/po/es.po +++ b/po/es.po @@ -9,8 +9,8 @@ msgid "" msgstr "" "Project-Id-Version: purism-chatty\n" "Report-Msgid-Bugs-To: https://source.puri.sm/Librem5/chatty/issues\n" -"POT-Creation-Date: 2022-04-25 03:24+0000\n" -"PO-Revision-Date: 2022-04-25 17:14+0200\n" +"POT-Creation-Date: 2022-09-13 03:24+0000\n" +"PO-Revision-Date: 2022-09-13 10:10+0200\n" "Last-Translator: Daniel Mustieles GarcÃa <daniel.mustieles@gmail.com>\n" "Language-Team: Spanish - Spain <gnome-es-list@gnome.org>\n" "Language: es_ES\n" @@ -21,17 +21,14 @@ msgstr "" "Plural-Forms: nplurals=2; plural=(n != 1)\n" #: data/sm.puri.Chatty.desktop.in:3 data/sm.puri.Chatty.metainfo.xml.in:6 -#: src/chatty-application.c:333 src/ui/chatty-window.ui:226 +#: src/chatty-application.c:338 src/chatty-window.c:339 +#: src/ui/chatty-window.ui:265 msgid "Chats" -msgstr "Chats" - -#: data/sm.puri.Chatty.desktop.in:5 -msgid "sm.puri.Chatty" -msgstr "sm.puri.Chatty" +msgstr "Chatty" #: data/sm.puri.Chatty.desktop.in:6 msgid "SMS and XMPP chat application" -msgstr "Aplicación de mensajerÃa por SMS y XMPP" +msgstr "Una Aplicación de mensajerÃa" #: data/sm.puri.Chatty.desktop.in:7 msgid "XMPP;SMS;chat;jabber;messaging;modem" @@ -75,13 +72,11 @@ msgstr "Habilitar sincronización de archivo MAM del servidor" #: data/sm.puri.Chatty.gschema.xml:37 msgid "Enable purple" -msgstr "" +msgstr "Activar purple" #: data/sm.puri.Chatty.gschema.xml:38 src/ui/chatty-settings-dialog.ui:511 -#, fuzzy -#| msgid "Disable all accounts" msgid "Enable purple accounts" -msgstr "Desactivar todas las cuentas" +msgstr "Activar las cuentas de purple" #: data/sm.puri.Chatty.gschema.xml:43 msgid "Send typing notifications" @@ -105,31 +100,31 @@ msgstr "Reemplazar texto a emoticonos" #: data/sm.puri.Chatty.gschema.xml:62 msgid "Convert text matching emoticons as real emoticons" -msgstr "Reemplazar emoticonos textuales que coincidan" +msgstr "Reemplazar texto por Emoticonos reales" #: data/sm.puri.Chatty.gschema.xml:67 msgid "Enter key sends the message" -msgstr "La tecla intro envÃa el mensaje" +msgstr "La tecla Intro envÃa el mensaje" #: data/sm.puri.Chatty.gschema.xml:68 msgid "Whether pressing Enter key sends the message" -msgstr "Si presionar la tecla Intro enviá el mensaje" +msgstr "Si presionar la tecla Intro envÃa el mensaje" #: data/sm.puri.Chatty.gschema.xml:73 msgid "Request SMS delivery reports" -msgstr "" +msgstr "Solicitar entrega de envÃo de SMS" #: data/sm.puri.Chatty.gschema.xml:74 msgid "Whether to request delivery reports for outgoing SMS" -msgstr "" +msgstr "Ya sea para solicitar entregas de SMS salientes" #: data/sm.puri.Chatty.gschema.xml:79 msgid "Enable experimental features" -msgstr "" +msgstr "Habilitar funciones experimentales" #: data/sm.puri.Chatty.gschema.xml:80 msgid "Whether to enable experimental features" -msgstr "" +msgstr "Ya sea para habilitar funciones experimentales" #: data/sm.puri.Chatty.gschema.xml:85 msgid "Window maximized" @@ -153,33 +148,33 @@ msgstr "Una aplicación de mensajerÃa" #: data/sm.puri.Chatty.metainfo.xml.in:9 msgid "Chats is a messaging application supporting XMPP and SMS." -msgstr "Chats es una aplicación de mensajerÃa que soporte SMS y XMPP" +msgstr "Chatty es una aplicación de mensajerÃa que soporta SMS y XMPP" #: data/sm.puri.Chatty.metainfo.xml.in:16 msgid "Chats message window" -msgstr "Ventana de mensajes de Chats" +msgstr "Ventana de mensajes de Chatty" -#: src/chatty-application.c:76 +#: src/chatty-application.c:78 msgid "Show release version" msgstr "Mostrar versión de lanzamiento" -#: src/chatty-application.c:77 +#: src/chatty-application.c:79 msgid "Start in daemon mode" -msgstr "Iniciar en modo de daemon" +msgstr "Iniciar en modo demonio" -#: src/chatty-application.c:78 +#: src/chatty-application.c:80 msgid "Disable all accounts" msgstr "Desactivar todas las cuentas" -#: src/chatty-application.c:79 +#: src/chatty-application.c:81 msgid "Enable libpurple debug messages" msgstr "Activar mensajes de depuración de libpurple" -#: src/chatty-application.c:81 +#: src/chatty-application.c:83 msgid "Enable verbose libpurple debug messages" msgstr "Activar mensajes detallados de depuración de libpurple" -#: src/chatty-application.c:142 +#: src/chatty-application.c:144 #, c-format msgid "" "There was an error displaying help:\n" @@ -190,37 +185,29 @@ msgstr "" msgid "Send To" msgstr "Enviar a" -#: src/chatty-chat-list.c:152 -#, fuzzy -#| msgid "" -#| "Select an <b>Instant Message</b> contact with the \"+\" button in the " -#| "titlebar." +#: src/chatty-chat-list.c:175 msgid "Select a contact with the <b>“+â€</b> button in the titlebar." -msgstr "" -"Seleccionar un contacto de <b>Mensaje instantáneo</b> con el botón \"+\" en " -"la barra de tÃtulo." - -#: src/chatty-chat-list.c:156 -#, fuzzy -#| msgid "" -#| "For <b>Instant Messaging</b> add or activate an account in <i>" -#| "\"preferences\"</i>." +msgstr "Seleccionar un contacto con el botón <b>«+»</b> en la barra de tÃtulo." + +#: src/chatty-chat-list.c:179 msgid "Add instant messaging accounts in Preferences." -msgstr "" -"Para usar <b>MensajerÃa instantánea</b> agregar o activar una cuenta en <i>" -"\"Preferencias\"</i>." +msgstr "Agregar una cuenta de mensajerÃa instantánea en Preferencias." -#: src/chatty-chat-list.c:192 src/chatty-contact-list.c:282 +#: src/chatty-chat-list.c:215 src/chatty-contact-list.c:282 msgid "No Search Results" -msgstr "" +msgstr "Sin resultados de búsqueda" -#: src/chatty-chat-list.c:193 +#: src/chatty-chat-list.c:216 msgid "Try different search" -msgstr "" +msgstr "Intentar búsqueda diferente" + +#: src/chatty-chat-list.c:220 +msgid "No archived chats" +msgstr "No hay Conversaciones archivadas" -#: src/chatty-chat-list.c:196 +#: src/chatty-chat-list.c:222 msgid "Start Chatting" -msgstr "Empezar a chatear" +msgstr "Iniciar a Conversar" #: src/chatty-chat-view.c:212 msgid "This is an SMS conversation" @@ -236,18 +223,18 @@ msgstr "Esta es una conversación de mensajerÃa instantánea" #: src/chatty-chat-view.c:224 msgid "Your messages are encrypted" -msgstr "Sus mensajes están cifrados" +msgstr "Los mensajes están cifrados" #: src/chatty-chat-view.c:227 msgid "Your messages are not encrypted" -msgstr "Sus mensajes no están cifrados" +msgstr "Los mensajes no están cifrados" -#: src/chatty-chat-view.c:414 +#: src/chatty-chat-view.c:420 msgid "Select File..." msgstr "Seleccionar archivo..." -#: src/chatty-chat-view.c:417 src/purple/chatty-purple-request.c:175 -#: src/chatty-window.c:352 src/dialogs/chatty-pp-chat-info.c:90 +#: src/chatty-chat-view.c:423 src/purple/chatty-purple-request.c:175 +#: src/chatty-window.c:422 src/dialogs/chatty-pp-chat-info.c:90 #: src/dialogs/chatty-ma-account-details.c:119 #: src/dialogs/chatty-pp-account-details.c:91 #: src/ui/chatty-dialog-join-muc.ui:21 src/ui/chatty-info-dialog.ui:44 @@ -255,7 +242,7 @@ msgstr "Seleccionar archivo..." msgid "Cancel" msgstr "Cancelar" -#: src/chatty-chat-view.c:418 src/purple/chatty-purple-request.c:177 +#: src/chatty-chat-view.c:424 src/purple/chatty-purple-request.c:177 #: src/dialogs/chatty-pp-chat-info.c:89 #: src/dialogs/chatty-ma-account-details.c:118 #: src/dialogs/chatty-pp-account-details.c:90 @@ -289,7 +276,7 @@ msgstr "" #: src/chatty-contact-list.c:287 msgid "No Contacts" -msgstr "Ningún contacto" +msgstr "No hay contactos" #: src/chatty-clock.c:86 msgid "Just Now" @@ -299,8 +286,6 @@ msgstr "Justo ahora" #. * See https://docs.gtk.org/glib/method.DateTime.format.html #. #: src/chatty-clock.c:95 src/chatty-clock.c:125 -#, fuzzy -#| msgid "%I:%M %p" msgid "%I∶%M %p" msgstr "%I:%M %p" @@ -313,25 +298,25 @@ msgstr[1] "Hace %lu minutos" #: src/chatty-clock.c:112 msgid "Today %H∶%M" -msgstr "" +msgstr "Hoy %H∶%M" #. TRANSLATORS: Timestamp with 12 hour time, e.g. “Today 06∶42 PMâ€. #. * See https://docs.gtk.org/glib/method.DateTime.format.html #. #: src/chatty-clock.c:120 msgid "Today %I∶%M %p" -msgstr "" +msgstr "Hoy %I∶%M %p" #: src/chatty-clock.c:132 msgid "Yesterday %H∶%M" -msgstr "" +msgstr "Ayer %H∶%M" #. TRANSLATORS: Timestamp with 12 hour time, e.g. “Yesterday 06∶42 PMâ€. #. * See https://docs.gtk.org/glib/method.DateTime.format.html #. #: src/chatty-clock.c:137 msgid "Yesterday %I∶%M %p" -msgstr "" +msgstr "Ayer %I∶%M %p" #. TRANSLATORS: Timestamp from more than 7 days ago or future date #. * (eg: when the system time is wrong), e.g. “2022-01-01â€. @@ -345,11 +330,11 @@ msgstr "%d-%m-%Y" #: src/chatty-fp-row.c:131 #, c-format msgid "Device ID %s fingerprint:" -msgstr "" +msgstr "Id. dispositivo %s huella digital:" #: src/chatty-list-row.c:118 msgid "Owner" -msgstr "Dueño" +msgstr "Propietario" #: src/chatty-list-row.c:121 msgid "Moderator" @@ -359,15 +344,19 @@ msgstr "Moderador" msgid "Member" msgstr "Miembro" -#: src/chatty-manager.c:727 +#: src/chatty-manager.c:724 #, c-format msgid "“%s†is not a valid URI" -msgstr "" +msgstr "“%s†no es un URI válido" #: src/chatty-message-row.c:82 msgid "Copy" msgstr "Copiar" +#: src/chatty-message.c:233 +msgid "Got an encrypted message, but couldn't decrypt due to missing keys" +msgstr "Recibà un mensaje cifrado, pero no pude descifrarlo debido a falta de claves" + #: src/chatty-notification.c:181 msgid "Open Message" msgstr "Abrir mensaje" @@ -381,7 +370,7 @@ msgstr "Mensaje nuevo de %s" msgid "Message Received" msgstr "Mensaje recibido" -#: src/purple/chatty-purple-notify.c:44 src/matrix/matrix-utils.c:505 +#: src/purple/chatty-purple-notify.c:44 msgid "Close" msgstr "Cerrar" @@ -411,11 +400,11 @@ msgstr "Usuario %s ha agregado %s a los contactos" msgid "Authorize %s?" msgstr "¿Autorizar %s?" -#: src/purple/chatty-purple.c:264 src/matrix/matrix-utils.c:509 +#: src/purple/chatty-purple.c:264 msgid "Reject" msgstr "Rechazar" -#: src/purple/chatty-purple.c:265 src/matrix/matrix-utils.c:510 +#: src/purple/chatty-purple.c:265 msgid "Accept" msgstr "Aceptar" @@ -424,70 +413,69 @@ msgstr "Aceptar" msgid "Add %s to contact list" msgstr "Agregar %s a la lista de contactos" -#: src/purple/chatty-purple.c:587 +#: src/purple/chatty-purple.c:586 msgid "Login failed" msgstr "Ingreso ha fallado" -#: src/purple/chatty-purple.c:592 +#: src/purple/chatty-purple.c:591 msgid "Please check ID and password" msgstr "Por favor verifique la ID y contraseña" -#: src/chatty-secret-store.c:98 -#, c-format -msgid "Chatty password for \"%s\"" -msgstr "" - -#: src/chatty-window.c:338 +#: src/chatty-window.c:408 #, c-format msgid "Delete chat with “%sâ€" msgstr "Borrar chat con «%s»" -#: src/chatty-window.c:339 +#: src/chatty-window.c:409 msgid "This deletes the conversation history" msgstr "Este borra el historial de conversaciones" -#: src/chatty-window.c:341 +#: src/chatty-window.c:411 #, c-format msgid "Disconnect group chat “%sâ€" msgstr "Desconectar del chat en grupo «%s»" -#: src/chatty-window.c:342 +#: src/chatty-window.c:412 msgid "This removes chat from chats list" msgstr "Este quita el chat de la lista de chats" -#: src/chatty-window.c:354 +#: src/chatty-window.c:424 msgid "Delete" msgstr "Borrar" -#: src/chatty-window.c:433 +#: src/chatty-window.c:502 msgid "You shall no longer be notified for new messages, continue?" msgstr "" -#: src/chatty-window.c:529 +#: src/chatty-window.c:577 +msgid "Archived" +msgstr "Archivado" + +#: src/chatty-window.c:630 msgid "An SMS and XMPP messaging client" msgstr "Un cliente de mensajerÃa SMS y XMPP" -#: src/chatty-window.c:536 +#: src/chatty-window.c:637 msgid "translator-credits" msgstr "reconocimiento-de-traductores" -#: src/chatty-window.c:874 +#: src/chatty-window.c:989 msgid "Any Protocol" msgstr "Cualquier protocolo" -#: src/chatty-window.c:875 src/ui/chatty-settings-dialog.ui:627 +#: src/chatty-window.c:990 src/ui/chatty-settings-dialog.ui:627 msgid "Matrix" msgstr "Matrix" -#: src/chatty-window.c:876 +#: src/chatty-window.c:991 msgid "SMS/MMS" msgstr "SMS/MMS" -#: src/chatty-window.c:879 src/ui/chatty-settings-dialog.ui:615 +#: src/chatty-window.c:994 src/ui/chatty-settings-dialog.ui:615 msgid "XMPP" msgstr "XMPP" -#: src/chatty-window.c:882 src/ui/chatty-settings-dialog.ui:641 +#: src/chatty-window.c:997 src/ui/chatty-settings-dialog.ui:641 msgid "Telegram" msgstr "Telegram" @@ -525,44 +513,35 @@ msgid "Encryption not available" msgstr "Cifrado no disponible" #: src/dialogs/chatty-pp-chat-info.c:259 -#, fuzzy, c-format -#| msgid "Member" +#, c-format msgid "%u Member" msgid_plural "%u Members" -msgstr[0] "Miembro" -msgstr[1] "Miembro" +msgstr[0] "%u Miembro" +msgstr[1] "%u Miembros" #: src/dialogs/chatty-pp-chat-info.c:293 msgid "Phone Number:" msgstr "Número telefónico" #: src/dialogs/chatty-pp-chat-info.c:295 -#, fuzzy -#| msgid "XMPP ID" msgid "XMPP ID:" -msgstr "ID de XMPP" +msgstr "ID de XMPP:" #: src/dialogs/chatty-pp-chat-info.c:300 -#, fuzzy -#| msgid "Matrix" msgid "Matrix ID:" -msgstr "Matrix" +msgstr "ID de Matrix:" #: src/dialogs/chatty-pp-chat-info.c:303 -#, fuzzy -#| msgid "Telegram" msgid "Telegram ID:" -msgstr "Telegram" +msgstr "ID de Telegram:" #: src/dialogs/chatty-new-chat-dialog.c:171 -#, fuzzy -#| msgid "_Add" msgid "Add" -msgstr "_Añadir" +msgstr "Añadir" #: src/dialogs/chatty-new-chat-dialog.c:181 msgid "Create" -msgstr "" +msgstr "Crear" #: src/dialogs/chatty-new-chat-dialog.c:187 msgid "Add Contact" @@ -579,7 +558,7 @@ msgstr "SMS" #: src/dialogs/chatty-new-chat-dialog.c:640 msgid "You" -msgstr "" +msgstr "Usted" #: src/dialogs/chatty-ma-account-details.c:385 #: src/dialogs/chatty-pp-account-details.c:178 @@ -589,139 +568,107 @@ msgstr "conectado" #: src/dialogs/chatty-ma-account-details.c:387 #: src/dialogs/chatty-pp-account-details.c:180 msgid "connecting…" -msgstr "esta conectando..." +msgstr "está conectando..." #: src/dialogs/chatty-ma-account-details.c:389 #: src/dialogs/chatty-pp-account-details.c:182 msgid "disconnected" msgstr "desconectado" -#: src/dialogs/chatty-settings-dialog.c:255 -#: src/dialogs/chatty-settings-dialog.c:405 +#: src/dialogs/chatty-settings-dialog.c:251 +#, c-format +msgid "Failed to verify server: %s" +msgstr "Falló al verificar el servidor: %s" + +#: src/dialogs/chatty-settings-dialog.c:253 msgid "Failed to verify server" -msgstr "" +msgstr "Falló al verificar el servidor" -#: src/dialogs/chatty-settings-dialog.c:306 -#: src/dialogs/chatty-settings-dialog.c:385 +#: src/dialogs/chatty-settings-dialog.c:259 msgid "Couldn't get Home server address" msgstr "" -#: src/dialogs/chatty-settings-dialog.c:510 +#: src/dialogs/chatty-settings-dialog.c:430 #: src/ui/chatty-ma-account-details.ui:182 #: src/ui/chatty-pp-account-details.ui:199 msgid "Delete Account" -msgstr "Borrar cuenta" +msgstr "Eliminar cuenta" -#: src/dialogs/chatty-settings-dialog.c:513 +#: src/dialogs/chatty-settings-dialog.c:433 #, c-format msgid "Delete account %s?" -msgstr "¿Borra la cuenta de %s?" +msgstr "¿Eliminar la cuenta de %s?" -#: src/dialogs/chatty-settings-dialog.c:654 +#: src/dialogs/chatty-settings-dialog.c:584 msgid "Restart chatty to disable purple" msgstr "" -#: src/dialogs/chatty-settings-dialog.c:656 +#: src/dialogs/chatty-settings-dialog.c:586 #: src/ui/chatty-settings-dialog.ui:512 -#, fuzzy -#| msgid "Enable libpurple debug messages" msgid "Enable purple plugin" -msgstr "Activar mensajes de depuración de libpurple" +msgstr "Activar complemento purple" -#: src/dialogs/chatty-settings-dialog.c:670 +#: src/dialogs/chatty-settings-dialog.c:600 #: src/ui/chatty-settings-dialog.ui:279 msgid "SMS and MMS Settings" msgstr "" -#: src/dialogs/chatty-settings-dialog.c:672 +#: src/dialogs/chatty-settings-dialog.c:602 #: src/ui/chatty-settings-dialog.ui:298 -#, fuzzy -#| msgid "Open Account Settings" msgid "Purple Settings" -msgstr "Abrir configuración de cuenta" +msgstr "Configuración de purple" -#: src/dialogs/chatty-settings-dialog.c:674 -#, fuzzy -#| msgid "Accounts" +#: src/dialogs/chatty-settings-dialog.c:604 msgid "New Account" -msgstr "Cuentas" +msgstr "Nueva cuenta" -#: src/dialogs/chatty-settings-dialog.c:676 +#: src/dialogs/chatty-settings-dialog.c:606 #: src/ui/chatty-settings-dialog.ui:364 -#, fuzzy -#| msgid "Add Contact" msgid "Blocked Contacts" -msgstr "Agregar contacto" +msgstr "Contactos bloqueados" -#: src/dialogs/chatty-settings-dialog.c:678 src/ui/chatty-settings-dialog.ui:7 -#: src/ui/chatty-window.ui:17 +#: src/dialogs/chatty-settings-dialog.c:608 src/ui/chatty-settings-dialog.ui:7 +#: src/ui/chatty-window.ui:30 msgid "Preferences" msgstr "Preferencias" -#: src/dialogs/chatty-settings-dialog.c:707 +#: src/dialogs/chatty-settings-dialog.c:636 msgid "Select Protocol" msgstr "Seleccionar protocolo" #. TRANSLATORS: Only translate 'or' -#: src/dialogs/chatty-settings-dialog.c:948 +#: src/dialogs/chatty-settings-dialog.c:880 msgid "@user:matrix.org or user@example.com" msgstr "" -#: src/dialogs/chatty-settings-dialog.c:1057 +#: src/dialogs/chatty-settings-dialog.c:989 msgid "Unblock contact" msgstr "" -#: src/matrix/chatty-ma-account.c:296 +#: src/matrix/chatty-ma-account.c:106 msgid "Incorrect password" msgstr "" -#: src/matrix/chatty-ma-account.c:299 +#: src/matrix/chatty-ma-account.c:109 msgid "_OK" msgstr "" -#: src/matrix/chatty-ma-account.c:300 src/ui/chatty-dialog-new-chat.ui:74 +#: src/matrix/chatty-ma-account.c:110 src/ui/chatty-dialog-new-chat.ui:74 #: src/ui/chatty-settings-dialog.ui:47 src/ui/chatty-settings-dialog.ui:108 msgid "_Cancel" msgstr "_Cancelar" -#: src/matrix/chatty-ma-account.c:306 -#, c-format -msgid "Please enter password for “%sâ€" -msgstr "" - -#: src/matrix/matrix-utils.c:474 -#, c-format -msgid "The certificate for ‘%s’ has unknown CA" -msgstr "" - -#: src/matrix/matrix-utils.c:476 -#, c-format -msgid "The certificate for ‘%s’ is self-signed" -msgstr "" - -#: src/matrix/matrix-utils.c:480 +#: src/matrix/chatty-ma-account.c:117 #, c-format -msgid "The certificate for ‘%s’ has expired" -msgstr "" - -#: src/matrix/matrix-utils.c:484 -#, c-format -msgid "The certificate for ‘%s’ has been revoked" -msgstr "" - -#: src/matrix/matrix-utils.c:493 -#, c-format -msgid "Error validating certificate for ‘%s’" +msgid "Please enter password for “%sâ€, homeserver: %s" msgstr "" #. TRANSLATORS: Timestamp for minute accuracy, e.g. “2020-08-11 15:27â€. #. See https://developer.gnome.org/glib/stable/glib-GDateTime.html#g-date-time-format #. #: src/mm/chatty-mmsd.c:827 -#, fuzzy -#| msgid "%Y-%m-%d" msgid "%Y-%m-%d %H∶%M" -msgstr "%d-%m-%Y" +msgstr "%H:%M %d-%m-%Y" #: src/mm/chatty-mmsd.c:1111 #, c-format @@ -749,16 +696,12 @@ msgid "Password (optional)" msgstr "Contraseña (opcional)" #: src/ui/chatty-dialog-new-chat.ui:126 -#, fuzzy -#| msgid "Group Details" msgid "Group Title" -msgstr "Detalles del grupo" +msgstr "TÃtulo del grupo" #: src/ui/chatty-dialog-new-chat.ui:177 -#, fuzzy -#| msgid "Add %s to contact list" msgid "Add members from contacts…" -msgstr "Agregar %s a la lista de contactos" +msgstr "Agregar miembros desde la lista de contactos" #: src/ui/chatty-dialog-new-chat.ui:223 msgid "Send To:" @@ -776,7 +719,7 @@ msgstr "Añadir a Contactos" msgid "Remove Attachment" msgstr "" -#: src/ui/chatty-info-dialog.ui:17 src/ui/chatty-window.ui:126 +#: src/ui/chatty-info-dialog.ui:17 src/ui/chatty-window.ui:139 msgid "Chat Details" msgstr "Detalles del chat" @@ -793,10 +736,8 @@ msgid "Matrix ID" msgstr "ID de Matrix" #: src/ui/chatty-ma-chat-info.ui:93 src/ui/chatty-pp-chat-info.ui:289 -#, fuzzy -#| msgid "Chat Details" msgid "Chat settings" -msgstr "Detalles del chat" +msgstr "Configuración del chat" #: src/ui/chatty-ma-chat-info.ui:113 src/ui/chatty-pp-chat-info.ui:238 #: src/ui/chatty-pp-chat-info.ui:344 @@ -846,19 +787,17 @@ msgstr "Huellas digitales" #: src/ui/chatty-mm-chat-info.ui:29 msgid "Title" -msgstr "" +msgstr "TÃtulo" #: src/ui/chatty-mm-chat-info.ui:33 msgid "Blank for default" msgstr "" #: src/ui/chatty-mm-chat-info.ui:43 -#, fuzzy -#| msgid "0 members" msgid "Group Members" -msgstr "0 miembros" +msgstr "Miembros del grupo" -#: src/ui/chatty-list-row.ui:151 src/ui/chatty-window.ui:379 +#: src/ui/chatty-list-row.ui:151 src/ui/chatty-window.ui:433 msgid "Call" msgstr "" @@ -916,7 +855,7 @@ msgstr "_Guardar" #: src/ui/chatty-settings-dialog.ui:92 msgid "_Apply" -msgstr "" +msgstr "_Aplicar" #: src/ui/chatty-settings-dialog.ui:169 msgid "Accounts" @@ -956,16 +895,12 @@ msgstr "" #. TRANSLATORS: Return is the Enter key. #: src/ui/chatty-settings-dialog.ui:251 -#, fuzzy -#| msgid "Send message with return key" msgid "Send Messages with Return" msgstr "Manda mensaje con la tecla Entrar" #: src/ui/chatty-settings-dialog.ui:267 -#, fuzzy -#| msgid "Room settings" msgid "Protocol Settings" -msgstr "Configuración de sala" +msgstr "Configuración de protocolo" #: src/ui/chatty-settings-dialog.ui:333 msgid "Request Delivery Reports" @@ -1019,71 +954,78 @@ msgstr "" msgid "Add _new account…" msgstr "Agregar una cuenta nueva…" -#: src/ui/chatty-window.ui:28 +#: src/ui/chatty-window.ui:17 +msgctxt "show archived chat list when clicked" +msgid "Archived" +msgstr "Archivado" + +#: src/ui/chatty-window.ui:41 msgid "Keyboard _Shortcuts" msgstr "" -#: src/ui/chatty-window.ui:35 +#: src/ui/chatty-window.ui:48 msgid "Help" msgstr "Ayuda" -#: src/ui/chatty-window.ui:44 +#: src/ui/chatty-window.ui:57 msgid "About Chats" msgstr "Acerca de Chats" -#: src/ui/chatty-window.ui:72 +#: src/ui/chatty-window.ui:85 msgid "New Message…" msgstr "Mensaje nuevo…" -#: src/ui/chatty-window.ui:85 +#: src/ui/chatty-window.ui:98 msgid "New SMS/MMS Message…" msgstr "Nuevo SMS/MMS…" -#: src/ui/chatty-window.ui:98 +#: src/ui/chatty-window.ui:111 msgid "New Group Message…" msgstr "Mensaje nuevo en grupo…" -#: src/ui/chatty-window.ui:151 +#: src/ui/chatty-window.ui:164 msgid "Leave Chat" msgstr "Salir del chat" -#: src/ui/chatty-window.ui:164 +#: src/ui/chatty-window.ui:177 msgid "Block Contact" msgstr "Bloquear contacto" -#: src/ui/chatty-window.ui:177 -#, fuzzy -#| msgid "Invite Contact" +#: src/ui/chatty-window.ui:190 msgid "Unblock Contact" -msgstr "Invitar a contactarse" +msgstr "Desbloquear contacto" -#: src/ui/chatty-window.ui:190 +#: src/ui/chatty-window.ui:203 +msgid "Archive chat" +msgstr "Achivar conversación" + +#: src/ui/chatty-window.ui:216 +msgid "Unarchive chat" +msgstr "Desarchivar conversación" + +#: src/ui/chatty-window.ui:229 msgid "Delete Chat" msgstr "Eliminar chat" #: src/ui/help-overlay.ui:14 msgctxt "shortcut window" msgid "General" -msgstr "" +msgstr "General" #: src/ui/help-overlay.ui:19 -#, fuzzy -#| msgid "Open Message" msgctxt "shortcut window" msgid "Open Menu" -msgstr "Abrir mensaje" +msgstr "Abrir menú" #: src/ui/help-overlay.ui:27 -#, fuzzy -#| msgid "Open Message" msgctxt "shortcut window" msgid "Open Search" -msgstr "Abrir mensaje" +msgstr "Abrir búsqueda" #: src/ui/help-overlay.ui:35 msgctxt "shortcut window" msgid "Show Shortcuts" -msgstr "" +msgstr "Mostrar atajos de teclado" #: src/users/chatty-contact.c:325 msgid "Mobile: " @@ -1097,6 +1039,9 @@ msgstr "Trabajo: " msgid "Other: " msgstr "Otro: " +#~ msgid "sm.puri.Chatty" +#~ msgstr "sm.puri.Chatty" + #~ msgid "Mark offline users differently" #~ msgstr "Marcar a los usuarios desconectados de manera diferente" diff --git a/po/he.po b/po/he.po new file mode 100644 index 0000000000000000000000000000000000000000..0964e657752aab8b24fbab3432c153aee6fb2fd5 --- /dev/null +++ b/po/he.po @@ -0,0 +1,1216 @@ +# Hebrew translation for chatty. +# Copyright (C) 2021 chatty's COPYRIGHT HOLDER +# This file is distributed under the same license as the chatty package. +# Yaron Shahrabani <sh.yaron@gmail.com>, 2021. +# Yosef Or Boczko <yoseforb@gmail.com>, 2022. +# +msgid "" +msgstr "" +"Project-Id-Version: chatty master\n" +"Report-Msgid-Bugs-To: https://source.puri.sm/Librem5/chatty/issues\n" +"POT-Creation-Date: 2022-09-08 03:24+0000\n" +"PO-Revision-Date: 2022-09-11 07:37+0300\n" +"Last-Translator: Yosef Or Boczko <yoseforb@gmail.com>\n" +"Language-Team: Hebrew <>\n" +"Language: he\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=4; plural=(n==1 ? 0 : n==2 ? 1 : n>10 && n%10==0 ? " +"2 : 3)\n" +"X-Generator: Gtranslator 40.0\n" + +#: data/sm.puri.Chatty.desktop.in:3 data/sm.puri.Chatty.metainfo.xml.in:6 +#: src/chatty-application.c:338 src/chatty-window.c:339 +#: src/ui/chatty-window.ui:265 +msgid "Chats" +msgstr "התכתבויות" + +#: data/sm.puri.Chatty.desktop.in:6 +msgid "SMS and XMPP chat application" +msgstr "×™×™×©×•× ×”×ª×›×ª×‘×•×ª ×‘×ž×¡×¨×•× ×™× ×•×‘Ö¾XMPP" + +#: data/sm.puri.Chatty.desktop.in:7 +msgid "XMPP;SMS;chat;jabber;messaging;modem" +msgstr "‫XMPP;מסרון;×ž×¡×¨×•× ×™×;×ס ×× ×ס;SMS;התכתבות;צ׳×ט;שיח;מוד×;ג׳×בר" + +#: data/sm.puri.Chatty.gschema.xml:7 data/sm.puri.Chatty.gschema.xml:8 +msgid "Whether the application is launching the first time" +msgstr "×”×× ×–×ת ×”×˜×¢×™× ×” הר××©×•× ×” של היישומון" + +#: data/sm.puri.Chatty.gschema.xml:13 +msgid "Country code in ISO 3166-1 alpha-2 format" +msgstr "קוד ×ž×“×™× ×” בתצורת ISO 3166-1 alpha-2" + +#: data/sm.puri.Chatty.gschema.xml:14 +msgid "Two letter country code of the last available SIM/Network" +msgstr "קוד ×ž×“×™× ×” בשתי ספרות של הרשת/SIM ×”××—×¨×•× ×™× ×”×–×ž×™× ×™×" + +#: data/sm.puri.Chatty.gschema.xml:19 +msgid "Send message read receipts" +msgstr "לשלוח קבלות קרי××” על הודעות" + +#: data/sm.puri.Chatty.gschema.xml:20 +msgid "Whether to send the status of the message if read" +msgstr "×”×× ×œ×©×œ×•×— ×ת מצב קרי×ת ההודעה" + +#: data/sm.puri.Chatty.gschema.xml:25 +msgid "Message carbon copies" +msgstr "×¢×•×ª×§×™× ×©×œ הודעות" + +#: data/sm.puri.Chatty.gschema.xml:26 src/ui/chatty-settings-dialog.ui:554 +msgid "Share chat history among devices" +msgstr "שיתוף היסטוריית ההתכתבות בין מכשירי×" + +#: data/sm.puri.Chatty.gschema.xml:31 +msgid "Enable Message Archive Management" +msgstr "ל×פשר × ×™×”×•×œ ×רכיון הודעות" + +#: data/sm.puri.Chatty.gschema.xml:32 +msgid "Enable MAM archive synchronization from the server" +msgstr "ל×פשר ×¡× ×›×¨×•×Ÿ ×רכיון MAM מהשרת" + +#: data/sm.puri.Chatty.gschema.xml:37 +msgid "Enable purple" +msgstr "ל×פשר purple" + +#: data/sm.puri.Chatty.gschema.xml:38 src/ui/chatty-settings-dialog.ui:511 +msgid "Enable purple accounts" +msgstr "ל×פשר ×—×©×‘×•× ×•×ª purple" + +#: data/sm.puri.Chatty.gschema.xml:43 +msgid "Send typing notifications" +msgstr "לשלוח התר×ות הקלדה" + +#: data/sm.puri.Chatty.gschema.xml:44 +msgid "Whether to Send typing notifications" +msgstr "×”×× ×œ×©×œ×•×— התר×ות הקלדה" + +#: data/sm.puri.Chatty.gschema.xml:49 data/sm.puri.Chatty.gschema.xml:50 +msgid "Mark Idle users differently" +msgstr "לסמן ×ž×©×ª×ž×©×™× ×‘×œ×ª×™ ×¤×¢×™×œ×™× ×חרת" + +#: data/sm.puri.Chatty.gschema.xml:55 data/sm.puri.Chatty.gschema.xml:56 +msgid "Indicate unknown contacts" +msgstr "לציין ×× ×©×™ קשר בלתי ידועי×" + +#: data/sm.puri.Chatty.gschema.xml:61 +msgid "Convert text to emoticons" +msgstr "להמיר טקסט ×œ×¨×’×©×•× ×™×" + +#: data/sm.puri.Chatty.gschema.xml:62 +msgid "Convert text matching emoticons as real emoticons" +msgstr "להמיר טקסט שמחקה ×¨×’×©×•× ×™× ×œ×¨×’×©×•× ×™× ×מתיי×" + +#: data/sm.puri.Chatty.gschema.xml:67 +msgid "Enter key sends the message" +msgstr "‫Enter שולח ×ת ההודעה" + +#: data/sm.puri.Chatty.gschema.xml:68 +msgid "Whether pressing Enter key sends the message" +msgstr "×”×× ×œ×—×™×¦×” על Enter תשלח ×ת ההודעה" + +#: data/sm.puri.Chatty.gschema.xml:73 +msgid "Request SMS delivery reports" +msgstr "דרישת דיווחי מסירת הודעות SMS" + +#: data/sm.puri.Chatty.gschema.xml:74 +msgid "Whether to request delivery reports for outgoing SMS" +msgstr "×”×× ×œ×“×¨×•×© דיווח על שליחה של הודעות SMS יוצ×ות" + +#: data/sm.puri.Chatty.gschema.xml:79 +msgid "Enable experimental features" +msgstr "להפעיל ×ª×›×•× ×•×ª × ×™×¡×™×•× ×™×•×ª" + +#: data/sm.puri.Chatty.gschema.xml:80 +msgid "Whether to enable experimental features" +msgstr "×”×× ×œ×”×¤×¢×™×œ ×ª×›×•× ×•×ª × ×™×¡×™×•× ×™×•×ª" + +#: data/sm.puri.Chatty.gschema.xml:85 +msgid "Window maximized" +msgstr "החלון מוגדל לחלוטין" + +#: data/sm.puri.Chatty.gschema.xml:86 +msgid "Window maximized state" +msgstr "מצב הגדלת חלון" + +#: data/sm.puri.Chatty.gschema.xml:91 +msgid "Window size" +msgstr "גודל חלון" + +#: data/sm.puri.Chatty.gschema.xml:92 +msgid "Window size (width, height)." +msgstr "גודל חלון (רוחב, גובה)." + +#: data/sm.puri.Chatty.metainfo.xml.in:7 +msgid "A messaging application" +msgstr "יישומון התכתבות" + +#: data/sm.puri.Chatty.metainfo.xml.in:9 +msgid "Chats is a messaging application supporting XMPP and SMS." +msgstr "התכתבויות ×”×•× ×™×™×©×•×ž×•×Ÿ התכתבות שתומך ב־XMPP ×•×‘×ž×¡×¨×•× ×™×." + +#: data/sm.puri.Chatty.metainfo.xml.in:16 +msgid "Chats message window" +msgstr "חלון ההודעות של התכתבויות" + +#: src/chatty-application.c:78 +msgid "Show release version" +msgstr "להציג גרסה" + +#: src/chatty-application.c:79 +msgid "Start in daemon mode" +msgstr "לעלות במצב רקע" + +#: src/chatty-application.c:80 +msgid "Disable all accounts" +msgstr "להשבית ×ת כל ×”×—×©×‘×•× ×•×ª" + +#: src/chatty-application.c:81 +msgid "Enable libpurple debug messages" +msgstr "ל×פשר הודעות × ×™×¤×•×™ שגי×ות של libpurple" + +#: src/chatty-application.c:83 +msgid "Enable verbose libpurple debug messages" +msgstr "להפעיל הודעות × ×™×¤×•×™ שגי×ות מפורטות של libpurple" + +#: src/chatty-application.c:144 +#, c-format +msgid "" +"There was an error displaying help:\n" +"%s" +msgstr "" +"×ירעה שגי××” בהצגת העזרה:\n" +"%s" + +#: src/chatty-avatar.c:158 src/chatty-contact-list.c:391 +msgid "Send To" +msgstr "לשלוח ×ל" + +#: src/chatty-chat-list.c:175 +msgid "Select a contact with the <b>“+â€</b> button in the titlebar." +msgstr "× × ×œ×‘×—×•×¨ ב×יש קשר ×¢× ×”×›×¤×ª×•×¨ <b>â€+„</b> שבשורת הכותרת." + +#: src/chatty-chat-list.c:179 +msgid "Add instant messaging accounts in Preferences." +msgstr "הוספת ×—×©×‘×•× ×•×ª הודעות מידיות בהעדפות." + +#: src/chatty-chat-list.c:215 src/chatty-contact-list.c:282 +msgid "No Search Results" +msgstr "×ין תוצ×ות חיפוש" + +#: src/chatty-chat-list.c:216 +msgid "Try different search" +msgstr "יש ×œ× ×¡×•×ª חיפוש ×©×•× ×”" + +#: src/chatty-chat-list.c:220 +msgid "No archived chats" +msgstr "×ין שיחות מ×ורכבות" + +#: src/chatty-chat-list.c:222 +msgid "Start Chatting" +msgstr "התחלת התכתבות" + +#: src/chatty-chat-view.c:212 +msgid "This is an SMS conversation" +msgstr "זו התכתבות ×‘×ž×¡×¨×•× ×™× (התמסררות)" + +#: src/chatty-chat-view.c:214 src/chatty-chat-view.c:220 +msgid "Your messages are not encrypted, and carrier rates may apply" +msgstr "ההודעות שלך ××™× ×Ÿ ×ž×•×¦×¤× ×•×ª, ×•×™×ª×›× ×• ×—×™×•×‘×™× ×ž×”×¡×¤×§" + +#: src/chatty-chat-view.c:218 +msgid "This is an IM conversation" +msgstr "×–×ת שיחה בהודעות פרטיות" + +#: src/chatty-chat-view.c:224 +msgid "Your messages are encrypted" +msgstr "ההודעות שלך ×ž×•×¦×¤× ×•×ª" + +#: src/chatty-chat-view.c:227 +msgid "Your messages are not encrypted" +msgstr "ההודעות שלך ××™× ×Ÿ ×ž×•×¦×¤× ×•×ª" + +#: src/chatty-chat-view.c:420 +msgid "Select File..." +msgstr "בחירת קובץ…" + +#: src/chatty-chat-view.c:423 src/purple/chatty-purple-request.c:175 +#: src/chatty-window.c:422 src/dialogs/chatty-pp-chat-info.c:90 +#: src/dialogs/chatty-ma-account-details.c:119 +#: src/dialogs/chatty-pp-account-details.c:91 +#: src/ui/chatty-dialog-join-muc.ui:21 src/ui/chatty-info-dialog.ui:44 +#: src/ui/chatty-info-dialog.ui:54 +msgid "Cancel" +msgstr "ביטול" + +#: src/chatty-chat-view.c:424 src/purple/chatty-purple-request.c:177 +#: src/dialogs/chatty-pp-chat-info.c:89 +#: src/dialogs/chatty-ma-account-details.c:118 +#: src/dialogs/chatty-pp-account-details.c:90 +msgid "Open" +msgstr "פתיחה" + +#: src/chatty-chat.c:635 +msgid "Empty room" +msgstr "חדר ריק" + +#. TRANSLATORS: %s are name/user-id/phone numbers of two users +#: src/chatty-chat.c:642 +#, c-format +msgid "%s and %s" +msgstr "‫%s ×•×’× %s" + +#: src/chatty-chat.c:644 +#, c-format +msgid "%s and %u other" +msgid_plural "%s and %u others" +msgstr[0] "‫%s ועוד ×חד" +msgstr[1] "‫%s ועוד ×©× ×™×™×" +msgstr[2] "‫%s ועוד %u × ×•×¡×¤×™×" +msgstr[3] "‫%s ועוד %u × ×•×¡×¤×™×" + +#: src/chatty-contact-list.c:247 src/dialogs/chatty-mm-chat-info.c:121 +msgid "Unknown Contact" +msgstr "×יש קשר ×œ× ×™×“×•×¢" + +#: src/chatty-contact-list.c:283 +msgid "Try different search, or type a valid number to create new chat" +msgstr "כד××™ ×œ× ×¡×•×ª חיפוש ×חר ×ו להקליד מספר טלפון ×ª×§× ×™ כדי ליצור התכתבות חדשה" + +#: src/chatty-contact-list.c:287 +msgid "No Contacts" +msgstr "×ין ×× ×©×™ קשר" + +#: src/chatty-clock.c:86 +msgid "Just Now" +msgstr "ממש עכשיו" + +#. TRANSLATORS: Timestamp with 12 hour time, e.g. “06∶42 PMâ€. +#. * See https://docs.gtk.org/glib/method.DateTime.format.html +#. +#: src/chatty-clock.c:95 src/chatty-clock.c:125 +msgid "%I∶%M %p" +msgstr "%I:%M %p" + +#: src/chatty-clock.c:104 +#, c-format +msgid "%lu minute ago" +msgid_plural "%lu minutes ago" +msgstr[0] "×œ×¤× ×™ דקה" +msgstr[1] "×œ×¤× ×™ %lu דקות" +msgstr[2] "×œ×¤× ×™ %lu דקות" +msgstr[3] "×œ×¤× ×™ %lu דקות" + +#: src/chatty-clock.c:112 +msgid "Today %H∶%M" +msgstr "היו×, ‎%H∶%M" + +#. TRANSLATORS: Timestamp with 12 hour time, e.g. “Today 06∶42 PMâ€. +#. * See https://docs.gtk.org/glib/method.DateTime.format.html +#. +#: src/chatty-clock.c:120 +msgid "Today %I∶%M %p" +msgstr "היו×, ‎%H∶%M â€%p" + +#: src/chatty-clock.c:132 +msgid "Yesterday %H∶%M" +msgstr "×תמול, ‎%H∶%M" + +#. TRANSLATORS: Timestamp with 12 hour time, e.g. “Yesterday 06∶42 PMâ€. +#. * See https://docs.gtk.org/glib/method.DateTime.format.html +#. +#: src/chatty-clock.c:137 +msgid "Yesterday %I∶%M %p" +msgstr "×תמול, ‎%H∶%M â€%p" + +#. TRANSLATORS: Timestamp from more than 7 days ago or future date +#. * (eg: when the system time is wrong), e.g. “2022-01-01â€. +#. * See https://docs.gtk.org/glib/method.DateTime.format.html +#. +#: src/chatty-clock.c:153 +msgid "%Y-%m-%d" +msgstr "%Y-%m-%d" + +#. TRANSLATORS: %s is the Device ID +#: src/chatty-fp-row.c:131 +#, c-format +msgid "Device ID %s fingerprint:" +msgstr "טביעת ×צבע של מזהה מכשיר %s:" + +#: src/chatty-list-row.c:118 +msgid "Owner" +msgstr "בעלי×" + +#: src/chatty-list-row.c:121 +msgid "Moderator" +msgstr "מפקח" + +#: src/chatty-list-row.c:124 +msgid "Member" +msgstr "חבר" + +#: src/chatty-manager.c:724 +#, c-format +msgid "“%s†is not a valid URI" +msgstr "הכתובת „%s†××™× ×” ×ª×§×™× ×”" + +#: src/chatty-message-row.c:82 +msgid "Copy" +msgstr "העתקה" + +#: src/chatty-message.c:233 +msgid "Got an encrypted message, but couldn't decrypt due to missing keys" +msgstr "התקבלה הודעה ×ž×•×¦×¤× ×ª, ×ך ×œ× × ×™×ª×Ÿ ×œ×¤×¢× ×— ×ותה עקב מחסור במפתחות" + +#: src/chatty-notification.c:181 +msgid "Open Message" +msgstr "לפתוח הודעה" + +#: src/chatty-notification.c:214 +#, c-format +msgid "New message from %s" +msgstr "הודעה חדשה מ×ת %s" + +#: src/chatty-notification.c:216 +msgid "Message Received" +msgstr "התקבלה הודעה" + +#: src/purple/chatty-purple-notify.c:44 +msgid "Close" +msgstr "סגירה" + +#: src/purple/chatty-purple-request.c:170 +msgid "Save File..." +msgstr "שמירת קובץ…" + +#: src/purple/chatty-purple-request.c:171 +msgid "Open File..." +msgstr "פתיחת קובץ…" + +#: src/purple/chatty-purple-request.c:177 +msgid "Save" +msgstr "שמירה" + +#: src/purple/chatty-purple.c:240 +msgid "Contact added" +msgstr "×יש הקשר × ×•×¡×£" + +#: src/purple/chatty-purple.c:242 +#, c-format +msgid "User %s has added %s to the contacts" +msgstr "המשתמש %s הוסיף ×ת %s ל×× ×©×™ קשר" + +#: src/purple/chatty-purple.c:262 +#, c-format +msgid "Authorize %s?" +msgstr "ל×שר ×ת %s?" + +#: src/purple/chatty-purple.c:264 +msgid "Reject" +msgstr "לדחות" + +#: src/purple/chatty-purple.c:265 +msgid "Accept" +msgstr "לקבל" + +#: src/purple/chatty-purple.c:268 +#, c-format +msgid "Add %s to contact list" +msgstr "להוסיף ×ת %s לרשימת ×× ×©×™ הקשר" + +#: src/purple/chatty-purple.c:586 +msgid "Login failed" +msgstr "×”×›× ×™×¡×” × ×›×©×œ×”" + +#: src/purple/chatty-purple.c:591 +msgid "Please check ID and password" +msgstr "× × ×œ×‘×“×•×§ מזהה וססמה" + +#: src/chatty-window.c:408 +#, c-format +msgid "Delete chat with “%sâ€" +msgstr "מחיקת ההתכתבות ×¢× â€ž%sâ€" + +#: src/chatty-window.c:409 +msgid "This deletes the conversation history" +msgstr "פעולה זו מוחקת ×ת היסטוריית ההתכתבות" + +#: src/chatty-window.c:411 +#, c-format +msgid "Disconnect group chat “%sâ€" +msgstr "×”×ª× ×ª×§×•×ª מהתכתבות קבוצתית „%sâ€" + +#: src/chatty-window.c:412 +msgid "This removes chat from chats list" +msgstr "פעולה זו מסירה ×ת ההתכתבות מרשימת ההתכתבויות" + +#: src/chatty-window.c:424 +msgid "Delete" +msgstr "מחיקה" + +#: src/chatty-window.c:502 +msgid "You shall no longer be notified for new messages, continue?" +msgstr "פעולה זו תשבית ×ת ההתר×ות על הודעות חדשות, להמשיך?" + +#: src/chatty-window.c:577 +msgid "Archived" +msgstr "×רכיון" + +#: src/chatty-window.c:630 +msgid "An SMS and XMPP messaging client" +msgstr "לקוח התכתבות ×‘×ž×¡×¨×•× ×™× ×•×‘Ö¾XMPP" + +#: src/chatty-window.c:637 +msgid "translator-credits" +msgstr "" +"ירון ×©×”×¨×‘× ×™ <sh.yaron@gmail.com>\n" +"יוסף ×ור בוצ׳קו <yoseforb@gmail.com>" + +#: src/chatty-window.c:989 +msgid "Any Protocol" +msgstr "כל פרוטוקול" + +#: src/chatty-window.c:990 src/ui/chatty-settings-dialog.ui:627 +msgid "Matrix" +msgstr "‫Matrix" + +#: src/chatty-window.c:991 +msgid "SMS/MMS" +msgstr "SMS/MMS" + +#: src/chatty-window.c:994 src/ui/chatty-settings-dialog.ui:615 +msgid "XMPP" +msgstr "‫XMPP" + +#: src/chatty-window.c:997 src/ui/chatty-settings-dialog.ui:641 +msgid "Telegram" +msgstr "טלגר×" + +#: src/dialogs/chatty-mm-chat-info.c:140 +msgctxt "Refer to self in contact list" +msgid "You" +msgstr "×× ×™" + +#: src/dialogs/chatty-pp-chat-info.c:86 +#: src/dialogs/chatty-ma-account-details.c:115 +#: src/dialogs/chatty-pp-account-details.c:87 +msgid "Set Avatar" +msgstr "הגדרת ×ª×ž×•× ×” ייצוגית" + +#: src/dialogs/chatty-pp-chat-info.c:96 +#: src/dialogs/chatty-ma-account-details.c:125 +#: src/dialogs/chatty-pp-account-details.c:97 +msgid "Images" +msgstr "×ª×ž×•× ×•×ª" + +#: src/dialogs/chatty-pp-chat-info.c:206 +msgid "Encryption is not available" +msgstr "×ין ×”×¦×¤× ×” ×–×ž×™× ×”" + +#: src/dialogs/chatty-pp-chat-info.c:210 +msgid "This chat is encrypted" +msgstr "התכתבות זו ×ž×•×¦×¤× ×ª" + +#: src/dialogs/chatty-pp-chat-info.c:214 +msgid "This chat is not encrypted" +msgstr "התכתבות זו ××™× ×” ×ž×•×¦×¤× ×ª" + +#: src/dialogs/chatty-pp-chat-info.c:238 +msgid "Encryption not available" +msgstr "×”×¦×¤× ×” ××™× ×” ×–×ž×™× ×”" + +#: src/dialogs/chatty-pp-chat-info.c:259 +#, c-format +msgid "%u Member" +msgid_plural "%u Members" +msgstr[0] "חבר ×חד" +msgstr[1] "×©× ×™ חברי×" +msgstr[2] "‫%u חברי×" +msgstr[3] "‫%u חברי×" + +#: src/dialogs/chatty-pp-chat-info.c:293 +msgid "Phone Number:" +msgstr "מספר טלפון:" + +#: src/dialogs/chatty-pp-chat-info.c:295 +msgid "XMPP ID:" +msgstr "מזהה XMPP:" + +#: src/dialogs/chatty-pp-chat-info.c:300 +msgid "Matrix ID:" +msgstr "מזהה Matrix:" + +#: src/dialogs/chatty-pp-chat-info.c:303 +msgid "Telegram ID:" +msgstr "מזהה טלגר×:" + +#: src/dialogs/chatty-new-chat-dialog.c:171 +msgid "Add" +msgstr "הוספה" + +#: src/dialogs/chatty-new-chat-dialog.c:181 +msgid "Create" +msgstr "יצירה" + +#: src/dialogs/chatty-new-chat-dialog.c:187 +msgid "Add Contact" +msgstr "הוספת ×יש קשר" + +#: src/dialogs/chatty-new-chat-dialog.c:224 +#, c-format +msgid "Error opening GNOME Contacts: %s" +msgstr "שגי××” בפתיחת ×× ×©×™ הקשר של GNOME:†%s" + +#: src/dialogs/chatty-new-chat-dialog.c:452 +msgid "SMS" +msgstr "SMS" + +#: src/dialogs/chatty-new-chat-dialog.c:640 +msgid "You" +msgstr "×× ×™" + +#: src/dialogs/chatty-ma-account-details.c:385 +#: src/dialogs/chatty-pp-account-details.c:178 +msgid "connected" +msgstr "מחובר" + +#: src/dialogs/chatty-ma-account-details.c:387 +#: src/dialogs/chatty-pp-account-details.c:180 +msgid "connecting…" +msgstr "מתחבר…" + +#: src/dialogs/chatty-ma-account-details.c:389 +#: src/dialogs/chatty-pp-account-details.c:182 +msgid "disconnected" +msgstr "×ž× ×•×ª×§" + +#: src/dialogs/chatty-settings-dialog.c:251 +#, c-format +msgid "Failed to verify server: %s" +msgstr "×רע כשל ×ימות השרת: %s" + +#: src/dialogs/chatty-settings-dialog.c:253 +msgid "Failed to verify server" +msgstr "×רע כשל ×ימות השרת" + +#: src/dialogs/chatty-settings-dialog.c:259 +msgid "Couldn't get Home server address" +msgstr "×œ× × ×™×ª×Ÿ לקבל ×ת כתובת השרת הביתי" + +#: src/dialogs/chatty-settings-dialog.c:428 +#: src/ui/chatty-ma-account-details.ui:182 +#: src/ui/chatty-pp-account-details.ui:199 +msgid "Delete Account" +msgstr "מחיקת חשבון" + +#: src/dialogs/chatty-settings-dialog.c:431 +#, c-format +msgid "Delete account %s?" +msgstr "למחוק ×ת החשבון %s?" + +#: src/dialogs/chatty-settings-dialog.c:582 +msgid "Restart chatty to disable purple" +msgstr "הפעלה מחדש של Chatty להשבתת Purple" + +#: src/dialogs/chatty-settings-dialog.c:584 +#: src/ui/chatty-settings-dialog.ui:512 +msgid "Enable purple plugin" +msgstr "ל×פשר תוסף purple" + +#: src/dialogs/chatty-settings-dialog.c:598 +#: src/ui/chatty-settings-dialog.ui:279 +msgid "SMS and MMS Settings" +msgstr "הגדרות SMS ו־MMS" + +#: src/dialogs/chatty-settings-dialog.c:600 +#: src/ui/chatty-settings-dialog.ui:298 +msgid "Purple Settings" +msgstr "הגדרות Purple" + +#: src/dialogs/chatty-settings-dialog.c:602 +msgid "New Account" +msgstr "חשבון חדש" + +#: src/dialogs/chatty-settings-dialog.c:604 +#: src/ui/chatty-settings-dialog.ui:364 +msgid "Blocked Contacts" +msgstr "×× ×©×™ קשר חסומי×" + +#: src/dialogs/chatty-settings-dialog.c:606 src/ui/chatty-settings-dialog.ui:7 +#: src/ui/chatty-window.ui:30 +msgid "Preferences" +msgstr "העדפות" + +#: src/dialogs/chatty-settings-dialog.c:634 +msgid "Select Protocol" +msgstr "בחירת פרוטוקול" + +#. TRANSLATORS: Only translate 'or' +#: src/dialogs/chatty-settings-dialog.c:878 +msgid "@user:matrix.org or user@example.com" +msgstr "â€â€Ž@user:matrix.org ×ו ‎user@example.com" + +#: src/dialogs/chatty-settings-dialog.c:987 +msgid "Unblock contact" +msgstr "שחרור ×יש קשר" + +#: src/matrix/chatty-ma-account.c:105 +msgid "Incorrect password" +msgstr "ססמה שגויה" + +#: src/matrix/chatty-ma-account.c:108 +msgid "_OK" +msgstr "_×ישור" + +#: src/matrix/chatty-ma-account.c:109 src/ui/chatty-dialog-new-chat.ui:74 +#: src/ui/chatty-settings-dialog.ui:47 src/ui/chatty-settings-dialog.ui:108 +msgid "_Cancel" +msgstr "_ביטול" + +#: src/matrix/chatty-ma-account.c:115 +#, c-format +msgid "Please enter password for “%sâ€, homeserver: %s" +msgstr "× × ×œ×ž×œ× ×¡×¡×ž×” עבור „%sâ€, שרת בית: %s" + +#. TRANSLATORS: Timestamp for minute accuracy, e.g. “2020-08-11 15:27â€. +#. See https://developer.gnome.org/glib/stable/glib-GDateTime.html#g-date-time-format +#. +#: src/mm/chatty-mmsd.c:827 +msgid "%Y-%m-%d %H∶%M" +msgstr "%m-%d-%Y %H:%M" + +#: src/mm/chatty-mmsd.c:1111 +#, c-format +msgid "You received an MMS, but it expired on: %s" +msgstr "קיבלת הודעת MMS, ×ך תוקפה פג: %s" + +#: src/mm/chatty-mmsd.c:1114 +msgid "You received an empty MMS." +msgstr "קיבלת הודעת MMS ריקה." + +#: src/ui/chatty-dialog-join-muc.ui:17 +msgid "New Group Chat" +msgstr "התכתבות קבוצתית חדשה" + +#: src/ui/chatty-dialog-join-muc.ui:34 +msgid "Join Chat" +msgstr "הצטרפות להתכתבות" + +#: src/ui/chatty-dialog-join-muc.ui:82 src/ui/chatty-dialog-new-chat.ui:269 +msgid "Select chat account" +msgstr "× × ×œ×‘×—×•×¨ חשבון התכתבות" + +#: src/ui/chatty-dialog-join-muc.ui:152 +msgid "Password (optional)" +msgstr "ססמה (רשות)" + +#: src/ui/chatty-dialog-new-chat.ui:126 +msgid "Group Title" +msgstr "כותרת לקבוצה" + +#: src/ui/chatty-dialog-new-chat.ui:177 +#| msgid "Add %s to contact list" +msgid "Add members from contacts…" +msgstr "הוספת ×—×‘×¨×™× ×ž×× ×©×™ הקשר…" + +#: src/ui/chatty-dialog-new-chat.ui:223 +msgid "Send To:" +msgstr "לשלוח ×ל:" + +#: src/ui/chatty-dialog-new-chat.ui:308 +msgid "Name (optional)" +msgstr "×©× (רשות)" + +#: src/ui/chatty-dialog-new-chat.ui:351 +msgid "Add to Contacts" +msgstr "הוספה ל×× ×©×™ קשר" + +#: src/ui/chatty-file-item.ui:28 +msgid "Remove Attachment" +msgstr "הסרת צרופה" + +#: src/ui/chatty-info-dialog.ui:17 src/ui/chatty-window.ui:139 +msgid "Chat Details" +msgstr "פרטי התכתבות" + +#: src/ui/chatty-info-dialog.ui:64 +msgid "Apply" +msgstr "החלה" + +#: src/ui/chatty-info-dialog.ui:82 +msgid "Invite" +msgstr "להזמין" + +#: src/ui/chatty-ma-chat-info.ui:67 src/ui/chatty-ma-account-details.ui:233 +#| msgid "Matrix ID:" +msgid "Matrix ID" +msgstr "מזהה Matrix" + +#: src/ui/chatty-ma-chat-info.ui:93 src/ui/chatty-pp-chat-info.ui:289 +msgid "Chat settings" +msgstr "הגדרות התכתבות" + +#: src/ui/chatty-ma-chat-info.ui:113 src/ui/chatty-pp-chat-info.ui:238 +#: src/ui/chatty-pp-chat-info.ui:344 +msgid "Encryption" +msgstr "×”×¦×¤× ×”" + +#: src/ui/chatty-ma-chat-info.ui:114 src/ui/chatty-pp-chat-info.ui:345 +msgid "Encrypt Messages" +msgstr "×”×¦×¤× ×ª הודעות" + +#: src/ui/chatty-pp-chat-info.ui:43 src/ui/chatty-pp-account-details.ui:60 +#| msgid "Set Avatar" +msgid "Change Avatar" +msgstr "×©×™× ×•×™ ×ª×ž×•× ×” ייצוגית" + +#: src/ui/chatty-pp-chat-info.ui:69 src/ui/chatty-pp-account-details.ui:34 +#| msgid "Set Avatar" +msgid "Delete Avatar" +msgstr "מחיקת ×ª×ž×•× ×” ייצוגית" + +#: src/ui/chatty-pp-chat-info.ui:108 +msgid "Room topic" +msgstr "× ×•×©× ×”×—×“×¨" + +#: src/ui/chatty-pp-chat-info.ui:211 +msgid "XMPP ID" +msgstr "מזהה XMPP" + +#: src/ui/chatty-pp-chat-info.ui:264 src/ui/chatty-ma-account-details.ui:67 +#: src/ui/chatty-pp-account-details.ui:142 +msgid "Status" +msgstr "מצב" + +#: src/ui/chatty-pp-chat-info.ui:310 +msgid "Notifications" +msgstr "התר×ות" + +#: src/ui/chatty-pp-chat-info.ui:327 +msgid "Status Messages" +msgstr "הודעות מצב" + +#: src/ui/chatty-pp-chat-info.ui:328 +msgid "Show status messages in chat" +msgstr "להציג הודעות מצב בהתכתבות" + +#: src/ui/chatty-pp-chat-info.ui:367 +msgid "Fingerprints" +msgstr "טביעות ×צבע" + +#: src/ui/chatty-mm-chat-info.ui:29 +msgid "Title" +msgstr "כותרת" + +#: src/ui/chatty-mm-chat-info.ui:33 +msgid "Blank for default" +msgstr "ריק בברירת מחדל" + +#: src/ui/chatty-mm-chat-info.ui:43 +#| msgid "%u Member" +#| msgid_plural "%u Members" +msgid "Group Members" +msgstr "חברי הקבוצה" + +#: src/ui/chatty-list-row.ui:151 src/ui/chatty-window.ui:433 +msgid "Call" +msgstr "שיחה" + +#: src/ui/chatty-ma-account-details.ui:93 +msgid "Name" +msgstr "ש×" + +#: src/ui/chatty-ma-account-details.ui:123 +msgid "Email" +msgstr "דו×״ל" + +#: src/ui/chatty-ma-account-details.ui:152 +msgid "Phone" +msgstr "טלפון" + +#: src/ui/chatty-ma-account-details.ui:196 +msgid "Advanced information" +msgstr "מידע מתקד×" + +#: src/ui/chatty-ma-account-details.ui:207 +#| msgid "Matrix Home Server" +msgid "Homeserver" +msgstr "×©× ×©×¨×ª" + +#: src/ui/chatty-ma-account-details.ui:259 +msgid "Device ID" +msgstr "מזהה התקן" + +#: src/ui/chatty-pp-account-details.ui:92 +msgid "Account ID" +msgstr "מזהה חשבון" + +#: src/ui/chatty-pp-account-details.ui:116 +#| msgid "Any Protocol" +msgid "Protocol" +msgstr "פרוטוקול" + +#: src/ui/chatty-pp-account-details.ui:168 src/ui/chatty-settings-dialog.ui:701 +msgid "Password" +msgstr "ססמה" + +#: src/ui/chatty-pp-account-details.ui:213 +msgid "Own Fingerprint" +msgstr "טביעת ×צבע עצמית" + +#: src/ui/chatty-settings-dialog.ui:27 +msgid "Back" +msgstr "חזרה" + +#: src/ui/chatty-settings-dialog.ui:60 +msgid "_Add" +msgstr "_הוספה" + +#: src/ui/chatty-settings-dialog.ui:76 +msgid "_Save" +msgstr "_שמירה" + +#: src/ui/chatty-settings-dialog.ui:92 +msgid "_Apply" +msgstr "×”_חלה" + +#: src/ui/chatty-settings-dialog.ui:169 +#| msgid "Account ID" +msgid "Accounts" +msgstr "משתמשי×" + +#: src/ui/chatty-settings-dialog.ui:189 src/ui/chatty-settings-dialog.ui:531 +msgid "Privacy" +msgstr "פרטיות" + +#: src/ui/chatty-settings-dialog.ui:195 +msgid "Message Receipts" +msgstr "קבלות על הודעות" + +#: src/ui/chatty-settings-dialog.ui:196 +msgid "Confirm received messages" +msgstr "ל×שר שהודעות התקבלו" + +#: src/ui/chatty-settings-dialog.ui:211 +msgid "Typing Notification" +msgstr "התר×ות הקלדה" + +#: src/ui/chatty-settings-dialog.ui:212 +msgid "Send typing messages" +msgstr "שליחת הודעות הקלדה" + +#: src/ui/chatty-settings-dialog.ui:230 +msgid "Editor" +msgstr "עורך" + +#: src/ui/chatty-settings-dialog.ui:235 +msgid "Graphical Emoticons" +msgstr "×¨×’×©×•× ×™× ×’×¨×¤×™×™×" + +#: src/ui/chatty-settings-dialog.ui:236 +msgid "if you type :) it will be changed to 😃" +msgstr "הקלדה של :) ×ª×©×ª× ×” לחייכן 😃" + +#. TRANSLATORS: Return is the Enter key. +#: src/ui/chatty-settings-dialog.ui:251 +#| msgid "Send message with return key" +msgid "Send Messages with Return" +msgstr "לשלוח הודעה ×¢× ×ž×§×© Return" + +#: src/ui/chatty-settings-dialog.ui:267 +#| msgid "Purple Settings" +msgid "Protocol Settings" +msgstr "הגדרות פרוטוקול" + +#: src/ui/chatty-settings-dialog.ui:333 +#| msgid "Request SMS delivery reports" +msgid "Request Delivery Reports" +msgstr "דרישת דיווחי מסירת הודעות" + +#: src/ui/chatty-settings-dialog.ui:348 +msgid "SMIL for MMS" +msgstr "â€SMIL עבור MMS" + +#: src/ui/chatty-settings-dialog.ui:384 +#| msgid "SMS and MMS Settings" +msgid "MMS Carrier Settings" +msgstr "הגדרות חברה ל־MMS" + +#: src/ui/chatty-settings-dialog.ui:388 +msgid "MMSC" +msgstr "MMSC" + +#: src/ui/chatty-settings-dialog.ui:403 +msgid "APN" +msgstr "APN" + +#: src/ui/chatty-settings-dialog.ui:418 +msgid "Proxy" +msgstr "מתווך" + +#: src/ui/chatty-settings-dialog.ui:447 +#| msgid "You shall no longer be notified for new messages, continue?" +msgid "You shall not be notified for the messages from blocked contacts" +msgstr "×œ× ×ª×ª×§×‘×œ× ×” התר×ות על הודעות מ×× ×©×™ קשר חסומי×" + +#: src/ui/chatty-settings-dialog.ui:481 +msgid "Blocked chat list empty" +msgstr "רשימת שיחות חסומות ריקה" + +#: src/ui/chatty-settings-dialog.ui:537 +msgid "Message Archive Management" +msgstr "× ×™×”×•×œ ×רכיון הודעות" + +#: src/ui/chatty-settings-dialog.ui:538 +msgid "Sync conversations with chat server" +msgstr "×œ×¡× ×›×¨×Ÿ התכתבויות ×¢× ×©×¨×ª התכתבויות" + +#: src/ui/chatty-settings-dialog.ui:553 +msgid "Message Carbon Copies" +msgstr "×¢×•×ª×§×™× ×©×œ הודעות" + +#: src/ui/chatty-settings-dialog.ui:737 +#| msgid "Matrix Home Server" +msgid "Home server" +msgstr "שרת ביתי" + +#: src/ui/chatty-settings-dialog.ui:773 +#| msgid "Add new account…" +msgid "Add _new account…" +msgstr "הוספת חשבון _חדש…" + +#: src/ui/chatty-window.ui:17 +#| msgid "Archived" +msgctxt "show archived chat list when clicked" +msgid "Archived" +msgstr "×רכיון" + +#: src/ui/chatty-window.ui:41 +msgid "Keyboard _Shortcuts" +msgstr "_צירופי מקשי×" + +#: src/ui/chatty-window.ui:48 +msgid "Help" +msgstr "עזרה" + +#: src/ui/chatty-window.ui:57 +msgid "About Chats" +msgstr "על התכתבויות" + +#: src/ui/chatty-window.ui:85 +msgid "New Message…" +msgstr "הודעה חדשה…" + +#: src/ui/chatty-window.ui:98 +#| msgid "New Message…" +msgid "New SMS/MMS Message…" +msgstr "הודעת SMS/MMS חדשה…" + +#: src/ui/chatty-window.ui:111 +msgid "New Group Message…" +msgstr "הודעה קבוצתית חדשה…" + +#: src/ui/chatty-window.ui:164 +msgid "Leave Chat" +msgstr "לצ×ת מההתכתבות" + +#: src/ui/chatty-window.ui:177 +#| msgid "Blocked Contacts" +msgid "Block Contact" +msgstr "חסימת ×יש קשר" + +#: src/ui/chatty-window.ui:190 +#| msgid "Unblock contact" +msgid "Unblock Contact" +msgstr "שחרור ×יש קשר" + +#: src/ui/chatty-window.ui:203 +#| msgid "No archived chats" +msgid "Archive chat" +msgstr "×רכוב שיחה" + +#: src/ui/chatty-window.ui:216 +#| msgid "No archived chats" +msgid "Unarchive chat" +msgstr "הוצ×ת שיחה מ×רכיון" + +#: src/ui/chatty-window.ui:229 +msgid "Delete Chat" +msgstr "למחוק התכתבות" + +#: src/ui/help-overlay.ui:14 +msgctxt "shortcut window" +msgid "General" +msgstr "כללי" + +#: src/ui/help-overlay.ui:19 +#| msgid "Open Message" +msgctxt "shortcut window" +msgid "Open Menu" +msgstr "פתיחת תפריט" + +#: src/ui/help-overlay.ui:27 +#| msgid "Open Message" +msgctxt "shortcut window" +msgid "Open Search" +msgstr "פתיחת חיפוש" + +#: src/ui/help-overlay.ui:35 +msgctxt "shortcut window" +msgid "Show Shortcuts" +msgstr "הצגת צירופי מקשי×" + +#: src/users/chatty-contact.c:325 +msgid "Mobile: " +msgstr "× ×™×™×“: " + +#: src/users/chatty-contact.c:327 +msgid "Work: " +msgstr "עבודה: " + +#: src/users/chatty-contact.c:329 +msgid "Other: " +msgstr "×חר: " + +#, c-format +#~ msgid "Chatty password for \"%s\"" +#~ msgstr "ססמה של Chatty עבור „%sâ€" + +#, c-format +#~ msgid "The certificate for ‘%s’ has unknown CA" +#~ msgstr "ל×ישור עבור ‚%s’ יש רשות ××™×©×•×¨×™× ×‘×œ×ª×™ מוכרת" + +#, c-format +#~ msgid "The certificate for ‘%s’ is self-signed" +#~ msgstr "×”×ישור ×”×–×” עבור ‚%s’ × ×—×ª× ×¢×¦×ž×™×ª" + +#, c-format +#~ msgid "The certificate for ‘%s’ has expired" +#~ msgstr "×”×ישור של ‚%s’ פג" + +#, c-format +#~ msgid "The certificate for ‘%s’ has been revoked" +#~ msgstr "×”×ישור עבור ‚%s’ × ×©×œ×œ" + +#, c-format +#~ msgid "Error validating certificate for ‘%s’" +#~ msgstr "שגי××” ב×ימות ×ישור עבור ‚%s’" + +#~ msgid "sm.puri.Chatty" +#~ msgstr "sm.puri.Chatty" + +#~ msgid "Mark offline users differently" +#~ msgstr "לסמן ×ž×©×ª×ž×©×™× ×‘×œ×ª×™Ö¾×ž×§×•×•× ×™× ×חרת" + +#~ msgid "ask your counterpart to use E2EE." +#~ msgstr "× ×™×ª×Ÿ לבקש מהצד ×”×©× ×™ בשיחה להפעיל E2EE (×”×¦×¤× ×” מקצה לקצה)." + +#~ msgid "Your messages are secured" +#~ msgstr "ההודעות שלך מ×ובטחות" + +#~ msgid "by end-to-end encryption." +#~ msgstr "×‘×”×¦×¤× ×” מקצה לקצה." + +#~ msgid "and carrier rates may apply." +#~ msgstr "יכול להיות שהשיחה תעלה כסף." + +#~ msgctxt "timestamp-suffix-seconds" +#~ msgid "s" +#~ msgstr "×©× ×³" + +#~ msgctxt "timestamp-suffix-minute" +#~ msgid "m" +#~ msgstr "ח׳" + +#~ msgctxt "timestamp-suffix-minutes" +#~ msgid "m" +#~ msgstr "ח׳" + +#~ msgctxt "timestamp-suffix-hour" +#~ msgid "h" +#~ msgstr "שע׳" + +#~ msgctxt "timestamp-suffix-hours" +#~ msgid "h" +#~ msgstr "שע׳" + +#~ msgctxt "timestamp-suffix-day" +#~ msgid "d" +#~ msgstr "י׳" + +#~ msgctxt "timestamp-suffix-days" +#~ msgid "d" +#~ msgstr "י׳" + +#~ msgctxt "timestamp-suffix-month" +#~ msgid "mo" +#~ msgstr "חודש" + +#~ msgctxt "timestamp-suffix-months" +#~ msgid "mos" +#~ msgstr "חודשי×" + +#~ msgctxt "timestamp-suffix-year" +#~ msgid "y" +#~ msgstr "×©× ×”" + +#~ msgctxt "timestamp-suffix-years" +#~ msgid "y" +#~ msgstr "×©× ×™×" + +#~ msgid "Over" +#~ msgstr "למעלה מ־" + +#~ msgid "Almost" +#~ msgstr "כמעט" + +#~ msgid "Choose a contact" +#~ msgstr "× × ×œ×‘×—×•×¨ ×יש קשר" + +#~ msgid "" +#~ "Select an <b>SMS</b> or <b>Instant Message</b> contact with the <b>\"+\"</" +#~ "b> button in the titlebar." +#~ msgstr "" +#~ "× ×™×ª×Ÿ לבחור ×יש קשר ל<b>×ž×¡×¨×•× ×™×</b> ×ו ל<b>הודעה מיידית</b> ×¢× ×”×›×¤×ª×•×¨ " +#~ "<b>„+â€</b> בשורת הכותרת." + +#~ msgid "Start a <b>SMS</b> chat with the \"+\" button in the titlebar." +#~ msgstr "× ×™×ª×Ÿ ליצור התכתבות ב<b>×ž×¡×¨×•× ×™×</b> ×¢× ×”×›×¤×ª×•×¨ „+†שבשורת הכותרת." + +#, c-format +#~ msgid "Error saving contact: %s" +#~ msgstr "שגי××” בשמירת ×יש קשר: %s" + +#~ msgid "Start Chat" +#~ msgstr "להתחיל התכתבות" + +#~ msgid "Chats List" +#~ msgstr "רשימת התכתבויות" + +#~ msgid "Indicate Offline Contacts" +#~ msgstr "לציין ×× ×©×™ קשר בלתי ×ž×§×•×•× ×™×" + +#~ msgid "Grey out avatars from offline contacts" +#~ msgstr "לה×פיר ×ª×ž×•× ×•×ª ייצוגיות של ×× ×©×™ קשר בלתי ×ž×§×•×•× ×™×" + +#~ msgid "Indicate Idle Contacts" +#~ msgstr "לציין ×× ×©×™ קשר בלתי פעילי×" + +#~ msgid "Blur avatars from idle contacts" +#~ msgstr "לטשטש ×ª×ž×•× ×•×ª ייצוגיות של ×× ×©×™ קשר בלתי פעילי×" + +#~ msgid "Color unknown contact ID red" +#~ msgstr "לצבוע מזהי ×× ×©×™ קשר בלתי ×ž×•×›×¨×™× ×‘×דו×" + +#~ msgid "Convert ASCII emoticons" +#~ msgstr "להמיר ASCII ×¨×’×©×•× ×™×" + +#~ msgid "Return = Send Message" +#~ msgstr "‫Return = לשלוח הודעה" + +#~ msgid "_Accept" +#~ msgstr "ל_קבל" + +#~ msgid "New Bulk SMS…" +#~ msgstr "מסרון מרוכז חדש…" diff --git a/po/it.po b/po/it.po index e4fcaa77cd71308e535612cb10d535e37e52262b..1eb114cc3251ec33d479014680f2c7579e2b4c5d 100644 --- a/po/it.po +++ b/po/it.po @@ -5,8 +5,8 @@ msgid "" msgstr "" "Project-Id-Version: purism-chatty\n" "Report-Msgid-Bugs-To: https://source.puri.sm/Librem5/chatty/issues\n" -"POT-Creation-Date: 2022-04-28 15:25+0000\n" -"PO-Revision-Date: 2022-05-03 15:18+0200\n" +"POT-Creation-Date: 2022-09-09 15:24+0000\n" +"PO-Revision-Date: 2022-09-12 15:42+0200\n" "Last-Translator: antpanlinux <ant.pandolfo@gmail.com>\n" "Language-Team: Italian\n" "Language: it\n" @@ -17,7 +17,7 @@ msgstr "" "Plural-Forms: nplurals=2; plural=(n != 1)\n" #: data/sm.puri.Chatty.desktop.in:3 data/sm.puri.Chatty.metainfo.xml.in:6 -#: src/chatty-application.c:333 src/chatty-window.c:330 +#: src/chatty-application.c:338 src/chatty-window.c:339 #: src/ui/chatty-window.ui:265 msgid "Chats" msgstr "Chats" @@ -150,27 +150,27 @@ msgstr "Chats è un'applicazione di messaggistica che supporta XMPP e SMS." msgid "Chats message window" msgstr "Finestra di messaggio Chats" -#: src/chatty-application.c:76 +#: src/chatty-application.c:78 msgid "Show release version" msgstr "Mostra versione" -#: src/chatty-application.c:77 +#: src/chatty-application.c:79 msgid "Start in daemon mode" msgstr "Avvia in modalità demone" -#: src/chatty-application.c:78 +#: src/chatty-application.c:80 msgid "Disable all accounts" msgstr "Disattiva tutti gli account" -#: src/chatty-application.c:79 +#: src/chatty-application.c:81 msgid "Enable libpurple debug messages" msgstr "Attiva i messaggi di debug libpurple" -#: src/chatty-application.c:81 +#: src/chatty-application.c:83 msgid "Enable verbose libpurple debug messages" msgstr "Attiva i messaggi dettagliati di debug libpurple" -#: src/chatty-application.c:142 +#: src/chatty-application.c:144 #, c-format msgid "" "There was an error displaying help:\n" @@ -230,12 +230,12 @@ msgstr "Il tuo messaggio è cifrato" msgid "Your messages are not encrypted" msgstr "Il tuo messaggio non è cifrato" -#: src/chatty-chat-view.c:414 +#: src/chatty-chat-view.c:420 msgid "Select File..." msgstr "Seleziona file..." -#: src/chatty-chat-view.c:417 src/purple/chatty-purple-request.c:175 -#: src/chatty-window.c:413 src/dialogs/chatty-pp-chat-info.c:90 +#: src/chatty-chat-view.c:423 src/purple/chatty-purple-request.c:175 +#: src/chatty-window.c:422 src/dialogs/chatty-pp-chat-info.c:90 #: src/dialogs/chatty-ma-account-details.c:119 #: src/dialogs/chatty-pp-account-details.c:91 #: src/ui/chatty-dialog-join-muc.ui:21 src/ui/chatty-info-dialog.ui:44 @@ -243,7 +243,7 @@ msgstr "Seleziona file..." msgid "Cancel" msgstr "Annulla" -#: src/chatty-chat-view.c:418 src/purple/chatty-purple-request.c:177 +#: src/chatty-chat-view.c:424 src/purple/chatty-purple-request.c:177 #: src/dialogs/chatty-pp-chat-info.c:89 #: src/dialogs/chatty-ma-account-details.c:118 #: src/dialogs/chatty-pp-account-details.c:90 @@ -347,7 +347,7 @@ msgstr "Moderatore" msgid "Member" msgstr "Membro" -#: src/chatty-manager.c:727 +#: src/chatty-manager.c:724 #, c-format msgid "“%s†is not a valid URI" msgstr "“%s†non è un URI valido" @@ -356,6 +356,12 @@ msgstr "“%s†non è un URI valido" msgid "Copy" msgstr "Copia" +#: src/chatty-message.c:233 +msgid "Got an encrypted message, but couldn't decrypt due to missing keys" +msgstr "" +"È stato ricevuto un messaggio cifrato, ma non è stato possibile decifrarlo a " +"causa di chiavi mancanti" + #: src/chatty-notification.c:181 msgid "Open Message" msgstr "Apri messaggio" @@ -369,7 +375,7 @@ msgstr "Nuovo messaggio da %s" msgid "Message Received" msgstr "Messaggio ricevuto" -#: src/purple/chatty-purple-notify.c:44 src/matrix/matrix-utils.c:505 +#: src/purple/chatty-purple-notify.c:44 msgid "Close" msgstr "Chiudi" @@ -399,11 +405,11 @@ msgstr "L'utente %s ha aggiunto %s ai contatti" msgid "Authorize %s?" msgstr "Autorizzi %s?" -#: src/purple/chatty-purple.c:264 src/matrix/matrix-utils.c:509 +#: src/purple/chatty-purple.c:264 msgid "Reject" msgstr "Rifiuta" -#: src/purple/chatty-purple.c:265 src/matrix/matrix-utils.c:510 +#: src/purple/chatty-purple.c:265 msgid "Accept" msgstr "Accetta" @@ -412,76 +418,71 @@ msgstr "Accetta" msgid "Add %s to contact list" msgstr "Aggiungi %s alla lista contatti" -#: src/purple/chatty-purple.c:587 +#: src/purple/chatty-purple.c:586 msgid "Login failed" msgstr "Login fallito" -#: src/purple/chatty-purple.c:592 +#: src/purple/chatty-purple.c:591 msgid "Please check ID and password" msgstr "Per favore controlla ID e password" -#: src/chatty-secret-store.c:98 -#, c-format -msgid "Chatty password for \"%s\"" -msgstr "Password Chatty per \"%s\"" - -#: src/chatty-window.c:399 +#: src/chatty-window.c:408 #, c-format msgid "Delete chat with “%sâ€" msgstr "Elimina chat con “%sâ€" -#: src/chatty-window.c:400 +#: src/chatty-window.c:409 msgid "This deletes the conversation history" msgstr "Questo elimina la cronologia delle conversazioni" -#: src/chatty-window.c:402 +#: src/chatty-window.c:411 #, c-format msgid "Disconnect group chat “%sâ€" msgstr "Disconnetti la chat di gruppo “%sâ€" -#: src/chatty-window.c:403 +#: src/chatty-window.c:412 msgid "This removes chat from chats list" msgstr "Questo rimuove la chat dalla lista delle chat" -#: src/chatty-window.c:415 +#: src/chatty-window.c:424 msgid "Delete" msgstr "Elimina" -#: src/chatty-window.c:493 +#: src/chatty-window.c:502 msgid "You shall no longer be notified for new messages, continue?" msgstr "Non sarai più notificato se riceverai nuovi messaggi. Vuoi continuare?" -#: src/chatty-window.c:568 +#: src/chatty-window.c:577 msgid "Archived" msgstr "Archiviata" -#: src/chatty-window.c:621 +#: src/chatty-window.c:630 msgid "An SMS and XMPP messaging client" msgstr "Un client di messaggistica SMS e XMPP" -#: src/chatty-window.c:628 +#: src/chatty-window.c:637 msgid "translator-credits" msgstr "" "Pandolfo Antonio <ant.pandolfo@gmail.com>\n" "Vittorio <postav@pm.me>, 2021" -#: src/chatty-window.c:978 +#: src/chatty-window.c:989 msgid "Any Protocol" msgstr "Qualsiasi Protocollo" -#: src/chatty-window.c:979 src/ui/chatty-settings-dialog.ui:627 +#: src/chatty-window.c:990 src/ui/chatty-settings-dialog.ui:627 msgid "Matrix" msgstr "Matrix" -#: src/chatty-window.c:980 +#: src/chatty-window.c:991 msgid "SMS/MMS" msgstr "SMS/MMS" -#: src/chatty-window.c:983 src/ui/chatty-settings-dialog.ui:615 +#: src/chatty-window.c:994 src/ui/chatty-settings-dialog.ui:615 msgid "XMPP" msgstr "XMPP" -#: src/chatty-window.c:986 src/ui/chatty-settings-dialog.ui:641 +#: src/chatty-window.c:997 src/ui/chatty-settings-dialog.ui:641 msgid "Telegram" msgstr "Telegram" @@ -581,115 +582,93 @@ msgstr "connessione in corso…" msgid "disconnected" msgstr "disconnesso" -#: src/dialogs/chatty-settings-dialog.c:255 -#: src/dialogs/chatty-settings-dialog.c:405 +#: src/dialogs/chatty-settings-dialog.c:251 +#, c-format +msgid "Failed to verify server: %s" +msgstr "Impossibile verificare il server: %s" + +#: src/dialogs/chatty-settings-dialog.c:253 msgid "Failed to verify server" msgstr "Impossibile verificare il server" -#: src/dialogs/chatty-settings-dialog.c:306 -#: src/dialogs/chatty-settings-dialog.c:385 +#: src/dialogs/chatty-settings-dialog.c:259 msgid "Couldn't get Home server address" msgstr "Impossibile ottenere l'indirizzo del server personale" -#: src/dialogs/chatty-settings-dialog.c:510 +#: src/dialogs/chatty-settings-dialog.c:428 #: src/ui/chatty-ma-account-details.ui:182 #: src/ui/chatty-pp-account-details.ui:199 msgid "Delete Account" msgstr "Elimina l'account" -#: src/dialogs/chatty-settings-dialog.c:513 +#: src/dialogs/chatty-settings-dialog.c:431 #, c-format msgid "Delete account %s?" msgstr "Eliminare l'account %s?" -#: src/dialogs/chatty-settings-dialog.c:654 +#: src/dialogs/chatty-settings-dialog.c:582 msgid "Restart chatty to disable purple" msgstr "Riavvia Chatty per disattivare purple" -#: src/dialogs/chatty-settings-dialog.c:656 +#: src/dialogs/chatty-settings-dialog.c:584 #: src/ui/chatty-settings-dialog.ui:512 msgid "Enable purple plugin" msgstr "Attiva il plugin purple" -#: src/dialogs/chatty-settings-dialog.c:670 +#: src/dialogs/chatty-settings-dialog.c:598 #: src/ui/chatty-settings-dialog.ui:279 msgid "SMS and MMS Settings" msgstr "Impostazioni SMS e MMS" -#: src/dialogs/chatty-settings-dialog.c:672 +#: src/dialogs/chatty-settings-dialog.c:600 #: src/ui/chatty-settings-dialog.ui:298 msgid "Purple Settings" msgstr "Impostazioni purple" -#: src/dialogs/chatty-settings-dialog.c:674 +#: src/dialogs/chatty-settings-dialog.c:602 msgid "New Account" msgstr "Nuovo account" -#: src/dialogs/chatty-settings-dialog.c:676 +#: src/dialogs/chatty-settings-dialog.c:604 #: src/ui/chatty-settings-dialog.ui:364 msgid "Blocked Contacts" msgstr "Contatti bloccati" -#: src/dialogs/chatty-settings-dialog.c:678 src/ui/chatty-settings-dialog.ui:7 +#: src/dialogs/chatty-settings-dialog.c:606 src/ui/chatty-settings-dialog.ui:7 #: src/ui/chatty-window.ui:30 msgid "Preferences" msgstr "Preferenze" -#: src/dialogs/chatty-settings-dialog.c:707 +#: src/dialogs/chatty-settings-dialog.c:634 msgid "Select Protocol" msgstr "Seleziona Protocollo" #. TRANSLATORS: Only translate 'or' -#: src/dialogs/chatty-settings-dialog.c:948 +#: src/dialogs/chatty-settings-dialog.c:878 msgid "@user:matrix.org or user@example.com" msgstr "@user:matrix.org oppure user@example.com" -#: src/dialogs/chatty-settings-dialog.c:1057 +#: src/dialogs/chatty-settings-dialog.c:987 msgid "Unblock contact" msgstr "Sblocca contatto" -#: src/matrix/chatty-ma-account.c:296 +#: src/matrix/chatty-ma-account.c:105 msgid "Incorrect password" msgstr "Password errata" -#: src/matrix/chatty-ma-account.c:299 +#: src/matrix/chatty-ma-account.c:108 msgid "_OK" msgstr "_OK" -#: src/matrix/chatty-ma-account.c:300 src/ui/chatty-dialog-new-chat.ui:74 +#: src/matrix/chatty-ma-account.c:109 src/ui/chatty-dialog-new-chat.ui:74 #: src/ui/chatty-settings-dialog.ui:47 src/ui/chatty-settings-dialog.ui:108 msgid "_Cancel" msgstr "_Annulla" -#: src/matrix/chatty-ma-account.c:306 -#, c-format -msgid "Please enter password for “%sâ€" -msgstr "Inserire la password per “%sâ€" - -#: src/matrix/matrix-utils.c:474 -#, c-format -msgid "The certificate for ‘%s’ has unknown CA" -msgstr "Il certificato per ‘%s’ ha una CA sconosciuta" - -#: src/matrix/matrix-utils.c:476 -#, c-format -msgid "The certificate for ‘%s’ is self-signed" -msgstr "Il certificato per ‘%s’ è autofirmato" - -#: src/matrix/matrix-utils.c:480 -#, c-format -msgid "The certificate for ‘%s’ has expired" -msgstr "Il certificato per ‘%s’ è scaduto" - -#: src/matrix/matrix-utils.c:484 -#, c-format -msgid "The certificate for ‘%s’ has been revoked" -msgstr "Il certificato per ‘%s’ è stato revocato" - -#: src/matrix/matrix-utils.c:493 +#: src/matrix/chatty-ma-account.c:115 #, c-format -msgid "Error validating certificate for ‘%s’" -msgstr "Errore durante la convalida del certificato per ‘%s’" +msgid "Please enter password for “%sâ€, homeserver: %s" +msgstr "Inserire la password per “%sâ€, server personale: %s" #. TRANSLATORS: Timestamp for minute accuracy, e.g. “2020-08-11 15:27â€. #. See https://developer.gnome.org/glib/stable/glib-GDateTime.html#g-date-time-format diff --git a/po/pt_BR.po b/po/pt_BR.po index 885d798dbf121edf2c2fbd01c9051724bc8733ef..b2e8d307a69ab292ec863513e124c76e4dd2bad8 100644 --- a/po/pt_BR.po +++ b/po/pt_BR.po @@ -3,33 +3,30 @@ # This file is distributed under the same license as the chatty package. # LuÃs Fernando Stürmer da Rosa <luisfsr@dismail.de>, 2018-2020. # Bruno Lopes <brunolopesdsilv@gmail.com>, 2020. -# Rafael Fontenelle <rafaelff@gnome.org>, 2020-2021. # Matheus Barbosa <mdpb.matheus@gmail.com>, 2022. +# Rafael Fontenelle <rafaelff@gnome.org>, 2020-2022. # msgid "" msgstr "" "Project-Id-Version: purism-chatty\n" "Report-Msgid-Bugs-To: https://source.puri.sm/Librem5/chatty/issues\n" -"POT-Creation-Date: 2022-02-01 15:25+0000\n" -"PO-Revision-Date: 2022-01-31 14:22-0300\n" -"Last-Translator: Matheus Barbosa <mdpb.matheus@gmail.com>\n" +"POT-Creation-Date: 2022-08-08 03:24+0000\n" +"PO-Revision-Date: 2022-08-30 09:25-0300\n" +"Last-Translator: Rafael Fontenelle <rafaelff@gnome.org>\n" "Language-Team: Brazilian Portuguese <gnome-pt_br-list@gnome.org>\n" "Language: pt_BR\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" -"X-Generator: Gtranslator 40.0\n" "Plural-Forms: nplurals=2; plural=(n > 1)\n" +"X-Generator: Gtranslator 40.0\n" #: data/sm.puri.Chatty.desktop.in:3 data/sm.puri.Chatty.metainfo.xml.in:6 -#: src/chatty-application.c:333 src/ui/chatty-window.ui:200 +#: src/chatty-application.c:336 src/chatty-window.c:339 +#: src/ui/chatty-window.ui:265 msgid "Chats" msgstr "Conversas" -#: data/sm.puri.Chatty.desktop.in:5 -msgid "sm.puri.Chatty" -msgstr "sm.puri.Chatty" - #: data/sm.puri.Chatty.desktop.in:6 msgid "SMS and XMPP chat application" msgstr "Aplicativo de conversa via XMPP e SMS" @@ -62,7 +59,7 @@ msgstr "Se é para enviar o status da mensagem lida" msgid "Message carbon copies" msgstr "Cópias de mensagens" -#: data/sm.puri.Chatty.gschema.xml:26 src/ui/chatty-settings-dialog.ui:479 +#: data/sm.puri.Chatty.gschema.xml:26 src/ui/chatty-settings-dialog.ui:554 msgid "Share chat history among devices" msgstr "Sincronizar histórico entre dispositivos" @@ -78,7 +75,7 @@ msgstr "Habilitar a sincronia de mensagens arquivadas pelo servidor" msgid "Enable purple" msgstr "Habilitar purple" -#: data/sm.puri.Chatty.gschema.xml:38 src/ui/chatty-settings-dialog.ui:436 +#: data/sm.puri.Chatty.gschema.xml:38 src/ui/chatty-settings-dialog.ui:511 msgid "Enable purple accounts" msgstr "Habilitar contas purple" @@ -158,27 +155,27 @@ msgstr "Conversas é um aplicativo compatÃvel com XMPP e SMS." msgid "Chats message window" msgstr "Janela de mensagens" -#: src/chatty-application.c:76 +#: src/chatty-application.c:78 msgid "Show release version" msgstr "Exibir versão" -#: src/chatty-application.c:77 +#: src/chatty-application.c:79 msgid "Start in daemon mode" msgstr "Inicializar em segundo plano" -#: src/chatty-application.c:78 +#: src/chatty-application.c:80 msgid "Disable all accounts" msgstr "Desabilitar contas" -#: src/chatty-application.c:79 +#: src/chatty-application.c:81 msgid "Enable libpurple debug messages" msgstr "Habilitar mensagens de depuração do libpurple" -#: src/chatty-application.c:81 +#: src/chatty-application.c:83 msgid "Enable verbose libpurple debug messages" msgstr "Habilitar mensagens de depuração detalhada do libpurple" -#: src/chatty-application.c:142 +#: src/chatty-application.c:144 #, c-format msgid "" "There was an error displaying help:\n" @@ -191,23 +188,27 @@ msgstr "" msgid "Send To" msgstr "Enviar para" -#: src/chatty-chat-list.c:152 +#: src/chatty-chat-list.c:175 msgid "Select a contact with the <b>“+â€</b> button in the titlebar." msgstr "Escolha um contato com o botão <b>“+â€</b> na barra de tÃtulo." -#: src/chatty-chat-list.c:156 +#: src/chatty-chat-list.c:179 msgid "Add instant messaging accounts in Preferences." msgstr "Adicionar contas de mensagens instantâneas nas Preferências." -#: src/chatty-chat-list.c:192 src/chatty-contact-list.c:282 +#: src/chatty-chat-list.c:215 src/chatty-contact-list.c:282 msgid "No Search Results" msgstr "Sem resultados da pesquisa" -#: src/chatty-chat-list.c:193 +#: src/chatty-chat-list.c:216 msgid "Try different search" msgstr "Tente uma pesquisa diferente" -#: src/chatty-chat-list.c:196 +#: src/chatty-chat-list.c:220 +msgid "No archived chats" +msgstr "Nenhuma conversa arquivada" + +#: src/chatty-chat-list.c:222 msgid "Start Chatting" msgstr "Começar a conversar" @@ -233,12 +234,12 @@ msgstr "Suas mensagens estão criptografadas" msgid "Your messages are not encrypted" msgstr "Suas mensagens não estão criptografadas" -#: src/chatty-chat-view.c:414 +#: src/chatty-chat-view.c:420 msgid "Select File..." msgstr "Selecionar arquivo…" -#: src/chatty-chat-view.c:417 src/purple/chatty-purple-request.c:175 -#: src/chatty-window.c:347 src/dialogs/chatty-pp-chat-info.c:90 +#: src/chatty-chat-view.c:423 src/purple/chatty-purple-request.c:175 +#: src/chatty-window.c:422 src/dialogs/chatty-pp-chat-info.c:90 #: src/dialogs/chatty-ma-account-details.c:119 #: src/dialogs/chatty-pp-account-details.c:91 #: src/ui/chatty-dialog-join-muc.ui:21 src/ui/chatty-info-dialog.ui:44 @@ -246,24 +247,24 @@ msgstr "Selecionar arquivo…" msgid "Cancel" msgstr "Cancelar" -#: src/chatty-chat-view.c:418 src/purple/chatty-purple-request.c:177 +#: src/chatty-chat-view.c:424 src/purple/chatty-purple-request.c:177 #: src/dialogs/chatty-pp-chat-info.c:89 #: src/dialogs/chatty-ma-account-details.c:118 #: src/dialogs/chatty-pp-account-details.c:90 msgid "Open" msgstr "Abrir" -#: src/chatty-chat.c:632 +#: src/chatty-chat.c:635 msgid "Empty room" msgstr "Sala vazia" #. TRANSLATORS: %s are name/user-id/phone numbers of two users -#: src/chatty-chat.c:639 +#: src/chatty-chat.c:642 #, c-format msgid "%s and %s" msgstr "%s e %s" -#: src/chatty-chat.c:641 +#: src/chatty-chat.c:644 #, c-format msgid "%s and %u other" msgid_plural "%s and %u others" @@ -350,7 +351,7 @@ msgstr "Moderador" msgid "Member" msgstr "Participante" -#: src/chatty-manager.c:727 +#: src/chatty-manager.c:729 #, c-format msgid "“%s†is not a valid URI" msgstr "“%s†não é um URI válido" @@ -372,7 +373,7 @@ msgstr "Nova mensagem de %s" msgid "Message Received" msgstr "Mensagem recebida" -#: src/purple/chatty-purple-notify.c:44 src/matrix/matrix-utils.c:505 +#: src/purple/chatty-purple-notify.c:44 src/matrix/matrix-utils.c:504 msgid "Close" msgstr "Fechar" @@ -402,11 +403,11 @@ msgstr "O usuário %s adicionou %s aos contatos" msgid "Authorize %s?" msgstr "Autorizar %s?" -#: src/purple/chatty-purple.c:264 src/matrix/matrix-utils.c:509 +#: src/purple/chatty-purple.c:264 src/matrix/matrix-utils.c:508 msgid "Reject" msgstr "Rejeitar" -#: src/purple/chatty-purple.c:265 src/matrix/matrix-utils.c:510 +#: src/purple/chatty-purple.c:265 src/matrix/matrix-utils.c:509 msgid "Accept" msgstr "Aceitar" @@ -423,38 +424,46 @@ msgstr "O login falhou" msgid "Please check ID and password" msgstr "Verifique o ID e a senha" -#: src/chatty-secret-store.c:98 +#: src/chatty-secret-store.c:99 #, c-format msgid "Chatty password for \"%s\"" msgstr "Senha do Chatty para “%sâ€" -#: src/chatty-window.c:333 +#: src/chatty-window.c:408 #, c-format msgid "Delete chat with “%sâ€" msgstr "Apagar conversa com “%sâ€" -#: src/chatty-window.c:334 +#: src/chatty-window.c:409 msgid "This deletes the conversation history" msgstr "Isto fará com que o histórico de conversas seja apagado" -#: src/chatty-window.c:336 +#: src/chatty-window.c:411 #, c-format msgid "Disconnect group chat “%sâ€" msgstr "Sair da conversa em grupo “%sâ€" -#: src/chatty-window.c:337 +#: src/chatty-window.c:412 msgid "This removes chat from chats list" msgstr "Isto removerá a conversa da lista" -#: src/chatty-window.c:349 +#: src/chatty-window.c:424 msgid "Delete" msgstr "Apagar" -#: src/chatty-window.c:473 +#: src/chatty-window.c:502 +msgid "You shall no longer be notified for new messages, continue?" +msgstr "Você não receberá mais notificação sobre novas mensagens, continuar?" + +#: src/chatty-window.c:577 +msgid "Archived" +msgstr "Arquivada" + +#: src/chatty-window.c:630 msgid "An SMS and XMPP messaging client" msgstr "Um aplicativo de conversa via XMPP e SMS" -#: src/chatty-window.c:480 +#: src/chatty-window.c:637 msgid "translator-credits" msgstr "" "LuÃs Fernando Stürmer da Rosa <luisfsr@dismail.de>\n" @@ -462,23 +471,23 @@ msgstr "" "Bruno Lopes <brunolopesdsilv@gmail.com>\n" "Matheus Barbosa <mdpb.matheus@gmail.com>" -#: src/chatty-window.c:814 +#: src/chatty-window.c:989 msgid "Any Protocol" msgstr "Qualquer protocolo" -#: src/chatty-window.c:815 src/ui/chatty-settings-dialog.ui:552 +#: src/chatty-window.c:990 src/ui/chatty-settings-dialog.ui:627 msgid "Matrix" msgstr "Matrix" -#: src/chatty-window.c:816 +#: src/chatty-window.c:991 msgid "SMS/MMS" msgstr "SMS/MMS" -#: src/chatty-window.c:819 src/ui/chatty-settings-dialog.ui:540 +#: src/chatty-window.c:994 src/ui/chatty-settings-dialog.ui:615 msgid "XMPP" msgstr "XMPP" -#: src/chatty-window.c:822 src/ui/chatty-settings-dialog.ui:566 +#: src/chatty-window.c:997 src/ui/chatty-settings-dialog.ui:641 msgid "Telegram" msgstr "Telegram" @@ -578,103 +587,117 @@ msgstr "conectando…" msgid "disconnected" msgstr "desconectado" -#: src/dialogs/chatty-settings-dialog.c:210 -#: src/dialogs/chatty-settings-dialog.c:374 +#: src/dialogs/chatty-settings-dialog.c:267 +#, c-format +msgid "Failed to verify server: %s" +msgstr "Falha ao verificar o servidor: %s" + +#: src/dialogs/chatty-settings-dialog.c:269 +#: src/dialogs/chatty-settings-dialog.c:479 msgid "Failed to verify server" msgstr "Erro ao verificar o servidor" -#: src/dialogs/chatty-settings-dialog.c:261 -#: src/dialogs/chatty-settings-dialog.c:354 +#: src/dialogs/chatty-settings-dialog.c:275 +#: src/dialogs/chatty-settings-dialog.c:459 msgid "Couldn't get Home server address" msgstr "Não foi possÃvel obter o endereço do servidor Home" -#: src/dialogs/chatty-settings-dialog.c:479 +#: src/dialogs/chatty-settings-dialog.c:584 #: src/ui/chatty-ma-account-details.ui:182 #: src/ui/chatty-pp-account-details.ui:199 msgid "Delete Account" msgstr "Apagar conta" -#: src/dialogs/chatty-settings-dialog.c:482 +#: src/dialogs/chatty-settings-dialog.c:587 #, c-format msgid "Delete account %s?" msgstr "Apagar conta %s?" -#: src/dialogs/chatty-settings-dialog.c:618 +#: src/dialogs/chatty-settings-dialog.c:728 msgid "Restart chatty to disable purple" msgstr "Reinicie o Chatty para desabilitar o purple" -#: src/dialogs/chatty-settings-dialog.c:620 -#: src/ui/chatty-settings-dialog.ui:437 +#: src/dialogs/chatty-settings-dialog.c:730 +#: src/ui/chatty-settings-dialog.ui:512 msgid "Enable purple plugin" msgstr "Habilite plugin do purple" -#: src/dialogs/chatty-settings-dialog.c:634 +#: src/dialogs/chatty-settings-dialog.c:744 #: src/ui/chatty-settings-dialog.ui:279 msgid "SMS and MMS Settings" msgstr "Configurações de SMS e MMS" -#: src/dialogs/chatty-settings-dialog.c:636 +#: src/dialogs/chatty-settings-dialog.c:746 #: src/ui/chatty-settings-dialog.ui:298 msgid "Purple Settings" msgstr "Configurações do purple" -#: src/dialogs/chatty-settings-dialog.c:638 +#: src/dialogs/chatty-settings-dialog.c:748 msgid "New Account" msgstr "Nova conta" -#: src/dialogs/chatty-settings-dialog.c:640 src/ui/chatty-settings-dialog.ui:7 -#: src/ui/chatty-window.ui:17 +#: src/dialogs/chatty-settings-dialog.c:750 +#: src/ui/chatty-settings-dialog.ui:364 +msgid "Blocked Contacts" +msgstr "Contatos bloqueados" + +#: src/dialogs/chatty-settings-dialog.c:752 src/ui/chatty-settings-dialog.ui:7 +#: src/ui/chatty-window.ui:30 msgid "Preferences" msgstr "Preferências" -#: src/dialogs/chatty-settings-dialog.c:669 +#: src/dialogs/chatty-settings-dialog.c:781 msgid "Select Protocol" msgstr "Selecione o protocolo" #. TRANSLATORS: Only translate 'or' -#: src/dialogs/chatty-settings-dialog.c:865 +#: src/dialogs/chatty-settings-dialog.c:1022 msgid "@user:matrix.org or user@example.com" msgstr "@usuário:matrix.org ou usuário@example.com" -#: src/matrix/chatty-ma-account.c:296 +#: src/dialogs/chatty-settings-dialog.c:1131 +msgid "Unblock contact" +msgstr "Desbloquear contato" + +#: src/matrix/chatty-ma-account.c:231 msgid "Incorrect password" msgstr "Senha incorreta" -#: src/matrix/chatty-ma-account.c:299 +#: src/matrix/chatty-ma-account.c:234 msgid "_OK" msgstr "_OK" -#: src/matrix/chatty-ma-account.c:300 src/ui/chatty-dialog-new-chat.ui:74 +#: src/matrix/chatty-ma-account.c:235 src/ui/chatty-dialog-new-chat.ui:74 #: src/ui/chatty-settings-dialog.ui:47 src/ui/chatty-settings-dialog.ui:108 msgid "_Cancel" msgstr "_Cancelar" -#: src/matrix/chatty-ma-account.c:306 +#: src/matrix/chatty-ma-account.c:241 #, c-format -msgid "Please enter password for “%sâ€" -msgstr "Por favor, insira a senha para “%sâ€" +msgid "Please enter password for “%sâ€, homeserver: %s" +msgstr "Por favor, insira a senha para “%sâ€, servidor: %s" -#: src/matrix/matrix-utils.c:474 +#: src/matrix/matrix-utils.c:473 #, c-format msgid "The certificate for ‘%s’ has unknown CA" msgstr "O certificado para ‘%s’ é de uma CA desconhecida" -#: src/matrix/matrix-utils.c:476 +#: src/matrix/matrix-utils.c:475 #, c-format msgid "The certificate for ‘%s’ is self-signed" msgstr "O certificado para ‘%s’ é auto-assinado" -#: src/matrix/matrix-utils.c:480 +#: src/matrix/matrix-utils.c:479 #, c-format msgid "The certificate for ‘%s’ has expired" msgstr "O certificado para ‘%s’ expirou" -#: src/matrix/matrix-utils.c:484 +#: src/matrix/matrix-utils.c:483 #, c-format msgid "The certificate for ‘%s’ has been revoked" msgstr "O certificado para ‘%s’ foi revogado" -#: src/matrix/matrix-utils.c:493 +#: src/matrix/matrix-utils.c:492 #, c-format msgid "Error validating certificate for ‘%s’" msgstr "Erro ao validar o certificado para ‘%s’" @@ -682,16 +705,16 @@ msgstr "Erro ao validar o certificado para ‘%s’" #. TRANSLATORS: Timestamp for minute accuracy, e.g. “2020-08-11 15:27â€. #. See https://developer.gnome.org/glib/stable/glib-GDateTime.html#g-date-time-format #. -#: src/mm/chatty-mmsd.c:826 +#: src/mm/chatty-mmsd.c:827 msgid "%Y-%m-%d %H∶%M" msgstr "%Y-%m-%d %H∶%M" -#: src/mm/chatty-mmsd.c:1107 +#: src/mm/chatty-mmsd.c:1111 #, c-format msgid "You received an MMS, but it expired on: %s" msgstr "Você recebeu um MMS, mas ele expirou em: %s" -#: src/mm/chatty-mmsd.c:1110 +#: src/mm/chatty-mmsd.c:1114 msgid "You received an empty MMS." msgstr "Você recebeu um MMS vazio." @@ -735,7 +758,7 @@ msgstr "Adicionar aos contatos" msgid "Remove Attachment" msgstr "Remover anexo" -#: src/ui/chatty-info-dialog.ui:17 src/ui/chatty-window.ui:126 +#: src/ui/chatty-info-dialog.ui:17 src/ui/chatty-window.ui:139 msgid "Chat Details" msgstr "Detalhes da conversa" @@ -813,7 +836,7 @@ msgstr "Em branco por padrão" msgid "Group Members" msgstr "Membros do grupo" -#: src/ui/chatty-list-row.ui:151 src/ui/chatty-window.ui:353 +#: src/ui/chatty-list-row.ui:151 src/ui/chatty-window.ui:433 msgid "Call" msgstr "Ligação" @@ -835,7 +858,7 @@ msgstr "Informações avançadas" #: src/ui/chatty-ma-account-details.ui:207 msgid "Homeserver" -msgstr "Servidor doméstico" +msgstr "Servidor" #: src/ui/chatty-ma-account-details.ui:259 msgid "Device ID" @@ -849,7 +872,7 @@ msgstr "ID da conta" msgid "Protocol" msgstr "Protocolo" -#: src/ui/chatty-pp-account-details.ui:168 src/ui/chatty-settings-dialog.ui:626 +#: src/ui/chatty-pp-account-details.ui:168 src/ui/chatty-settings-dialog.ui:701 msgid "Password" msgstr "Senha" @@ -877,7 +900,7 @@ msgstr "_Aplicar" msgid "Accounts" msgstr "Contas" -#: src/ui/chatty-settings-dialog.ui:189 src/ui/chatty-settings-dialog.ui:456 +#: src/ui/chatty-settings-dialog.ui:189 src/ui/chatty-settings-dialog.ui:531 msgid "Privacy" msgstr "Privacidade" @@ -926,71 +949,100 @@ msgstr "Solicitar relatórios de entrega" msgid "SMIL for MMS" msgstr "SMIL para MMS" -#: src/ui/chatty-settings-dialog.ui:366 +#: src/ui/chatty-settings-dialog.ui:384 msgid "MMS Carrier Settings" msgstr "Configurações de MMS da operadora" -#: src/ui/chatty-settings-dialog.ui:370 +#: src/ui/chatty-settings-dialog.ui:388 msgid "MMSC" msgstr "MMSC" -#: src/ui/chatty-settings-dialog.ui:385 +#: src/ui/chatty-settings-dialog.ui:403 msgid "APN" msgstr "Pontos de acesso" -#: src/ui/chatty-settings-dialog.ui:400 +#: src/ui/chatty-settings-dialog.ui:418 msgid "Proxy" msgstr "Proxy" -#: src/ui/chatty-settings-dialog.ui:462 +#: src/ui/chatty-settings-dialog.ui:447 +msgid "You shall not be notified for the messages from blocked contacts" +msgstr "Você não será notificado sobre as mensagens de contatos bloqueados" + +#: src/ui/chatty-settings-dialog.ui:481 +msgid "Blocked chat list empty" +msgstr "Lista de conversas bloqueadas vazia" + +#: src/ui/chatty-settings-dialog.ui:537 msgid "Message Archive Management" msgstr "Gerenciador de mensagens arquivadas" -#: src/ui/chatty-settings-dialog.ui:463 +#: src/ui/chatty-settings-dialog.ui:538 msgid "Sync conversations with chat server" msgstr "Sincronizar conversas com o servidor" -#: src/ui/chatty-settings-dialog.ui:478 +#: src/ui/chatty-settings-dialog.ui:553 msgid "Message Carbon Copies" msgstr "Copiar as mensagens" -#: src/ui/chatty-settings-dialog.ui:662 +#: src/ui/chatty-settings-dialog.ui:737 msgid "Home server" msgstr "Servidor doméstico" -#: src/ui/chatty-settings-dialog.ui:698 +#: src/ui/chatty-settings-dialog.ui:773 msgid "Add _new account…" msgstr "Adicionar _nova conta…" -#: src/ui/chatty-window.ui:28 +#: src/ui/chatty-window.ui:17 +msgctxt "show archived chat list when clicked" +msgid "Archived" +msgstr "Arquivada" + +#: src/ui/chatty-window.ui:41 msgid "Keyboard _Shortcuts" msgstr "Atalhos de _teclado" -#: src/ui/chatty-window.ui:35 +#: src/ui/chatty-window.ui:48 msgid "Help" msgstr "Ajuda" -#: src/ui/chatty-window.ui:44 +#: src/ui/chatty-window.ui:57 msgid "About Chats" msgstr "Sobre o Conversas" -#: src/ui/chatty-window.ui:72 +#: src/ui/chatty-window.ui:85 msgid "New Message…" msgstr "Nova mensagem…" -#: src/ui/chatty-window.ui:85 +#: src/ui/chatty-window.ui:98 msgid "New SMS/MMS Message…" msgstr "Nova mensagem SMS/MMS…" -#: src/ui/chatty-window.ui:98 +#: src/ui/chatty-window.ui:111 msgid "New Group Message…" msgstr "Nova mensagem de grupo…" -#: src/ui/chatty-window.ui:151 +#: src/ui/chatty-window.ui:164 msgid "Leave Chat" msgstr "Sair da conversa" -#: src/ui/chatty-window.ui:164 +#: src/ui/chatty-window.ui:177 +msgid "Block Contact" +msgstr "Bloquear contato" + +#: src/ui/chatty-window.ui:190 +msgid "Unblock Contact" +msgstr "Desbloquear contato" + +#: src/ui/chatty-window.ui:203 +msgid "Archive chat" +msgstr "Conversa arquivada" + +#: src/ui/chatty-window.ui:216 +msgid "Unarchive chat" +msgstr "Conversa desarquivada" + +#: src/ui/chatty-window.ui:229 msgid "Delete Chat" msgstr "Apagar conversa" @@ -1026,6 +1078,9 @@ msgstr "No trabalho: " msgid "Other: " msgstr "Outro: " +#~ msgid "sm.puri.Chatty" +#~ msgstr "sm.puri.Chatty" + #~ msgctxt "timestamp-suffix-seconds" #~ msgid "s" #~ msgstr "s" diff --git a/po/ro.po b/po/ro.po index b0f74220ce1f575c2f642ad6bd22f35cf380631d..8f37e6d1970b4800e54cc94cd078cf808bdb781c 100644 --- a/po/ro.po +++ b/po/ro.po @@ -7,8 +7,8 @@ msgid "" msgstr "" "Project-Id-Version: chatty master\n" "Report-Msgid-Bugs-To: https://source.puri.sm/Librem5/chatty/issues\n" -"POT-Creation-Date: 2022-04-28 15:25+0000\n" -"PO-Revision-Date: 2022-04-28 18:29+0200\n" +"POT-Creation-Date: 2022-09-08 03:24+0000\n" +"PO-Revision-Date: 2022-09-08 16:54+0200\n" "Last-Translator: Florentina MuÈ™at <florentina.musat.28@gmail.com>\n" "Language-Team: Romanian <gnomero-list@lists.sourceforge.net>\n" "Language: ro\n" @@ -21,7 +21,7 @@ msgstr "" "X-Poedit-SourceCharset: UTF-8\n" #: data/sm.puri.Chatty.desktop.in:3 data/sm.puri.Chatty.metainfo.xml.in:6 -#: src/chatty-application.c:333 src/chatty-window.c:330 +#: src/chatty-application.c:338 src/chatty-window.c:339 #: src/ui/chatty-window.ui:265 msgid "Chats" msgstr "DiscuÈ›ii" @@ -155,27 +155,27 @@ msgstr "DiscuÈ›ii este o aplicaÈ›ie de mesagerie XMPP È™i SMS." msgid "Chats message window" msgstr "Fereastra de mesaj pentru DiscuÈ›ii" -#: src/chatty-application.c:76 +#: src/chatty-application.c:78 msgid "Show release version" msgstr "Arată versiunea" -#: src/chatty-application.c:77 +#: src/chatty-application.c:79 msgid "Start in daemon mode" msgstr "PorneÈ™te în modul serviciu" -#: src/chatty-application.c:78 +#: src/chatty-application.c:80 msgid "Disable all accounts" msgstr "Dezactivează toate conturile" -#: src/chatty-application.c:79 +#: src/chatty-application.c:81 msgid "Enable libpurple debug messages" msgstr "Activează mesajele de depanare libpurple" -#: src/chatty-application.c:81 +#: src/chatty-application.c:83 msgid "Enable verbose libpurple debug messages" msgstr "Activează mesajele detaliate de depanare libpurple" -#: src/chatty-application.c:142 +#: src/chatty-application.c:144 #, c-format msgid "" "There was an error displaying help:\n" @@ -232,12 +232,12 @@ msgstr "Mesajele sunt cifrate" msgid "Your messages are not encrypted" msgstr "Mesajele nu sunt cifrate" -#: src/chatty-chat-view.c:414 +#: src/chatty-chat-view.c:420 msgid "Select File..." msgstr "Alege fiÈ™ierul..." -#: src/chatty-chat-view.c:417 src/purple/chatty-purple-request.c:175 -#: src/chatty-window.c:413 src/dialogs/chatty-pp-chat-info.c:90 +#: src/chatty-chat-view.c:423 src/purple/chatty-purple-request.c:175 +#: src/chatty-window.c:422 src/dialogs/chatty-pp-chat-info.c:90 #: src/dialogs/chatty-ma-account-details.c:119 #: src/dialogs/chatty-pp-account-details.c:91 #: src/ui/chatty-dialog-join-muc.ui:21 src/ui/chatty-info-dialog.ui:44 @@ -245,7 +245,7 @@ msgstr "Alege fiÈ™ierul..." msgid "Cancel" msgstr "Anulează" -#: src/chatty-chat-view.c:418 src/purple/chatty-purple-request.c:177 +#: src/chatty-chat-view.c:424 src/purple/chatty-purple-request.c:177 #: src/dialogs/chatty-pp-chat-info.c:89 #: src/dialogs/chatty-ma-account-details.c:118 #: src/dialogs/chatty-pp-account-details.c:90 @@ -349,7 +349,7 @@ msgstr "Moderator" msgid "Member" msgstr "Membru" -#: src/chatty-manager.c:727 +#: src/chatty-manager.c:724 #, c-format msgid "“%s†is not a valid URI" msgstr "„%s†nu este un URI valabil" @@ -358,6 +358,12 @@ msgstr "„%s†nu este un URI valabil" msgid "Copy" msgstr "Copiază" +#: src/chatty-message.c:233 +msgid "Got an encrypted message, but couldn't decrypt due to missing keys" +msgstr "" +"S-a primit un mesaj cifrat, dar nu a putut fi descifrat din cauza lipsei " +"cheilor" + #: src/chatty-notification.c:181 msgid "Open Message" msgstr "Deschide mesajul" @@ -371,7 +377,7 @@ msgstr "Mesaj nou de la %s" msgid "Message Received" msgstr "Mesaj primit" -#: src/purple/chatty-purple-notify.c:44 src/matrix/matrix-utils.c:505 +#: src/purple/chatty-purple-notify.c:44 msgid "Close" msgstr "ÃŽnchide" @@ -401,11 +407,11 @@ msgstr "Utilizatorul %s a adăugat %s la contacte" msgid "Authorize %s?" msgstr "Autorizezi %s?" -#: src/purple/chatty-purple.c:264 src/matrix/matrix-utils.c:509 +#: src/purple/chatty-purple.c:264 msgid "Reject" msgstr "Respinge" -#: src/purple/chatty-purple.c:265 src/matrix/matrix-utils.c:510 +#: src/purple/chatty-purple.c:265 msgid "Accept" msgstr "Acceptă" @@ -414,76 +420,71 @@ msgstr "Acceptă" msgid "Add %s to contact list" msgstr "Adaugă %s la lista de contacte" -#: src/purple/chatty-purple.c:587 +#: src/purple/chatty-purple.c:586 msgid "Login failed" msgstr "Autentificarea a eÈ™uat" -#: src/purple/chatty-purple.c:592 +#: src/purple/chatty-purple.c:591 msgid "Please check ID and password" msgstr "Verifică ID-ul È™i parola" -#: src/chatty-secret-store.c:98 -#, c-format -msgid "Chatty password for \"%s\"" -msgstr "Parola Chatty pentru „%sâ€" - -#: src/chatty-window.c:399 +#: src/chatty-window.c:408 #, c-format msgid "Delete chat with “%sâ€" msgstr "Șterge discuÈ›ia cu „%sâ€" -#: src/chatty-window.c:400 +#: src/chatty-window.c:409 msgid "This deletes the conversation history" msgstr "Aceasta È™terge istoricul conversaÈ›iei" -#: src/chatty-window.c:402 +#: src/chatty-window.c:411 #, c-format msgid "Disconnect group chat “%sâ€" msgstr "Deconectează discuÈ›ia în grup „%sâ€" -#: src/chatty-window.c:403 +#: src/chatty-window.c:412 msgid "This removes chat from chats list" msgstr "Aceasta elimină discuÈ›ia din lista discuÈ›iilor" -#: src/chatty-window.c:415 +#: src/chatty-window.c:424 msgid "Delete" msgstr "Șterge" -#: src/chatty-window.c:493 +#: src/chatty-window.c:502 msgid "You shall no longer be notified for new messages, continue?" msgstr "Nu vei mai fi înÈ™tiinÈ›at dacă vei primi mesaje noi. Continui?" -#: src/chatty-window.c:568 +#: src/chatty-window.c:577 msgid "Archived" msgstr "Arhivată" -#: src/chatty-window.c:621 +#: src/chatty-window.c:630 msgid "An SMS and XMPP messaging client" msgstr "Un client de mesagerie SMS È™i XMPP" -#: src/chatty-window.c:628 +#: src/chatty-window.c:637 msgid "translator-credits" msgstr "" "Florentina MuÈ™at <florentina [dot] musat [dot] 28 [at] gmail [dot] com>, " "2020-2021" -#: src/chatty-window.c:978 +#: src/chatty-window.c:989 msgid "Any Protocol" msgstr "Orice Protocol" -#: src/chatty-window.c:979 src/ui/chatty-settings-dialog.ui:627 +#: src/chatty-window.c:990 src/ui/chatty-settings-dialog.ui:627 msgid "Matrix" msgstr "Matrix" -#: src/chatty-window.c:980 +#: src/chatty-window.c:991 msgid "SMS/MMS" msgstr "SMS/MMS" -#: src/chatty-window.c:983 src/ui/chatty-settings-dialog.ui:615 +#: src/chatty-window.c:994 src/ui/chatty-settings-dialog.ui:615 msgid "XMPP" msgstr "XMPP" -#: src/chatty-window.c:986 src/ui/chatty-settings-dialog.ui:641 +#: src/chatty-window.c:997 src/ui/chatty-settings-dialog.ui:641 msgid "Telegram" msgstr "Telegram" @@ -584,115 +585,93 @@ msgstr "se conectează…" msgid "disconnected" msgstr "deconectat" -#: src/dialogs/chatty-settings-dialog.c:255 -#: src/dialogs/chatty-settings-dialog.c:405 +#: src/dialogs/chatty-settings-dialog.c:251 +#, c-format +msgid "Failed to verify server: %s" +msgstr "Nu s-a putut verifica serverul: %s" + +#: src/dialogs/chatty-settings-dialog.c:253 msgid "Failed to verify server" msgstr "Serverul nu a putut fi verificat" -#: src/dialogs/chatty-settings-dialog.c:306 -#: src/dialogs/chatty-settings-dialog.c:385 +#: src/dialogs/chatty-settings-dialog.c:259 msgid "Couldn't get Home server address" msgstr "Nu s-a putut obÈ›ine adresa serverului de acasă" -#: src/dialogs/chatty-settings-dialog.c:510 +#: src/dialogs/chatty-settings-dialog.c:428 #: src/ui/chatty-ma-account-details.ui:182 #: src/ui/chatty-pp-account-details.ui:199 msgid "Delete Account" msgstr "Șterge contul" -#: src/dialogs/chatty-settings-dialog.c:513 +#: src/dialogs/chatty-settings-dialog.c:431 #, c-format msgid "Delete account %s?" msgstr "Ștergi contul %s?" -#: src/dialogs/chatty-settings-dialog.c:654 +#: src/dialogs/chatty-settings-dialog.c:582 msgid "Restart chatty to disable purple" msgstr "ReporneÈ™te Chatty pentru a dezactiva purple" -#: src/dialogs/chatty-settings-dialog.c:656 +#: src/dialogs/chatty-settings-dialog.c:584 #: src/ui/chatty-settings-dialog.ui:512 msgid "Enable purple plugin" msgstr "Activează modulul purple" -#: src/dialogs/chatty-settings-dialog.c:670 +#: src/dialogs/chatty-settings-dialog.c:598 #: src/ui/chatty-settings-dialog.ui:279 msgid "SMS and MMS Settings" msgstr "OpÈ›iuni SMS È™i MMS" -#: src/dialogs/chatty-settings-dialog.c:672 +#: src/dialogs/chatty-settings-dialog.c:600 #: src/ui/chatty-settings-dialog.ui:298 msgid "Purple Settings" msgstr "OpÈ›iuni purple" -#: src/dialogs/chatty-settings-dialog.c:674 +#: src/dialogs/chatty-settings-dialog.c:602 msgid "New Account" msgstr "Cont nou" -#: src/dialogs/chatty-settings-dialog.c:676 +#: src/dialogs/chatty-settings-dialog.c:604 #: src/ui/chatty-settings-dialog.ui:364 msgid "Blocked Contacts" msgstr "Contacte blocate" -#: src/dialogs/chatty-settings-dialog.c:678 src/ui/chatty-settings-dialog.ui:7 +#: src/dialogs/chatty-settings-dialog.c:606 src/ui/chatty-settings-dialog.ui:7 #: src/ui/chatty-window.ui:30 msgid "Preferences" msgstr "PreferinÈ›e" -#: src/dialogs/chatty-settings-dialog.c:707 +#: src/dialogs/chatty-settings-dialog.c:634 msgid "Select Protocol" msgstr "Alege protocolul" #. TRANSLATORS: Only translate 'or' -#: src/dialogs/chatty-settings-dialog.c:948 +#: src/dialogs/chatty-settings-dialog.c:878 msgid "@user:matrix.org or user@example.com" msgstr "@user:matrix.org sau user@example.com" -#: src/dialogs/chatty-settings-dialog.c:1057 +#: src/dialogs/chatty-settings-dialog.c:987 msgid "Unblock contact" msgstr "Deblochează contactul" -#: src/matrix/chatty-ma-account.c:296 +#: src/matrix/chatty-ma-account.c:105 msgid "Incorrect password" msgstr "Parolă greÈ™ită" -#: src/matrix/chatty-ma-account.c:299 +#: src/matrix/chatty-ma-account.c:108 msgid "_OK" msgstr "_OK" -#: src/matrix/chatty-ma-account.c:300 src/ui/chatty-dialog-new-chat.ui:74 +#: src/matrix/chatty-ma-account.c:109 src/ui/chatty-dialog-new-chat.ui:74 #: src/ui/chatty-settings-dialog.ui:47 src/ui/chatty-settings-dialog.ui:108 msgid "_Cancel" msgstr "_Anulează" -#: src/matrix/chatty-ma-account.c:306 -#, c-format -msgid "Please enter password for “%sâ€" -msgstr "Introdu parola pentru „%sâ€" - -#: src/matrix/matrix-utils.c:474 -#, c-format -msgid "The certificate for ‘%s’ has unknown CA" -msgstr "Certificatul pentru „%s†are CA necunoscută" - -#: src/matrix/matrix-utils.c:476 -#, c-format -msgid "The certificate for ‘%s’ is self-signed" -msgstr "Certificatul pentru „%s†este autosemnat" - -#: src/matrix/matrix-utils.c:480 -#, c-format -msgid "The certificate for ‘%s’ has expired" -msgstr "Certificatul pentru „%s†a expirat" - -#: src/matrix/matrix-utils.c:484 -#, c-format -msgid "The certificate for ‘%s’ has been revoked" -msgstr "Certificatul pentru „%s†a fost anulat" - -#: src/matrix/matrix-utils.c:493 +#: src/matrix/chatty-ma-account.c:115 #, c-format -msgid "Error validating certificate for ‘%s’" -msgstr "Eroare la validarea certificatului pentru „%sâ€" +msgid "Please enter password for “%sâ€, homeserver: %s" +msgstr "Introdu parola pentru „%sâ€, server de acasă: %s" #. TRANSLATORS: Timestamp for minute accuracy, e.g. “2020-08-11 15:27â€. #. See https://developer.gnome.org/glib/stable/glib-GDateTime.html#g-date-time-format @@ -1070,5 +1049,3 @@ msgstr "Serviciu: " msgid "Other: " msgstr "Altele: " -#~ msgid "sm.puri.Chatty" -#~ msgstr "sm.puri.Chatty" diff --git a/po/sr.po b/po/sr.po index 9766363f55bcba573bf8306628ab97dd6ebc9081..36c3784c457df800239c8c380d30d47c5605f1af 100644 --- a/po/sr.po +++ b/po/sr.po @@ -7,8 +7,8 @@ msgid "" msgstr "" "Project-Id-Version: chatty master\n" "Report-Msgid-Bugs-To: https://source.puri.sm/Librem5/chatty/issues\n" -"POT-Creation-Date: 2020-09-15 15:25+0000\n" -"PO-Revision-Date: 2020-09-15 23:52+0200\n" +"POT-Creation-Date: 2022-08-08 03:24+0000\n" +"PO-Revision-Date: 2022-08-20 09:11+0200\n" "Last-Translator: Марко Ðœ. КоÑтић <marko.m.kostic@gmail.com>\n" "Language-Team: Serbian <gnome-sr@googlegroups.com>\n" "Language: sr\n" @@ -17,17 +17,14 @@ msgstr "" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=4; plural=n==1? 3 : n%10==1 && n%100!=11 ? 0 : n" "%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2;\n" -"X-Generator: Poedit 2.4.1\n" +"X-Generator: Poedit 3.1.1\n" #: data/sm.puri.Chatty.desktop.in:3 data/sm.puri.Chatty.metainfo.xml.in:6 -#: src/chatty-application.c:304 src/ui/chatty-window.ui:225 +#: src/chatty-application.c:336 src/chatty-window.c:339 +#: src/ui/chatty-window.ui:265 msgid "Chats" msgstr "Разговори" -#: data/sm.puri.Chatty.desktop.in:5 -msgid "sm.puri.Chatty" -msgstr "sm.puri.Chatty" - #: data/sm.puri.Chatty.desktop.in:6 msgid "SMS and XMPP chat application" msgstr "СМС и ИкÑ-МПП апликација за разговоре" @@ -62,7 +59,7 @@ msgstr "Да ли треба Ñлати Ñтање поруке ако је пр msgid "Message carbon copies" msgstr "Индиго копије поруке" -#: data/sm.puri.Chatty.gschema.xml:26 src/ui/chatty-settings-dialog.ui:153 +#: data/sm.puri.Chatty.gschema.xml:26 src/ui/chatty-settings-dialog.ui:554 msgid "Share chat history among devices" msgstr "Дели иÑторијат разговора између уређаја" @@ -75,17 +72,21 @@ msgid "Enable MAM archive synchronization from the server" msgstr "Омогући уÑклађивање ÐœÐÐœ архива Ñа Ñервера" #: data/sm.puri.Chatty.gschema.xml:37 +msgid "Enable purple" +msgstr "Омогући библиотеку purple" + +#: data/sm.puri.Chatty.gschema.xml:38 src/ui/chatty-settings-dialog.ui:511 +msgid "Enable purple accounts" +msgstr "Омогући purple налоге" + +#: data/sm.puri.Chatty.gschema.xml:43 msgid "Send typing notifications" msgstr "Шаљи обавештења о куцању" -#: data/sm.puri.Chatty.gschema.xml:38 +#: data/sm.puri.Chatty.gschema.xml:44 msgid "Whether to Send typing notifications" msgstr "Да ли треба Ñлати обавештења о куцању" -#: data/sm.puri.Chatty.gschema.xml:43 data/sm.puri.Chatty.gschema.xml:44 -msgid "Mark offline users differently" -msgstr "Означи кориÑнике ван мреже другачије" - #: data/sm.puri.Chatty.gschema.xml:49 data/sm.puri.Chatty.gschema.xml:50 msgid "Mark Idle users differently" msgstr "Означи кориÑнике у мировању другачије" @@ -111,18 +112,34 @@ msgid "Whether pressing Enter key sends the message" msgstr "Да ли Ñе порука шаље притиÑком на таÑтер Enter" #: data/sm.puri.Chatty.gschema.xml:73 +msgid "Request SMS delivery reports" +msgstr "Затражи извештаје о доÑтави СМС-а" + +#: data/sm.puri.Chatty.gschema.xml:74 +msgid "Whether to request delivery reports for outgoing SMS" +msgstr "Да ли треба тражити извештаје о доÑтави одлазних СМС-ова" + +#: data/sm.puri.Chatty.gschema.xml:79 +msgid "Enable experimental features" +msgstr "Омогући пробне функције" + +#: data/sm.puri.Chatty.gschema.xml:80 +msgid "Whether to enable experimental features" +msgstr "Да ли треба омогућити екÑперименталне функције" + +#: data/sm.puri.Chatty.gschema.xml:85 msgid "Window maximized" msgstr "Увећан прозор" -#: data/sm.puri.Chatty.gschema.xml:74 +#: data/sm.puri.Chatty.gschema.xml:86 msgid "Window maximized state" msgstr "Стање увећаног прозора" -#: data/sm.puri.Chatty.gschema.xml:79 +#: data/sm.puri.Chatty.gschema.xml:91 msgid "Window size" msgstr "Величина прозора" -#: data/sm.puri.Chatty.gschema.xml:80 +#: data/sm.puri.Chatty.gschema.xml:92 msgid "Window size (width, height)." msgstr "Величина прозора (ширина, виÑина)." @@ -138,674 +155,1084 @@ msgstr "Разговори је апликација за поруке која msgid "Chats message window" msgstr "Прозор Ñа порукама разговора" -#: src/chatty-application.c:68 +#: src/chatty-application.c:78 msgid "Show release version" msgstr "Прикажи издање програма" -#: src/chatty-application.c:69 +#: src/chatty-application.c:79 msgid "Start in daemon mode" msgstr "Покрени у режиму демона" -#: src/chatty-application.c:70 +#: src/chatty-application.c:80 msgid "Disable all accounts" msgstr "Онемогући Ñве налоге" -#: src/chatty-application.c:71 +#: src/chatty-application.c:81 msgid "Enable libpurple debug messages" msgstr "Омогући libpurple поруке Ñа грешкама" -#: src/chatty-application.c:72 +#: src/chatty-application.c:83 msgid "Enable verbose libpurple debug messages" msgstr "Омогући причљиве libpurple поруке Ñа грешкама" -#: src/chatty-application.c:108 +#: src/chatty-application.c:144 #, c-format -msgid "Authorize %s?" -msgstr "ОвлаÑтити „%s“?" +msgid "" +"There was an error displaying help:\n" +"%s" +msgstr "" +"Догодила Ñе грешка приликом приказа помоћи:\n" +"%s" -#: src/chatty-application.c:112 -msgid "Reject" -msgstr "Одбаци" +#: src/chatty-avatar.c:158 src/chatty-contact-list.c:391 +msgid "Send To" +msgstr "Пошаљи на" -#: src/chatty-application.c:114 -msgid "Accept" -msgstr "Прихвати" +#: src/chatty-chat-list.c:175 +msgid "Select a contact with the <b>“+â€</b> button in the titlebar." +msgstr "" +"Изабери контакт Ñа дугметом <b>\"+\"</b> које Ñе налази у наÑловној траци." + +#: src/chatty-chat-list.c:179 +msgid "Add instant messaging accounts in Preferences." +msgstr "Додајте налоге брзог ћаÑкања у ПоÑтавкама." + +#: src/chatty-chat-list.c:215 src/chatty-contact-list.c:282 +msgid "No Search Results" +msgstr "Ðема резултата претраге" + +#: src/chatty-chat-list.c:216 +msgid "Try different search" +msgstr "Пробајте другачију претрагу" + +#: src/chatty-chat-list.c:220 +msgid "No archived chats" +msgstr "Ðема архивираних ћаÑкања" + +#: src/chatty-chat-list.c:222 +msgid "Start Chatting" +msgstr "Започни разговор" + +#: src/chatty-chat-view.c:212 +msgid "This is an SMS conversation" +msgstr "Ово је СМС разговор" + +#: src/chatty-chat-view.c:214 src/chatty-chat-view.c:220 +msgid "Your messages are not encrypted, and carrier rates may apply" +msgstr "" +"Ваше поруке ниÑу шифроване, и накнаде доÑтављача уÑлуге могу бити примењене" + +#: src/chatty-chat-view.c:218 +msgid "This is an IM conversation" +msgstr "Ово је разговор преко брзих порука" + +#: src/chatty-chat-view.c:224 +msgid "Your messages are encrypted" +msgstr "Ваше поруке Ñу шифроване" + +#: src/chatty-chat-view.c:227 +msgid "Your messages are not encrypted" +msgstr "Ваше поруке ниÑу шифроване" + +#: src/chatty-chat-view.c:420 +msgid "Select File..." +msgstr "Изабери датотеку…" + +#: src/chatty-chat-view.c:423 src/purple/chatty-purple-request.c:175 +#: src/chatty-window.c:422 src/dialogs/chatty-pp-chat-info.c:90 +#: src/dialogs/chatty-ma-account-details.c:119 +#: src/dialogs/chatty-pp-account-details.c:91 +#: src/ui/chatty-dialog-join-muc.ui:21 src/ui/chatty-info-dialog.ui:44 +#: src/ui/chatty-info-dialog.ui:54 +msgid "Cancel" +msgstr "Откажи" + +#: src/chatty-chat-view.c:424 src/purple/chatty-purple-request.c:177 +#: src/dialogs/chatty-pp-chat-info.c:89 +#: src/dialogs/chatty-ma-account-details.c:118 +#: src/dialogs/chatty-pp-account-details.c:90 +msgid "Open" +msgstr "Отвори" + +#: src/chatty-chat.c:635 +msgid "Empty room" +msgstr "Празна Ñоба" -#: src/chatty-application.c:119 +#. TRANSLATORS: %s are name/user-id/phone numbers of two users +#: src/chatty-chat.c:642 #, c-format -msgid "Add %s to contact list" -msgstr "Додај контакт „%s“ у ÑпиÑак контаката" +msgid "%s and %s" +msgstr "%s и %s" -#: src/chatty-application.c:142 -msgid "Contact added" -msgstr "Контакт је додат" +#: src/chatty-chat.c:644 +#, c-format +msgid "%s and %u other" +msgid_plural "%s and %u others" +msgstr[0] "%s и %u других" +msgstr[1] "%s и %u других" +msgstr[2] "%s и %u других" +msgstr[3] "%s и %u других" + +#: src/chatty-contact-list.c:247 src/dialogs/chatty-mm-chat-info.c:121 +msgid "Unknown Contact" +msgstr "Ðепознати контакт" + +#: src/chatty-contact-list.c:283 +msgid "Try different search, or type a valid number to create new chat" +msgstr "Пробајте другачију претрагу или укуцајте иÑправан број за ново ћаÑкање" + +#: src/chatty-contact-list.c:287 +msgid "No Contacts" +msgstr "Ðема контаката" + +#: src/chatty-clock.c:86 +msgid "Just Now" +msgstr "Управо Ñада" + +#. TRANSLATORS: Timestamp with 12 hour time, e.g. “06∶42 PMâ€. +#. * See https://docs.gtk.org/glib/method.DateTime.format.html +#. +#: src/chatty-clock.c:95 src/chatty-clock.c:125 +msgid "%I∶%M %p" +msgstr "%I∶%M %p" -#: src/chatty-application.c:145 +#: src/chatty-clock.c:104 #, c-format -msgid "User %s has added %s to the contacts" -msgstr "КориÑник „%s“ је додао контакт „%s“" +msgid "%lu minute ago" +msgid_plural "%lu minutes ago" +msgstr[0] "пре %lu минута" +msgstr[1] "пре %lu минута" +msgstr[2] "пре %lu минута" +msgstr[3] "пре %lu минута" + +#: src/chatty-clock.c:112 +msgid "Today %H∶%M" +msgstr "Ð”Ð°Ð½Ð°Ñ Ñƒ %H:%M" + +#. TRANSLATORS: Timestamp with 12 hour time, e.g. “Today 06∶42 PMâ€. +#. * See https://docs.gtk.org/glib/method.DateTime.format.html +#. +#: src/chatty-clock.c:120 +msgid "Today %I∶%M %p" +msgstr "Ð”Ð°Ð½Ð°Ñ Ñƒ %I:%M %p" -#: src/chatty-application.c:165 -msgid "Login failed" -msgstr "Пријава није уÑпела" +#: src/chatty-clock.c:132 +msgid "Yesterday %H∶%M" +msgstr "Јуче у %H:%M" -#: src/chatty-application.c:171 -msgid "Please check ID and password" -msgstr "Проверите ИБ и лозинку" +#. TRANSLATORS: Timestamp with 12 hour time, e.g. “Yesterday 06∶42 PMâ€. +#. * See https://docs.gtk.org/glib/method.DateTime.format.html +#. +#: src/chatty-clock.c:137 +msgid "Yesterday %I∶%M %p" +msgstr "Јуче у %I:%M %p" -#: src/chatty-chat-view.c:70 src/chatty-chat-view.c:75 -msgid "This is an IM conversation." -msgstr "Ово је разговор преко брзих порука." +#. TRANSLATORS: Timestamp from more than 7 days ago or future date +#. * (eg: when the system time is wrong), e.g. “2022-01-01â€. +#. * See https://docs.gtk.org/glib/method.DateTime.format.html +#. +#: src/chatty-clock.c:153 +msgid "%Y-%m-%d" +msgstr "%d.%m.%Y" -#: src/chatty-chat-view.c:71 src/chatty-chat-view.c:81 -msgid "Your messages are not encrypted," -msgstr "Ваше поруке ниÑу шифроване," +#. TRANSLATORS: %s is the Device ID +#: src/chatty-fp-row.c:131 +#, c-format +msgid "Device ID %s fingerprint:" +msgstr "УређајÑког ИБ-ја %s отиÑак:" -#: src/chatty-chat-view.c:72 -msgid "ask your counterpart to use E2EE." -msgstr "" -"питајте Ñаговорника да иÑкориÑти \n" -"шифровање Ñ-краја-на-крај." - -#: src/chatty-chat-view.c:76 -msgid "Your messages are secured" -msgstr "Ваше поруке Ñу безбедне" - -#: src/chatty-chat-view.c:77 -msgid "by end-to-end encryption." -msgstr "због шифровања Ñ-краја-на-крај." - -#: src/chatty-chat-view.c:80 -msgid "This is an SMS conversation." -msgstr "Ово је СМС разговор." - -#: src/chatty-chat-view.c:82 -msgid "and carrier rates may apply." -msgstr "и наплата трошкова оператера је могућа." - -#. Translators: Timestamp seconds suffix -#: src/chatty-list-row.c:73 -msgctxt "timestamp-suffix-seconds" -msgid "s" -msgstr "Ñ" - -#. Translators: Timestamp minute suffix -#: src/chatty-list-row.c:75 -msgctxt "timestamp-suffix-minute" -msgid "m" -msgstr "м" - -#. Translators: Timestamp minutes suffix -#: src/chatty-list-row.c:77 -msgctxt "timestamp-suffix-minutes" -msgid "m" -msgstr "м" - -#. Translators: Timestamp hour suffix -#: src/chatty-list-row.c:79 -msgctxt "timestamp-suffix-hour" -msgid "h" -msgstr "ч" - -#. Translators: Timestamp hours suffix -#: src/chatty-list-row.c:81 -msgctxt "timestamp-suffix-hours" -msgid "h" -msgstr "ч" - -#. Translators: Timestamp day suffix -#: src/chatty-list-row.c:83 -msgctxt "timestamp-suffix-day" -msgid "d" -msgstr "д" - -#. Translators: Timestamp days suffix -#: src/chatty-list-row.c:85 -msgctxt "timestamp-suffix-days" -msgid "d" -msgstr "д" - -#. Translators: Timestamp month suffix -#: src/chatty-list-row.c:87 -msgctxt "timestamp-suffix-month" -msgid "mo" -msgstr "меÑ." - -#. Translators: Timestamp months suffix -#: src/chatty-list-row.c:89 -msgctxt "timestamp-suffix-months" -msgid "mos" -msgstr "меÑ." - -#. Translators: Timestamp year suffix -#: src/chatty-list-row.c:91 -msgctxt "timestamp-suffix-year" -msgid "y" -msgstr "г" - -#. Translators: Timestamp years suffix -#: src/chatty-list-row.c:93 -msgctxt "timestamp-suffix-years" -msgid "y" -msgstr "г" - -#. Translators: Timestamp prefix (e.g. Over 5h) -#: src/chatty-list-row.c:195 -msgid "Over" -msgstr "Више од" - -#. Translators: Timestamp prefix (e.g. Almost 5h) -#: src/chatty-list-row.c:200 -msgid "Almost" -msgstr "Скоро" - -#: src/chatty-list-row.c:217 +#: src/chatty-list-row.c:118 msgid "Owner" msgstr "ВлаÑник" -#: src/chatty-list-row.c:220 +#: src/chatty-list-row.c:121 msgid "Moderator" msgstr "Модератор" -#: src/chatty-list-row.c:223 +#: src/chatty-list-row.c:124 msgid "Member" msgstr "Члан" -#: src/chatty-manager.c:891 +#: src/chatty-manager.c:729 #, c-format -msgid "New message from %s" -msgstr "Ðова порука од кориÑника „%s“" +msgid "“%s†is not a valid URI" +msgstr "%s није иÑправан УРИ" -#: src/chatty-message-row.c:89 +#: src/chatty-message-row.c:82 msgid "Copy" msgstr "Копирај" -#: src/chatty-notify.c:67 +#: src/chatty-notification.c:181 msgid "Open Message" msgstr "Отвори поруку" -#: src/chatty-notify.c:72 +#: src/chatty-notification.c:214 +#, c-format +msgid "New message from %s" +msgstr "Ðова порука од кориÑника „%s“" + +#: src/chatty-notification.c:216 msgid "Message Received" msgstr "Порука је примљена" -#: src/chatty-notify.c:78 -msgid "Message Error" -msgstr "Грешка у поруци" - -#: src/chatty-notify.c:84 -msgid "Account Info" -msgstr "Подаци налога" - -#: src/chatty-notify.c:90 -msgid "Account Connected" -msgstr "Ðалог је повезан" - -#: src/chatty-purple-notify.c:44 +#: src/purple/chatty-purple-notify.c:44 src/matrix/matrix-utils.c:504 msgid "Close" msgstr "Затвори" -#: src/chatty-purple-request.c:186 +#: src/purple/chatty-purple-request.c:170 msgid "Save File..." msgstr "Сачувај датотеку..." -#: src/chatty-purple-request.c:187 +#: src/purple/chatty-purple-request.c:171 msgid "Open File..." msgstr "Отвори датотеку..." -#: src/chatty-purple-request.c:191 src/chatty-window.c:587 -#: src/dialogs/chatty-settings-dialog.c:443 -#: src/dialogs/chatty-user-info-dialog.c:66 src/ui/chatty-dialog-join-muc.ui:16 -#: src/ui/chatty-dialog-muc-info.ui:56 -msgid "Cancel" -msgstr "Откажи" - -#: src/chatty-purple-request.c:193 +#: src/purple/chatty-purple-request.c:177 msgid "Save" msgstr "Сачувај" -#: src/chatty-purple-request.c:193 src/dialogs/chatty-settings-dialog.c:442 -#: src/dialogs/chatty-user-info-dialog.c:65 -msgid "Open" -msgstr "Отвори" - -#. TRANSLATORS: Timestamp from the last week with 24 hour time, e.g. “Tuesday 18∶42â€. -#. See https://developer.gnome.org/glib/stable/glib-GDateTime.html#g-date-time-format -#. -#: src/chatty-utils.c:291 -msgid "%A %H∶%M" -msgstr "%A %H∶%M" +#: src/purple/chatty-purple.c:240 +msgid "Contact added" +msgstr "Контакт је додат" -#. TRANSLATORS: Timestamp from the last week with 12 hour time, e.g. “Tuesday 06∶42 PMâ€. -#. See https://developer.gnome.org/glib/stable/glib-GDateTime.html#g-date-time-format -#. -#: src/chatty-utils.c:296 -msgid "%A %I∶%M %p" -msgstr "%A %I∶%M %p" +#: src/purple/chatty-purple.c:242 +#, c-format +msgid "User %s has added %s to the contacts" +msgstr "КориÑник „%s“ је додао контакт „%s“" -#. TRANSLATORS: Timestamp from more than 7 days ago, e.g. “2020-08-11â€. -#. See https://developer.gnome.org/glib/stable/glib-GDateTime.html#g-date-time-format -#. -#: src/chatty-utils.c:303 -msgid "%Y-%m-%d" -msgstr "%d.%m.%Y" +#: src/purple/chatty-purple.c:262 +#, c-format +msgid "Authorize %s?" +msgstr "ОвлаÑтити „%s“?" -#: src/chatty-window.c:104 src/chatty-window.c:109 src/chatty-window.c:114 -msgid "Choose a contact" -msgstr "Изабери контакт" +#: src/purple/chatty-purple.c:264 src/matrix/matrix-utils.c:508 +msgid "Reject" +msgstr "Одбаци" -#: src/chatty-window.c:105 -msgid "" -"Select an <b>SMS</b> or <b>Instant Message</b> contact with the <b>\"+\"</b> " -"button in the titlebar." -msgstr "" -"Изабери опцију <b>СМС</b> или <b>Брза порука</b> за контакт уз помоћ дугмета " -"<b>\"+\"</b> које Ñе налази у наÑловној траци." +#: src/purple/chatty-purple.c:265 src/matrix/matrix-utils.c:509 +msgid "Accept" +msgstr "Прихвати" -#: src/chatty-window.c:110 -msgid "" -"Select an <b>Instant Message</b> contact with the \"+\" button in the " -"titlebar." -msgstr "" -"Изабери опцију <b>Брза порука</b> за контакт уз помоћ дугмета <b>\"+\"</b> " -"које Ñе налази у наÑловној траци." +#: src/purple/chatty-purple.c:268 +#, c-format +msgid "Add %s to contact list" +msgstr "Додај контакт „%s“ у ÑпиÑак контаката" -#: src/chatty-window.c:115 -msgid "Start a <b>SMS</b> chat with the \"+\" button in the titlebar." -msgstr "" -"Започни <b>СМС</b> разговор уз помоћ дугмета <b>\"+\"</b> које Ñе налази у " -"наÑловној траци." +#: src/purple/chatty-purple.c:587 +msgid "Login failed" +msgstr "Пријава није уÑпела" -#: src/chatty-window.c:116 src/chatty-window.c:120 -msgid "" -"For <b>Instant Messaging</b> add or activate an account in <i>\"preferences" -"\"</i>." -msgstr "" -"За опцију <b>Брза порука</b> додајте или активирајте налог у одељку " -"<i>„поÑтавке“</i>." +#: src/purple/chatty-purple.c:592 +msgid "Please check ID and password" +msgstr "Проверите ИБ и лозинку" -#: src/chatty-window.c:119 -msgid "Start chatting" -msgstr "Започни разговор" +#: src/chatty-secret-store.c:99 +#, c-format +msgid "Chatty password for \"%s\"" +msgstr "Лозинка Разговора за „%s“" -#: src/chatty-window.c:572 -msgid "Delete chat with" -msgstr "Обриши разговор Ñа" +#: src/chatty-window.c:408 +#, c-format +msgid "Delete chat with “%sâ€" +msgstr "Обриши разговор Ñа „%s“" -#: src/chatty-window.c:573 +#: src/chatty-window.c:409 msgid "This deletes the conversation history" msgstr "Ово брише иÑторијат разговора" -#: src/chatty-window.c:575 -msgid "Disconnect group chat" -msgstr "Откачи групно разговор" +#: src/chatty-window.c:411 +#, c-format +msgid "Disconnect group chat “%sâ€" +msgstr "Откачи групно разговор „%s“" -#: src/chatty-window.c:576 +#: src/chatty-window.c:412 msgid "This removes chat from chats list" msgstr "Ово уклања разговор Ñа ÑпиÑка разговора" -#: src/chatty-window.c:589 +#: src/chatty-window.c:424 msgid "Delete" msgstr "Обриши" -#: src/chatty-window.c:799 +#: src/chatty-window.c:502 +msgid "You shall no longer be notified for new messages, continue?" +msgstr "Више нећете примати обавештења о новим порукама, наÑтавити?" + +#: src/chatty-window.c:577 +msgid "Archived" +msgstr "Ðрхивирано" + +#: src/chatty-window.c:630 msgid "An SMS and XMPP messaging client" msgstr "Клијент за СМС и ИкÑ-МПП разговор" -#: src/chatty-window.c:806 +#: src/chatty-window.c:637 msgid "translator-credits" msgstr "" "Марко Ðœ. КоÑтић <marko.m.kostic@gmail.com>\n" "\n" "https://гном.Ñрб — превод Гнома на ÑрпÑки језик" -#: src/dialogs/chatty-muc-info-dialog.c:319 -msgid "members" -msgstr "чланови" +#: src/chatty-window.c:989 +msgid "Any Protocol" +msgstr "Било који протокол" -#: src/dialogs/chatty-new-chat-dialog.c:148 -msgid "Send To" -msgstr "Пошаљи на" +#: src/chatty-window.c:990 src/ui/chatty-settings-dialog.ui:627 +msgid "Matrix" +msgstr "МатрикÑ" + +#: src/chatty-window.c:991 +msgid "SMS/MMS" +msgstr "СМС/ММС" + +#: src/chatty-window.c:994 src/ui/chatty-settings-dialog.ui:615 +msgid "XMPP" +msgstr "ИкÑ-МПП" + +#: src/chatty-window.c:997 src/ui/chatty-settings-dialog.ui:641 +msgid "Telegram" +msgstr "Телеграм" + +#: src/dialogs/chatty-mm-chat-info.c:140 +msgctxt "Refer to self in contact list" +msgid "You" +msgstr "Ја" + +#: src/dialogs/chatty-pp-chat-info.c:86 +#: src/dialogs/chatty-ma-account-details.c:115 +#: src/dialogs/chatty-pp-account-details.c:87 +msgid "Set Avatar" +msgstr "ПоÑтави аватар" + +#: src/dialogs/chatty-pp-chat-info.c:96 +#: src/dialogs/chatty-ma-account-details.c:125 +#: src/dialogs/chatty-pp-account-details.c:97 +msgid "Images" +msgstr "Слике" -#: src/dialogs/chatty-new-chat-dialog.c:192 +#: src/dialogs/chatty-pp-chat-info.c:206 +msgid "Encryption is not available" +msgstr "Шифровање није доÑтупно" + +#: src/dialogs/chatty-pp-chat-info.c:210 +msgid "This chat is encrypted" +msgstr "Овај разговор је шифрован" + +#: src/dialogs/chatty-pp-chat-info.c:214 +msgid "This chat is not encrypted" +msgstr "Овај разговор није шифрован" + +#: src/dialogs/chatty-pp-chat-info.c:238 +msgid "Encryption not available" +msgstr "Шифровање недоÑтупно" + +#: src/dialogs/chatty-pp-chat-info.c:259 +#, c-format +msgid "%u Member" +msgid_plural "%u Members" +msgstr[0] "%u члан" +msgstr[1] "%u члана" +msgstr[2] "%u чланова" +msgstr[3] "%u члан" + +#: src/dialogs/chatty-pp-chat-info.c:293 +msgid "Phone Number:" +msgstr "Број телефона:" + +#: src/dialogs/chatty-pp-chat-info.c:295 +msgid "XMPP ID:" +msgstr "ИкÑ-МПП ИБ:" + +#: src/dialogs/chatty-pp-chat-info.c:300 +msgid "Matrix ID:" +msgstr "ÐœÐ°Ñ‚Ñ€Ð¸ÐºÑ Ð˜Ð‘:" + +#: src/dialogs/chatty-pp-chat-info.c:303 +msgid "Telegram ID:" +msgstr "Телеграм ИБ:" + +#: src/dialogs/chatty-new-chat-dialog.c:171 +msgid "Add" +msgstr "Додај" + +#: src/dialogs/chatty-new-chat-dialog.c:181 +msgid "Create" +msgstr "Ðаправи" + +#: src/dialogs/chatty-new-chat-dialog.c:187 +msgid "Add Contact" +msgstr "Додај контакт" + +#: src/dialogs/chatty-new-chat-dialog.c:224 #, c-format msgid "Error opening GNOME Contacts: %s" msgstr "Грешка при отварању Гномових Контаката: %s" -#: src/dialogs/chatty-settings-dialog.c:319 -msgid "Select Protocol" -msgstr "Изабери протокол" +#: src/dialogs/chatty-new-chat-dialog.c:452 +msgid "SMS" +msgstr "СМС" -#: src/dialogs/chatty-settings-dialog.c:324 -msgid "Add XMPP account" -msgstr "Додај ИкÑ-МПП налог" +#: src/dialogs/chatty-new-chat-dialog.c:640 +msgid "You" +msgstr "Ви" -#: src/dialogs/chatty-settings-dialog.c:355 +#: src/dialogs/chatty-ma-account-details.c:385 +#: src/dialogs/chatty-pp-account-details.c:178 msgid "connected" msgstr "повезан" -#: src/dialogs/chatty-settings-dialog.c:357 +#: src/dialogs/chatty-ma-account-details.c:387 +#: src/dialogs/chatty-pp-account-details.c:180 msgid "connecting…" msgstr "повезујем Ñе…" -#: src/dialogs/chatty-settings-dialog.c:359 +#: src/dialogs/chatty-ma-account-details.c:389 +#: src/dialogs/chatty-pp-account-details.c:182 msgid "disconnected" msgstr "откачен" -#: src/dialogs/chatty-settings-dialog.c:439 -#: src/dialogs/chatty-user-info-dialog.c:62 -msgid "Set Avatar" -msgstr "ПоÑтави аватар" - -#: src/dialogs/chatty-settings-dialog.c:509 -#: src/ui/chatty-settings-dialog.ui:477 +#: src/dialogs/chatty-settings-dialog.c:267 +#, c-format +msgid "Failed to verify server: %s" +msgstr "ÐиÑам уÑпео да потврдим Ñервер: %s" + +#: src/dialogs/chatty-settings-dialog.c:269 +#: src/dialogs/chatty-settings-dialog.c:479 +msgid "Failed to verify server" +msgstr "ÐиÑам уÑпео да потврдим Ñервер" + +#: src/dialogs/chatty-settings-dialog.c:275 +#: src/dialogs/chatty-settings-dialog.c:459 +msgid "Couldn't get Home server address" +msgstr "Ðе могу добавити адреÑу кућног Ñервера" + +#: src/dialogs/chatty-settings-dialog.c:584 +#: src/ui/chatty-ma-account-details.ui:182 +#: src/ui/chatty-pp-account-details.ui:199 msgid "Delete Account" msgstr "Обриши налог" -#: src/dialogs/chatty-settings-dialog.c:512 +#: src/dialogs/chatty-settings-dialog.c:587 #, c-format msgid "Delete account %s?" msgstr "ОбриÑати налог „%s“?" -#: src/dialogs/chatty-user-info-dialog.c:136 -msgid "Encryption not available" -msgstr "Шифровање недоÑтупно" +#: src/dialogs/chatty-settings-dialog.c:728 +msgid "Restart chatty to disable purple" +msgstr "Поново покрените ћаÑкање за онемогућавање библиотеке purple" -#: src/dialogs/chatty-user-info-dialog.c:174 -msgid "Encryption is not available" -msgstr "Шифровање није доÑтупно" +#: src/dialogs/chatty-settings-dialog.c:730 +#: src/ui/chatty-settings-dialog.ui:512 +msgid "Enable purple plugin" +msgstr "Омогући purple прикључак" -#: src/dialogs/chatty-user-info-dialog.c:176 -msgid "This chat is encrypted" -msgstr "Овај разговор је шифрован" +#: src/dialogs/chatty-settings-dialog.c:744 +#: src/ui/chatty-settings-dialog.ui:279 +msgid "SMS and MMS Settings" +msgstr "СМС/ММС подешавања" -#: src/dialogs/chatty-user-info-dialog.c:178 -msgid "This chat is not encrypted" -msgstr "Овај разговор није шифрован" +#: src/dialogs/chatty-settings-dialog.c:746 +#: src/ui/chatty-settings-dialog.ui:298 +msgid "Purple Settings" +msgstr "Purple подешавања" -#: src/dialogs/chatty-user-info-dialog.c:246 -msgid "Phone Number:" -msgstr "Број телефона:" +#: src/dialogs/chatty-settings-dialog.c:748 +msgid "New Account" +msgstr "Ðови налог" + +#: src/dialogs/chatty-settings-dialog.c:750 +#: src/ui/chatty-settings-dialog.ui:364 +msgid "Blocked Contacts" +msgstr "Блокирани контакти" + +#: src/dialogs/chatty-settings-dialog.c:752 src/ui/chatty-settings-dialog.ui:7 +#: src/ui/chatty-window.ui:30 +msgid "Preferences" +msgstr "ПоÑтавке" + +#: src/dialogs/chatty-settings-dialog.c:781 +msgid "Select Protocol" +msgstr "Изабери протокол" + +#. TRANSLATORS: Only translate 'or' +#: src/dialogs/chatty-settings-dialog.c:1022 +msgid "@user:matrix.org or user@example.com" +msgstr "@korisnik:matrix.org или korisnik@primer.rs" + +#: src/dialogs/chatty-settings-dialog.c:1131 +msgid "Unblock contact" +msgstr "Деблокирај контакт" -#: src/ui/chatty-dialog-join-muc.ui:12 +#: src/matrix/chatty-ma-account.c:231 +msgid "Incorrect password" +msgstr "Ðетачна лозинка" + +#: src/matrix/chatty-ma-account.c:234 +msgid "_OK" +msgstr "У _реду" + +#: src/matrix/chatty-ma-account.c:235 src/ui/chatty-dialog-new-chat.ui:74 +#: src/ui/chatty-settings-dialog.ui:47 src/ui/chatty-settings-dialog.ui:108 +msgid "_Cancel" +msgstr "_Откажи" + +#: src/matrix/chatty-ma-account.c:241 +#, c-format +msgid "Please enter password for “%sâ€, homeserver: %s" +msgstr "УнеÑите лозинку за „%s“, кућни Ñервер: %s" + +#: src/matrix/matrix-utils.c:473 +#, c-format +msgid "The certificate for ‘%s’ has unknown CA" +msgstr "Сертификат за „%s“ Ñадржи непознато Ñертификационо тело" + +#: src/matrix/matrix-utils.c:475 +#, c-format +msgid "The certificate for ‘%s’ is self-signed" +msgstr "Сертификат за „%s“ је ÑамопотпиÑан" + +#: src/matrix/matrix-utils.c:479 +#, c-format +msgid "The certificate for ‘%s’ has expired" +msgstr "Сертификат за „%s“ је иÑтекао" + +#: src/matrix/matrix-utils.c:483 +#, c-format +msgid "The certificate for ‘%s’ has been revoked" +msgstr "Сертификат за „%s“ је опозван" + +#: src/matrix/matrix-utils.c:492 +#, c-format +msgid "Error validating certificate for ‘%s’" +msgstr "Грешка приликом потврђивања Ñертификата за „%s“" + +#. TRANSLATORS: Timestamp for minute accuracy, e.g. “2020-08-11 15:27â€. +#. See https://developer.gnome.org/glib/stable/glib-GDateTime.html#g-date-time-format +#. +#: src/mm/chatty-mmsd.c:827 +msgid "%Y-%m-%d %H∶%M" +msgstr "%d.%m.%Y %H∶%M" + +#: src/mm/chatty-mmsd.c:1111 +#, c-format +msgid "You received an MMS, but it expired on: %s" +msgstr "Добили Ñте ММС али је он иÑтекао у: %s" + +#: src/mm/chatty-mmsd.c:1114 +msgid "You received an empty MMS." +msgstr "Добили Ñте празан ММС." + +#: src/ui/chatty-dialog-join-muc.ui:17 msgid "New Group Chat" msgstr "Ðови групни разговор" -#: src/ui/chatty-dialog-join-muc.ui:28 +#: src/ui/chatty-dialog-join-muc.ui:34 msgid "Join Chat" msgstr "Придружи Ñе разговору" -#: src/ui/chatty-dialog-join-muc.ui:81 src/ui/chatty-dialog-new-chat.ui:237 +#: src/ui/chatty-dialog-join-muc.ui:82 src/ui/chatty-dialog-new-chat.ui:269 msgid "Select chat account" msgstr "Изабери налог ћаÑкања" -#: src/ui/chatty-dialog-join-muc.ui:155 +#: src/ui/chatty-dialog-join-muc.ui:152 msgid "Password (optional)" msgstr "Лозинка (изборно)" -#: src/ui/chatty-dialog-muc-info.ui:26 -msgid "Group Details" -msgstr "ПојединоÑти групе" +#: src/ui/chatty-dialog-new-chat.ui:126 +msgid "Group Title" +msgstr "Ðазив групе" -#: src/ui/chatty-dialog-muc-info.ui:52 -msgid "Invite Contact" -msgstr "Позови контакт" +#: src/ui/chatty-dialog-new-chat.ui:177 +msgid "Add members from contacts…" +msgstr "Додај чланове из контаката…" -#: src/ui/chatty-dialog-muc-info.ui:69 +#: src/ui/chatty-dialog-new-chat.ui:223 +msgid "Send To:" +msgstr "Пошаљи на:" + +#: src/ui/chatty-dialog-new-chat.ui:308 +msgid "Name (optional)" +msgstr "Име (изборно)" + +#: src/ui/chatty-dialog-new-chat.ui:351 +msgid "Add to Contacts" +msgstr "Додај у контакте" + +#: src/ui/chatty-file-item.ui:28 +msgid "Remove Attachment" +msgstr "Уклони прилог" + +#: src/ui/chatty-info-dialog.ui:17 src/ui/chatty-window.ui:139 +msgid "Chat Details" +msgstr "ПојединоÑти разговора" + +#: src/ui/chatty-info-dialog.ui:64 +msgid "Apply" +msgstr "Примени" + +#: src/ui/chatty-info-dialog.ui:82 msgid "Invite" msgstr "Позови" -#: src/ui/chatty-dialog-muc-info.ui:166 +#: src/ui/chatty-ma-chat-info.ui:67 src/ui/chatty-ma-account-details.ui:233 +msgid "Matrix ID" +msgstr "ÐœÐ°Ñ‚Ñ€Ð¸ÐºÑ Ð˜Ð‘" + +#: src/ui/chatty-ma-chat-info.ui:93 src/ui/chatty-pp-chat-info.ui:289 +msgid "Chat settings" +msgstr "Подешавања разговора" + +#: src/ui/chatty-ma-chat-info.ui:113 src/ui/chatty-pp-chat-info.ui:238 +#: src/ui/chatty-pp-chat-info.ui:344 +msgid "Encryption" +msgstr "Шифровање" + +#: src/ui/chatty-ma-chat-info.ui:114 src/ui/chatty-pp-chat-info.ui:345 +msgid "Encrypt Messages" +msgstr "Шифроване поруке" + +#: src/ui/chatty-pp-chat-info.ui:43 src/ui/chatty-pp-account-details.ui:60 +msgid "Change Avatar" +msgstr "Измени аватар" + +#: src/ui/chatty-pp-chat-info.ui:69 src/ui/chatty-pp-account-details.ui:34 +msgid "Delete Avatar" +msgstr "Обриши аватар" + +#: src/ui/chatty-pp-chat-info.ui:108 msgid "Room topic" msgstr "Тема Ñобе" -#: src/ui/chatty-dialog-muc-info.ui:225 -msgid "Room settings" -msgstr "Подешавања Ñобе" +#: src/ui/chatty-pp-chat-info.ui:211 +msgid "XMPP ID" +msgstr "ИкÑ-МПП ИБ" + +#: src/ui/chatty-pp-chat-info.ui:264 src/ui/chatty-ma-account-details.ui:67 +#: src/ui/chatty-pp-account-details.ui:142 +msgid "Status" +msgstr "Стање" -#: src/ui/chatty-dialog-muc-info.ui:248 src/ui/chatty-dialog-user-info.ui:209 +#: src/ui/chatty-pp-chat-info.ui:310 msgid "Notifications" msgstr "Обавештења" -#: src/ui/chatty-dialog-muc-info.ui:249 -msgid "Show notification badge" -msgstr "Прикажи врпцу обавештења" - -#: src/ui/chatty-dialog-muc-info.ui:265 +#: src/ui/chatty-pp-chat-info.ui:327 msgid "Status Messages" msgstr "Поруке Ñтања" -#: src/ui/chatty-dialog-muc-info.ui:266 +#: src/ui/chatty-pp-chat-info.ui:328 msgid "Show status messages in chat" msgstr "Прикажи поруке Ñтања у разговору" -#: src/ui/chatty-dialog-muc-info.ui:289 -msgid "0 members" -msgstr "0 чланова" +#: src/ui/chatty-pp-chat-info.ui:367 +msgid "Fingerprints" +msgstr "ОтиÑци прÑта" -#: src/ui/chatty-dialog-muc-info.ui:377 -msgid "Invite Message" -msgstr "Позивница" +#: src/ui/chatty-mm-chat-info.ui:29 +msgid "Title" +msgstr "ÐаÑлов" -#: src/ui/chatty-dialog-new-chat.ui:26 -msgid "Start Chat" -msgstr "Започни разговор" +#: src/ui/chatty-mm-chat-info.ui:33 +msgid "Blank for default" +msgstr "Подразумевано празно" -#: src/ui/chatty-dialog-new-chat.ui:55 -msgid "New Contact" -msgstr "Ðови контакт" +#: src/ui/chatty-mm-chat-info.ui:43 +msgid "Group Members" +msgstr "Чланови групе" -#: src/ui/chatty-dialog-new-chat.ui:83 src/ui/chatty-window.ui:126 -msgid "Add Contact" -msgstr "Додај контакт" +#: src/ui/chatty-list-row.ui:151 src/ui/chatty-window.ui:433 +msgid "Call" +msgstr "Позив" -#: src/ui/chatty-dialog-new-chat.ui:149 -msgid "Send To:" -msgstr "Пошаљи на:" +#: src/ui/chatty-ma-account-details.ui:93 +msgid "Name" +msgstr "Име" -#: src/ui/chatty-dialog-new-chat.ui:288 -msgid "Name (optional)" -msgstr "Име (изборно)" +#: src/ui/chatty-ma-account-details.ui:123 +msgid "Email" +msgstr "Е-адреÑа" -#: src/ui/chatty-dialog-new-chat.ui:328 src/ui/chatty-window.ui:152 -msgid "Add to Contacts" -msgstr "Додај у контакте" +#: src/ui/chatty-ma-account-details.ui:152 +msgid "Phone" +msgstr "Телефон" -#: src/ui/chatty-dialog-user-info.ui:12 src/ui/chatty-window.ui:112 -msgid "Chat Details" -msgstr "ПојединоÑти разговора" +#: src/ui/chatty-ma-account-details.ui:196 +msgid "Advanced information" +msgstr "Ðапредни подаци" -#: src/ui/chatty-dialog-user-info.ui:96 -msgid "XMPP ID" -msgstr "ИкÑ-МПП ИБ" +#: src/ui/chatty-ma-account-details.ui:207 +msgid "Homeserver" +msgstr "Кућни Ñервер" -#: src/ui/chatty-dialog-user-info.ui:111 src/ui/chatty-dialog-user-info.ui:225 -msgid "Encryption" -msgstr "Шифровање" +#: src/ui/chatty-ma-account-details.ui:259 +msgid "Device ID" +msgstr "ИБ уређаја" -#: src/ui/chatty-dialog-user-info.ui:126 src/ui/chatty-settings-dialog.ui:390 -msgid "Status" -msgstr "Стање" +#: src/ui/chatty-pp-account-details.ui:92 +msgid "Account ID" +msgstr "ИБ налога" -#: src/ui/chatty-dialog-user-info.ui:226 -msgid "Secure messaging using OMEMO" -msgstr "Безбедно допиÑивање уз ОМЕМО" +#: src/ui/chatty-pp-account-details.ui:116 +msgid "Protocol" +msgstr "Протокол" -#: src/ui/chatty-dialog-user-info.ui:247 -msgid "Fingerprints" -msgstr "ОтиÑци прÑта" +#: src/ui/chatty-pp-account-details.ui:168 src/ui/chatty-settings-dialog.ui:701 +msgid "Password" +msgstr "Лозинка" -#: src/ui/chatty-settings-dialog.ui:12 src/ui/chatty-window.ui:18 -msgid "Preferences" -msgstr "ПоÑтавке" +#: src/ui/chatty-pp-account-details.ui:213 +msgid "Own Fingerprint" +msgstr "СопÑтвени отиÑак прÑта" -#: src/ui/chatty-settings-dialog.ui:21 +#: src/ui/chatty-settings-dialog.ui:27 msgid "Back" msgstr "Ðазад" -#: src/ui/chatty-settings-dialog.ui:41 +#: src/ui/chatty-settings-dialog.ui:60 msgid "_Add" msgstr "Дод_ај" -#: src/ui/chatty-settings-dialog.ui:57 +#: src/ui/chatty-settings-dialog.ui:76 msgid "_Save" msgstr "_Сачувај" -#: src/ui/chatty-settings-dialog.ui:91 +#: src/ui/chatty-settings-dialog.ui:92 +msgid "_Apply" +msgstr "_Примени" + +#: src/ui/chatty-settings-dialog.ui:169 msgid "Accounts" msgstr "Ðалози" -#: src/ui/chatty-settings-dialog.ui:105 -msgid "Add new account…" -msgstr "Додај нови налог…" - -#: src/ui/chatty-settings-dialog.ui:117 +#: src/ui/chatty-settings-dialog.ui:189 src/ui/chatty-settings-dialog.ui:531 msgid "Privacy" msgstr "ПриватноÑÑ‚" -#: src/ui/chatty-settings-dialog.ui:122 +#: src/ui/chatty-settings-dialog.ui:195 msgid "Message Receipts" msgstr "Повратнице порука" -#: src/ui/chatty-settings-dialog.ui:123 +#: src/ui/chatty-settings-dialog.ui:196 msgid "Confirm received messages" msgstr "Потврди примљене поруке" -#: src/ui/chatty-settings-dialog.ui:137 -msgid "Message Archive Management" -msgstr "Управљање архивама порука" - -#: src/ui/chatty-settings-dialog.ui:138 -msgid "Sync conversations with chat server" -msgstr "УÑклади разговоре Ñа Ñервером разговора" - -#: src/ui/chatty-settings-dialog.ui:152 -msgid "Message Carbon Copies" -msgstr "Индиго копије поруке" - -#: src/ui/chatty-settings-dialog.ui:167 +#: src/ui/chatty-settings-dialog.ui:211 msgid "Typing Notification" msgstr "Обавештења куцања" -#: src/ui/chatty-settings-dialog.ui:168 +#: src/ui/chatty-settings-dialog.ui:212 msgid "Send typing messages" msgstr "Шаљи поруке о куцању" -#: src/ui/chatty-settings-dialog.ui:185 -msgid "Chats List" -msgstr "СпиÑак разговора" +#: src/ui/chatty-settings-dialog.ui:230 +msgid "Editor" +msgstr "Уређивач" -#: src/ui/chatty-settings-dialog.ui:190 -msgid "Indicate Offline Contacts" -msgstr "ИÑтакни контакте ван мреже" +#: src/ui/chatty-settings-dialog.ui:235 +msgid "Graphical Emoticons" +msgstr "Графички емотикони" -#: src/ui/chatty-settings-dialog.ui:191 -msgid "Grey out avatars from offline contacts" -msgstr "ЗаÑиви аватаре контаката ван мреже" +#: src/ui/chatty-settings-dialog.ui:236 +msgid "if you type :) it will be changed to 😃" +msgstr "Ðко укуцате :) промениће Ñе у 😃" -#: src/ui/chatty-settings-dialog.ui:205 -msgid "Indicate Idle Contacts" -msgstr "ИÑтакни контакте у мировању" +#. TRANSLATORS: Return is the Enter key. +#: src/ui/chatty-settings-dialog.ui:251 +msgid "Send Messages with Return" +msgstr "Шаљи поруке Ñа УнеÑи таÑтером" -#: src/ui/chatty-settings-dialog.ui:206 -msgid "Blur avatars from idle contacts" -msgstr "Замути аватаре контаката у мировању" +#: src/ui/chatty-settings-dialog.ui:267 +msgid "Protocol Settings" +msgstr "Подешавања протокола" -#: src/ui/chatty-settings-dialog.ui:220 -msgid "Indicate Unknown Contacts" -msgstr "ИÑтакни непознате контакте" +#: src/ui/chatty-settings-dialog.ui:333 +msgid "Request Delivery Reports" +msgstr "Захтевај извештаје о доÑтави" -#: src/ui/chatty-settings-dialog.ui:221 -msgid "Color unknown contact ID red" -msgstr "Обоји црвеном ИБ-јеве непознатих контаката" +#: src/ui/chatty-settings-dialog.ui:348 +msgid "SMIL for MMS" +msgstr "СМИЛ за ММС" -#: src/ui/chatty-settings-dialog.ui:238 -msgid "Editor" -msgstr "Уређивач" +#: src/ui/chatty-settings-dialog.ui:384 +msgid "MMS Carrier Settings" +msgstr "Подешавања ММС доÑтављача" -#: src/ui/chatty-settings-dialog.ui:243 -msgid "Graphical Emoticons" -msgstr "Графички емотикони" +#: src/ui/chatty-settings-dialog.ui:388 +msgid "MMSC" +msgstr "ММСЦ" -#: src/ui/chatty-settings-dialog.ui:244 -msgid "Convert ASCII emoticons" -msgstr "Претвори текÑтуалне емотиконе" +#: src/ui/chatty-settings-dialog.ui:403 +msgid "APN" +msgstr "ÐПÐ" -#: src/ui/chatty-settings-dialog.ui:258 -msgid "Return = Send Message" -msgstr "Enter = пошаљи поруку" +#: src/ui/chatty-settings-dialog.ui:418 +msgid "Proxy" +msgstr "ПоÑредник" -#: src/ui/chatty-settings-dialog.ui:259 -msgid "Send message with return key" -msgstr "Шаљи поруку преко таÑтера Enter" +#: src/ui/chatty-settings-dialog.ui:447 +msgid "You shall not be notified for the messages from blocked contacts" +msgstr "Ðећете добијати обавештења о порукама блокираних контаката" -#: src/ui/chatty-settings-dialog.ui:328 -msgid "Account ID" -msgstr "ИБ налога" +#: src/ui/chatty-settings-dialog.ui:481 +msgid "Blocked chat list empty" +msgstr "СпиÑак блокираних ћаÑкања је празан" -#: src/ui/chatty-settings-dialog.ui:361 -msgid "Protocol" -msgstr "Протокол" +#: src/ui/chatty-settings-dialog.ui:537 +msgid "Message Archive Management" +msgstr "Управљање архивама порука" -#: src/ui/chatty-settings-dialog.ui:419 src/ui/chatty-settings-dialog.ui:716 -msgid "Password" -msgstr "Лозинка" +#: src/ui/chatty-settings-dialog.ui:538 +msgid "Sync conversations with chat server" +msgstr "УÑклади разговоре Ñа Ñервером разговора" -#: src/ui/chatty-settings-dialog.ui:494 -msgid "Own Fingerprint" -msgstr "СопÑтвени отиÑак прÑта" +#: src/ui/chatty-settings-dialog.ui:553 +msgid "Message Carbon Copies" +msgstr "Индиго копије поруке" -#: src/ui/chatty-settings-dialog.ui:520 -msgid "Other Devices" -msgstr "Други уређаји" +#: src/ui/chatty-settings-dialog.ui:737 +msgid "Home server" +msgstr "Кућни Ñервер" -#: src/ui/chatty-settings-dialog.ui:604 -msgid "XMPP" -msgstr "ИкÑ-МПП" +#: src/ui/chatty-settings-dialog.ui:773 +msgid "Add _new account…" +msgstr "Додај _нови налог…" -#: src/ui/chatty-settings-dialog.ui:618 -msgid "Matrix" -msgstr "МатрикÑ" +#: src/ui/chatty-window.ui:17 +msgctxt "show archived chat list when clicked" +msgid "Archived" +msgstr "Ðрхивирано" -#: src/ui/chatty-settings-dialog.ui:633 -msgid "Telegram" -msgstr "Телеграм" +#: src/ui/chatty-window.ui:41 +msgid "Keyboard _Shortcuts" +msgstr "Пр_ечице на таÑтатури" -#: src/ui/chatty-settings-dialog.ui:691 -msgid "Provider" -msgstr "ДоÑтављач" +#: src/ui/chatty-window.ui:48 +msgid "Help" +msgstr "Помоћ" -#: src/ui/chatty-window.ui:31 +#: src/ui/chatty-window.ui:57 msgid "About Chats" msgstr "О Разговорима" -#: src/ui/chatty-window.ui:59 +#: src/ui/chatty-window.ui:85 msgid "New Message…" msgstr "Ðова порука…" -#: src/ui/chatty-window.ui:72 +#: src/ui/chatty-window.ui:98 +msgid "New SMS/MMS Message…" +msgstr "Ðова СМС/ММС порука…" + +#: src/ui/chatty-window.ui:111 msgid "New Group Message…" msgstr "Ðова групна порука…" -#: src/ui/chatty-window.ui:85 -msgid "New Bulk SMS…" -msgstr "Ðови вишеÑтруки СМС…" - -#: src/ui/chatty-window.ui:177 +#: src/ui/chatty-window.ui:164 msgid "Leave Chat" msgstr "ÐапуÑти разговор" +#: src/ui/chatty-window.ui:177 +msgid "Block Contact" +msgstr "Блокирај контакт" + #: src/ui/chatty-window.ui:190 +msgid "Unblock Contact" +msgstr "Деблокирај контакт" + +#: src/ui/chatty-window.ui:203 +msgid "Archive chat" +msgstr "Ðрхивирај ћаÑкање" + +#: src/ui/chatty-window.ui:216 +msgid "Unarchive chat" +msgstr "Деархивирај ћаÑкање" + +#: src/ui/chatty-window.ui:229 msgid "Delete Chat" msgstr "Обриши разговор" -#: src/users/chatty-contact.c:313 +#: src/ui/help-overlay.ui:14 +msgctxt "shortcut window" +msgid "General" +msgstr "Опште" + +#: src/ui/help-overlay.ui:19 +msgctxt "shortcut window" +msgid "Open Menu" +msgstr "Отвори мени" + +#: src/ui/help-overlay.ui:27 +msgctxt "shortcut window" +msgid "Open Search" +msgstr "Отворите претрагу" + +#: src/ui/help-overlay.ui:35 +msgctxt "shortcut window" +msgid "Show Shortcuts" +msgstr "Прикажи пречице" + +#: src/users/chatty-contact.c:325 msgid "Mobile: " -msgstr "Мобилни:" +msgstr "Мобилни: " -#: src/users/chatty-contact.c:315 +#: src/users/chatty-contact.c:327 msgid "Work: " -msgstr "ПоÑао:" +msgstr "ПоÑао: " -#: src/users/chatty-contact.c:317 +#: src/users/chatty-contact.c:329 msgid "Other: " -msgstr "Друго:" +msgstr "Друго: " + +#~ msgid "sm.puri.Chatty" +#~ msgstr "sm.puri.Chatty" + +#~ msgctxt "timestamp-suffix-seconds" +#~ msgid "s" +#~ msgstr "Ñ" + +#~ msgctxt "timestamp-suffix-minute" +#~ msgid "m" +#~ msgstr "м" + +#~ msgctxt "timestamp-suffix-minutes" +#~ msgid "m" +#~ msgstr "м" + +#~ msgctxt "timestamp-suffix-hour" +#~ msgid "h" +#~ msgstr "ч" + +#~ msgctxt "timestamp-suffix-hours" +#~ msgid "h" +#~ msgstr "ч" + +#~ msgctxt "timestamp-suffix-day" +#~ msgid "d" +#~ msgstr "д" + +#~ msgctxt "timestamp-suffix-days" +#~ msgid "d" +#~ msgstr "д" + +#~ msgctxt "timestamp-suffix-month" +#~ msgid "mo" +#~ msgstr "меÑ." + +#~ msgctxt "timestamp-suffix-months" +#~ msgid "mos" +#~ msgstr "меÑ." + +#~ msgctxt "timestamp-suffix-year" +#~ msgid "y" +#~ msgstr "г" + +#~ msgctxt "timestamp-suffix-years" +#~ msgid "y" +#~ msgstr "г" + +#~ msgid "Over" +#~ msgstr "Више од" + +#~ msgid "Almost" +#~ msgstr "Скоро" + +#, fuzzy, c-format +#~| msgid "Error opening GNOME Contacts: %s" +#~ msgid "Error saving contact: %s" +#~ msgstr "Грешка при отварању Гномових Контаката: %s" + +#~ msgid "Start Chat" +#~ msgstr "Започни разговор" + +#~ msgid "Chats List" +#~ msgstr "СпиÑак разговора" + +#~ msgid "Indicate Idle Contacts" +#~ msgstr "ИÑтакни контакте у мировању" + +#~ msgid "Blur avatars from idle contacts" +#~ msgstr "Замути аватаре контаката у мировању" + +#~ msgid "Color unknown contact ID red" +#~ msgstr "Обоји црвеном ИБ-јеве непознатих контаката" + +#~ msgid "Convert ASCII emoticons" +#~ msgstr "Претвори текÑтуалне емотиконе" + +#~ msgid "Return = Send Message" +#~ msgstr "Enter = пошаљи поруку" + +#~ msgid "Matrix Home Server" +#~ msgstr "ÐœÐ°Ñ‚Ñ€Ð¸ÐºÑ ÐºÑƒÑ›Ð½Ð¸ Ñервер" + +#~ msgid "_Accept" +#~ msgstr "Прихв_ати" + +#~ msgid "Mark offline users differently" +#~ msgstr "Означи кориÑнике ван мреже другачије" + +#~ msgid "ask your counterpart to use E2EE." +#~ msgstr "" +#~ "питајте Ñаговорника да иÑкориÑти \n" +#~ "шифровање Ñ-краја-на-крај." + +#~ msgid "Your messages are secured" +#~ msgstr "Ваше поруке Ñу безбедне" + +#~ msgid "by end-to-end encryption." +#~ msgstr "због шифровања Ñ-краја-на-крај." + +#~ msgid "and carrier rates may apply." +#~ msgstr "и наплата трошкова оператера је могућа." + +#~ msgid "Message Error" +#~ msgstr "Грешка у поруци" + +#~ msgid "Account Info" +#~ msgstr "Подаци налога" + +#~ msgid "Account Connected" +#~ msgstr "Ðалог је повезан" + +#~ msgid "Choose a contact" +#~ msgstr "Изабери контакт" + +#~ msgid "" +#~ "Select an <b>SMS</b> or <b>Instant Message</b> contact with the <b>\"+\"</" +#~ "b> button in the titlebar." +#~ msgstr "" +#~ "Изабери опцију <b>СМС</b> или <b>Брза порука</b> за контакт уз помоћ " +#~ "дугмета <b>\"+\"</b> које Ñе налази у наÑловној траци." + +#~ msgid "Start a <b>SMS</b> chat with the \"+\" button in the titlebar." +#~ msgstr "" +#~ "Започни <b>СМС</b> разговор уз помоћ дугмета <b>\"+\"</b> које Ñе налази " +#~ "у наÑловној траци." + +#~ msgid "members" +#~ msgstr "чланови" + +#~ msgid "Add XMPP account" +#~ msgstr "Додај ИкÑ-МПП налог" + +#~ msgid "Show notification badge" +#~ msgstr "Прикажи врпцу обавештења" + +#~ msgid "0 members" +#~ msgstr "0 чланова" + +#~ msgid "Secure messaging using OMEMO" +#~ msgstr "Безбедно допиÑивање уз ОМЕМО" + +#~ msgid "Indicate Offline Contacts" +#~ msgstr "ИÑтакни контакте ван мреже" + +#~ msgid "Grey out avatars from offline contacts" +#~ msgstr "ЗаÑиви аватаре контаката ван мреже" + +#~ msgid "Other Devices" +#~ msgstr "Други уређаји" + +#~ msgid "Provider" +#~ msgstr "ДоÑтављач" + +#~ msgid "New Bulk SMS…" +#~ msgstr "Ðови вишеÑтруки СМС…" diff --git a/po/sv.po b/po/sv.po index 1ebe8a55ff8f25fbc07afde74074b99070a6ccd3..35380ab6b63410f5396ede9200ba5df3451a010a 100644 --- a/po/sv.po +++ b/po/sv.po @@ -8,8 +8,8 @@ msgid "" msgstr "" "Project-Id-Version: chatty master\n" "Report-Msgid-Bugs-To: https://source.puri.sm/Librem5/chatty/issues\n" -"POT-Creation-Date: 2022-04-26 15:24+0000\n" -"PO-Revision-Date: 2022-04-26 19:31+0200\n" +"POT-Creation-Date: 2022-09-08 03:24+0000\n" +"PO-Revision-Date: 2022-09-07 17:59+0200\n" "Last-Translator: Anders Jonsson <anders.jonsson@norsjovallen.se>\n" "Language-Team: Swedish <tp-sv@listor.tp-sv.se>\n" "Language: sv\n" @@ -17,18 +17,14 @@ msgstr "" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=2; plural=(n != 1);\n" -"X-Generator: Poedit 3.0.1\n" +"X-Generator: Poedit 3.1.1\n" #: data/sm.puri.Chatty.desktop.in:3 data/sm.puri.Chatty.metainfo.xml.in:6 -#: src/chatty-application.c:333 src/chatty-window.c:330 +#: src/chatty-application.c:338 src/chatty-window.c:339 #: src/ui/chatty-window.ui:265 msgid "Chats" msgstr "Chattar" -#: data/sm.puri.Chatty.desktop.in:5 -msgid "sm.puri.Chatty" -msgstr "sm.puri.Chatty" - #: data/sm.puri.Chatty.desktop.in:6 msgid "SMS and XMPP chat application" msgstr "SMS- och XMPP-chattprogram" @@ -157,27 +153,27 @@ msgstr "Chattar är ett meddelandeprogram med stöd för XMPP och SMS." msgid "Chats message window" msgstr "Meddelandefönster för Chattar" -#: src/chatty-application.c:76 +#: src/chatty-application.c:78 msgid "Show release version" msgstr "Visa utgÃ¥vans version" -#: src/chatty-application.c:77 +#: src/chatty-application.c:79 msgid "Start in daemon mode" msgstr "Starta i demonläge" -#: src/chatty-application.c:78 +#: src/chatty-application.c:80 msgid "Disable all accounts" msgstr "Inaktivera alla konton" -#: src/chatty-application.c:79 +#: src/chatty-application.c:81 msgid "Enable libpurple debug messages" msgstr "Aktivera libpurple-felsökningsmeddelanden" -#: src/chatty-application.c:81 +#: src/chatty-application.c:83 msgid "Enable verbose libpurple debug messages" msgstr "Aktivera utförliga libpurple-felsökningsmeddelanden" -#: src/chatty-application.c:142 +#: src/chatty-application.c:144 #, c-format msgid "" "There was an error displaying help:\n" @@ -234,12 +230,12 @@ msgstr "Dina meddelanden krypteras" msgid "Your messages are not encrypted" msgstr "Dina meddelanden krypteras inte" -#: src/chatty-chat-view.c:414 +#: src/chatty-chat-view.c:420 msgid "Select File..." msgstr "Välj fil…" -#: src/chatty-chat-view.c:417 src/purple/chatty-purple-request.c:175 -#: src/chatty-window.c:413 src/dialogs/chatty-pp-chat-info.c:90 +#: src/chatty-chat-view.c:423 src/purple/chatty-purple-request.c:175 +#: src/chatty-window.c:422 src/dialogs/chatty-pp-chat-info.c:90 #: src/dialogs/chatty-ma-account-details.c:119 #: src/dialogs/chatty-pp-account-details.c:91 #: src/ui/chatty-dialog-join-muc.ui:21 src/ui/chatty-info-dialog.ui:44 @@ -247,7 +243,7 @@ msgstr "Välj fil…" msgid "Cancel" msgstr "Avbryt" -#: src/chatty-chat-view.c:418 src/purple/chatty-purple-request.c:177 +#: src/chatty-chat-view.c:424 src/purple/chatty-purple-request.c:177 #: src/dialogs/chatty-pp-chat-info.c:89 #: src/dialogs/chatty-ma-account-details.c:118 #: src/dialogs/chatty-pp-account-details.c:90 @@ -305,25 +301,25 @@ msgstr[1] "%lu minuter sedan" #: src/chatty-clock.c:112 msgid "Today %H∶%M" -msgstr "Idag %H∶%M" +msgstr "I dag %H∶%M" #. TRANSLATORS: Timestamp with 12 hour time, e.g. “Today 06∶42 PMâ€. #. * See https://docs.gtk.org/glib/method.DateTime.format.html #. #: src/chatty-clock.c:120 msgid "Today %I∶%M %p" -msgstr "Idag %I∶%M %p" +msgstr "I dag %I∶%M %p" #: src/chatty-clock.c:132 msgid "Yesterday %H∶%M" -msgstr "IgÃ¥r %H∶%M" +msgstr "I gÃ¥r %H∶%M" #. TRANSLATORS: Timestamp with 12 hour time, e.g. “Yesterday 06∶42 PMâ€. #. * See https://docs.gtk.org/glib/method.DateTime.format.html #. #: src/chatty-clock.c:137 msgid "Yesterday %I∶%M %p" -msgstr "IgÃ¥r %I∶%M %p" +msgstr "I gÃ¥r %I∶%M %p" #. TRANSLATORS: Timestamp from more than 7 days ago or future date #. * (eg: when the system time is wrong), e.g. “2022-01-01â€. @@ -351,7 +347,7 @@ msgstr "Moderator" msgid "Member" msgstr "Medlem" -#: src/chatty-manager.c:727 +#: src/chatty-manager.c:724 #, c-format msgid "“%s†is not a valid URI" msgstr "â€%s†är inte en giltig URI" @@ -360,6 +356,12 @@ msgstr "â€%s†är inte en giltig URI" msgid "Copy" msgstr "Kopiera" +#: src/chatty-message.c:233 +msgid "Got an encrypted message, but couldn't decrypt due to missing keys" +msgstr "" +"Fick ett krypterat meddelande, men kunde inte dekryptera pÃ¥ grund av saknade " +"nycklar" + #: src/chatty-notification.c:181 msgid "Open Message" msgstr "Öppna meddelande" @@ -373,7 +375,7 @@ msgstr "Nytt meddelande frÃ¥n %s" msgid "Message Received" msgstr "Meddelande togs emot" -#: src/purple/chatty-purple-notify.c:44 src/matrix/matrix-utils.c:505 +#: src/purple/chatty-purple-notify.c:44 msgid "Close" msgstr "Stäng" @@ -403,11 +405,11 @@ msgstr "Användaren %s har lagt till %s till kontakterna" msgid "Authorize %s?" msgstr "Auktorisera %s?" -#: src/purple/chatty-purple.c:264 src/matrix/matrix-utils.c:509 +#: src/purple/chatty-purple.c:264 msgid "Reject" msgstr "Avvisa" -#: src/purple/chatty-purple.c:265 src/matrix/matrix-utils.c:510 +#: src/purple/chatty-purple.c:265 msgid "Accept" msgstr "Acceptera" @@ -416,76 +418,71 @@ msgstr "Acceptera" msgid "Add %s to contact list" msgstr "Lägg till %s till kontaktlista" -#: src/purple/chatty-purple.c:587 +#: src/purple/chatty-purple.c:586 msgid "Login failed" msgstr "Inloggning misslyckades" -#: src/purple/chatty-purple.c:592 +#: src/purple/chatty-purple.c:591 msgid "Please check ID and password" msgstr "Kontrollera ID och lösenord" -#: src/chatty-secret-store.c:98 -#, c-format -msgid "Chatty password for \"%s\"" -msgstr "Chatty-lösenord för â€%sâ€" - -#: src/chatty-window.c:399 +#: src/chatty-window.c:408 #, c-format msgid "Delete chat with “%sâ€" msgstr "Ta bort chatt med â€%sâ€" -#: src/chatty-window.c:400 +#: src/chatty-window.c:409 msgid "This deletes the conversation history" msgstr "Det här tar bort meddelandehistoriken" -#: src/chatty-window.c:402 +#: src/chatty-window.c:411 #, c-format msgid "Disconnect group chat “%sâ€" msgstr "Lämna gruppchatten â€%sâ€" -#: src/chatty-window.c:403 +#: src/chatty-window.c:412 msgid "This removes chat from chats list" msgstr "Detta tar bort chatten frÃ¥n chattlistan" -#: src/chatty-window.c:415 +#: src/chatty-window.c:424 msgid "Delete" msgstr "Ta bort" -#: src/chatty-window.c:493 +#: src/chatty-window.c:502 msgid "You shall no longer be notified for new messages, continue?" msgstr "Du kommer inte längre att meddelas om nya meddelanden, fortsätt?" -#: src/chatty-window.c:568 +#: src/chatty-window.c:577 msgid "Archived" msgstr "Arkiverade" -#: src/chatty-window.c:621 +#: src/chatty-window.c:630 msgid "An SMS and XMPP messaging client" msgstr "En SMS- och XMPP-meddelandeklient" -#: src/chatty-window.c:628 +#: src/chatty-window.c:637 msgid "translator-credits" msgstr "" "Anders Jonsson <anders.jonsson@norsjovallen.se>\n" "Luna Jernberg <droidbittin@gmail.com>" -#: src/chatty-window.c:978 +#: src/chatty-window.c:989 msgid "Any Protocol" msgstr "Alla protokoll" -#: src/chatty-window.c:979 src/ui/chatty-settings-dialog.ui:627 +#: src/chatty-window.c:990 src/ui/chatty-settings-dialog.ui:627 msgid "Matrix" msgstr "Matrix" -#: src/chatty-window.c:980 +#: src/chatty-window.c:991 msgid "SMS/MMS" msgstr "SMS/MMS" -#: src/chatty-window.c:983 src/ui/chatty-settings-dialog.ui:615 +#: src/chatty-window.c:994 src/ui/chatty-settings-dialog.ui:615 msgid "XMPP" msgstr "XMPP" -#: src/chatty-window.c:986 src/ui/chatty-settings-dialog.ui:641 +#: src/chatty-window.c:997 src/ui/chatty-settings-dialog.ui:641 msgid "Telegram" msgstr "Telegram" @@ -585,115 +582,93 @@ msgstr "ansluter…" msgid "disconnected" msgstr "frÃ¥nkopplad" -#: src/dialogs/chatty-settings-dialog.c:255 -#: src/dialogs/chatty-settings-dialog.c:405 +#: src/dialogs/chatty-settings-dialog.c:251 +#, c-format +msgid "Failed to verify server: %s" +msgstr "Misslyckades med att verifiera server: %s" + +#: src/dialogs/chatty-settings-dialog.c:253 msgid "Failed to verify server" msgstr "Misslyckades med att verifiera server" -#: src/dialogs/chatty-settings-dialog.c:306 -#: src/dialogs/chatty-settings-dialog.c:385 +#: src/dialogs/chatty-settings-dialog.c:259 msgid "Couldn't get Home server address" msgstr "Kunde inte erhÃ¥lla hemserverns adress" -#: src/dialogs/chatty-settings-dialog.c:510 +#: src/dialogs/chatty-settings-dialog.c:428 #: src/ui/chatty-ma-account-details.ui:182 #: src/ui/chatty-pp-account-details.ui:199 msgid "Delete Account" msgstr "Ta bort konto" -#: src/dialogs/chatty-settings-dialog.c:513 +#: src/dialogs/chatty-settings-dialog.c:431 #, c-format msgid "Delete account %s?" msgstr "Ta bort kontot %s?" -#: src/dialogs/chatty-settings-dialog.c:654 +#: src/dialogs/chatty-settings-dialog.c:582 msgid "Restart chatty to disable purple" msgstr "Starta om chatty för att inaktivera purple" -#: src/dialogs/chatty-settings-dialog.c:656 +#: src/dialogs/chatty-settings-dialog.c:584 #: src/ui/chatty-settings-dialog.ui:512 msgid "Enable purple plugin" msgstr "Aktivera purple-insticksmodul" -#: src/dialogs/chatty-settings-dialog.c:670 +#: src/dialogs/chatty-settings-dialog.c:598 #: src/ui/chatty-settings-dialog.ui:279 msgid "SMS and MMS Settings" msgstr "SMS och MMS-inställningar" -#: src/dialogs/chatty-settings-dialog.c:672 +#: src/dialogs/chatty-settings-dialog.c:600 #: src/ui/chatty-settings-dialog.ui:298 msgid "Purple Settings" msgstr "Purple-inställningar" -#: src/dialogs/chatty-settings-dialog.c:674 +#: src/dialogs/chatty-settings-dialog.c:602 msgid "New Account" msgstr "Nytt konto" -#: src/dialogs/chatty-settings-dialog.c:676 +#: src/dialogs/chatty-settings-dialog.c:604 #: src/ui/chatty-settings-dialog.ui:364 msgid "Blocked Contacts" msgstr "Blockerade kontakter" -#: src/dialogs/chatty-settings-dialog.c:678 src/ui/chatty-settings-dialog.ui:7 +#: src/dialogs/chatty-settings-dialog.c:606 src/ui/chatty-settings-dialog.ui:7 #: src/ui/chatty-window.ui:30 msgid "Preferences" msgstr "Inställningar" -#: src/dialogs/chatty-settings-dialog.c:707 +#: src/dialogs/chatty-settings-dialog.c:634 msgid "Select Protocol" msgstr "Välj protokoll" #. TRANSLATORS: Only translate 'or' -#: src/dialogs/chatty-settings-dialog.c:948 +#: src/dialogs/chatty-settings-dialog.c:878 msgid "@user:matrix.org or user@example.com" msgstr "@user:matrix.org eller user@example.com" -#: src/dialogs/chatty-settings-dialog.c:1057 +#: src/dialogs/chatty-settings-dialog.c:987 msgid "Unblock contact" msgstr "Avblockera kontakt" -#: src/matrix/chatty-ma-account.c:296 +#: src/matrix/chatty-ma-account.c:105 msgid "Incorrect password" msgstr "Felaktigt lösenord" -#: src/matrix/chatty-ma-account.c:299 +#: src/matrix/chatty-ma-account.c:108 msgid "_OK" msgstr "_OK" -#: src/matrix/chatty-ma-account.c:300 src/ui/chatty-dialog-new-chat.ui:74 +#: src/matrix/chatty-ma-account.c:109 src/ui/chatty-dialog-new-chat.ui:74 #: src/ui/chatty-settings-dialog.ui:47 src/ui/chatty-settings-dialog.ui:108 msgid "_Cancel" msgstr "A_vbryt" -#: src/matrix/chatty-ma-account.c:306 +#: src/matrix/chatty-ma-account.c:115 #, c-format -msgid "Please enter password for “%sâ€" -msgstr "Ange lösenordet för â€%sâ€" - -#: src/matrix/matrix-utils.c:474 -#, c-format -msgid "The certificate for ‘%s’ has unknown CA" -msgstr "Certifikatet för â€%s†har okänd CA" - -#: src/matrix/matrix-utils.c:476 -#, c-format -msgid "The certificate for ‘%s’ is self-signed" -msgstr "Certifikatet för â€%s†är självsignerat" - -#: src/matrix/matrix-utils.c:480 -#, c-format -msgid "The certificate for ‘%s’ has expired" -msgstr "Certifikatet för â€%s†har gÃ¥tt ut" - -#: src/matrix/matrix-utils.c:484 -#, c-format -msgid "The certificate for ‘%s’ has been revoked" -msgstr "Certifikatet för â€%s†har Ã¥terkallats" - -#: src/matrix/matrix-utils.c:493 -#, c-format -msgid "Error validating certificate for ‘%s’" -msgstr "Fel vid validering av certifikat för â€%sâ€" +msgid "Please enter password for “%sâ€, homeserver: %s" +msgstr "Ange lösenordet för â€%sâ€, hemserver: %s" #. TRANSLATORS: Timestamp for minute accuracy, e.g. “2020-08-11 15:27â€. #. See https://developer.gnome.org/glib/stable/glib-GDateTime.html#g-date-time-format @@ -1070,3 +1045,27 @@ msgstr "Arbete: " #: src/users/chatty-contact.c:329 msgid "Other: " msgstr "Annat: " + +#, c-format +#~ msgid "Chatty password for \"%s\"" +#~ msgstr "Chatty-lösenord för â€%sâ€" + +#, c-format +#~ msgid "The certificate for ‘%s’ has unknown CA" +#~ msgstr "Certifikatet för â€%s†har okänd CA" + +#, c-format +#~ msgid "The certificate for ‘%s’ is self-signed" +#~ msgstr "Certifikatet för â€%s†är självsignerat" + +#, c-format +#~ msgid "The certificate for ‘%s’ has expired" +#~ msgstr "Certifikatet för â€%s†har gÃ¥tt ut" + +#, c-format +#~ msgid "The certificate for ‘%s’ has been revoked" +#~ msgstr "Certifikatet för â€%s†har Ã¥terkallats" + +#, c-format +#~ msgid "Error validating certificate for ‘%s’" +#~ msgstr "Fel vid validering av certifikat för â€%sâ€" diff --git a/po/tr.po b/po/tr.po index 28bb507c43c8feab28d87f18be3df7395364e1ce..f5773a95e5044bdaecc4fe489ba9d2824684821c 100644 --- a/po/tr.po +++ b/po/tr.po @@ -5,8 +5,8 @@ msgid "" msgstr "" "Project-Id-Version: purism-chatty\n" "Report-Msgid-Bugs-To: https://source.puri.sm/Librem5/chatty/issues\n" -"POT-Creation-Date: 2022-02-02 15:33+0000\n" -"PO-Revision-Date: 2022-03-19 13:04+0300\n" +"POT-Creation-Date: 2022-07-18 15:24+0000\n" +"PO-Revision-Date: 2022-09-04 13:31+0300\n" "Last-Translator: Emin Tufan Çetin <etcetin@gmail.com>\n" "Language-Team: Turkish <gnome-turk@gnome.org>\n" "Language: tr\n" @@ -17,14 +17,11 @@ msgstr "" "Plural-Forms: nplurals=1; plural=0;\n" #: data/sm.puri.Chatty.desktop.in:3 data/sm.puri.Chatty.metainfo.xml.in:6 -#: src/chatty-application.c:333 src/ui/chatty-window.ui:200 +#: src/chatty-application.c:336 src/chatty-window.c:339 +#: src/ui/chatty-window.ui:265 msgid "Chats" msgstr "KonuÅŸmalar" -#: data/sm.puri.Chatty.desktop.in:5 -msgid "sm.puri.Chatty" -msgstr "sm.puri.Chatty" - #: data/sm.puri.Chatty.desktop.in:6 msgid "SMS and XMPP chat application" msgstr "SMS ve XMPP konuÅŸma uygulaması" @@ -32,7 +29,8 @@ msgstr "SMS ve XMPP konuÅŸma uygulaması" #: data/sm.puri.Chatty.desktop.in:7 msgid "XMPP;SMS;chat;jabber;messaging;modem" msgstr "" -"XMPP;SMS;chat;jabber;mesajlaÅŸma;iletiÅŸim;modem;ileti;kısamesaj;kısaileti;MMS;multimedyamesajı;multimedyailetisi;çokluortammesajı;çokluortamiletisi;" +"XMPP;SMS;chat;jabber;mesajlaÅŸma;iletiÅŸim;modem;ileti;kısamesaj;kısaileti;MMS;" +"multimedyamesajı;multimedyailetisi;çokluortammesajı;çokluortamiletisi;" #: data/sm.puri.Chatty.gschema.xml:7 data/sm.puri.Chatty.gschema.xml:8 msgid "Whether the application is launching the first time" @@ -58,7 +56,7 @@ msgstr "Ä°letinin okunma durumununun gönderilip gönderilmeyeceÄŸi" msgid "Message carbon copies" msgstr "Ä°leti karbon kopyaları" -#: data/sm.puri.Chatty.gschema.xml:26 src/ui/chatty-settings-dialog.ui:479 +#: data/sm.puri.Chatty.gschema.xml:26 src/ui/chatty-settings-dialog.ui:554 msgid "Share chat history among devices" msgstr "KonuÅŸma geçmiÅŸini aygıtlar arası paylaÅŸ" @@ -74,7 +72,7 @@ msgstr "Sunucudan Ä°AY arÅŸiv eÅŸzamanlamasını etkinleÅŸtir" msgid "Enable purple" msgstr "Purple’ı etkinleÅŸtir" -#: data/sm.puri.Chatty.gschema.xml:38 src/ui/chatty-settings-dialog.ui:436 +#: data/sm.puri.Chatty.gschema.xml:38 src/ui/chatty-settings-dialog.ui:511 msgid "Enable purple accounts" msgstr "Purple hesaplarını etkinleÅŸtir" @@ -154,27 +152,27 @@ msgstr "KonuÅŸmalar, XMPP ve SMS’i destekleyen mesajlaÅŸma uygulamasıdır." msgid "Chats message window" msgstr "KonuÅŸmalar ileti penceresi" -#: src/chatty-application.c:76 +#: src/chatty-application.c:78 msgid "Show release version" msgstr "Dağıtım sürümünü göster" -#: src/chatty-application.c:77 +#: src/chatty-application.c:79 msgid "Start in daemon mode" msgstr "Art alan kipinde baÅŸlat" -#: src/chatty-application.c:78 +#: src/chatty-application.c:80 msgid "Disable all accounts" msgstr "Tüm hesapları devre dışı bırak" -#: src/chatty-application.c:79 +#: src/chatty-application.c:81 msgid "Enable libpurple debug messages" msgstr "libpurple hata ayıklama iletilerini etkinleÅŸtir" -#: src/chatty-application.c:81 +#: src/chatty-application.c:83 msgid "Enable verbose libpurple debug messages" msgstr "Ayrıntılı libpurple hata ayıklama iletilerini etkinleÅŸtir" -#: src/chatty-application.c:142 +#: src/chatty-application.c:144 #, c-format msgid "" "There was an error displaying help:\n" @@ -187,23 +185,27 @@ msgstr "" msgid "Send To" msgstr "Åžuna Gönder" -#: src/chatty-chat-list.c:152 +#: src/chatty-chat-list.c:175 msgid "Select a contact with the <b>“+â€</b> button in the titlebar." msgstr "BaÅŸlık çubuÄŸundaki <b>“+â€</b> düğmesiyle kiÅŸi seçin." -#: src/chatty-chat-list.c:156 +#: src/chatty-chat-list.c:179 msgid "Add instant messaging accounts in Preferences." msgstr "Tercihlerde anında mesajlaÅŸma hesapları ekleyin." -#: src/chatty-chat-list.c:192 src/chatty-contact-list.c:282 +#: src/chatty-chat-list.c:215 src/chatty-contact-list.c:282 msgid "No Search Results" msgstr "Arama Sonucu Yok" -#: src/chatty-chat-list.c:193 +#: src/chatty-chat-list.c:216 msgid "Try different search" msgstr "BaÅŸka arama dene" -#: src/chatty-chat-list.c:196 +#: src/chatty-chat-list.c:220 +msgid "No archived chats" +msgstr "ArÅŸivli konuÅŸma yok" + +#: src/chatty-chat-list.c:222 msgid "Start Chatting" msgstr "KonuÅŸma BaÅŸlat" @@ -227,12 +229,12 @@ msgstr "Ä°letileriniz ÅŸifreleniyor" msgid "Your messages are not encrypted" msgstr "Ä°letileriniz ÅŸifrelenmiyor" -#: src/chatty-chat-view.c:414 +#: src/chatty-chat-view.c:420 msgid "Select File..." msgstr "Dosyayı Seç…" -#: src/chatty-chat-view.c:417 src/purple/chatty-purple-request.c:175 -#: src/chatty-window.c:347 src/dialogs/chatty-pp-chat-info.c:90 +#: src/chatty-chat-view.c:423 src/purple/chatty-purple-request.c:175 +#: src/chatty-window.c:422 src/dialogs/chatty-pp-chat-info.c:90 #: src/dialogs/chatty-ma-account-details.c:119 #: src/dialogs/chatty-pp-account-details.c:91 #: src/ui/chatty-dialog-join-muc.ui:21 src/ui/chatty-info-dialog.ui:44 @@ -240,24 +242,24 @@ msgstr "Dosyayı Seç…" msgid "Cancel" msgstr "Ä°ptal Et" -#: src/chatty-chat-view.c:418 src/purple/chatty-purple-request.c:177 +#: src/chatty-chat-view.c:424 src/purple/chatty-purple-request.c:177 #: src/dialogs/chatty-pp-chat-info.c:89 #: src/dialogs/chatty-ma-account-details.c:118 #: src/dialogs/chatty-pp-account-details.c:90 msgid "Open" msgstr "Aç" -#: src/chatty-chat.c:632 +#: src/chatty-chat.c:635 msgid "Empty room" msgstr "BoÅŸ oda" #. TRANSLATORS: %s are name/user-id/phone numbers of two users -#: src/chatty-chat.c:639 +#: src/chatty-chat.c:642 #, c-format msgid "%s and %s" msgstr "%s ve %s" -#: src/chatty-chat.c:641 +#: src/chatty-chat.c:644 #, c-format msgid "%s and %u other" msgid_plural "%s and %u others" @@ -340,7 +342,7 @@ msgstr "Moderatör" msgid "Member" msgstr "Ãœye" -#: src/chatty-manager.c:727 +#: src/chatty-manager.c:729 #, c-format msgid "“%s†is not a valid URI" msgstr "“%s†geçerli URI deÄŸil" @@ -362,7 +364,7 @@ msgstr "%s kiÅŸisinden ileti" msgid "Message Received" msgstr "Ä°leti Alındı" -#: src/purple/chatty-purple-notify.c:44 src/matrix/matrix-utils.c:505 +#: src/purple/chatty-purple-notify.c:44 src/matrix/matrix-utils.c:504 msgid "Close" msgstr "Kapat" @@ -392,11 +394,11 @@ msgstr "%s kullanıcısı, %s kiÅŸisini kiÅŸilerine ekledi" msgid "Authorize %s?" msgstr "Yetkilendir: %s?" -#: src/purple/chatty-purple.c:264 src/matrix/matrix-utils.c:509 +#: src/purple/chatty-purple.c:264 src/matrix/matrix-utils.c:508 msgid "Reject" msgstr "Reddet" -#: src/purple/chatty-purple.c:265 src/matrix/matrix-utils.c:510 +#: src/purple/chatty-purple.c:265 src/matrix/matrix-utils.c:509 msgid "Accept" msgstr "Kabul Et" @@ -413,58 +415,66 @@ msgstr "GiriÅŸ baÅŸarısız" msgid "Please check ID and password" msgstr "Lütfen kimliÄŸi ve parolayı denetleyin" -#: src/chatty-secret-store.c:98 +#: src/chatty-secret-store.c:99 #, c-format msgid "Chatty password for \"%s\"" msgstr "“%s†için Chatty parolası" -#: src/chatty-window.c:333 +#: src/chatty-window.c:408 #, c-format msgid "Delete chat with “%sâ€" msgstr "“%s†ile konuÅŸmayı sil" -#: src/chatty-window.c:334 +#: src/chatty-window.c:409 msgid "This deletes the conversation history" msgstr "Bu, konuÅŸma geçmiÅŸini siler" -#: src/chatty-window.c:336 +#: src/chatty-window.c:411 #, c-format msgid "Disconnect group chat “%sâ€" msgstr "“%s†küme konuÅŸmasından ayrıl" -#: src/chatty-window.c:337 +#: src/chatty-window.c:412 msgid "This removes chat from chats list" msgstr "Bu, konuÅŸmayı konuÅŸma listesinden kaldırır" -#: src/chatty-window.c:349 +#: src/chatty-window.c:424 msgid "Delete" msgstr "Sil" -#: src/chatty-window.c:474 +#: src/chatty-window.c:502 +msgid "You shall no longer be notified for new messages, continue?" +msgstr "Bundan sonra yeni iletiler bildirilmeyecek, sürdürülsün mü?" + +#: src/chatty-window.c:577 +msgid "Archived" +msgstr "ArÅŸivlendi" + +#: src/chatty-window.c:630 msgid "An SMS and XMPP messaging client" msgstr "SMS ve XMPP mesajlaÅŸma istemcisi" -#: src/chatty-window.c:481 +#: src/chatty-window.c:637 msgid "translator-credits" msgstr "Emin Tufan Çetin <etcetin@gmail.com>" -#: src/chatty-window.c:815 +#: src/chatty-window.c:989 msgid "Any Protocol" msgstr "Herhangi Bir Ä°letiÅŸim Kuralı" -#: src/chatty-window.c:816 src/ui/chatty-settings-dialog.ui:552 +#: src/chatty-window.c:990 src/ui/chatty-settings-dialog.ui:627 msgid "Matrix" msgstr "Matrix" -#: src/chatty-window.c:817 +#: src/chatty-window.c:991 msgid "SMS/MMS" msgstr "SMS/MMS" -#: src/chatty-window.c:820 src/ui/chatty-settings-dialog.ui:540 +#: src/chatty-window.c:994 src/ui/chatty-settings-dialog.ui:615 msgid "XMPP" msgstr "XMPP" -#: src/chatty-window.c:823 src/ui/chatty-settings-dialog.ui:566 +#: src/chatty-window.c:997 src/ui/chatty-settings-dialog.ui:641 msgid "Telegram" msgstr "Telegram" @@ -563,103 +573,117 @@ msgstr "baÄŸlanıyor…" msgid "disconnected" msgstr "baÄŸlantı kesildi" -#: src/dialogs/chatty-settings-dialog.c:210 -#: src/dialogs/chatty-settings-dialog.c:374 +#: src/dialogs/chatty-settings-dialog.c:267 +#, c-format +msgid "Failed to verify server: %s" +msgstr "Sunucu doÄŸrulanamadı: %s" + +#: src/dialogs/chatty-settings-dialog.c:269 +#: src/dialogs/chatty-settings-dialog.c:479 msgid "Failed to verify server" msgstr "Sunucu doÄŸrulanamadı" -#: src/dialogs/chatty-settings-dialog.c:261 -#: src/dialogs/chatty-settings-dialog.c:354 +#: src/dialogs/chatty-settings-dialog.c:275 +#: src/dialogs/chatty-settings-dialog.c:459 msgid "Couldn't get Home server address" msgstr "Ev sunucusu adresi alınamadı." -#: src/dialogs/chatty-settings-dialog.c:479 +#: src/dialogs/chatty-settings-dialog.c:584 #: src/ui/chatty-ma-account-details.ui:182 #: src/ui/chatty-pp-account-details.ui:199 msgid "Delete Account" msgstr "Hesabı Sil" -#: src/dialogs/chatty-settings-dialog.c:482 +#: src/dialogs/chatty-settings-dialog.c:587 #, c-format msgid "Delete account %s?" msgstr "%s hesabını sil?" -#: src/dialogs/chatty-settings-dialog.c:618 +#: src/dialogs/chatty-settings-dialog.c:728 msgid "Restart chatty to disable purple" msgstr "Purple’ı devre dışı bırakmak için chatty’yi yeniden baÅŸlat" -#: src/dialogs/chatty-settings-dialog.c:620 -#: src/ui/chatty-settings-dialog.ui:437 +#: src/dialogs/chatty-settings-dialog.c:730 +#: src/ui/chatty-settings-dialog.ui:512 msgid "Enable purple plugin" msgstr "Purple eklentisini etkinleÅŸtir" -#: src/dialogs/chatty-settings-dialog.c:634 +#: src/dialogs/chatty-settings-dialog.c:744 #: src/ui/chatty-settings-dialog.ui:279 msgid "SMS and MMS Settings" msgstr "SMS ve MMS Ayarları" -#: src/dialogs/chatty-settings-dialog.c:636 +#: src/dialogs/chatty-settings-dialog.c:746 #: src/ui/chatty-settings-dialog.ui:298 msgid "Purple Settings" msgstr "Purple Ayarları" -#: src/dialogs/chatty-settings-dialog.c:638 +#: src/dialogs/chatty-settings-dialog.c:748 msgid "New Account" msgstr "Yeni Hesap" -#: src/dialogs/chatty-settings-dialog.c:640 src/ui/chatty-settings-dialog.ui:7 -#: src/ui/chatty-window.ui:17 +#: src/dialogs/chatty-settings-dialog.c:750 +#: src/ui/chatty-settings-dialog.ui:364 +msgid "Blocked Contacts" +msgstr "Engellenen KiÅŸiler" + +#: src/dialogs/chatty-settings-dialog.c:752 src/ui/chatty-settings-dialog.ui:7 +#: src/ui/chatty-window.ui:30 msgid "Preferences" msgstr "Tercihler" -#: src/dialogs/chatty-settings-dialog.c:669 +#: src/dialogs/chatty-settings-dialog.c:781 msgid "Select Protocol" msgstr "Ä°letiÅŸim Kuralı Seç" #. TRANSLATORS: Only translate 'or' -#: src/dialogs/chatty-settings-dialog.c:865 +#: src/dialogs/chatty-settings-dialog.c:1022 msgid "@user:matrix.org or user@example.com" msgstr "@kullanici:matrix.org veya kullanici@ornek.com" -#: src/matrix/chatty-ma-account.c:296 +#: src/dialogs/chatty-settings-dialog.c:1131 +msgid "Unblock contact" +msgstr "Engeli kaldır" + +#: src/matrix/chatty-ma-account.c:231 msgid "Incorrect password" msgstr "Geçersiz parola" -#: src/matrix/chatty-ma-account.c:299 +#: src/matrix/chatty-ma-account.c:234 msgid "_OK" msgstr "_Tamam" -#: src/matrix/chatty-ma-account.c:300 src/ui/chatty-dialog-new-chat.ui:74 +#: src/matrix/chatty-ma-account.c:235 src/ui/chatty-dialog-new-chat.ui:74 #: src/ui/chatty-settings-dialog.ui:47 src/ui/chatty-settings-dialog.ui:108 msgid "_Cancel" msgstr "Ä°ptal _Et" -#: src/matrix/chatty-ma-account.c:306 +#: src/matrix/chatty-ma-account.c:241 #, c-format -msgid "Please enter password for “%sâ€" -msgstr "Lütfen “%s†için parola girin" +msgid "Please enter password for “%sâ€, homeserver: %s" +msgstr "Lütfen “%s†için parola girin, ana sunucu: %s" -#: src/matrix/matrix-utils.c:474 +#: src/matrix/matrix-utils.c:473 #, c-format msgid "The certificate for ‘%s’ has unknown CA" msgstr "‘%s’ için sertifikanın bilinmeyen CAʼsı var" -#: src/matrix/matrix-utils.c:476 +#: src/matrix/matrix-utils.c:475 #, c-format msgid "The certificate for ‘%s’ is self-signed" msgstr "‘%s’ için sertifikanın imzacısı kendidir" -#: src/matrix/matrix-utils.c:480 +#: src/matrix/matrix-utils.c:479 #, c-format msgid "The certificate for ‘%s’ has expired" msgstr "‘%s’ için sertifikanın süresi dolmuÅŸ" -#: src/matrix/matrix-utils.c:484 +#: src/matrix/matrix-utils.c:483 #, c-format msgid "The certificate for ‘%s’ has been revoked" msgstr "‘%s’ için sertifika iptal edilmiÅŸ" -#: src/matrix/matrix-utils.c:493 +#: src/matrix/matrix-utils.c:492 #, c-format msgid "Error validating certificate for ‘%s’" msgstr "‘%s’ için sertifika doÄŸrulanırken hata" @@ -667,16 +691,16 @@ msgstr "‘%s’ için sertifika doÄŸrulanırken hata" #. TRANSLATORS: Timestamp for minute accuracy, e.g. “2020-08-11 15:27â€. #. See https://developer.gnome.org/glib/stable/glib-GDateTime.html#g-date-time-format #. -#: src/mm/chatty-mmsd.c:826 +#: src/mm/chatty-mmsd.c:827 msgid "%Y-%m-%d %H∶%M" msgstr "%Y-%m-%d %H∶%M" -#: src/mm/chatty-mmsd.c:1107 +#: src/mm/chatty-mmsd.c:1111 #, c-format msgid "You received an MMS, but it expired on: %s" msgstr "MMS aldınız ancak ÅŸunda süresi doldu: %s" -#: src/mm/chatty-mmsd.c:1110 +#: src/mm/chatty-mmsd.c:1114 msgid "You received an empty MMS." msgstr "BoÅŸ MMS aldınız." @@ -720,7 +744,7 @@ msgstr "KiÅŸilere Ekle" msgid "Remove Attachment" msgstr "Eki Kaldır" -#: src/ui/chatty-info-dialog.ui:17 src/ui/chatty-window.ui:126 +#: src/ui/chatty-info-dialog.ui:17 src/ui/chatty-window.ui:139 msgid "Chat Details" msgstr "KonuÅŸma Ayrıntıları" @@ -798,7 +822,7 @@ msgstr "Öntanımlı için boÅŸ" msgid "Group Members" msgstr "Küme Ãœyeleri" -#: src/ui/chatty-list-row.ui:151 src/ui/chatty-window.ui:353 +#: src/ui/chatty-list-row.ui:151 src/ui/chatty-window.ui:433 msgid "Call" msgstr "Ara" @@ -834,7 +858,7 @@ msgstr "Kullanıcı KimliÄŸi" msgid "Protocol" msgstr "Ä°letiÅŸim Kuralı" -#: src/ui/chatty-pp-account-details.ui:168 src/ui/chatty-settings-dialog.ui:626 +#: src/ui/chatty-pp-account-details.ui:168 src/ui/chatty-settings-dialog.ui:701 msgid "Password" msgstr "Parola" @@ -862,7 +886,7 @@ msgstr "_Uygula" msgid "Accounts" msgstr "Hesaplar" -#: src/ui/chatty-settings-dialog.ui:189 src/ui/chatty-settings-dialog.ui:456 +#: src/ui/chatty-settings-dialog.ui:189 src/ui/chatty-settings-dialog.ui:531 msgid "Privacy" msgstr "Gizlilik" @@ -911,71 +935,100 @@ msgstr "Teslim Raporları Ä°ste" msgid "SMIL for MMS" msgstr "MMS için SMIL" -#: src/ui/chatty-settings-dialog.ui:366 +#: src/ui/chatty-settings-dialog.ui:384 msgid "MMS Carrier Settings" msgstr "MMS Ulak Ayarları" -#: src/ui/chatty-settings-dialog.ui:370 +#: src/ui/chatty-settings-dialog.ui:388 msgid "MMSC" msgstr "MMSC" -#: src/ui/chatty-settings-dialog.ui:385 +#: src/ui/chatty-settings-dialog.ui:403 msgid "APN" msgstr "APN" -#: src/ui/chatty-settings-dialog.ui:400 +#: src/ui/chatty-settings-dialog.ui:418 msgid "Proxy" msgstr "Vekil" -#: src/ui/chatty-settings-dialog.ui:462 +#: src/ui/chatty-settings-dialog.ui:447 +msgid "You shall not be notified for the messages from blocked contacts" +msgstr "Bundan sonra engelli kiÅŸilerden iletiler bildirilmeyecek" + +#: src/ui/chatty-settings-dialog.ui:481 +msgid "Blocked chat list empty" +msgstr "Engelli konuÅŸma listesi boÅŸ" + +#: src/ui/chatty-settings-dialog.ui:537 msgid "Message Archive Management" msgstr "Ä°leti ArÅŸiv Yönetimi" -#: src/ui/chatty-settings-dialog.ui:463 +#: src/ui/chatty-settings-dialog.ui:538 msgid "Sync conversations with chat server" msgstr "KonuÅŸmaları konuÅŸma sunucusuyla eÅŸzamanlandır" -#: src/ui/chatty-settings-dialog.ui:478 +#: src/ui/chatty-settings-dialog.ui:553 msgid "Message Carbon Copies" msgstr "Ä°leti Karbon Kopyaları" -#: src/ui/chatty-settings-dialog.ui:662 +#: src/ui/chatty-settings-dialog.ui:737 msgid "Home server" msgstr "Ev sunucusu" -#: src/ui/chatty-settings-dialog.ui:698 +#: src/ui/chatty-settings-dialog.ui:773 msgid "Add _new account…" msgstr "_Yeni hesap ekle…" -#: src/ui/chatty-window.ui:28 +#: src/ui/chatty-window.ui:17 +msgctxt "show archived chat list when clicked" +msgid "Archived" +msgstr "ArÅŸivli" + +#: src/ui/chatty-window.ui:41 msgid "Keyboard _Shortcuts" msgstr "Klavye _Kısayolları" -#: src/ui/chatty-window.ui:35 +#: src/ui/chatty-window.ui:48 msgid "Help" msgstr "Yardım" -#: src/ui/chatty-window.ui:44 +#: src/ui/chatty-window.ui:57 msgid "About Chats" msgstr "KonuÅŸmalar Hakkında" -#: src/ui/chatty-window.ui:72 +#: src/ui/chatty-window.ui:85 msgid "New Message…" msgstr "Ä°letiyi Aç…" -#: src/ui/chatty-window.ui:85 +#: src/ui/chatty-window.ui:98 msgid "New SMS/MMS Message…" msgstr "Yeni SMS/MMS Ä°letisi…" -#: src/ui/chatty-window.ui:98 +#: src/ui/chatty-window.ui:111 msgid "New Group Message…" msgstr "Yeni Küme KonuÅŸması…" -#: src/ui/chatty-window.ui:151 +#: src/ui/chatty-window.ui:164 msgid "Leave Chat" msgstr "KonuÅŸmadan Ayrıl" -#: src/ui/chatty-window.ui:164 +#: src/ui/chatty-window.ui:177 +msgid "Block Contact" +msgstr "KiÅŸiyi Engelle" + +#: src/ui/chatty-window.ui:190 +msgid "Unblock Contact" +msgstr "Engeli Kaldır" + +#: src/ui/chatty-window.ui:203 +msgid "Archive chat" +msgstr "KonuÅŸmayı ArÅŸivle" + +#: src/ui/chatty-window.ui:216 +msgid "Unarchive chat" +msgstr "ArÅŸivden Çıkar" + +#: src/ui/chatty-window.ui:229 msgid "Delete Chat" msgstr "KonuÅŸmayı Sil" @@ -1010,117 +1063,3 @@ msgstr "Ä°ÅŸ: " #: src/users/chatty-contact.c:329 msgid "Other: " msgstr "DiÄŸer: " - -#~ msgid "Mark offline users differently" -#~ msgstr "Çevrim dışı kullanıcıları baÅŸka imle" - -#~ msgid "ask your counterpart to use E2EE." -#~ msgstr "eÅŸinize E2EE kullanmasını söyleyin." - -#~ msgid "Your messages are secured" -#~ msgstr "Ä°letileriniz uçtan uca" - -#~ msgid "by end-to-end encryption." -#~ msgstr "ÅŸifrelemeyle korunmaktadır." - -#~ msgid "and carrier rates may apply." -#~ msgstr "ve operatör tarifeleri uygulanabilir." - -#~ msgctxt "timestamp-suffix-seconds" -#~ msgid "s" -#~ msgstr "sn" - -#~ msgctxt "timestamp-suffix-minute" -#~ msgid "m" -#~ msgstr "d" - -#~ msgctxt "timestamp-suffix-minutes" -#~ msgid "m" -#~ msgstr "d" - -#~ msgctxt "timestamp-suffix-hour" -#~ msgid "h" -#~ msgstr "s" - -#~ msgctxt "timestamp-suffix-hours" -#~ msgid "h" -#~ msgstr "s" - -#~ msgctxt "timestamp-suffix-day" -#~ msgid "d" -#~ msgstr "g" - -#~ msgctxt "timestamp-suffix-days" -#~ msgid "d" -#~ msgstr "g" - -#~ msgctxt "timestamp-suffix-month" -#~ msgid "mo" -#~ msgstr "a" - -#~ msgctxt "timestamp-suffix-months" -#~ msgid "mos" -#~ msgstr "a" - -#~ msgctxt "timestamp-suffix-year" -#~ msgid "y" -#~ msgstr "y" - -#~ msgctxt "timestamp-suffix-years" -#~ msgid "y" -#~ msgstr "y" - -#~ msgid "Over" -#~ msgstr "Geçik" - -#~ msgid "Almost" -#~ msgstr "Neredeyse" - -#~ msgid "Choose a contact" -#~ msgstr "KiÅŸi seç" - -#~ msgid "" -#~ "Select an <b>SMS</b> or <b>Instant Message</b> contact with the <b>\"+\"</" -#~ "b> button in the titlebar." -#~ msgstr "" -#~ "BaÅŸlık çubuÄŸundaki <b>\"+\"</b> düğmesiyle <b>SMS</b> veya <b>Anında " -#~ "Mesaj</b> kiÅŸisi seçin." - -#~ msgid "Start a <b>SMS</b> chat with the \"+\" button in the titlebar." -#~ msgstr "BaÅŸlık çubuÄŸundaki \"+\" düğmesiyle <b>SMS</b> konuÅŸması baÅŸlat." - -#~ msgid "Error saving contact: %s" -#~ msgstr "KiÅŸi kaydedilirken hata: %s" - -#~ msgid "Start Chat" -#~ msgstr "KonuÅŸma BaÅŸlat" - -#~ msgid "Chats List" -#~ msgstr "KonuÅŸma Listesi" - -#~ msgid "Indicate Offline Contacts" -#~ msgstr "Çevrim Dışı KiÅŸileri Ä°mle" - -#~ msgid "Grey out avatars from offline contacts" -#~ msgstr "Çevrim dışı kiÅŸilerin avatarlarını grile" - -#~ msgid "Indicate Idle Contacts" -#~ msgstr "Eylemsiz KiÅŸileri Ä°mle" - -#~ msgid "Blur avatars from idle contacts" -#~ msgstr "Eylemsiz kiÅŸilerin avatarlarını bulanıklaÅŸtır" - -#~ msgid "Color unknown contact ID red" -#~ msgstr "Bilinmeyen kiÅŸi kimliÄŸini kırmızı renk yap" - -#~ msgid "Convert ASCII emoticons" -#~ msgstr "ASCII yüz ifadelerini dönüştür" - -#~ msgid "Return = Send Message" -#~ msgstr "Dönüş = Ä°letiyi Gönder" - -#~ msgid "_Accept" -#~ msgstr "_Kabul Et" - -#~ msgid "New Bulk SMS…" -#~ msgstr "Yeni Çoklu SMS…" diff --git a/po/uk.po b/po/uk.po index fc6889f1d962ab3c3ecdf6ff1348cb9c5ff11a92..9b55a4eb949a60392865a2ce612905a0551ff2d9 100644 --- a/po/uk.po +++ b/po/uk.po @@ -7,8 +7,8 @@ msgid "" msgstr "" "Project-Id-Version: purism-chatty\n" "Report-Msgid-Bugs-To: https://source.puri.sm/Librem5/chatty/issues\n" -"POT-Creation-Date: 2022-04-26 15:24+0000\n" -"PO-Revision-Date: 2022-04-26 20:04+0300\n" +"POT-Creation-Date: 2022-10-07 03:24+0000\n" +"PO-Revision-Date: 2022-10-07 19:11+0300\n" "Last-Translator: Yuri Chornoivan <yurchor@ukr.net>\n" "Language-Team: Ukrainian <trans-uk@lists.fedoraproject.org>\n" "Language: uk\n" @@ -16,19 +16,15 @@ msgstr "" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "X-Generator: Lokalize 20.12.0\n" -"Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<" -"=4 && (n%100<10 || n%100>=20) ? 1 : 2)\n" +"Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n" +"%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2)\n" #: data/sm.puri.Chatty.desktop.in:3 data/sm.puri.Chatty.metainfo.xml.in:6 -#: src/chatty-application.c:333 src/chatty-window.c:330 -#: src/ui/chatty-window.ui:265 +#: src/chatty-application.c:394 src/chatty-header-bar.c:250 +#: src/ui/chatty-header-bar.ui:15 msgid "Chats" msgstr "СпілкуваннÑ" -#: data/sm.puri.Chatty.desktop.in:5 -msgid "sm.puri.Chatty" -msgstr "sm.puri.Chatty" - #: data/sm.puri.Chatty.desktop.in:6 msgid "SMS and XMPP chat application" msgstr "Програма Ð´Ð»Ñ ÑÐ¿Ñ–Ð»ÐºÑƒÐ²Ð°Ð½Ð½Ñ Ð·Ð° допомогою SMS Ñ– XMPP" @@ -161,27 +157,35 @@ msgstr "" msgid "Chats message window" msgstr "Вікно повідомлень ÑпілкуваннÑ" -#: src/chatty-application.c:76 +#: src/chatty-application.c:78 msgid "Show release version" msgstr "Показати верÑÑ–ÑŽ випуÑку" -#: src/chatty-application.c:77 +#: src/chatty-application.c:79 msgid "Start in daemon mode" msgstr "ЗапуÑкати у режимі Ñлужби" -#: src/chatty-application.c:78 +#: src/chatty-application.c:80 msgid "Disable all accounts" msgstr "Вимкнути уÑÑ– облікові запиÑи" -#: src/chatty-application.c:79 +#: src/chatty-application.c:81 msgid "Enable libpurple debug messages" msgstr "Увімкнути діагноÑтичні Ð¿Ð¾Ð²Ñ–Ð´Ð¾Ð¼Ð»ÐµÐ½Ð½Ñ libpurple" -#: src/chatty-application.c:81 +#: src/chatty-application.c:83 msgid "Enable verbose libpurple debug messages" msgstr "Увімкнути докладні діагноÑтичні Ð¿Ð¾Ð²Ñ–Ð´Ð¾Ð¼Ð»ÐµÐ½Ð½Ñ libpurple" -#: src/chatty-application.c:142 +#: src/chatty-application.c:167 +msgid "An SMS and XMPP messaging client" +msgstr "Клієнт обміну повідомленнÑми SMS Ñ– XMPP" + +#: src/chatty-application.c:174 +msgid "translator-credits" +msgstr "Юрій Чорноіван <yurchor@ukr.net>, 2020" + +#: src/chatty-application.c:198 #, c-format msgid "" "There was an error displaying help:\n" @@ -219,34 +223,34 @@ msgstr "Ðемає архівованих Ñпілкувань" msgid "Start Chatting" msgstr "Почати ÑпілкуваннÑ" -#: src/chatty-chat-view.c:212 +#: src/chatty-chat-view.c:211 msgid "This is an SMS conversation" msgstr "Це обмін повідомленнÑми SMS" -#: src/chatty-chat-view.c:214 src/chatty-chat-view.c:220 +#: src/chatty-chat-view.c:213 src/chatty-chat-view.c:219 msgid "Your messages are not encrypted, and carrier rates may apply" msgstr "" "Ваші Ð¿Ð¾Ð²Ñ–Ð´Ð¾Ð¼Ð»ÐµÐ½Ð½Ñ Ð½Ðµ зашифровано, оператор може ÑÑ‚Ñгувати Ñплату за " "Ð¿ÐµÑ€ÐµÐ´Ð°Ð²Ð°Ð½Ð½Ñ Ð´Ð°Ð½Ð¸Ñ…" -#: src/chatty-chat-view.c:218 +#: src/chatty-chat-view.c:217 msgid "This is an IM conversation" msgstr "Це ÑÐ¿Ñ–Ð»ÐºÑƒÐ²Ð°Ð½Ð½Ñ Ñƒ Ñлужбі обміну повідомленнÑми" -#: src/chatty-chat-view.c:224 +#: src/chatty-chat-view.c:223 msgid "Your messages are encrypted" msgstr "Ваші Ð¿Ð¾Ð²Ñ–Ð´Ð¾Ð¼Ð»ÐµÐ½Ð½Ñ Ð·Ð°ÑˆÐ¸Ñ„Ñ€Ð¾Ð²Ð°Ð½Ð¾" -#: src/chatty-chat-view.c:227 +#: src/chatty-chat-view.c:226 msgid "Your messages are not encrypted" msgstr "Ваші Ð¿Ð¾Ð²Ñ–Ð´Ð¾Ð¼Ð»ÐµÐ½Ð½Ñ Ð½Ðµ зашифровано" -#: src/chatty-chat-view.c:414 +#: src/chatty-chat-view.c:419 msgid "Select File..." msgstr "Вибір файла…" -#: src/chatty-chat-view.c:417 src/purple/chatty-purple-request.c:175 -#: src/chatty-window.c:413 src/dialogs/chatty-pp-chat-info.c:90 +#: src/chatty-chat-view.c:422 src/purple/chatty-purple-request.c:175 +#: src/chatty-window.c:529 src/dialogs/chatty-pp-chat-info.c:90 #: src/dialogs/chatty-ma-account-details.c:119 #: src/dialogs/chatty-pp-account-details.c:91 #: src/ui/chatty-dialog-join-muc.ui:21 src/ui/chatty-info-dialog.ui:44 @@ -254,24 +258,24 @@ msgstr "Вибір файла…" msgid "Cancel" msgstr "СкаÑувати" -#: src/chatty-chat-view.c:418 src/purple/chatty-purple-request.c:177 +#: src/chatty-chat-view.c:423 src/purple/chatty-purple-request.c:177 #: src/dialogs/chatty-pp-chat-info.c:89 #: src/dialogs/chatty-ma-account-details.c:118 #: src/dialogs/chatty-pp-account-details.c:90 msgid "Open" msgstr "Відкрити" -#: src/chatty-chat.c:635 +#: src/chatty-chat.c:709 msgid "Empty room" msgstr "ÐŸÐ¾Ñ€Ð¾Ð¶Ð½Ñ ÐºÑ–Ð¼Ð½Ð°Ñ‚Ð°" #. TRANSLATORS: %s are name/user-id/phone numbers of two users -#: src/chatty-chat.c:642 +#: src/chatty-chat.c:716 #, c-format msgid "%s and %s" msgstr "%s Ñ– %s" -#: src/chatty-chat.c:644 +#: src/chatty-chat.c:718 #, c-format msgid "%s and %u other" msgid_plural "%s and %u others" @@ -342,12 +346,21 @@ msgstr "Вчора %H:%M" msgid "%Y-%m-%d" msgstr "%d-%m-%Y" +#: src/chatty-header-bar.c:246 +msgid "Archived" +msgstr "Ðрхівовано" + #. TRANSLATORS: %s is the Device ID #: src/chatty-fp-row.c:131 #, c-format msgid "Device ID %s fingerprint:" msgstr "Відбиток ідентифікатора приÑтрою %s:" +#: src/chatty-invite-view.c:72 +#, c-format +msgid "Do you want to join “%sâ€" +msgstr "Хочете долучитиÑÑ Ð´Ð¾ «%s»" + #: src/chatty-list-row.c:118 msgid "Owner" msgstr "ВлаÑник" @@ -360,7 +373,7 @@ msgstr "Модератор" msgid "Member" msgstr "Член" -#: src/chatty-manager.c:727 +#: src/chatty-manager.c:733 #, c-format msgid "“%s†is not a valid URI" msgstr "«%s» не Ñ” коректною адреÑою" @@ -369,6 +382,12 @@ msgstr "«%s» не Ñ” коректною адреÑою" msgid "Copy" msgstr "Копіювати" +#: src/chatty-message.c:233 +msgid "Got an encrypted message, but couldn't decrypt due to missing keys" +msgstr "" +"Отримано зашифроване повідомленнÑ, але його не вдалоÑÑ Ñ€Ð¾Ð·ÑˆÐ¸Ñ„Ñ€ÑƒÐ²Ð°Ñ‚Ð¸ через " +"брак ключів" + #: src/chatty-notification.c:181 msgid "Open Message" msgstr "Відкрити повідомленнÑ" @@ -382,7 +401,7 @@ msgstr "Ðове Ð¿Ð¾Ð²Ñ–Ð´Ð¾Ð¼Ð»ÐµÐ½Ð½Ñ Ð²Ñ–Ð´ %s" msgid "Message Received" msgstr "Ðадійшло повідомленнÑ" -#: src/purple/chatty-purple-notify.c:44 src/matrix/matrix-utils.c:505 +#: src/purple/chatty-purple-notify.c:44 msgid "Close" msgstr "Закрити" @@ -412,11 +431,11 @@ msgstr "КориÑтувач %s додав %s до контактів" msgid "Authorize %s?" msgstr "Уповноважити %s?" -#: src/purple/chatty-purple.c:264 src/matrix/matrix-utils.c:509 +#: src/purple/chatty-purple.c:264 msgid "Reject" msgstr "Відмовити" -#: src/purple/chatty-purple.c:265 src/matrix/matrix-utils.c:510 +#: src/purple/chatty-purple.c:265 msgid "Accept" msgstr "ПрийнÑти" @@ -425,74 +444,57 @@ msgstr "ПрийнÑти" msgid "Add %s to contact list" msgstr "Додати %s до ÑпиÑку контактів" -#: src/purple/chatty-purple.c:587 +#: src/purple/chatty-purple.c:586 msgid "Login failed" msgstr "Ðевдала Ñпроба увійти" -#: src/purple/chatty-purple.c:592 +#: src/purple/chatty-purple.c:591 msgid "Please check ID and password" msgstr "Будь лаÑка, перевірте ідентифікатор Ñ– пароль" -#: src/chatty-secret-store.c:98 -#, c-format -msgid "Chatty password for \"%s\"" -msgstr "Пароль Chatty до «%s»" +#: src/chatty-window.c:465 +msgid "You shall no longer be notified for new messages, continue?" +msgstr "Програма більше не Ñповіщатиме Ð²Ð°Ñ Ð¿Ñ€Ð¾ нові повідомленнÑ. Продовжити?" -#: src/chatty-window.c:399 +#: src/chatty-window.c:515 #, c-format msgid "Delete chat with “%sâ€" msgstr "Вилучити ÑÐ¿Ñ–Ð»ÐºÑƒÐ²Ð°Ð½Ð½Ñ Ð· «%s»" -#: src/chatty-window.c:400 +#: src/chatty-window.c:516 msgid "This deletes the conversation history" msgstr "У результаті буде вилучено журнал ÑпілкуваннÑ" -#: src/chatty-window.c:402 +#: src/chatty-window.c:518 #, c-format msgid "Disconnect group chat “%sâ€" msgstr "Від'єднатиÑÑ Ð²Ñ–Ð´ групового ÑÐ¿Ñ–Ð»ÐºÑƒÐ²Ð°Ð½Ð½Ñ Â«%s»" -#: src/chatty-window.c:403 +#: src/chatty-window.c:519 msgid "This removes chat from chats list" msgstr "У результаті ÑÐ¿Ñ–Ð»ÐºÑƒÐ²Ð°Ð½Ð½Ñ Ð±ÑƒÐ´Ðµ вилучено зі ÑпиÑку Ñпілкувань" -#: src/chatty-window.c:415 +#: src/chatty-window.c:530 msgid "Delete" msgstr "Вилучити" -#: src/chatty-window.c:493 -msgid "You shall no longer be notified for new messages, continue?" -msgstr "Програма більше не Ñповіщатиме Ð²Ð°Ñ Ð¿Ñ€Ð¾ нові повідомленнÑ. Продовжити?" - -#: src/chatty-window.c:568 -msgid "Archived" -msgstr "Ðрхівовано" - -#: src/chatty-window.c:621 -msgid "An SMS and XMPP messaging client" -msgstr "Клієнт обміну повідомленнÑми SMS Ñ– XMPP" - -#: src/chatty-window.c:628 -msgid "translator-credits" -msgstr "Юрій Чорноіван <yurchor@ukr.net>, 2020" - -#: src/chatty-window.c:978 +#: src/chatty-window.c:886 msgid "Any Protocol" msgstr "Будь-Ñкий протокол" -#: src/chatty-window.c:979 src/ui/chatty-settings-dialog.ui:627 +#: src/chatty-window.c:887 src/ui/chatty-settings-dialog.ui:627 msgid "Matrix" msgstr "Matrix" -#: src/chatty-window.c:980 +#: src/chatty-window.c:888 msgid "SMS/MMS" msgstr "SMS/MMS" -#: src/chatty-window.c:983 src/ui/chatty-settings-dialog.ui:615 +#: src/chatty-window.c:891 src/ui/chatty-settings-dialog.ui:615 msgid "XMPP" msgstr "XMPP" -#: src/chatty-window.c:986 src/ui/chatty-settings-dialog.ui:641 +#: src/chatty-window.c:894 src/ui/chatty-settings-dialog.ui:641 msgid "Telegram" msgstr "Telegram" @@ -593,115 +595,93 @@ msgstr "З'єднаннÑ…" msgid "disconnected" msgstr "від'єднано" -#: src/dialogs/chatty-settings-dialog.c:255 -#: src/dialogs/chatty-settings-dialog.c:405 +#: src/dialogs/chatty-settings-dialog.c:251 +#, c-format +msgid "Failed to verify server: %s" +msgstr "Ðе вдалоÑÑ Ð¿ÐµÑ€ÐµÐ²Ñ–Ñ€Ð¸Ñ‚Ð¸ Ñервер: %s" + +#: src/dialogs/chatty-settings-dialog.c:253 msgid "Failed to verify server" msgstr "Ðе вдалоÑÑ Ð¿ÐµÑ€ÐµÐ²Ñ–Ñ€Ð¸Ñ‚Ð¸ Ñервер" -#: src/dialogs/chatty-settings-dialog.c:306 -#: src/dialogs/chatty-settings-dialog.c:385 +#: src/dialogs/chatty-settings-dialog.c:259 msgid "Couldn't get Home server address" msgstr "Ðе вдалоÑÑ Ð¾Ñ‚Ñ€Ð¸Ð¼Ð°Ñ‚Ð¸ адреÑу домашнього Ñервера." -#: src/dialogs/chatty-settings-dialog.c:510 +#: src/dialogs/chatty-settings-dialog.c:438 #: src/ui/chatty-ma-account-details.ui:182 #: src/ui/chatty-pp-account-details.ui:199 msgid "Delete Account" msgstr "Вилучити запиÑ" -#: src/dialogs/chatty-settings-dialog.c:513 +#: src/dialogs/chatty-settings-dialog.c:441 #, c-format msgid "Delete account %s?" msgstr "Вилучити Ð·Ð°Ð¿Ð¸Ñ %s?" -#: src/dialogs/chatty-settings-dialog.c:654 +#: src/dialogs/chatty-settings-dialog.c:598 msgid "Restart chatty to disable purple" msgstr "ПерезапуÑÑ‚Ñ–Ñ‚ÑŒ chatty, щоб вимкнути purple" -#: src/dialogs/chatty-settings-dialog.c:656 +#: src/dialogs/chatty-settings-dialog.c:600 #: src/ui/chatty-settings-dialog.ui:512 msgid "Enable purple plugin" msgstr "Увімкнути додаток purple" -#: src/dialogs/chatty-settings-dialog.c:670 +#: src/dialogs/chatty-settings-dialog.c:614 #: src/ui/chatty-settings-dialog.ui:279 msgid "SMS and MMS Settings" msgstr "Параметри SMS Ñ– MMS" -#: src/dialogs/chatty-settings-dialog.c:672 +#: src/dialogs/chatty-settings-dialog.c:616 #: src/ui/chatty-settings-dialog.ui:298 msgid "Purple Settings" msgstr "Параметри Purple" -#: src/dialogs/chatty-settings-dialog.c:674 +#: src/dialogs/chatty-settings-dialog.c:618 msgid "New Account" msgstr "Ðовий обліковий запиÑ" -#: src/dialogs/chatty-settings-dialog.c:676 +#: src/dialogs/chatty-settings-dialog.c:620 #: src/ui/chatty-settings-dialog.ui:364 msgid "Blocked Contacts" msgstr "Заблоковані контакти" -#: src/dialogs/chatty-settings-dialog.c:678 src/ui/chatty-settings-dialog.ui:7 -#: src/ui/chatty-window.ui:30 +#: src/dialogs/chatty-settings-dialog.c:622 src/ui/chatty-header-bar.ui:244 +#: src/ui/chatty-settings-dialog.ui:7 msgid "Preferences" msgstr "Параметри" -#: src/dialogs/chatty-settings-dialog.c:707 +#: src/dialogs/chatty-settings-dialog.c:650 msgid "Select Protocol" msgstr "Виберіть протокол" #. TRANSLATORS: Only translate 'or' -#: src/dialogs/chatty-settings-dialog.c:948 +#: src/dialogs/chatty-settings-dialog.c:894 msgid "@user:matrix.org or user@example.com" msgstr "@user:matrix.org або user@example.com" -#: src/dialogs/chatty-settings-dialog.c:1057 +#: src/dialogs/chatty-settings-dialog.c:1003 msgid "Unblock contact" msgstr "Розблокувати контакт" -#: src/matrix/chatty-ma-account.c:296 +#: src/matrix/chatty-ma-account.c:106 msgid "Incorrect password" msgstr "Помилковий пароль" -#: src/matrix/chatty-ma-account.c:299 +#: src/matrix/chatty-ma-account.c:109 msgid "_OK" msgstr "_Гаразд" -#: src/matrix/chatty-ma-account.c:300 src/ui/chatty-dialog-new-chat.ui:74 +#: src/matrix/chatty-ma-account.c:110 src/ui/chatty-dialog-new-chat.ui:74 #: src/ui/chatty-settings-dialog.ui:47 src/ui/chatty-settings-dialog.ui:108 msgid "_Cancel" msgstr "_СкаÑувати" -#: src/matrix/chatty-ma-account.c:306 +#: src/matrix/chatty-ma-account.c:117 #, c-format -msgid "Please enter password for “%sâ€" -msgstr "Будь лаÑка, введіть пароль до «%s»" - -#: src/matrix/matrix-utils.c:474 -#, c-format -msgid "The certificate for ‘%s’ has unknown CA" -msgstr "Сертифікат «%s» міÑтить невідомий Ð·Ð°Ð¿Ð¸Ñ Ñлужби Ñертифікації" - -#: src/matrix/matrix-utils.c:476 -#, c-format -msgid "The certificate for ‘%s’ is self-signed" -msgstr "Сертифікат «%s» Ñ” ÑамопідпиÑаним" - -#: src/matrix/matrix-utils.c:480 -#, c-format -msgid "The certificate for ‘%s’ has expired" -msgstr "Строк дії Ñертифіката «%s» вичерпано" - -#: src/matrix/matrix-utils.c:484 -#, c-format -msgid "The certificate for ‘%s’ has been revoked" -msgstr "Сертифікат «%s» було відкликано" - -#: src/matrix/matrix-utils.c:493 -#, c-format -msgid "Error validating certificate for ‘%s’" -msgstr "Помилка під Ñ‡Ð°Ñ Ñпроби перевірити чинніÑÑ‚ÑŒ Ñертифіката «%s»" +msgid "Please enter password for “%sâ€, homeserver: %s" +msgstr "Будь лаÑка, введіть пароль до «%s», домашній Ñервер: %s" #. TRANSLATORS: Timestamp for minute accuracy, e.g. “2020-08-11 15:27â€. #. See https://developer.gnome.org/glib/stable/glib-GDateTime.html#g-date-time-format @@ -759,10 +739,84 @@ msgstr "Додати до контактів" msgid "Remove Attachment" msgstr "Вилучити долученнÑ" -#: src/ui/chatty-info-dialog.ui:17 src/ui/chatty-window.ui:139 +#: src/ui/chatty-header-bar.ui:24 src/ui/chatty-header-bar.ui:133 +#: src/ui/chatty-settings-dialog.ui:27 +msgid "Back" +msgstr "Ðазад" + +#: src/ui/chatty-header-bar.ui:45 +msgid "Add Chat" +msgstr "Додати ÑпілкуваннÑ" + +#: src/ui/chatty-header-bar.ui:65 +msgid "Menu" +msgstr "Меню" + +#: src/ui/chatty-header-bar.ui:87 +msgid "Search" +msgstr "Пошук" + +#: src/ui/chatty-header-bar.ui:198 src/ui/chatty-list-row.ui:151 +msgid "Call" +msgstr "Виклик" + +#: src/ui/chatty-header-bar.ui:236 +msgctxt "show archived chat list when clicked" +msgid "Archived" +msgstr "Ðрхівовано" + +#: src/ui/chatty-header-bar.ui:251 +msgid "Keyboard _Shortcuts" +msgstr "_Клавіатурні ÑкороченнÑ" + +#: src/ui/chatty-header-bar.ui:258 +msgid "Help" +msgstr "Довідка" + +#: src/ui/chatty-header-bar.ui:266 +msgid "About Chats" +msgstr "Про «СпілкуваннÑ»" + +#: src/ui/chatty-header-bar.ui:286 +msgid "New Message…" +msgstr "Ðове повідомленнÑ…" + +#: src/ui/chatty-header-bar.ui:294 +msgid "New SMS/MMS Message…" +msgstr "Ðове Ð¿Ð¾Ð²Ñ–Ð´Ð¾Ð¼Ð»ÐµÐ½Ð½Ñ SMS/MMS…" + +#: src/ui/chatty-header-bar.ui:302 +msgid "New Group Message…" +msgstr "Ðове групове повідомленнÑ…" + +#: src/ui/chatty-header-bar.ui:322 src/ui/chatty-info-dialog.ui:17 msgid "Chat Details" msgstr "Подробиці ÑпілкуваннÑ" +#: src/ui/chatty-header-bar.ui:338 +msgid "Leave Chat" +msgstr "Полишити ÑпілкуваннÑ" + +#: src/ui/chatty-header-bar.ui:346 +msgid "Block Contact" +msgstr "Заблокувати контакт" + +#: src/ui/chatty-header-bar.ui:354 +msgid "Unblock Contact" +msgstr "Розблокувати контакт" + +#: src/ui/chatty-header-bar.ui:363 +msgid "Archive chat" +msgstr "Ðрхівувати ÑпілкуваннÑ" + +#: src/ui/chatty-header-bar.ui:371 +msgid "Unarchive chat" +msgstr "СкаÑувати архівуваннÑ" + +#: src/ui/chatty-header-bar.ui:380 +msgid "Delete Chat" +msgstr "Вилучити ÑпілкуваннÑ" + #: src/ui/chatty-info-dialog.ui:64 msgid "Apply" msgstr "ЗаÑтоÑувати" @@ -771,6 +825,16 @@ msgstr "ЗаÑтоÑувати" msgid "Invite" msgstr "ЗапроÑити" +#: src/ui/chatty-invite-view.ui:71 +#| msgid "Accept" +msgid "_Accept" +msgstr "При_йнÑти" + +#: src/ui/chatty-invite-view.ui:100 +#| msgid "Reject" +msgid "_Reject" +msgstr "Від_!мовити" + #: src/ui/chatty-ma-chat-info.ui:67 src/ui/chatty-ma-account-details.ui:233 msgid "Matrix ID" msgstr "Ід. Matrix" @@ -837,10 +901,6 @@ msgstr "Типово порожній" msgid "Group Members" msgstr "УчаÑники групи" -#: src/ui/chatty-list-row.ui:151 src/ui/chatty-window.ui:433 -msgid "Call" -msgstr "Виклик" - #: src/ui/chatty-ma-account-details.ui:93 msgid "Name" msgstr "Ім'Ñ" @@ -881,10 +941,6 @@ msgstr "Пароль" msgid "Own Fingerprint" msgstr "ВлаÑний відбиток" -#: src/ui/chatty-settings-dialog.ui:27 -msgid "Back" -msgstr "Ðазад" - #: src/ui/chatty-settings-dialog.ui:60 msgid "_Add" msgstr "_Додати" @@ -995,59 +1051,6 @@ msgstr "Домашній Ñервер" msgid "Add _new account…" msgstr "Додати _новий обліковий запиÑ…" -#: src/ui/chatty-window.ui:17 -msgctxt "show archived chat list when clicked" -msgid "Archived" -msgstr "Ðрхівовано" - -#: src/ui/chatty-window.ui:41 -msgid "Keyboard _Shortcuts" -msgstr "_Клавіатурні ÑкороченнÑ" - -#: src/ui/chatty-window.ui:48 -msgid "Help" -msgstr "Довідка" - -#: src/ui/chatty-window.ui:57 -msgid "About Chats" -msgstr "Про «СпілкуваннÑ»" - -#: src/ui/chatty-window.ui:85 -msgid "New Message…" -msgstr "Ðове повідомленнÑ…" - -#: src/ui/chatty-window.ui:98 -msgid "New SMS/MMS Message…" -msgstr "Ðове Ð¿Ð¾Ð²Ñ–Ð´Ð¾Ð¼Ð»ÐµÐ½Ð½Ñ SMS/MMS…" - -#: src/ui/chatty-window.ui:111 -msgid "New Group Message…" -msgstr "Ðове групове повідомленнÑ…" - -#: src/ui/chatty-window.ui:164 -msgid "Leave Chat" -msgstr "Полишити ÑпілкуваннÑ" - -#: src/ui/chatty-window.ui:177 -msgid "Block Contact" -msgstr "Заблокувати контакт" - -#: src/ui/chatty-window.ui:190 -msgid "Unblock Contact" -msgstr "Розблокувати контакт" - -#: src/ui/chatty-window.ui:203 -msgid "Archive chat" -msgstr "Ðрхівувати ÑпілкуваннÑ" - -#: src/ui/chatty-window.ui:216 -msgid "Unarchive chat" -msgstr "СкаÑувати архівуваннÑ" - -#: src/ui/chatty-window.ui:229 -msgid "Delete Chat" -msgstr "Вилучити ÑпілкуваннÑ" - #: src/ui/help-overlay.ui:14 msgctxt "shortcut window" msgid "General" diff --git a/src/chatty-application.c b/src/chatty-application.c index b8273ddd3026ad1671dd9af634fe96d7600d6c48..ac47aa1f2090f62ecab82985bc1cadcda681ff15 100644 --- a/src/chatty-application.c +++ b/src/chatty-application.c @@ -28,6 +28,8 @@ # include "version.h" #endif +#define CMATRIX_USE_EXPERIMENTAL_API +#include <cmatrix.h> #include <glib/gi18n.h> #include <handy.h> @@ -119,6 +121,60 @@ application_open_uri (ChattyApplication *self) return G_SOURCE_REMOVE; } +static void +chatty_application_show_about (GSimpleAction *action, + GVariant *parameter, + gpointer user_data) +{ + ChattyApplication *self = user_data; + + static const gchar *authors[] = { + "Adrien Plazas <kekun.plazas@laposte.net>", + "Andrea Schäfer <mosibasu@me.com>", + "Benedikt Wildenhain <benedikt.wildenhain@hs-bochum.de>", + "Chris Talbot (kop316) <chris@talbothome.com>", + "Guido Günther <agx@sigxcpu.org>", + "Julian Sparber <jsparber@gnome.org>", + "Leland Carlye <leland.carlye@protonmail.com>", + "Mohammed Sadiq https://www.sadiqpk.org/", + "Richard Bayerle (OMEMO Plugin) https://github.com/gkdr/lurch", + "Ruslan Marchenko <me@ruff.mobi>", + "and more...", + NULL + }; + + static const gchar *artists[] = { + "Tobias Bernard <tbernard@gnome.org>", + NULL + }; + + static const gchar *documenters[] = { + "Heather Ellsworth <heather.ellsworth@puri.sm>", + NULL + }; + + if (!self->main_window) + return; + + /* + * “program-name†defaults to g_get_application_name(). + * Don’t set it explicitly so that there is one less + * string to translate. + */ + gtk_show_about_dialog (gtk_application_get_active_window (user_data), + "logo-icon-name", CHATTY_APP_ID, + "version", GIT_VERSION, + "comments", _("An SMS and XMPP messaging client"), + "website", "https://source.puri.sm/Librem5/chatty", + "copyright", "© 2018–2022 Purism SPC", + "license-type", GTK_LICENSE_GPL_3_0, + "authors", authors, + "artists", artists, + "documenters", documenters, + "translator-credits", _("translator-credits"), + NULL); +} + static void chatty_application_show_help (GSimpleAction *action, GVariant *parameter, @@ -273,26 +329,28 @@ chatty_application_command_line (GApplication *application, options = g_application_command_line_get_options_dict (command_line); + if (g_variant_dict_contains (options, "nologin")) + chatty_manager_disable_auto_login (chatty_manager_get_default (), TRUE); + +#ifdef PURPLE_ENABLED + if (g_variant_dict_contains (options, "debug")) + chatty_purple_enable_debug (); +#endif + self->show_window = TRUE; if (g_variant_dict_contains (options, "daemon")) { /* Hold application only the first time daemon mode is set */ if (!self->daemon) g_application_hold (application); + chatty_manager_load (self->manager); + self->show_window = FALSE; self->daemon = TRUE; g_debug ("Enable daemon mode"); } - if (g_variant_dict_contains (options, "nologin")) - chatty_manager_disable_auto_login (chatty_manager_get_default (), TRUE); - -#ifdef PURPLE_ENABLED - if (g_variant_dict_contains (options, "debug")) - chatty_purple_enable_debug (); -#endif - arguments = g_application_command_line_get_arguments (command_line, &argc); /* Keep only the last URI, if there are many */ @@ -308,9 +366,11 @@ chatty_application_command_line (GApplication *application, } static const GActionEntry app_entries[] = { + { "about", chatty_application_show_about }, { "help", chatty_application_show_help, }, { "open-chat", chatty_application_open_chat, "(ssi)" }, - { "show-window", chatty_application_show_window } }; + { "show-window", chatty_application_show_window } +}; static void chatty_application_startup (GApplication *application) @@ -329,6 +389,7 @@ chatty_application_startup (GApplication *application) g_info ("%s %s, git version: %s", PACKAGE_NAME, PACKAGE_VERSION, GIT_VERSION); hdy_init (); + cm_init (TRUE); g_set_application_name (_("Chats")); @@ -347,7 +408,6 @@ chatty_application_startup (GApplication *application) g_signal_connect_object (self->manager, "open-chat", G_CALLBACK (application_open_chat), self, G_CONNECT_SWAPPED); - chatty_manager_load (self->manager); g_signal_connect_object (self, "window-removed", G_CALLBACK (app_window_removed_cb), @@ -374,6 +434,8 @@ chatty_application_activate (GApplication *application) g_assert (GTK_IS_APPLICATION (app)); + chatty_manager_load (self->manager); + if (!self->main_window && self->show_window) { g_set_weak_pointer (&self->main_window, chatty_window_new (app)); g_info ("New main window created"); diff --git a/src/chatty-avatar.c b/src/chatty-avatar.c index 3f21ca165eb54f9f3026a42e84afe7cab302b5d3..f7c54a219fd82ee78dab1a58053e18b89e4af582 100644 --- a/src/chatty-avatar.c +++ b/src/chatty-avatar.c @@ -17,6 +17,7 @@ #include <handy.h> #include "chatty-chat.h" +#include "chatty-ma-key-chat.h" #include "chatty-mm-chat.h" #include "chatty-avatar.h" @@ -56,7 +57,10 @@ avatar_changed_cb (ChattyAvatar *self) if (self->item) avatar = (GLoadableIcon *)chatty_item_get_avatar (self->item); - hdy_avatar_set_loadable_icon (HDY_AVATAR (self->avatar), avatar); + if (CHATTY_IS_MA_KEY_CHAT (self->item)) + hdy_avatar_set_icon_name (HDY_AVATAR (self->avatar), "system-lock-screen-symbolic"); + else + hdy_avatar_set_loadable_icon (HDY_AVATAR (self->avatar), avatar); } static void @@ -66,6 +70,8 @@ item_name_changed_cb (ChattyAvatar *self) !chatty_contact_is_dummy (CHATTY_CONTACT (self->item))) chatty_avatar_set_title (self, chatty_item_get_name (self->item)); + hdy_avatar_set_show_initials (HDY_AVATAR (self->avatar), !CHATTY_IS_MA_KEY_CHAT (self->item)); + if (CHATTY_IS_MM_CHAT (self->item)) { gboolean has_name; diff --git a/src/chatty-chat-view.c b/src/chatty-chat-view.c index ccb69c77765adb793f47196acc5e0a22748bf961..c94c826bbae626fb5ad5f8bfddf5a54ff1e41899 100644 --- a/src/chatty-chat-view.c +++ b/src/chatty-chat-view.c @@ -30,7 +30,6 @@ struct _ChattyChatView GtkStack parent_instance; GtkWidget *message_view; - GtkWidget *empty_view; GtkWidget *message_list; GtkWidget *loading_spinner; @@ -285,6 +284,12 @@ chat_view_attachment_revealer_notify_cb (ChattyChatView *self) has_text = gtk_text_buffer_get_char_count (self->message_input_buffer) > 0; gtk_widget_set_visible (self->send_message_button, has_files || has_text); + + if (!has_files) + { + gtk_widget_set_sensitive (self->send_file_button, TRUE); + gtk_widget_set_sensitive (self->message_input, TRUE); + } } static void @@ -429,6 +434,8 @@ chat_view_show_file_chooser (ChattyChatView *self) /* Currently multiple files are allowed only for MMS chats */ gtk_widget_set_sensitive (self->send_file_button, CHATTY_IS_MM_CHAT (self->chat)); + /* Files with message content is supported only by MMS chats */ + gtk_widget_set_sensitive (self->message_input, CHATTY_IS_MM_CHAT (self->chat)); } gtk_widget_destroy (dialog); @@ -445,8 +452,7 @@ chat_view_send_file_button_clicked_cb (ChattyChatView *self, if (CHATTY_IS_MM_CHAT (self->chat)) { chat_view_show_file_chooser (self); } else if (CHATTY_IS_MA_CHAT (self->chat)) { - /* TODO */ - + chat_view_show_file_chooser (self); } else { #ifdef PURPLE_ENABLED chatty_pp_chat_show_file_upload (CHATTY_PP_CHAT (self->chat)); @@ -525,7 +531,19 @@ chat_view_send_message_button_clicked_cb (ChattyChatView *self) gtk_text_buffer_delete (self->message_input_buffer, &start, &end); if (self->draft_message) { - chatty_message_set_text (self->draft_message, ""); + g_autofree char *uid = NULL; + + g_clear_object (&self->draft_message); + + uid = g_uuid_string_random (); + /* chatty-history expects the content of the message to not change. + * So instead of changing the content and resaving, create a new one + * with empty content to avoid a possible crash as the history thread + * may read the freed memory content otherwise + */ + self->draft_message = chatty_message_new (NULL, "", uid, time (NULL), + CHATTY_MESSAGE_TEXT, + CHATTY_DIRECTION_OUT, CHATTY_STATUS_DRAFT); chatty_history_add_message (self->history, self->chat, self->draft_message); } @@ -881,7 +899,6 @@ chatty_chat_view_class_init (ChattyChatViewClass *klass) "ui/chatty-chat-view.ui"); gtk_widget_class_bind_template_child (widget_class, ChattyChatView, message_view); - gtk_widget_class_bind_template_child (widget_class, ChattyChatView, empty_view); gtk_widget_class_bind_template_child (widget_class, ChattyChatView, scroll_down_button); gtk_widget_class_bind_template_child (widget_class, ChattyChatView, message_list); @@ -921,7 +938,6 @@ chatty_chat_view_init (ChattyChatView *self) gtk_widget_init_template (GTK_WIDGET (self)); gtk_list_box_set_placeholder (GTK_LIST_BOX (self->message_list), self->no_message_status); - gtk_stack_set_visible_child (GTK_STACK (self), self->empty_view); g_signal_connect_after (G_OBJECT (self), "file-requested", G_CALLBACK (chat_view_file_requested_cb), self); @@ -1007,11 +1023,6 @@ chatty_chat_view_set_chat (ChattyChatView *self, CHATTY_DIRECTION_OUT, CHATTY_STATUS_DRAFT); } - if (chat) - gtk_stack_set_visible_child (GTK_STACK (self), self->message_view); - else - gtk_stack_set_visible_child (GTK_STACK (self), self->empty_view); - if (!chat) { gtk_list_box_bind_model (GTK_LIST_BOX (self->message_list), NULL, NULL, NULL, NULL); diff --git a/src/chatty-chat.c b/src/chatty-chat.c index 2365677c938dc30efec142054fc95235fa9da1ba..e21d7dc03fbbf9d4b2db0deb8772512801482867 100644 --- a/src/chatty-chat.c +++ b/src/chatty-chat.c @@ -52,6 +52,7 @@ enum { PROP_0, PROP_ENCRYPT, PROP_BUDDY_TYPING, + PROP_CHAT_STATE, PROP_LOADING_HISTORY, N_PROPS }; @@ -89,6 +90,14 @@ chatty_chat_real_is_im (ChattyChat *self) return priv->is_im; } +static ChattyChatState +chatty_chat_real_get_chat_state (ChattyChat *self) +{ + g_assert (CHATTY_IS_CHAT (self)); + + return CHATTY_CHAT_JOINED; +} + static gboolean chatty_chat_real_has_file_upload (ChattyChat *self) { @@ -257,6 +266,55 @@ chatty_chat_real_set_encryption_async (ChattyChat *self, "Setting encryption not supported"); } +static void +chatty_chat_real_accept_invite_async (ChattyChat *self, + GAsyncReadyCallback callback, + gpointer user_data) +{ + g_assert (CHATTY_IS_CHAT (self)); + + g_task_report_new_error (self, callback, user_data, + chatty_chat_real_accept_invite_async, + G_IO_ERROR, + G_IO_ERROR_NOT_SUPPORTED, + "Accept invite not supported"); +} + +static gboolean +chatty_chat_real_accept_invite_finish (ChattyChat *self, + GAsyncResult *result, + GError **error) +{ + g_assert (CHATTY_IS_CHAT (self)); + g_assert (G_IS_TASK (result)); + + return g_task_propagate_boolean (G_TASK (result), error); +} + +static void +chatty_chat_real_reject_invite_async (ChattyChat *self, + GAsyncReadyCallback callback, + gpointer user_data) +{ + g_assert (CHATTY_IS_CHAT (self)); + + g_task_report_new_error (self, callback, user_data, + chatty_chat_real_reject_invite_async, + G_IO_ERROR, + G_IO_ERROR_NOT_SUPPORTED, + "Reject invite not supported"); +} + +static gboolean +chatty_chat_real_reject_invite_finish (ChattyChat *self, + GAsyncResult *result, + GError **error) +{ + g_assert (CHATTY_IS_CHAT (self)); + g_assert (G_IS_TASK (result)); + + return g_task_propagate_boolean (G_TASK (result), error); +} static gboolean chatty_chat_real_set_encryption_finish (ChattyChat *self, @@ -391,6 +449,10 @@ chatty_chat_get_property (GObject *object, g_value_set_boolean (value, chatty_chat_get_buddy_typing (self)); break; + case PROP_CHAT_STATE: + g_value_set_boolean (value, chatty_chat_get_chat_state (self)); + break; + case PROP_LOADING_HISTORY: g_value_set_boolean (value, chatty_chat_is_loading_history (self)); break; @@ -453,6 +515,7 @@ chatty_chat_class_init (ChattyChatClass *klass) klass->set_data = chatty_chat_real_set_data; klass->is_im = chatty_chat_real_is_im; + klass->get_chat_state = chatty_chat_real_get_chat_state; klass->has_file_upload = chatty_chat_real_has_file_upload; klass->get_chat_name = chatty_chat_real_get_chat_name; klass->get_account = chatty_chat_real_get_account; @@ -472,6 +535,10 @@ chatty_chat_class_init (ChattyChatClass *klass) klass->get_encryption = chatty_chat_real_get_encryption; klass->set_encryption = chatty_chat_real_set_encryption; klass->set_encryption_async = chatty_chat_real_set_encryption_async; + klass->accept_invite_async = chatty_chat_real_accept_invite_async; + klass->accept_invite_finish = chatty_chat_real_accept_invite_finish; + klass->reject_invite_async = chatty_chat_real_reject_invite_async; + klass->reject_invite_finish = chatty_chat_real_reject_invite_finish; klass->set_encryption_finish = chatty_chat_real_set_encryption_finish; klass->get_buddy_typing = chatty_chat_real_get_buddy_typing; klass->set_typing = chatty_chat_real_set_typing; @@ -486,6 +553,13 @@ chatty_chat_class_init (ChattyChatClass *klass) FALSE, G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS); + properties[PROP_CHAT_STATE] = + g_param_spec_boolean ("chat-state", + "Chat state", + "Whether the chat is having invite state or not", + FALSE, + G_PARAM_READABLE | G_PARAM_STATIC_STRINGS); + properties[PROP_BUDDY_TYPING] = g_param_spec_boolean ("buddy-typing", "Buddy typing", @@ -646,6 +720,14 @@ chatty_chat_generate_name (ChattyChat *self, name_a ?: "", count - 1); } +ChattyChatState +chatty_chat_get_chat_state (ChattyChat *self) +{ + g_return_val_if_fail (CHATTY_IS_CHAT (self), CHATTY_CHAT_UNKNOWN); + + return CHATTY_CHAT_GET_CLASS (self)->get_chat_state (self); +} + gboolean chatty_chat_has_file_upload (ChattyChat *self) { @@ -920,6 +1002,48 @@ chatty_chat_set_typing (ChattyChat *self, CHATTY_CHAT_GET_CLASS (self)->set_typing (self, !!is_typing); } +void +chatty_chat_accept_invite_async (ChattyChat *self, + GAsyncReadyCallback callback, + gpointer user_data) +{ + g_return_if_fail (CHATTY_IS_CHAT (self)); + + CHATTY_CHAT_GET_CLASS (self)->accept_invite_async (self, callback, user_data); +} + +gboolean +chatty_chat_accept_invite_finish (ChattyChat *self, + GAsyncResult *result, + GError **error) +{ + g_return_val_if_fail (CHATTY_IS_CHAT (self), FALSE); + g_return_val_if_fail (G_IS_ASYNC_RESULT (result), FALSE); + + return CHATTY_CHAT_GET_CLASS (self)->accept_invite_finish (self, result, error); +} + +void +chatty_chat_reject_invite_async (ChattyChat *self, + GAsyncReadyCallback callback, + gpointer user_data) +{ + g_return_if_fail (CHATTY_IS_CHAT (self)); + + CHATTY_CHAT_GET_CLASS (self)->reject_invite_async (self, callback, user_data); +} + +gboolean +chatty_chat_reject_invite_finish (ChattyChat *self, + GAsyncResult *result, + GError **error) +{ + g_return_val_if_fail (CHATTY_IS_CHAT (self), FALSE); + g_return_val_if_fail (G_IS_ASYNC_RESULT (result), FALSE); + + return CHATTY_CHAT_GET_CLASS (self)->reject_invite_finish (self, result, error); +} + gboolean chatty_chat_get_buddy_typing (ChattyChat *self) { diff --git a/src/chatty-chat.h b/src/chatty-chat.h index 2ac607e5d8b906805eb07e45dad9cc0a0be4ae19..7787ba5346bda68232cb3364431a2fe7a573a578 100644 --- a/src/chatty-chat.h +++ b/src/chatty-chat.h @@ -33,6 +33,7 @@ struct _ChattyChatClass gpointer history_db); gboolean (*is_im) (ChattyChat *self); gboolean (*has_file_upload) (ChattyChat *self); + ChattyChatState (*get_chat_state) (ChattyChat *self); const char *(*get_chat_name) (ChattyChat *self); ChattyAccount *(*get_account) (ChattyChat *self); GListModel *(*get_messages) (ChattyChat *self); @@ -71,6 +72,18 @@ struct _ChattyChatClass gboolean (*set_encryption_finish) (ChattyChat *self, GAsyncResult *result, GError **error); + void (*accept_invite_async) (ChattyChat *self, + GAsyncReadyCallback callback, + gpointer user_data); + gboolean (*accept_invite_finish) (ChattyChat *self, + GAsyncResult *result, + GError **error); + void (*reject_invite_async) (ChattyChat *self, + GAsyncReadyCallback callback, + gpointer user_data); + gboolean (*reject_invite_finish) (ChattyChat *self, + GAsyncResult *result, + GError **error); gboolean (*get_buddy_typing) (ChattyChat *self); void (*set_typing) (ChattyChat *self, gboolean is_typing); @@ -96,6 +109,7 @@ void chatty_chat_set_data (ChattyChat *self, gboolean chatty_chat_is_im (ChattyChat *self); char *chatty_chat_generate_name (ChattyChat *self, GListModel *members); +ChattyChatState chatty_chat_get_chat_state (ChattyChat *self); gboolean chatty_chat_has_file_upload (ChattyChat *self); const char *chatty_chat_get_chat_name (ChattyChat *self); ChattyAccount *chatty_chat_get_account (ChattyChat *self); @@ -136,6 +150,19 @@ void chatty_chat_set_encryption_async (ChattyChat *self, gboolean chatty_chat_set_encryption_finish (ChattyChat *self, GAsyncResult *result, GError **error); + +void chatty_chat_accept_invite_async (ChattyChat *self, + GAsyncReadyCallback callback, + gpointer user_data); +gboolean chatty_chat_accept_invite_finish (ChattyChat *self, + GAsyncResult *result, + GError **error); +void chatty_chat_reject_invite_async (ChattyChat *self, + GAsyncReadyCallback callback, + gpointer user_data); +gboolean chatty_chat_reject_invite_finish (ChattyChat *self, + GAsyncResult *result, + GError **error); gboolean chatty_chat_get_buddy_typing (ChattyChat *self); void chatty_chat_set_typing (ChattyChat *self, gboolean is_typing); diff --git a/src/chatty-enums.h b/src/chatty-enums.h index ea029fbc0b905bf244e1c95a0bff4b202a31ce76..e51853ea18b6c195b9f520c2535563d07f00bcb4 100644 --- a/src/chatty-enums.h +++ b/src/chatty-enums.h @@ -153,3 +153,12 @@ typedef enum CHATTY_ITEM_ARCHIVED, CHATTY_ITEM_BLOCKED, } ChattyItemState; + +typedef enum +{ + CHATTY_CHAT_UNKNOWN, + CHATTY_CHAT_VERIFICATION, + CHATTY_CHAT_INVITED, + CHATTY_CHAT_JOINED, + CHATTY_CHAT_LEFT +} ChattyChatState; diff --git a/src/chatty-header-bar.c b/src/chatty-header-bar.c new file mode 100644 index 0000000000000000000000000000000000000000..31b380494c30eb69a00e82e950715e06c548ec95 --- /dev/null +++ b/src/chatty-header-bar.c @@ -0,0 +1,353 @@ +/* -*- mode: c; c-basic-offset: 2; indent-tabs-mode: nil; -*- */ +/* chatty-header-bar.c + * + * Copyright 2022 Purism SPC + * + * Author(s): + * Mohammed Sadiq <sadiq@sadiqpk.org> + * + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +#define G_LOG_DOMAIN "chatty-header-bar" + +#ifdef HAVE_CONFIG_H +# include "config.h" +#endif + +#include <glib/gi18n.h> + +#include "chatty-avatar.h" +#include "chatty-manager.h" +#include "chatty-mm-account.h" +#include "matrix/chatty-ma-chat.h" +#include "mm/chatty-mm-chat.h" +#include "chatty-header-bar.h" + +struct _ChattyHeaderBar +{ + GtkBox parent_instance; + + GtkWidget *leaflet; + + GtkWidget *main_header_bar; + GtkWidget *back_button; + GtkWidget *add_chat_button; + GtkWidget *search_button; + + GtkWidget *content_header_bar; + GtkWidget *content_avatar; + GtkWidget *content_title; + GtkWidget *content_menu_button; + GtkWidget *call_button; + + GtkWidget *sidebar_group; + GtkWidget *content_group; + GtkWidget *header_group; + + GtkWidget *new_chat_button; + GtkWidget *new_sms_mms_button; + GtkWidget *new_group_chat_button; + + GtkWidget *leave_button; + GtkWidget *block_button; + GtkWidget *unblock_button; + GtkWidget *archive_button; + GtkWidget *unarchive_button; + GtkWidget *delete_button; + + GBinding *title_binding; + + ChattyItem *item; + + gulong content_handler; +}; + +G_DEFINE_TYPE (ChattyHeaderBar, chatty_header_bar, GTK_TYPE_BOX) + +enum { + BACK_CLICKED, + N_SIGNALS +}; + +static guint signals[N_SIGNALS]; + +static void +header_bar_update_item_state_button (ChattyHeaderBar *self, + ChattyItem *item) +{ + ChattyItemState state; + + gtk_widget_hide (self->block_button); + gtk_widget_hide (self->unblock_button); + gtk_widget_hide (self->archive_button); + gtk_widget_hide (self->unarchive_button); + + if (!item || !CHATTY_IS_MM_CHAT (item)) + return; + + state = chatty_item_get_state (item); + + if (state == CHATTY_ITEM_VISIBLE) { + gtk_widget_show (self->block_button); + gtk_widget_show (self->archive_button); + } else if (state == CHATTY_ITEM_ARCHIVED) { + gtk_widget_show (self->unarchive_button); + } else if (state == CHATTY_ITEM_BLOCKED) { + gtk_widget_show (self->unblock_button); + } +} + +static void +header_bar_chat_changed_cb (ChattyHeaderBar *self, + ChattyItem *item) +{ + GListModel *users; + + users = chatty_chat_get_users (CHATTY_CHAT (item)); + + /* allow changing state only for 1:1 SMS/MMS chats */ + if (item == self->item && + CHATTY_IS_MM_CHAT (item) && + g_list_model_get_n_items (users) == 1) + header_bar_update_item_state_button (self, item); +} + +static void +header_bar_active_protocols_changed_cb (ChattyHeaderBar *self) +{ + ChattyManager *manager; + ChattyAccount *mm_account; + ChattyProtocol protocols; + gboolean has_mms, has_sms, has_im; + + g_assert (CHATTY_IS_HEADER_BAR (self)); + + manager = chatty_manager_get_default (); + mm_account = chatty_manager_get_mm_account (manager); + protocols = chatty_manager_get_active_protocols (manager); + has_mms = chatty_mm_account_has_mms_feature (CHATTY_MM_ACCOUNT (mm_account)); + has_sms = !!(protocols & CHATTY_PROTOCOL_MMS_SMS); + has_im = !!(protocols & ~CHATTY_PROTOCOL_MMS_SMS); + + gtk_widget_set_sensitive (self->add_chat_button, has_sms || has_im); + gtk_widget_set_sensitive (self->new_group_chat_button, has_im); + gtk_widget_set_visible (self->new_sms_mms_button, has_mms && has_sms); +} + +static void +header_back_clicked_cb (ChattyHeaderBar *self) +{ + g_assert (CHATTY_IS_HEADER_BAR (self)); + + g_signal_emit (self, signals[BACK_CLICKED], 0); +} + +static void +chatty_header_bar_map (GtkWidget *widget) +{ + ChattyHeaderBar *self = (ChattyHeaderBar *)widget; + ChattyManager *manager; + + manager = chatty_manager_get_default (); + g_signal_connect_object (manager, "notify::active-protocols", + G_CALLBACK (header_bar_active_protocols_changed_cb), self, + G_CONNECT_SWAPPED); + + header_bar_active_protocols_changed_cb (self); + + GTK_WIDGET_CLASS (chatty_header_bar_parent_class)->map (widget); +} + +static void +chatty_header_bar_dispose (GObject *object) +{ + ChattyHeaderBar *self = (ChattyHeaderBar *)object; + + g_clear_object (&self->item); + + G_OBJECT_CLASS (chatty_header_bar_parent_class)->dispose (object); +} + +static void +chatty_header_bar_class_init (ChattyHeaderBarClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass); + + object_class->dispose = chatty_header_bar_dispose; + + widget_class->map = chatty_header_bar_map; + + gtk_widget_class_set_template_from_resource (widget_class, + "/sm/puri/Chatty/" + "ui/chatty-header-bar.ui"); + + gtk_widget_class_bind_template_child (widget_class, ChattyHeaderBar, leaflet); + + gtk_widget_class_bind_template_child (widget_class, ChattyHeaderBar, main_header_bar); + gtk_widget_class_bind_template_child (widget_class, ChattyHeaderBar, back_button); + gtk_widget_class_bind_template_child (widget_class, ChattyHeaderBar, add_chat_button); + gtk_widget_class_bind_template_child (widget_class, ChattyHeaderBar, search_button); + + gtk_widget_class_bind_template_child (widget_class, ChattyHeaderBar, content_header_bar); + gtk_widget_class_bind_template_child (widget_class, ChattyHeaderBar, content_avatar); + gtk_widget_class_bind_template_child (widget_class, ChattyHeaderBar, content_title); + gtk_widget_class_bind_template_child (widget_class, ChattyHeaderBar, content_menu_button); + gtk_widget_class_bind_template_child (widget_class, ChattyHeaderBar, call_button); + + gtk_widget_class_bind_template_child (widget_class, ChattyHeaderBar, sidebar_group); + gtk_widget_class_bind_template_child (widget_class, ChattyHeaderBar, content_group); + gtk_widget_class_bind_template_child (widget_class, ChattyHeaderBar, header_group); + + gtk_widget_class_bind_template_child (widget_class, ChattyHeaderBar, new_chat_button); + gtk_widget_class_bind_template_child (widget_class, ChattyHeaderBar, new_sms_mms_button); + gtk_widget_class_bind_template_child (widget_class, ChattyHeaderBar, new_group_chat_button); + + gtk_widget_class_bind_template_child (widget_class, ChattyHeaderBar, leave_button); + gtk_widget_class_bind_template_child (widget_class, ChattyHeaderBar, block_button); + gtk_widget_class_bind_template_child (widget_class, ChattyHeaderBar, unblock_button); + gtk_widget_class_bind_template_child (widget_class, ChattyHeaderBar, archive_button); + gtk_widget_class_bind_template_child (widget_class, ChattyHeaderBar, unarchive_button); + gtk_widget_class_bind_template_child (widget_class, ChattyHeaderBar, delete_button); + + gtk_widget_class_bind_template_callback (widget_class, header_back_clicked_cb); + + signals [BACK_CLICKED] = + g_signal_new ("back-clicked", + G_TYPE_FROM_CLASS (klass), + G_SIGNAL_RUN_LAST, + 0, NULL, NULL, NULL, + G_TYPE_NONE, 0); +} + +static void +chatty_header_bar_init (ChattyHeaderBar *self) +{ + gtk_widget_init_template (GTK_WIDGET (self)); +} + +void +chatty_header_bar_set_can_search (ChattyHeaderBar *self, + gboolean can_search) +{ + g_return_if_fail (CHATTY_IS_HEADER_BAR (self)); + + gtk_widget_set_visible (self->search_button, can_search); +} + +void +chatty_header_bar_show_archived (ChattyHeaderBar *self, + gboolean show_archived) +{ + g_return_if_fail (CHATTY_IS_HEADER_BAR (self)); + + if (show_archived) { + hdy_header_bar_set_title (HDY_HEADER_BAR (self->main_header_bar), _("Archived")); + gtk_widget_show (self->back_button); + gtk_widget_hide (self->add_chat_button); + } else { + hdy_header_bar_set_title (HDY_HEADER_BAR (self->main_header_bar), _("Chats")); + gtk_widget_hide (self->back_button); + gtk_widget_show (self->add_chat_button); + } +} + +void +chatty_header_bar_set_item (ChattyHeaderBar *self, + ChattyItem *item) +{ + g_return_if_fail (CHATTY_IS_HEADER_BAR (self)); + g_return_if_fail (!item || CHATTY_IS_ITEM (item)); + + if (self->item == item) + return; + + if (self->item) + g_clear_signal_handler (&self->content_handler, self->item); + + g_set_object (&self->item, item); + + g_clear_object (&self->title_binding); + gtk_label_set_label (GTK_LABEL (self->content_title), ""); + gtk_widget_set_visible (self->content_menu_button, !!item); + + header_bar_update_item_state_button (self, item); + gtk_widget_set_visible (self->content_avatar, !!item); + gtk_widget_hide (self->call_button); + + if (item) { + gtk_widget_set_visible (self->leave_button, !CHATTY_IS_MM_CHAT (item)); + /* We can't delete MaChat */ + gtk_widget_set_visible (self->delete_button, !CHATTY_IS_MA_CHAT (item)); + + if (CHATTY_IS_MM_CHAT (item)) { + GListModel *users; + const char *name; + + users = chatty_chat_get_users (CHATTY_CHAT (item)); + name = chatty_chat_get_chat_name (CHATTY_CHAT (item)); + + /* allow changing state only for 1:1 SMS/MMS chats */ + if (g_list_model_get_n_items (users) == 1) + header_bar_update_item_state_button (self, item); + + if (g_list_model_get_n_items (users) == 1 && + chatty_utils_username_is_valid (name, CHATTY_PROTOCOL_MMS_SMS)) { + g_autoptr(ChattyMmBuddy) buddy = NULL; + g_autoptr(GAppInfo) app_info = NULL; + + app_info = g_app_info_get_default_for_uri_scheme ("tel"); + buddy = g_list_model_get_item (users, 0); + + if (app_info) + gtk_widget_show (self->call_button); + } + } + + chatty_avatar_set_item (CHATTY_AVATAR (self->content_avatar), item); + self->title_binding = g_object_bind_property (item, "name", + self->content_title, "label", + G_BINDING_SYNC_CREATE); + + if (CHATTY_IS_CHAT (item)) + self->content_handler = g_signal_connect_object (item, "changed", + G_CALLBACK (header_bar_chat_changed_cb), + self, G_CONNECT_SWAPPED); + } +} + +void +chatty_header_bar_set_content_box (ChattyHeaderBar *self, + GtkWidget *widget) +{ + GtkWidget *child; + + g_return_if_fail (CHATTY_IS_HEADER_BAR (self)); + g_return_if_fail (HDY_IS_LEAFLET (widget)); + + g_object_bind_property (widget, "visible-child-name", + self->leaflet, "visible-child-name", + G_BINDING_SYNC_CREATE); + g_object_bind_property (widget, "folded", + self->header_group, "decorate-all", + G_BINDING_SYNC_CREATE); + + child = hdy_leaflet_get_child_by_name (HDY_LEAFLET (widget), "sidebar"); + gtk_size_group_add_widget (GTK_SIZE_GROUP (self->sidebar_group), child); + + child = hdy_leaflet_get_child_by_name (HDY_LEAFLET (widget), "content"); + gtk_size_group_add_widget (GTK_SIZE_GROUP (self->content_group), child); +} + +void +chatty_header_bar_set_search_bar (ChattyHeaderBar *self, + GtkWidget *widget) +{ + g_return_if_fail (CHATTY_IS_HEADER_BAR (self)); + g_return_if_fail (GTK_WIDGET (widget)); + + g_object_bind_property (widget, "search-mode-enabled", + self->search_button, "active", + G_BINDING_SYNC_CREATE | G_BINDING_BIDIRECTIONAL); +} diff --git a/src/chatty-header-bar.h b/src/chatty-header-bar.h new file mode 100644 index 0000000000000000000000000000000000000000..93cf6c703a0e05a38fa2426f41e3fdb8f82d9250 --- /dev/null +++ b/src/chatty-header-bar.h @@ -0,0 +1,35 @@ +/* -*- mode: c; c-basic-offset: 2; indent-tabs-mode: nil; -*- */ +/* chatty-header-bar.h + * + * Copyright 2022 Purism SPC + * + * Author(s): + * Mohammed Sadiq <sadiq@sadiqpk.org> + * + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +#pragma once + +#include <gtk/gtk.h> + +#include "chatty-chat.h" + +G_BEGIN_DECLS + +#define CHATTY_TYPE_HEADER_BAR (chatty_header_bar_get_type ()) + +G_DECLARE_FINAL_TYPE (ChattyHeaderBar, chatty_header_bar, CHATTY, HEADER_BAR, GtkBox) + +void chatty_header_bar_set_can_search (ChattyHeaderBar *self, + gboolean can_search); +void chatty_header_bar_show_archived (ChattyHeaderBar *self, + gboolean show_archived); +void chatty_header_bar_set_item (ChattyHeaderBar *self, + ChattyItem *item); +void chatty_header_bar_set_content_box (ChattyHeaderBar *self, + GtkWidget *widget); +void chatty_header_bar_set_search_bar (ChattyHeaderBar *self, + GtkWidget *widget); + +G_END_DECLS diff --git a/src/chatty-history.c b/src/chatty-history.c index 9a75ae9c465c6d0d91fe7fbd0eb6bf8ba636a191..278eecfac87e6221fa4ee936cb01ed7234fc0359 100644 --- a/src/chatty-history.c +++ b/src/chatty-history.c @@ -494,8 +494,7 @@ chatty_history_create_schema (ChattyHistory *self, /* XXX: SELECT * FROM files sounds better, WHERE file.id != x feels better too. * So what to name? file or files? */ - sql = "BEGIN TRANSACTION;" - + sql = "CREATE TABLE IF NOT EXISTS mime_type (" "id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, " "name TEXT NOT NULL UNIQUE);" @@ -620,7 +619,7 @@ chatty_history_create_schema (ChattyHistory *self, "FROM users " "WHERE users.username='"MM_NUMBER"';" - "COMMIT;"; + "PRAGMA user_version = " STRING (HISTORY_VERSION) ";"; status = sqlite3_exec (self->db, sql, NULL, NULL, &error); @@ -640,36 +639,6 @@ chatty_history_create_schema (ChattyHistory *self, return FALSE; } -static gboolean -chatty_history_update_version (ChattyHistory *self, - GTask *task) -{ - char *error = NULL; - const char *sql; - int status; - - g_assert (CHATTY_IS_HISTORY (self)); - g_assert (G_IS_TASK (task)); - g_assert (g_thread_self () == self->worker_thread); - g_assert (self->db); - - sql = "PRAGMA user_version = " STRING (HISTORY_VERSION) ";"; - - status = sqlite3_exec (self->db, sql, NULL, NULL, &error); - - if (status == SQLITE_OK) - return TRUE; - - g_task_return_new_error (task, - G_IO_ERROR, - G_IO_ERROR_FAILED, - "Couldn't set db version. errno: %d, desc: %s. %s", - status, sqlite3_errmsg (self->db), error); - sqlite3_free (error); - - return FALSE; -} - static int insert_or_ignore_user (ChattyHistory *self, ChattyProtocol protocol, @@ -1076,13 +1045,8 @@ chatty_history_migrate_db_to_v1_to_v3 (ChattyHistory *self, return FALSE; status = sqlite3_exec (self->db, - - "BEGIN TRANSACTION;" "UPDATE chatty_im SET account='"MM_NUMBER"' " "WHERE chatty_im.account='SMS';" - "COMMIT;" - - "BEGIN TRANSACTION;" /*** Users ***/ /* XMPP IM accounts */ @@ -1343,13 +1307,13 @@ chatty_history_migrate_db_to_v1_to_v3 (ChattyHistory *self, "AND chatty_chat.who IS NULL or chatty_chat.who GLOB '@?*:?*' " "ORDER BY timestamp ASC, chatty_chat.id ASC;" - "COMMIT;", + "PRAGMA user_version = 4;", NULL, NULL, &error); if (!e_phone_number_is_supported ()) g_debug ("Not compiled with libphonenumber"); - if (status == SQLITE_OK) { + { sqlite3_stmt *stmt; /* Get all numbers in international format */ @@ -1358,8 +1322,6 @@ chatty_history_migrate_db_to_v1_to_v3 (ChattyHistory *self, "SELECT DISTINCT account,who FROM chatty_im " "WHERE account GLOB '+[0-9]*[^@]*[0-9]';", -1, &stmt, NULL); - if (status == SQLITE_OK) - status = sqlite3_exec (self->db, "BEGIN TRANSACTION;", NULL, NULL, &error); while (sqlite3_step (stmt) == SQLITE_ROW) { g_autofree char *account_number = NULL; @@ -1429,13 +1391,10 @@ chatty_history_migrate_db_to_v1_to_v3 (ChattyHistory *self, "AND u.type="STRING(CHATTY_ID_PHONE_VALUE) ";", NULL, NULL, &error); - if (status == SQLITE_DONE || status == SQLITE_OK) - status = sqlite3_exec (self->db, "COMMIT;", NULL, NULL, &error); - sqlite3_finalize (stmt); } - if (status == SQLITE_OK) { + { sqlite3_stmt *stmt; status = sqlite3_prepare_v2 (self->db, /* Telegram Chats */ @@ -1443,9 +1402,6 @@ chatty_history_migrate_db_to_v1_to_v3 (ChattyHistory *self, "WHERE account GLOB '+[0-9]*[^@:.]*[0-9]';", -1, &stmt, NULL); - if (status == SQLITE_OK) - status = sqlite3_exec (self->db, "BEGIN TRANSACTION;", NULL, NULL, &error); - while (sqlite3_step (stmt) == SQLITE_ROW) { g_autofree char *account_number = NULL; g_autofree char *sender_number = NULL; @@ -1566,13 +1522,10 @@ chatty_history_migrate_db_to_v1_to_v3 (ChattyHistory *self, "AND u.type="STRING(CHATTY_ID_PHONE_VALUE) ";", NULL, NULL, &error); - if (status == SQLITE_DONE || status == SQLITE_OK) - status = sqlite3_exec (self->db, "COMMIT;", NULL, NULL, &error); - sqlite3_finalize (stmt); } - if (status == SQLITE_OK) { + { sqlite3_stmt *stmt; status = sqlite3_prepare_v2 (self->db, /* SMS users with phone numbers sorted */ @@ -1586,9 +1539,6 @@ chatty_history_migrate_db_to_v1_to_v3 (ChattyHistory *self, "WHERE chatty_im.account='"MM_NUMBER"' ORDER BY chatty_im.id ASC;", -1, &stmt, NULL); - if (status == SQLITE_OK) - status = sqlite3_exec (self->db, "BEGIN TRANSACTION;", NULL, NULL, &error); - while (sqlite3_step (stmt) == SQLITE_ROW) { g_autofree char *sender_number = NULL; sqlite3_stmt *insert_stmt = NULL; @@ -1638,26 +1588,17 @@ chatty_history_migrate_db_to_v1_to_v3 (ChattyHistory *self, "AND u.type="STRING(CHATTY_ID_PHONE_VALUE) ";", NULL, NULL, &error); - if (status == SQLITE_DONE || status == SQLITE_OK) - status = sqlite3_exec (self->db, "COMMIT;", NULL, NULL, &error); - sqlite3_finalize (stmt); } /* Drop old tables */ - if (status == SQLITE_OK) - status = sqlite3_exec (self->db, - "BEGIN TRANSACTION;" - "DROP TABLE chatty_chat;" - "DROP TABLE chatty_im;" - "COMMIT;", NULL, NULL, &error); - - if (status == SQLITE_OK || status == SQLITE_DONE) { - /* Update user_version pragma */ - if (!chatty_history_update_version (self, task)) - return FALSE; + status = sqlite3_exec (self->db, + "DROP TABLE chatty_chat;" + "DROP TABLE chatty_im;", + NULL, NULL, &error); + + if (status == SQLITE_OK || status == SQLITE_DONE) return TRUE; - } g_task_return_new_error (task, G_IO_ERROR, @@ -1684,8 +1625,8 @@ chatty_history_migrate_db_to_v2 (ChattyHistory *self, chatty_history_backup (self); status = sqlite3_exec (self->db, - "BEGIN TRANSACTION;" - "PRAGMA foreign_keys=OFF;" + /* "BEGIN TRANSACTION;" */ + /* "PRAGMA foreign_keys=OFF;" */ "DROP TABLE IF EXISTS media;" "DROP TABLE IF EXISTS files;" @@ -1755,15 +1696,12 @@ chatty_history_migrate_db_to_v2 (ChattyHistory *self, "DROP TABLE IF EXISTS threads;" "ALTER TABLE temp_threads RENAME TO threads;" - "COMMIT;", + "PRAGMA user_version = 2;", + /* "COMMIT;", */ NULL, NULL, &error); - if (status == SQLITE_OK || status == SQLITE_DONE) { - /* Update user_version pragma */ - if (!chatty_history_update_version (self, task)) - return FALSE; + if (status == SQLITE_OK || status == SQLITE_DONE) return TRUE; - } g_task_return_new_error (task, G_IO_ERROR, @@ -1790,21 +1728,14 @@ chatty_history_migrate_db_to_v3 (ChattyHistory *self, chatty_history_backup (self); status = sqlite3_exec (self->db, - "BEGIN TRANSACTION;" - "PRAGMA foreign_keys=OFF;" - "ALTER TABLE threads ADD COLUMN visibility INT NOT NULL DEFAULT " STRING(THREAD_VISIBILITY_VISIBLE) ";" - "COMMIT;", + "PRAGMA user_version = 3;", NULL, NULL, &error); - if (status == SQLITE_OK || status == SQLITE_DONE) { - /* Update user_version pragma */ - if (!chatty_history_update_version (self, task)) - return FALSE; + if (status == SQLITE_OK || status == SQLITE_DONE) return TRUE; - } g_task_return_new_error (task, G_IO_ERROR, @@ -1831,9 +1762,6 @@ chatty_history_migrate_db_to_v4 (ChattyHistory *self, chatty_history_backup (self); status = sqlite3_exec (self->db, - "BEGIN TRANSACTION;" - "PRAGMA foreign_keys=OFF;" - "ALTER TABLE threads ADD COLUMN notification INTEGER NOT NULL DEFAULT 1;" "ALTER TABLE messages ADD COLUMN subject TEXT;" @@ -1928,15 +1856,11 @@ chatty_history_migrate_db_to_v4 (ChattyHistory *self, "DROP TABLE IF EXISTS image;" "DROP TABLE IF EXISTS video;" - "COMMIT;", + "PRAGMA user_version = 4;", NULL, NULL, &error); - if (status == SQLITE_OK || status == SQLITE_DONE) { - /* Update user_version pragma */ - if (!chatty_history_update_version (self, task)) - return FALSE; + if (status == SQLITE_OK || status == SQLITE_DONE) return TRUE; - } g_task_return_new_error (task, G_IO_ERROR, @@ -2028,18 +1952,22 @@ history_open_db (ChattyHistory *self, if (status == SQLITE_OK) { self->db = db; + sqlite3_exec (self->db, "PRAGMA foreign_keys = OFF;", NULL, NULL, NULL); + sqlite3_exec (self->db, "BEGIN TRANSACTION;", NULL, NULL, NULL); if (db_exists) { - if (!chatty_history_migrate (self, task)) + if (!chatty_history_migrate (self, task)) { + sqlite3_exec (self->db, "END TRANSACTION;", NULL, NULL, NULL); return; + } } else { - if (!chatty_history_create_schema (self, task)) - return; - - if (!chatty_history_update_version (self, task)) + if (!chatty_history_create_schema (self, task)) { + sqlite3_exec (self->db, "END TRANSACTION;", NULL, NULL, NULL); return; + } } sqlite3_exec (self->db, "PRAGMA foreign_keys = ON;", NULL, NULL, NULL); + sqlite3_exec (self->db, "END TRANSACTION;", NULL, NULL, NULL); g_task_return_boolean (task, TRUE); } else { g_task_return_boolean (task, FALSE); @@ -2565,9 +2493,12 @@ history_add_message (ChattyHistory *self, if ((!who || !*who) && direction == CHATTY_DIRECTION_IN && chatty_chat_is_im (chat)) who = chatty_chat_get_chat_name (chat); + sqlite3_exec (self->db, "BEGIN TRANSACTION;", NULL, NULL, NULL); thread_id = insert_or_ignore_thread (self, chat, task); - if (!thread_id) + if (!thread_id) { + sqlite3_exec (self->db, "END TRANSACTION;", NULL, NULL, NULL); return; + } sender_id = insert_or_ignore_user (self, chatty_item_get_protocols (CHATTY_ITEM (chat)), who, alias, task); @@ -2576,6 +2507,7 @@ history_add_message (ChattyHistory *self, int message_id = 0; if (!msg) { + sqlite3_exec (self->db, "END TRANSACTION;", NULL, NULL, NULL); g_task_return_boolean (task, TRUE); return; } @@ -2605,6 +2537,7 @@ history_add_message (ChattyHistory *self, status = sqlite3_step (stmt); sqlite3_finalize (stmt); + sqlite3_exec (self->db, "END TRANSACTION;", NULL, NULL, NULL); if (status == SQLITE_DONE) g_task_return_boolean (task, TRUE); @@ -2667,6 +2600,7 @@ history_add_message (ChattyHistory *self, if (status == SQLITE_ROW) history_add_files (self, message, sqlite3_column_int (stmt, 0)); sqlite3_finalize (stmt); + sqlite3_exec (self->db, "END TRANSACTION;", NULL, NULL, NULL); if (status == SQLITE_DONE || status == SQLITE_ROW) g_task_return_boolean (task, TRUE); @@ -2750,7 +2684,6 @@ history_get_chats (ChattyHistory *self, ChattyAccount *account; sqlite3_stmt *stmt; const char *user_id; - int protocol, encrypted; g_assert (CHATTY_IS_HISTORY (self)); g_assert (G_IS_TASK (task)); @@ -2764,16 +2697,11 @@ history_get_chats (ChattyHistory *self, } account = g_object_get_data (G_OBJECT (task), "account"); - /* We currently handle only matrix accounts */ - g_assert (CHATTY_IS_MA_ACCOUNT (account) || CHATTY_IS_MM_ACCOUNT (account)); + /* We currently handle only MM accounts */ + g_assert (CHATTY_IS_MM_ACCOUNT (account)); user_id = chatty_item_get_username (CHATTY_ITEM (account)); - if (CHATTY_IS_MA_ACCOUNT (account)) - protocol = PROTOCOL_MATRIX; - else - protocol = PROTOCOL_MMS_SMS; - sqlite3_prepare_v2 (self->db, /* 0 1 2 3 4 */ "SELECT threads.id,threads.name,threads.alias,threads.encrypted,threads.type," @@ -2786,7 +2714,7 @@ history_get_chats (ChattyHistory *self, "WHERE visibility!=" STRING(THREAD_VISIBILITY_HIDDEN), -1, &stmt, NULL); history_bind_text (stmt, 1, user_id, "binding when getting threads"); - history_bind_int (stmt, 2, protocol, "binding when getting threads"); + history_bind_int (stmt, 2, PROTOCOL_MMS_SMS, "binding when getting threads"); while (sqlite3_step (stmt) == SQLITE_ROW) { g_autoptr(GPtrArray) messages = NULL; @@ -2794,6 +2722,7 @@ history_get_chats (ChattyHistory *self, const char *name, *alias; ChattyChat *chat; int thread_id, visibility; + int unread_count; if (!threads) threads = g_ptr_array_new_full (30, g_object_unref); @@ -2801,7 +2730,6 @@ history_get_chats (ChattyHistory *self, thread_id = sqlite3_column_int (stmt, 0); name = (const char *)sqlite3_column_text (stmt, 1); alias = (const char *)sqlite3_column_text (stmt, 2); - encrypted = sqlite3_column_int (stmt, 3); visibility = sqlite3_column_int (stmt, 7); if (sqlite3_column_text (stmt, 5)) { @@ -2810,9 +2738,7 @@ history_get_chats (ChattyHistory *self, file->path = g_strdup ((const char *)sqlite3_column_text (stmt, 6)); } - if (CHATTY_IS_MA_ACCOUNT (account)) { - chat = (gpointer)chatty_ma_chat_new (name, alias, file, encrypted); - } else if (sqlite3_column_int (stmt, 4) == THREAD_GROUP_CHAT) { + if (sqlite3_column_int (stmt, 4) == THREAD_GROUP_CHAT) { chat = (gpointer)chatty_mm_chat_new (name, alias, CHATTY_PROTOCOL_MMS, FALSE, history_value_to_visibility (visibility)); } else { @@ -2822,22 +2748,14 @@ history_get_chats (ChattyHistory *self, messages = get_messages_before_time (self, chat, NULL, thread_id, INT_MAX, 1); - /* For now, we handle unread count only for MM accounts */ - if (CHATTY_IS_MM_ACCOUNT (account)) { - int unread_count; + unread_count = get_unread_message_count (self, thread_id); + chatty_chat_set_unread_count (chat, unread_count); - unread_count = get_unread_message_count (self, thread_id); - chatty_chat_set_unread_count (chat, unread_count); - } - - if (CHATTY_IS_MA_ACCOUNT (account)) - chatty_ma_chat_add_messages (CHATTY_MA_CHAT (chat), messages); - else - chatty_mm_chat_prepend_messages (CHATTY_MM_CHAT (chat), messages); + chatty_mm_chat_prepend_messages (CHATTY_MM_CHAT (chat), messages); g_ptr_array_insert (threads, -1, chat); - if (protocol == PROTOCOL_MMS_SMS) { + { g_autoptr(GPtrArray) members = NULL; members = get_sms_thread_members (self, thread_id); @@ -2909,10 +2827,13 @@ history_update_user (ChattyHistory *self, return; } + sqlite3_exec (self->db, "BEGIN TRANSACTION;", NULL, NULL, NULL); id = insert_or_ignore_user (self, protocol, user_name, name, task); - if (!id) + if (!id) { + sqlite3_exec (self->db, "END TRANSACTION;", NULL, NULL, NULL); return; + } file_info = chatty_item_get_avatar_file (CHATTY_ITEM (account)); file_id = add_file_info (self, file_info); @@ -2926,6 +2847,8 @@ history_update_user (ChattyHistory *self, sqlite3_step (stmt); sqlite3_finalize (stmt); + sqlite3_exec (self->db, "END TRANSACTION;", NULL, NULL, NULL); + g_task_return_boolean (task, TRUE); } @@ -3017,8 +2940,8 @@ history_load_account (ChattyHistory *self, if (!user_name || !*user_name) { g_task_return_new_error (task, - G_IO_ERROR, G_IO_ERROR_FAILED, - "Error: username name is empty"); + G_IO_ERROR, G_IO_ERROR_NOT_FOUND, + "Username name is empty"); return; } @@ -3075,10 +2998,14 @@ history_set_last_read_msg (ChattyHistory *self, if (message) uid = chatty_message_get_uid (message); + + sqlite3_exec (self->db, "BEGIN TRANSACTION;", NULL, NULL, NULL); thread_id = insert_or_ignore_thread (self, chat, task); - if (!thread_id) + if (!thread_id) { + sqlite3_exec (self->db, "END TRANSACTION;", NULL, NULL, NULL); return; + } sqlite3_prepare_v2 (self->db, "SELECT messages.id FROM messages " @@ -3098,6 +3025,7 @@ history_set_last_read_msg (ChattyHistory *self, history_bind_int (stmt, 2, thread_id, "binding when setting last read message"); sqlite3_step (stmt); sqlite3_finalize (stmt); + sqlite3_exec (self->db, "END TRANSACTION;", NULL, NULL, NULL); g_task_return_boolean (task, TRUE); } @@ -3635,10 +3563,9 @@ chatty_history_get_chats_async (ChattyHistory *self, g_return_if_fail (CHATTY_IS_HISTORY (self)); g_return_if_fail (CHATTY_IS_ACCOUNT (account)); - /* Currently we handle only matrix and SMS accounts */ + /* Currently we handle only MM accounts */ protocol = chatty_account_get_protocol_name (account); - if (!g_str_equal (protocol, "Matrix") && - !g_str_equal (protocol, "SMS")) + if (!g_str_equal (protocol, "SMS")) g_return_if_reached (); task = g_task_new (self, NULL, callback, user_data); diff --git a/src/chatty-image-item.c b/src/chatty-image-item.c index be9ac4b1e0d9b2614521661c5547ae8cec57d8a7..d8bfe4f64b005fb81b1b3d84702c2366c390a0fc 100644 --- a/src/chatty-image-item.c +++ b/src/chatty-image-item.c @@ -36,50 +36,69 @@ struct _ChattyImageItem G_DEFINE_TYPE (ChattyImageItem, chatty_image_item, GTK_TYPE_BIN) -static gboolean -item_set_image (gpointer user_data) +static void +image_item_paint (ChattyImageItem *self, + GdkPixbuf *pixbuf) { - ChattyImageItem *self = user_data; - g_autoptr(GdkPixbuf) pixbuf = NULL; - cairo_surface_t *surface = NULL; - g_autofree char *path = NULL; - ChattyFileInfo *file; GtkStyleContext *sc; - GList *files; - int scale_factor; - files = chatty_message_get_files (self->message); - g_return_val_if_fail (files && files->data, G_SOURCE_REMOVE); - file = files->data; - scale_factor = gtk_widget_get_scale_factor (GTK_WIDGET (self)); sc = gtk_widget_get_style_context (self->image); - if (file->path) { - if (self->protocol == CHATTY_PROTOCOL_MMS_SMS || self->protocol == CHATTY_PROTOCOL_MMS) - path = g_build_filename (g_get_user_data_dir (), "chatty", file->path, NULL); - else - path = g_build_filename (g_get_user_cache_dir (), "chatty", file->path, NULL); - } + if (pixbuf) { + cairo_surface_t *surface = NULL; - if (path) - pixbuf = gdk_pixbuf_new_from_file_at_scale (path, 240 * scale_factor, - -1, TRUE, NULL); - if (pixbuf) surface = gdk_cairo_surface_create_from_pixbuf (pixbuf, 0, gtk_widget_get_window (GTK_WIDGET (self))); - - if (file->status == CHATTY_FILE_DOWNLOADED && pixbuf) { gtk_style_context_remove_class (sc, "dim-label"); gtk_image_set_from_surface (GTK_IMAGE (self->image), surface); + g_clear_pointer (&surface, cairo_surface_destroy); } else { gtk_style_context_add_class (sc, "dim-label"); gtk_image_set_from_icon_name (GTK_IMAGE (self->image), "image-x-generic-symbolic", GTK_ICON_SIZE_BUTTON); + return; } +} + +static void +image_item_get_stream_cb (GObject *object, + GAsyncResult *result, + gpointer user_data) +{ + g_autoptr(ChattyImageItem) self = user_data; + g_autoptr(GInputStream) stream = NULL; + g_autoptr(GdkPixbuf) pixbuf = NULL; + int scale_factor; + + if (gtk_widget_in_destruction (GTK_WIDGET (self))) + return; + + stream = chatty_message_get_file_stream_finish (self->message, result, NULL); + + if (!stream) + return; + + scale_factor = gtk_widget_get_scale_factor (GTK_WIDGET (self)); + pixbuf = gdk_pixbuf_new_from_stream_at_scale (stream, 240 * scale_factor, + -1, TRUE, NULL, NULL); + image_item_paint (self, pixbuf); +} + +static gboolean +item_set_image (gpointer user_data) +{ + ChattyImageItem *self = user_data; - g_clear_pointer (&surface, cairo_surface_destroy); + /* It's possible that we get signals after dispose(). + * Fix warning in those cases + */ + if (!self->message) + return G_SOURCE_REMOVE; + chatty_message_get_file_stream_async (self->message, NULL, self->protocol, NULL, + image_item_get_stream_cb, + g_object_ref (self)); return G_SOURCE_REMOVE; } diff --git a/src/chatty-invite-view.c b/src/chatty-invite-view.c new file mode 100644 index 0000000000000000000000000000000000000000..0ea01944a36c32762718b457736542f1815170e0 --- /dev/null +++ b/src/chatty-invite-view.c @@ -0,0 +1,224 @@ +/* -*- mode: c; c-basic-offset: 2; indent-tabs-mode: nil; -*- */ +/* chatty-invite-view.c + * + * Copyright 2022 Purism SPC + * + * Author(s): + * Mohammed Sadiq <sadiq@sadiqpk.org> + * + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +#define G_LOG_DOMAIN "chatty-invite-view" + +#ifdef HAVE_CONFIG_H +# include "config.h" +#endif + +#include <glib/gi18n.h> + +#include "chatty-avatar.h" +#include "chatty-manager.h" +#include "chatty-invite-view.h" + +struct _ChattyInviteView +{ + GtkBox parent_instance; + + GtkWidget *invite_title; + GtkWidget *chat_avatar; + GtkWidget *invite_subtitle; + + GtkWidget *accept_button; + GtkWidget *accept_spinner; + GtkWidget *reject_button; + GtkWidget *reject_spinner; + + ChattyAccount *account; + ChattyChat *chat; + + gulong name_handler; + gulong account_handler; +}; + +G_DEFINE_TYPE (ChattyInviteView, chatty_invite_view, GTK_TYPE_BOX) + +static void +invite_account_status_changed_cb (ChattyInviteView *self) +{ + gboolean can_connect; + + g_assert (CHATTY_IS_INVITE_VIEW (self)); + + if (!self->chat || chatty_chat_get_chat_state (self->chat) != CHATTY_CHAT_INVITED) + return; + + can_connect = chatty_account_get_status (self->account) == CHATTY_CONNECTED; + gtk_widget_set_sensitive (self->accept_button, can_connect); +} + +static void +chat_invite_name_changed_cb (ChattyInviteView *self) +{ + g_autofree char *title = NULL; + const char *name; + + g_assert (CHATTY_IS_INVITE_VIEW (self)); + + if (!self->chat || chatty_chat_get_chat_state (self->chat) != CHATTY_CHAT_INVITED) + return; + + name = chatty_item_get_name (CHATTY_ITEM (self->chat)); + /* TRANSLATORS: %s is the chat room name */ + title = g_strdup_printf (_("Do you want to join “%sâ€"), name); + gtk_label_set_label (GTK_LABEL (self->invite_title), title); +} + +static void +chat_accept_invite_cb (GObject *object, + GAsyncResult *result, + gpointer user_data) +{ + g_autoptr(ChattyInviteView) self = user_data; + g_autoptr(GError) error = NULL; + + if (gtk_widget_in_destruction (user_data)) + return; + + gtk_spinner_stop (GTK_SPINNER (self->accept_spinner)); + gtk_widget_set_sensitive (self->accept_button, TRUE); + + if (chatty_chat_accept_invite_finish (self->chat, result, &error)) { + ChattyManager *manager; + + manager = chatty_manager_get_default (); + /* The item will be deleted from invite list and added to joined list */ + g_signal_emit_by_name (manager, "chat-deleted", self->chat); + } + + if (error) + g_warning ("Error: %s", error->message); +} + +static void +invite_accept_clicked_cb (ChattyInviteView *self) +{ + g_assert (CHATTY_IS_INVITE_VIEW (self)); + + gtk_spinner_start (GTK_SPINNER (self->accept_spinner)); + gtk_widget_set_sensitive (self->accept_button, FALSE); + chatty_chat_accept_invite_async (self->chat, + chat_accept_invite_cb, + g_object_ref (self)); +} + +static void +chat_reject_invite_cb (GObject *object, + GAsyncResult *result, + gpointer user_data) +{ + g_autoptr(ChattyInviteView) self = user_data; + g_autoptr(GError) error = NULL; + + if (gtk_widget_in_destruction (user_data)) + return; + + gtk_spinner_stop (GTK_SPINNER (self->reject_spinner)); + gtk_widget_set_sensitive (self->accept_button, TRUE); + + if (chatty_chat_reject_invite_finish (self->chat, result, &error)) { + ChattyManager *manager; + + manager = chatty_manager_get_default (); + g_signal_emit_by_name (manager, "chat-deleted", self->chat); + } + + if (error) + g_warning ("Error: %s", error->message); +} + +static void +invite_reject_clicked_cb (ChattyInviteView *self) +{ + g_assert (CHATTY_IS_INVITE_VIEW (self)); + + gtk_spinner_start (GTK_SPINNER (self->reject_spinner)); + gtk_widget_set_sensitive (self->accept_button, FALSE); + chatty_chat_reject_invite_async (self->chat, + chat_reject_invite_cb, + g_object_ref (self)); +} + +static void +chatty_invite_view_dispose (GObject *object) +{ + ChattyInviteView *self = (ChattyInviteView *)object; + + g_clear_object (&self->chat); + + G_OBJECT_CLASS (chatty_invite_view_parent_class)->dispose (object); +} + +static void +chatty_invite_view_class_init (ChattyInviteViewClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass); + + object_class->dispose = chatty_invite_view_dispose; + + gtk_widget_class_set_template_from_resource (widget_class, + "/sm/puri/Chatty/" + "ui/chatty-invite-view.ui"); + + gtk_widget_class_bind_template_child (widget_class, ChattyInviteView, invite_title); + gtk_widget_class_bind_template_child (widget_class, ChattyInviteView, chat_avatar); + gtk_widget_class_bind_template_child (widget_class, ChattyInviteView, invite_subtitle); + + gtk_widget_class_bind_template_child (widget_class, ChattyInviteView, accept_button); + gtk_widget_class_bind_template_child (widget_class, ChattyInviteView, accept_spinner); + gtk_widget_class_bind_template_child (widget_class, ChattyInviteView, reject_button); + gtk_widget_class_bind_template_child (widget_class, ChattyInviteView, reject_spinner); + + gtk_widget_class_bind_template_callback (widget_class, invite_accept_clicked_cb); + gtk_widget_class_bind_template_callback (widget_class, invite_reject_clicked_cb); +} + +static void +chatty_invite_view_init (ChattyInviteView *self) +{ + gtk_widget_init_template (GTK_WIDGET (self)); +} + +void +chatty_invite_view_set_chat (ChattyInviteView *self, + ChattyChat *chat) +{ + g_return_if_fail (CHATTY_IS_INVITE_VIEW (self)); + g_return_if_fail (!chat || CHATTY_IS_CHAT (chat)); + + if (self->chat) { + g_clear_signal_handler (&self->account_handler, self->account); + g_clear_signal_handler (&self->name_handler, self->chat); + chatty_avatar_set_item (CHATTY_AVATAR (self->chat_avatar), NULL); + } + + g_set_object (&self->chat, chat); + + if (chat) { + if (chatty_chat_get_chat_state (chat) != CHATTY_CHAT_INVITED) + return; + + self->account = chatty_chat_get_account (chat); + chatty_avatar_set_item (CHATTY_AVATAR (self->chat_avatar), CHATTY_ITEM (chat)); + + self->account_handler = g_signal_connect_object (self->account, "notify::status", + G_CALLBACK (invite_account_status_changed_cb), + self, G_CONNECT_SWAPPED); + self->name_handler = g_signal_connect_object (chat, "notify::name", + G_CALLBACK (chat_invite_name_changed_cb), + self, G_CONNECT_SWAPPED); + invite_account_status_changed_cb (self); + chat_invite_name_changed_cb (self); + } +} diff --git a/src/chatty-invite-view.h b/src/chatty-invite-view.h new file mode 100644 index 0000000000000000000000000000000000000000..890c4304a9b417d3d831ef2346efe1fcbd8de3c9 --- /dev/null +++ b/src/chatty-invite-view.h @@ -0,0 +1,28 @@ +/* -*- mode: c; c-basic-offset: 2; indent-tabs-mode: nil; -*- */ +/* chatty-invite-view.h + * + * Copyright 2022 Purism SPC + * + * Author(s): + * Mohammed Sadiq <sadiq@sadiqpk.org> + * + * SPDX-License-Identifier: GPL-3.0-or-later + */ + + +#pragma once + +#include <gtk/gtk.h> + +#include "chatty-chat.h" + +G_BEGIN_DECLS + +#define CHATTY_TYPE_INVITE_VIEW (chatty_invite_view_get_type ()) + +G_DECLARE_FINAL_TYPE (ChattyInviteView, chatty_invite_view, CHATTY, INVITE_VIEW, GtkBox) + +void chatty_invite_view_set_chat (ChattyInviteView *self, + ChattyChat *chat); + +G_END_DECLS diff --git a/src/chatty-list-row.c b/src/chatty-list-row.c index 9b8bd64df6f1c9dd645cc04580d241c92f992ed0..555aec02fa3a58fb4ca4bf8f5bfe2de13fcfb598 100644 --- a/src/chatty-list-row.c +++ b/src/chatty-list-row.c @@ -20,6 +20,7 @@ #include "chatty-contact.h" #include "chatty-contact-list.h" #include "chatty-chat.h" +#include "chatty-ma-key-chat.h" #include "chatty-avatar.h" #include "chatty-clock.h" #include "chatty-list-row.h" @@ -206,6 +207,11 @@ chatty_list_row_update (ChattyListRow *self) last_message_time = chatty_chat_get_last_msg_time (item); gtk_widget_set_visible (self->last_modified, last_message_time > 0); + if (chatty_chat_get_chat_state (CHATTY_CHAT (self->item)) == CHATTY_CHAT_INVITED) { + gtk_label_set_text (GTK_LABEL (self->unread_message_count), "!"); + gtk_widget_show (self->unread_message_count); + } + chatty_list_row_update_last_modified (self); } @@ -373,6 +379,9 @@ chatty_list_row_set_item (ChattyListRow *self, self->title, "label", G_BINDING_SYNC_CREATE); + if (CHATTY_IS_MA_KEY_CHAT (item)) + self->hide_chat_details = TRUE; + if (CHATTY_IS_CHAT (item)) g_signal_connect_object (item, "changed", G_CALLBACK (chatty_list_row_update), diff --git a/src/chatty-log.c b/src/chatty-log.c index b3a2347b8b9fb389be3d46298f2b434837cc9ad8..3725907be6dcc782432f1c297dbd2fb8302f9598 100644 --- a/src/chatty-log.c +++ b/src/chatty-log.c @@ -23,16 +23,91 @@ static int verbosity; gboolean any_domain; gboolean no_anonymize; gboolean stderr_is_journal; +gboolean fatal_criticals, fatal_warnings; + +/* Copied from GLib, LGPLv2.1+ */ +static void +_g_log_abort (gboolean breakpoint) +{ + gboolean debugger_present; + + if (g_test_subprocess ()) + { + /* If this is a test case subprocess then it probably caused + * this error message on purpose, so just exit() rather than + * abort()ing, to avoid triggering any system crash-reporting + * daemon. + */ + _exit (1); + } + +#ifdef G_OS_WIN32 + debugger_present = IsDebuggerPresent (); +#else + /* Assume GDB is attached. */ + debugger_present = TRUE; +#endif /* !G_OS_WIN32 */ + + if (debugger_present && breakpoint) + G_BREAKPOINT (); + else + g_abort (); +} + +static gboolean +should_show_log_for_level (GLogLevelFlags log_level, + int verbosity_level) +{ + if (verbosity_level >= 5) + return TRUE; + + if (log_level & CHATTY_LOG_LEVEL_TRACE) + return verbosity_level >= 4; + + if (log_level & G_LOG_LEVEL_DEBUG) + return verbosity_level >= 3; + + if (log_level & G_LOG_LEVEL_INFO) + return verbosity_level >= 2; + + if (log_level & G_LOG_LEVEL_MESSAGE) + return verbosity_level >= 1; + + return FALSE; +} + +static gboolean +matches_domain (const char *log_domains, + const char *domain) +{ + g_auto(GStrv) domain_list = NULL; + + if (!log_domains || !*log_domains || + !domain || !*domain) + return FALSE; + + domain_list = g_strsplit (log_domains, ",", -1); + + for (guint i = 0; domain_list[i]; i++) + { + if (g_str_has_prefix (domain, domain_list[i])) + return TRUE; + } + + return FALSE; +} static gboolean should_log (const char *log_domain, GLogLevelFlags log_level) { + g_assert (log_domain); + /* Ignore custom flags set */ log_level = log_level & ~CHATTY_LOG_DETAILED; /* Don't skip serious logs */ - if (log_level & (G_LOG_LEVEL_ERROR | G_LOG_LEVEL_WARNING)) + if (log_level & (G_LOG_LEVEL_ERROR | G_LOG_LEVEL_CRITICAL | G_LOG_LEVEL_WARNING)) return TRUE; if (any_domain && domains) { @@ -44,53 +119,27 @@ should_log (const char *log_domain, return verbosity >= 4; } - if (!any_domain && domains && log_domain && strstr (domains, log_domain)) - return TRUE; - - switch ((int)log_level) - { - case G_LOG_LEVEL_MESSAGE: - if (verbosity < 1) - return FALSE; - break; - - case G_LOG_LEVEL_INFO: - if (verbosity < 2) - return FALSE; - break; - - case G_LOG_LEVEL_DEBUG: - if (verbosity < 3) - return FALSE; - break; - - case CHATTY_LOG_LEVEL_TRACE: - if (verbosity < 4) - return FALSE; - break; - - default: - break; - } + if (!domains && g_str_has_prefix (log_domain, DEFAULT_DOMAIN_PREFIX)) + return should_show_log_for_level (log_level, verbosity); - if (!log_domain) - log_domain = "**"; + if (domains && matches_domain (domains, log_domain)) + return should_show_log_for_level (log_level, verbosity); - /* Skip logs from other domains if verbosity level is low */ - if (any_domain && !domains && - verbosity < 5 && - log_level > G_LOG_LEVEL_MESSAGE && - !strstr (log_domain, DEFAULT_DOMAIN_PREFIX)) + /* If we didn't handle domains in the preceding statement, + * we should no longer log them */ + if (domains) return FALSE; /* GdkPixbuf logs are too much verbose, skip unless asked not to. */ - if (log_level >= G_LOG_LEVEL_MESSAGE && - verbosity < 7 && + if (verbosity < 8 && g_strcmp0 (log_domain, "GdkPixbuf") == 0 && (!domains || !strstr (domains, log_domain))) return FALSE; - return TRUE; + if (verbosity >= 6) + return TRUE; + + return FALSE; } static void @@ -170,18 +219,15 @@ chatty_log_write (GLogLevelFlags log_level, gpointer user_data) { g_autoptr(GString) log_str = NULL; - FILE *stream; + FILE *stream = stdout; gboolean can_color; - if (stderr_is_journal) - if (g_log_writer_journald (log_level, fields, n_fields, user_data) == G_LOG_WRITER_HANDLED) - return G_LOG_WRITER_HANDLED; + if (stderr_is_journal && + g_log_writer_journald (log_level, fields, n_fields, user_data) == G_LOG_WRITER_HANDLED) + return G_LOG_WRITER_HANDLED; - if (log_level & (G_LOG_LEVEL_ERROR | - G_LOG_LEVEL_CRITICAL | G_LOG_LEVEL_WARNING)) + if (log_level & (G_LOG_LEVEL_ERROR | G_LOG_LEVEL_CRITICAL | G_LOG_LEVEL_WARNING)) stream = stderr; - else - stream = stdout; log_str = g_string_new (NULL); @@ -202,7 +248,6 @@ chatty_log_write (GLogLevelFlags log_level, } can_color = g_log_writer_supports_color (fileno (stream)); - log_str_append_log_domain (log_str, log_domain, can_color); g_string_append_printf (log_str, "[%5d]:", getpid ()); @@ -239,6 +284,16 @@ chatty_log_write (GLogLevelFlags log_level, fprintf (stream, "%s\n", log_str->str); fflush (stream); + if (fatal_criticals && + (log_level & G_LOG_LEVEL_CRITICAL)) + log_level |= G_LOG_FLAG_FATAL; + else if (fatal_warnings && + (log_level & (G_LOG_LEVEL_CRITICAL | G_LOG_LEVEL_WARNING))) + log_level |= G_LOG_FLAG_FATAL; + + if (log_level & (G_LOG_FLAG_FATAL | G_LOG_LEVEL_ERROR)) + _g_log_abort (!(log_level & G_LOG_FLAG_RECURSION)); + return G_LOG_WRITER_HANDLED; } @@ -261,24 +316,17 @@ chatty_log_handler (GLogLevelFlags log_level, log_message = field->value; } - if (!should_log (log_domain, log_level)) - return G_LOG_WRITER_HANDLED; - if (!log_domain) log_domain = "**"; if (!log_message) log_message = "(NULL) message"; - if (any_domain) - return chatty_log_write (log_level, log_domain, log_message, - fields, n_fields, user_data); - - if (!log_domain || strstr (domains, log_domain)) - return chatty_log_write (log_level, log_domain, log_message, - fields, n_fields, user_data); + if (!should_log (log_domain, log_level)) + return G_LOG_WRITER_HANDLED; - return G_LOG_WRITER_HANDLED; + return chatty_log_write (log_level, log_domain, log_message, + fields, n_fields, user_data); } static void @@ -302,13 +350,18 @@ chatty_log_init (void) if (!domains || g_str_equal (domains, "all")) any_domain = TRUE; - if (domains && g_str_equal (domains, "no-anonymize")) + if (domains && strstr (domains, "no-anonymize")) { any_domain = TRUE; no_anonymize = TRUE; g_clear_pointer (&domains, g_free); } + if (g_strcmp0 (g_getenv ("G_DEBUG"), "fatal-criticals") == 0) + fatal_criticals = TRUE; + else if (g_strcmp0 (g_getenv ("G_DEBUG"), "fatal-warnings") == 0) + fatal_warnings = TRUE; + stderr_is_journal = g_log_writer_is_journald (fileno (stderr)); g_log_set_writer_func (chatty_log_handler, NULL, NULL); g_once_init_leave (&initialized, 1); @@ -393,6 +446,8 @@ void chatty_log_anonymize_value (GString *str, const char *value) { + gunichar c, next_c, prev_c; + if (!value || !*value) return; @@ -407,30 +462,30 @@ chatty_log_anonymize_value (GString *str, return; } - if (!isalnum (*value)) - g_string_append_c (str, *value++); - - if (*value) - g_string_append_c (str, *value++); + if (!g_utf8_validate (value, -1, NULL)) + { + g_string_append (str, "******"); + return; + } - if (*value) - g_string_append_c (str, *value++); + c = g_utf8_get_char (value); + g_string_append_unichar (str, c); - if (!*value) - return; + value = g_utf8_next_char (value); - /* Replace all but the last two alnum chars with 'x' */ while (*value) { - while (isalnum (*value) && value[1] && value[2]) - { - if (value[1] == ':' || value[1] == '@' || value[1] == ' ' || - value[-1] == ':' || value[-1] == '@' || value[-1] == ' ') - g_string_append_c (str, *value); - else - g_string_append_c (str, '#'); - value++; - } - g_string_append_c (str, *value++); + prev_c = c; + c = g_utf8_get_char (value); + + value = g_utf8_next_char (value); + next_c = g_utf8_get_char (value); + + if (!g_unichar_isalnum (c)) + g_string_append_unichar (str, c); + else if (!g_unichar_isalnum (prev_c) || !g_unichar_isalnum (next_c)) + g_string_append_unichar (str, c); + else + g_string_append_c (str, '#'); } } diff --git a/src/chatty-main-view.c b/src/chatty-main-view.c new file mode 100644 index 0000000000000000000000000000000000000000..bed839e176ccb6f78676ebfb068ba235fb41723b --- /dev/null +++ b/src/chatty-main-view.c @@ -0,0 +1,127 @@ +/* -*- mode: c; c-basic-offset: 2; indent-tabs-mode: nil; -*- */ +/* chatty-main-view.c + * + * Copyright 2022 Purism SPC + * + * Author(s): + * Mohammed Sadiq <sadiq@sadiqpk.org> + * + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +#define G_LOG_DOMAIN "chatty-main-view" + +#ifdef HAVE_CONFIG_H +# include "config.h" +#endif + +#include "chatty-chat.h" +#include "chatty-ma-key-chat.h" +#include "chatty-history.h" +#include "chatty-invite-view.h" +#include "chatty-verification-view.h" +#include "chatty-chat-view.h" +#include "chatty-main-view.h" + +struct _ChattyMainView +{ + GtkBox parent_instance; + + GtkWidget *main_stack; + GtkWidget *content_view; + GtkWidget *invite_view; + GtkWidget *verification_view; + GtkWidget *empty_view; + + ChattyItem *item; +}; + +G_DEFINE_TYPE (ChattyMainView, chatty_main_view, GTK_TYPE_BOX) + +static void +chatty_main_view_dispose (GObject *object) +{ + ChattyMainView *self = (ChattyMainView *)object; + + g_clear_object (&self->item); + + G_OBJECT_CLASS (chatty_main_view_parent_class)->dispose (object); +} + +static void +chatty_main_view_class_init (ChattyMainViewClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass); + + object_class->dispose = chatty_main_view_dispose; + + gtk_widget_class_set_template_from_resource (widget_class, + "/sm/puri/Chatty/" + "ui/chatty-main-view.ui"); + + gtk_widget_class_bind_template_child (widget_class, ChattyMainView, main_stack); + gtk_widget_class_bind_template_child (widget_class, ChattyMainView, content_view); + gtk_widget_class_bind_template_child (widget_class, ChattyMainView, invite_view); + gtk_widget_class_bind_template_child (widget_class, ChattyMainView, verification_view); + gtk_widget_class_bind_template_child (widget_class, ChattyMainView, empty_view); + + g_type_ensure (CHATTY_TYPE_CHAT_VIEW); + g_type_ensure (CHATTY_TYPE_VERIFICATION_VIEW); +} + +static void +chatty_main_view_init (ChattyMainView *self) +{ + gtk_widget_init_template (GTK_WIDGET (self)); +} + +void +chatty_main_view_set_db (ChattyMainView *self, + gpointer db) +{ + g_return_if_fail (CHATTY_IS_MAIN_VIEW (self)); + g_return_if_fail (CHATTY_IS_HISTORY (db)); + + chatty_chat_view_set_db (CHATTY_CHAT_VIEW (self->content_view), db); +} + +void +chatty_main_view_set_item (ChattyMainView *self, + ChattyItem *item) +{ + g_return_if_fail (CHATTY_IS_MAIN_VIEW (self)); + g_return_if_fail (!item || CHATTY_IS_ITEM (item)); + + g_set_object (&self->item, item); + chatty_verification_view_set_item (CHATTY_VERIFICATION_VIEW (self->verification_view), item); + + if (!item || (CHATTY_IS_CHAT (item) && !CHATTY_IS_MA_KEY_CHAT (item))) + chatty_chat_view_set_chat (CHATTY_CHAT_VIEW (self->content_view), (ChattyChat *)item); + + if (CHATTY_IS_CHAT (item)) { + ChattyChatState state; + + state = chatty_chat_get_chat_state (CHATTY_CHAT (item)); + + if (state == CHATTY_CHAT_INVITED) + chatty_invite_view_set_chat (CHATTY_INVITE_VIEW (self->invite_view), (ChattyChat *)item); + + if (state == CHATTY_CHAT_INVITED) + gtk_stack_set_visible_child (GTK_STACK (self->main_stack), self->invite_view); + else if (state == CHATTY_CHAT_VERIFICATION) + gtk_stack_set_visible_child (GTK_STACK (self->main_stack), self->verification_view); + else + gtk_stack_set_visible_child (GTK_STACK (self->main_stack), self->content_view); + } else { + gtk_stack_set_visible_child (GTK_STACK (self->main_stack), self->empty_view); + } +} + +ChattyItem * +chatty_main_view_get_item (ChattyMainView *self) +{ + g_return_val_if_fail (CHATTY_IS_MAIN_VIEW (self), NULL); + + return self->item; +} diff --git a/src/chatty-main-view.h b/src/chatty-main-view.h new file mode 100644 index 0000000000000000000000000000000000000000..0b453d3f9d584c5a855de8d318c384728f804b03 --- /dev/null +++ b/src/chatty-main-view.h @@ -0,0 +1,30 @@ +/* -*- mode: c; c-basic-offset: 2; indent-tabs-mode: nil; -*- */ +/* chatty-main-view.h + * + * Copyright 2022 Purism SPC + * + * Author(s): + * Mohammed Sadiq <sadiq@sadiqpk.org> + * + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +#pragma once + +#include <gtk/gtk.h> + +#include "chatty-item.h" + +G_BEGIN_DECLS + +#define CHATTY_TYPE_MAIN_VIEW (chatty_main_view_get_type ()) + +G_DECLARE_FINAL_TYPE (ChattyMainView, chatty_main_view, CHATTY, MAIN_VIEW, GtkBox) + +void chatty_main_view_set_db (ChattyMainView *self, + gpointer db); +void chatty_main_view_set_item (ChattyMainView *self, + ChattyItem *item); +ChattyItem *chatty_main_view_get_item (ChattyMainView *self); + +G_END_DECLS diff --git a/src/chatty-manager.c b/src/chatty-manager.c index 3868ecb064df89cea54be15d00af314e95618ebc..4713447b8632f9472de2464081803691632e020b 100644 --- a/src/chatty-manager.c +++ b/src/chatty-manager.c @@ -18,6 +18,8 @@ #define LIBFEEDBACK_USE_UNSTABLE_API #include <libfeedback.h> #include <glib/gi18n.h> +#define CMATRIX_USE_EXPERIMENTAL_API +#include "cmatrix.h" #include "contrib/gtk.h" #include "chatty-settings.h" @@ -107,11 +109,24 @@ manager_sort_chat_item (ChattyChat *a, ChattyChat *b, gpointer user_data) { + ChattyChatState a_state, b_state; time_t a_time, b_time; g_assert (CHATTY_IS_CHAT (a)); g_assert (CHATTY_IS_CHAT (b)); + a_state = chatty_chat_get_chat_state (a); + b_state = chatty_chat_get_chat_state (b); + + if (a_state == CHATTY_CHAT_VERIFICATION || + b_state == CHATTY_CHAT_VERIFICATION) + return a_state == CHATTY_CHAT_VERIFICATION ? -1 : 1; + + if (a_state != b_state && + (a_state == CHATTY_CHAT_INVITED || + b_state == CHATTY_CHAT_INVITED)) + return a_state == CHATTY_CHAT_INVITED ? -1 : 1; + a_time = chatty_chat_get_last_msg_time (a); b_time = chatty_chat_get_last_msg_time (b); @@ -418,8 +433,7 @@ chatty_manager_load (ChattyManager *self) chatty_mm_account_load_async (self->mm_account, NULL, NULL); /* Matrix Setup */ - self->matrix = chatty_matrix_new (chatty_manager_get_history (self), - self->disable_auto_login); + self->matrix = chatty_matrix_new (self->disable_auto_login); manager_add_to_flat_model (self->accounts, chatty_matrix_get_account_list (self->matrix)); manager_add_to_flat_model (self->chat_list, @@ -661,8 +675,7 @@ chatty_manager_find_account_with_name (ChattyManager *self, g_return_val_if_fail (CHATTY_IS_MANAGER (self), NULL); g_return_val_if_fail (account_id && *account_id, NULL); - if (protocol & CHATTY_PROTOCOL_MATRIX && - chatty_settings_get_experimental_features (chatty_settings_get_default ())) + if (protocol & CHATTY_PROTOCOL_MATRIX) return chatty_matrix_find_account_with_name (self->matrix, account_id); #ifdef PURPLE_ENABLED @@ -685,15 +698,12 @@ chatty_manager_find_chat_with_name (ChattyManager *self, return chatty_mm_account_find_chat (self->mm_account, chat_id); #ifdef PURPLE_ENABLED - if (protocol & (CHATTY_PROTOCOL_XMPP | CHATTY_PROTOCOL_TELEGRAM) || - (!chatty_settings_get_experimental_features (chatty_settings_get_default ()) && - protocol & CHATTY_PROTOCOL_MATRIX)) + if (protocol & (CHATTY_PROTOCOL_XMPP | CHATTY_PROTOCOL_TELEGRAM)) return chatty_purple_find_chat_with_name (chatty_purple_get_default (), protocol, account_id, chat_id); #endif - if (chatty_settings_get_experimental_features (chatty_settings_get_default ()) - && protocol == CHATTY_PROTOCOL_MATRIX) + if (protocol == CHATTY_PROTOCOL_MATRIX) return chatty_matrix_find_chat_with_name (self->matrix, protocol, account_id, chat_id); return NULL; @@ -749,3 +759,26 @@ chatty_manager_get_history (ChattyManager *self) return self->history; } + +gpointer +chatty_manager_matrix_client_new (ChattyManager *self) +{ + g_return_val_if_fail (CHATTY_IS_MANAGER (self), NULL); + + if (!self->matrix) + return NULL; + + return chatty_matrix_client_new (self->matrix); +} + +gboolean +chatty_manager_has_matrix_with_id (ChattyManager *self, + const char *user_id) +{ + g_return_val_if_fail (CHATTY_IS_MANAGER (self), FALSE); + + if (!self->matrix) + return FALSE; + + return chatty_matrix_has_user_id (self->matrix, user_id); +} diff --git a/src/chatty-manager.h b/src/chatty-manager.h index 47e690ae941df948209a30705b25c18e04b7e8fd..b1d5c61389fed165ef2001b0789741b30d618be5 100644 --- a/src/chatty-manager.h +++ b/src/chatty-manager.h @@ -64,5 +64,8 @@ gboolean chatty_manager_set_uri (ChattyManager *self, const char *uri, const char *name); ChattyHistory *chatty_manager_get_history (ChattyManager *self); +gpointer chatty_manager_matrix_client_new (ChattyManager *self); +gboolean chatty_manager_has_matrix_with_id (ChattyManager *self, + const char *user_id); G_END_DECLS diff --git a/src/chatty-message.c b/src/chatty-message.c index 91a24c91e9e56f4c49acc8397fbca0de15f5884a..f6f66889be2d894cbafd1f21593b7a9485f9cac5 100644 --- a/src/chatty-message.c +++ b/src/chatty-message.c @@ -15,6 +15,8 @@ # include "config.h" #endif +#include <glib/gi18n.h> + #include "chatty-mm-buddy.h" #include "chatty-message.h" #include "chatty-utils.h" @@ -36,6 +38,7 @@ struct _ChattyMessage char *message; char *uid; char *id; + CmEvent *cm_event; ChattyFileInfo *preview; GList *files; @@ -66,6 +69,7 @@ chatty_message_finalize (GObject *object) ChattyMessage *self = (ChattyMessage *)object; g_clear_object (&self->user); + g_clear_object (&self->cm_event); g_free (self->message); g_free (self->subject); g_free (self->uid); @@ -133,6 +137,132 @@ chatty_message_new (ChattyItem *user, return self; } +ChattyMessage * +chatty_message_new_from_event (ChattyItem *user, + CmEvent *event) +{ + ChattyMessage *self; + const char *body = NULL; + ChattyMsgType type = CHATTY_MESSAGE_UNKNOWN; + ChattyMsgDirection direction; + ChattyMsgStatus status; + + g_return_val_if_fail (CM_IS_EVENT (event), NULL); + g_return_val_if_fail (CHATTY_IS_ITEM (user), NULL); + + if (CM_IS_ROOM_MESSAGE_EVENT (event)) { + switch (cm_room_message_event_get_msg_type ((gpointer)event)) + { + case CM_CONTENT_TYPE_TEXT: + case CM_CONTENT_TYPE_EMOTE: + case CM_CONTENT_TYPE_NOTICE: + case CM_CONTENT_TYPE_SERVER_NOTICE: + type = CHATTY_MESSAGE_TEXT; + break; + + case CM_CONTENT_TYPE_IMAGE: + type = CHATTY_MESSAGE_IMAGE; + break; + + case CM_CONTENT_TYPE_FILE: + type = CHATTY_MESSAGE_FILE; + break; + + case CM_CONTENT_TYPE_AUDIO: + type = CHATTY_MESSAGE_AUDIO; + break; + + case CM_CONTENT_TYPE_LOCATION: + type = CHATTY_MESSAGE_LOCATION; + break; + + case CM_CONTENT_TYPE_VIDEO: + type = CHATTY_MESSAGE_VIDEO; + break; + + case CM_CONTENT_TYPE_UNKNOWN: + default: + break; + } + } + + switch (cm_event_get_state (event)) + { + case CM_EVENT_STATE_DRAFT: + status = CHATTY_STATUS_DRAFT; + break; + + case CM_EVENT_STATE_RECEIVED: + status = CHATTY_STATUS_RECEIVED; + break; + + case CM_EVENT_STATE_WAITING: + case CM_EVENT_STATE_SENDING: + status = CHATTY_STATUS_SENDING; + break; + + case CM_EVENT_STATE_SENDING_FAILED: + status = CHATTY_STATUS_SENDING_FAILED; + break; + + case CM_EVENT_STATE_SENT: + status = CHATTY_STATUS_SENT; + break; + + case CM_EVENT_STATE_UNKNOWN: + default: + status = CHATTY_STATUS_UNKNOWN; + break; + } + + if (CM_IS_ROOM_MESSAGE_EVENT (event)) + body = cm_room_message_event_get_body ((gpointer)event); + + if ((!body || !*body) && + cm_event_get_m_type (event) == CM_M_ROOM_ENCRYPTED) + direction = CHATTY_DIRECTION_SYSTEM; + else if (cm_event_get_state (event) == CM_EVENT_STATE_WAITING || + cm_event_get_state (event) == CM_EVENT_STATE_SENT || + cm_event_get_state (event) == CM_EVENT_STATE_SENDING || + cm_event_get_state (event) == CM_EVENT_STATE_SENDING_FAILED) + direction = CHATTY_DIRECTION_OUT; + else + direction = CHATTY_DIRECTION_IN; + + if (direction == CHATTY_DIRECTION_SYSTEM) + body = _("Got an encrypted message, but couldn't decrypt due to missing keys"); + else if (CM_IS_ROOM_MESSAGE_EVENT (event)) + body = cm_room_message_event_get_body ((gpointer)event); + else + body = ""; + + self = chatty_message_new (user, body, + cm_event_get_id (event), + cm_event_get_time_stamp (event) / 1000, + type, direction, status); + + self->cm_event = g_object_ref (event); + self->user = g_object_ref (user); + + if (type == CHATTY_MESSAGE_IMAGE || + type == CHATTY_MESSAGE_FILE || + type == CHATTY_MESSAGE_AUDIO || + type == CHATTY_MESSAGE_VIDEO) { + /* Add some dummy data, we handle files appropriately elsewhere */ + chatty_message_add_file_from_path (self, "/"); + } + + return self; +} + +CmEvent * +chatty_message_get_cm_event (ChattyMessage *self) +{ + g_return_val_if_fail (CHATTY_IS_MESSAGE (self), NULL); + + return self->cm_event; +} + const char * chatty_message_get_subject (ChattyMessage *self) { @@ -185,6 +315,115 @@ chatty_message_get_files (ChattyMessage *self) return self->files; } +static void +message_event_get_file_stream_cb (GObject *object, + GAsyncResult *result, + gpointer user_data) +{ + ChattyMessage *self; + g_autoptr(GTask) task = user_data; + GError *error = NULL; + ChattyFileInfo *file; + ChattyFileStatus old_status; + + g_assert (G_IS_TASK (task)); + + self = g_task_get_source_object (task); + g_assert (CHATTY_IS_MESSAGE (self)); + + file = self->files->data; + file->file_stream = cm_room_message_event_get_file_finish (CM_ROOM_MESSAGE_EVENT (object), result, &error); + + old_status = file->status; + + if (file->file_stream) + g_atomic_int_set (&file->status, CHATTY_FILE_DOWNLOADED); + else if (g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CANCELLED) || + g_error_matches (error, G_IO_ERROR, G_IO_ERROR_NETWORK_UNREACHABLE) || + g_error_matches (error, G_IO_ERROR, G_IO_ERROR_HOST_UNREACHABLE)) + g_atomic_int_set (&file->status, CHATTY_FILE_UNKNOWN); + else + g_atomic_int_set (&file->status, CHATTY_FILE_ERROR); + + if (old_status != file->status) + chatty_message_emit_updated (self); + + if (error) + g_task_return_error (task, error); + else + g_task_return_pointer (task, g_object_ref (file->file_stream), g_object_unref); +} + +void +chatty_message_get_file_stream_async (ChattyMessage *self, + ChattyFileInfo *file, + ChattyProtocol protocol, + GCancellable *cancellable, + GAsyncReadyCallback callback, + gpointer user_data) +{ + GTask *task; + + g_return_if_fail (CHATTY_IS_MESSAGE (self)); + g_return_if_fail (self->files); + + task = g_task_new (self, cancellable, callback, user_data); + + if (!file) + file = self->files->data; + + if (!file || !file->path) { + g_task_return_new_error (task, G_IO_ERROR, G_IO_ERROR_FAILED, + "Message has no file"); + return; + } + + if (file->file_stream) { + g_seekable_seek (G_SEEKABLE (file->file_stream), 0, G_SEEK_SET, NULL, NULL); + g_task_return_pointer (task, g_object_ref (file->file_stream), g_object_unref); + return; + } + + g_atomic_int_set (&file->status, CHATTY_FILE_DOWNLOADING); + chatty_message_emit_updated (self); + + if (self->cm_event) { + cm_room_message_event_get_file_async (CM_ROOM_MESSAGE_EVENT (self->cm_event), + cancellable, + NULL, NULL, + message_event_get_file_stream_cb, task); + } else { + g_autofree char *path = NULL; + + if (protocol == CHATTY_PROTOCOL_MMS_SMS || protocol == CHATTY_PROTOCOL_MMS) + path = g_build_filename (g_get_user_data_dir (), "chatty", file->path, NULL); + else + path = g_build_filename (g_get_user_cache_dir (), "chatty", file->path, NULL); + + if (!file->file) + file->file = g_file_new_for_path (path); + + file->file_stream = (gpointer)g_file_read (file->file, NULL, NULL); + g_task_return_pointer (task, g_object_ref (file->file_stream), g_object_unref); + + file->status = CHATTY_FILE_DOWNLOADED; + chatty_message_emit_updated (self); + } +} + +GInputStream * +chatty_message_get_file_stream_finish (ChattyMessage *self, + GAsyncResult *result, + GError **error) +{ + g_return_val_if_fail (CHATTY_IS_MESSAGE (self), FALSE); + g_return_val_if_fail (G_IS_TASK (result), FALSE); + g_return_val_if_fail (!error || !*error, FALSE); + + return g_task_propagate_pointer (G_TASK (result), error); +} + + /** * chatty_message_set_files: * @self: A #ChattyMessage @@ -444,7 +683,8 @@ chatty_message_set_status (ChattyMessage *self, { g_return_if_fail (CHATTY_IS_MESSAGE (self)); - self->status = status; + g_atomic_int_set (&self->status, status); + if (mtime) self->time = mtime; diff --git a/src/chatty-message.h b/src/chatty-message.h index c924d427238140c0a004a328ab0fa9a170e42f05..8041e8142bc4fe723ff2605feacbda9494c50078 100644 --- a/src/chatty-message.h +++ b/src/chatty-message.h @@ -11,6 +11,8 @@ #pragma once +#define CMATRIX_USE_EXPERIMENTAL_API +#include <cmatrix.h> #include <glib-object.h> #include "chatty-item.h" @@ -29,6 +31,9 @@ ChattyMessage *chatty_message_new (ChattyItem *user, ChattyMsgType type, ChattyMsgDirection direction, ChattyMsgStatus status); +ChattyMessage *chatty_message_new_from_event (ChattyItem *user, + CmEvent *event); +CmEvent *chatty_message_get_cm_event (ChattyMessage *self); const char *chatty_message_get_subject (ChattyMessage *self); void chatty_message_set_subject (ChattyMessage *self, @@ -39,6 +44,15 @@ void chatty_message_set_encrypted (ChattyMessage *self, gboolean is_encrypted); GList *chatty_message_get_files (ChattyMessage *self); +void chatty_message_get_file_stream_async (ChattyMessage *self, + ChattyFileInfo *file, + ChattyProtocol protocol, + GCancellable *cancellable, + GAsyncReadyCallback callback, + gpointer user_data); +GInputStream *chatty_message_get_file_stream_finish (ChattyMessage *self, + GAsyncResult *result, + GError **error); void chatty_message_set_files (ChattyMessage *self, GList *files); void chatty_message_add_file_from_path (ChattyMessage *self, diff --git a/src/chatty-secret-store.c b/src/chatty-secret-store.c index 4e9c8d9b66064366c6901b80e69c1e7c69665c05..2e03bdd6a3ed5ed84eb7652553dba74c338b04b4 100644 --- a/src/chatty-secret-store.c +++ b/src/chatty-secret-store.c @@ -43,84 +43,6 @@ secret_store_get_schema (void) return &password_schema; } -void -chatty_secret_store_save_async (ChattyAccount *account, - char *access_token, - const char *device_id, - char *pickle_key, - GCancellable *cancellable, - GAsyncReadyCallback callback, - gpointer user_data) -{ - const SecretSchema *schema; - g_autofree char *label = NULL; - const char *server, *old_pass, *username; - char *password = NULL, *token = NULL, *key = NULL; - char *credentials; - - /* Currently we support matrix accounts only */ - g_return_if_fail (CHATTY_IS_MA_ACCOUNT (account)); - g_return_if_fail (!cancellable || G_IS_CANCELLABLE (cancellable)); - - old_pass = chatty_account_get_password (account); - - if (old_pass && *old_pass) - password = g_strescape (old_pass, NULL); - if (access_token && *access_token) - token = g_strescape (access_token, NULL); - if (pickle_key && *pickle_key) - key = g_strescape (pickle_key, NULL); - - if (!device_id) - device_id = ""; - - if (CHATTY_IS_MA_ACCOUNT (account)) - username = chatty_ma_account_get_login_username (CHATTY_MA_ACCOUNT (account)); - else - username = chatty_item_get_username (CHATTY_ITEM (account)); - - CHATTY_TRACE (username, "Saving account, has password: %d, has access token: %d" - "has device-id: %d", - password && *password, token && *token, - device_id && *device_id); - - /* We don't use json APIs here so that we can manage memory better (and securely free them) */ - /* TODO: Use a non-pageable memory */ - /* XXX: We use a dumb string search, so don't change the order or spacing of the format string */ - credentials = g_strdup_printf ("{\"username\": \"%s\", \"password\": \"%s\"," - "\"access-token\": \"%s\", " - "\"pickle-key\": \"%s\", \"device-id\": \"%s\"}", - chatty_item_get_username (CHATTY_ITEM (account)), - password ? password : "", token ? token : "", - key ? key : "", device_id); - schema = secret_store_get_schema (); - server = chatty_ma_account_get_homeserver (CHATTY_MA_ACCOUNT (account)); - label = g_strdup_printf (_("Chatty password for \"%s\""), username); - - secret_password_store (schema, NULL, label, credentials, - cancellable, callback, user_data, - CHATTY_USERNAME_ATTRIBUTE, username, - CHATTY_SERVER_ATTRIBUTE, server, - CHATTY_PROTOCOL_ATTRIBUTE, PROTOCOL_MATRIX_STR, - NULL); - - matrix_utils_free_buffer (access_token); - matrix_utils_free_buffer (credentials); - matrix_utils_free_buffer (pickle_key); - matrix_utils_free_buffer (password); - matrix_utils_free_buffer (token); - matrix_utils_free_buffer (key); -} - -gboolean -chatty_secret_store_save_finish (GAsyncResult *result, - GError **error) -{ - g_return_val_if_fail (G_IS_ASYNC_RESULT (result), FALSE); - - return secret_password_store_finish (result, error); -} - static void secret_load_cb (GObject *object, GAsyncResult *result, @@ -142,19 +64,15 @@ secret_load_cb (GObject *object, } for (GList *item = secrets; item; item = item->next) { - ChattyMaAccount *account; - if (!accounts) accounts = g_ptr_array_new_full (5, g_object_unref); - account = chatty_ma_account_new_secret (item->data); - - if (account) - g_ptr_array_insert (accounts, -1, account); + if (item->data) + g_ptr_array_insert (accounts, -1, item->data); } if (secrets) - g_list_free_full (secrets, g_object_unref); + g_list_free (secrets); g_task_return_pointer (task, accounts, (GDestroyNotify)g_ptr_array_unref); } diff --git a/src/chatty-secret-store.h b/src/chatty-secret-store.h index 7e54b5704fef8848f5ae486a8f5fc796420dca68..d5da5f8052366aba0f1bd269f02648a8208b588c 100644 --- a/src/chatty-secret-store.h +++ b/src/chatty-secret-store.h @@ -21,16 +21,6 @@ G_BEGIN_DECLS -void chatty_secret_store_save_async (ChattyAccount *account, - char *access_token, - const char *device_id, - char *pickle_key, - GCancellable *cancellable, - GAsyncReadyCallback callback, - gpointer user_data); -gboolean chatty_secret_store_save_finish (GAsyncResult *result, - GError **error); - void chatty_secret_load_async (GCancellable *cancellable, GAsyncReadyCallback callback, gpointer user_data); diff --git a/src/chatty-text-item.c b/src/chatty-text-item.c index 4e74aaf923ebc3bef02c9a939280b3b60f5f0d2b..58f99b682ed5718817eff8eb4b3cb821d83a8ce7 100644 --- a/src/chatty-text-item.c +++ b/src/chatty-text-item.c @@ -221,13 +221,11 @@ static void text_item_update_message (ChattyTextItem *self) { g_autoptr(GString) str = NULL; - ChattySettings *settings; const char *text; g_assert (CHATTY_IS_TEXT_ITEM (self)); g_assert (self->message); - settings = chatty_settings_get_default (); text = chatty_message_get_text (self->message); str = g_string_sized_new (256); @@ -259,8 +257,7 @@ text_item_update_message (ChattyTextItem *self) } } - if ((self->protocol == CHATTY_PROTOCOL_MATRIX && - chatty_settings_get_experimental_features (settings)) || + if (self->protocol == CHATTY_PROTOCOL_MATRIX || self->protocol & (CHATTY_PROTOCOL_MMS_SMS | CHATTY_PROTOCOL_MMS)) { g_autofree char *content = NULL; const char *subject; diff --git a/src/chatty-utils.c b/src/chatty-utils.c index c95d938c24bdd7286096778f0bfe062260a59c44..ce83df4d9833ada024475f279c222963a25306be 100644 --- a/src/chatty-utils.c +++ b/src/chatty-utils.c @@ -422,6 +422,8 @@ chatty_file_info_free (ChattyFileInfo *file_info) if (!file_info) return; + g_clear_object (&file_info->file); + g_clear_object (&file_info->file_stream); g_free (file_info->file_name); g_free (file_info->url); g_free (file_info->path); @@ -550,6 +552,26 @@ utils_create_thumbnail (GTask *task, return; } +#if defined(GNOME_DESKTOP_PLATFORM_VERSION) && GNOME_DESKTOP_PLATFORM_VERSION >= 43 + thumbnail = gnome_desktop_thumbnail_factory_generate_thumbnail (factory, uri, content_type, NULL, &error); + if (!thumbnail) { + g_task_return_new_error (task, G_IO_ERROR, G_IO_ERROR_FAILED, "Failed to create thumbnail for file: %s (%s)", uri, error->message); + + g_warning ("Failed to create thumbnail for file: %s", uri); + + g_error_free (error); + return; + } + + gnome_desktop_thumbnail_factory_save_thumbnail (factory, thumbnail, uri, mtime, NULL, &error); + if (error) { + g_task_return_new_error (task, G_IO_ERROR, G_IO_ERROR_FAILED, "Failed to create thumbnail for file: %s (%s)", uri, error->message); + g_error_free (error); + return; + } + + g_task_return_boolean (task, TRUE); +#else thumbnail = gnome_desktop_thumbnail_factory_generate_thumbnail (factory, uri, content_type); if (thumbnail) { @@ -562,6 +584,7 @@ utils_create_thumbnail (GTask *task, } g_task_return_boolean (task, TRUE); +#endif } void diff --git a/src/chatty-utils.h b/src/chatty-utils.h index 9b7e95f7f881b8dfafe0e9c0e746a1e10beb5dfc..4527251749c3b8d92541a28643fbc58ed2ecba04 100644 --- a/src/chatty-utils.h +++ b/src/chatty-utils.h @@ -21,7 +21,8 @@ struct _ChattyFileInfo { char *url; char *path; char *mime_type; - gpointer user_data; + GFile *file; + GInputStream *file_stream; gsize width; gsize height; gsize size; diff --git a/src/chatty-verification-view.c b/src/chatty-verification-view.c new file mode 100644 index 0000000000000000000000000000000000000000..282ebdefb887242e5fc1c50f588f738d25acc9ff --- /dev/null +++ b/src/chatty-verification-view.c @@ -0,0 +1,508 @@ +/* -*- mode: c; c-basic-offset: 2; indent-tabs-mode: nil; -*- */ +/* chatty-verification-view.c + * + * Copyright 2022 Purism SPC + * + * Author(s): + * Mohammed Sadiq <sadiq@sadiqpk.org> + * + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +#define G_LOG_DOMAIN "chatty-verification-view" + +#ifdef HAVE_CONFIG_H +# include "config.h" +#endif + +#include <glib/gi18n.h> + +#include "chatty-manager.h" +#include "chatty-avatar.h" +#include "chatty-ma-key-chat.h" +#include "chatty-verification-view.h" + +static const char *emojis[][2] = { + /* TRANSLATORS: You may copy translations from https://github.com/matrix-org/matrix-spec-proposals/blob/old_master/data-definitions/sas-emoji.json + * if available */ + {"ðŸ¶", N_("Dog")}, /* "U+1F436" */ + {"ðŸ±", N_("Cat")}, /* "U+1F431" */ + {"ðŸ¦", N_("Lion")}, /* "U+1F981" */ + {"ðŸŽ", N_("Horse")}, /* "U+1F40E" */ + {"🦄", N_("Unicorn")}, /* "U+1F984" */ + {"ðŸ·", N_("Pig")}, /* "U+1F437" */ + {"ðŸ˜", N_("Elephant")}, /* "U+1F418" */ + {"ðŸ°", N_("Rabbit")}, /* "U+1F430" */ + {"ðŸ¼", N_("Panda")}, /* "U+1F43C" */ + {"ðŸ“", N_("Rooster")}, /* "U+1F413" */ + {"ðŸ§", N_("Penguin")}, /* "U+1F427" */ + {"ðŸ¢", N_("Turtle")}, /* "U+1F422" */ + {"ðŸŸ", N_("Fish")}, /* "U+1F41F" */ + {"ðŸ™", N_("Octopus")}, /* "U+1F419" */ + {"🦋", N_("Butterfly")}, /* "U+1F98B" */ + {"🌷", N_("Flower")}, /* "U+1F337" */ + {"🌳", N_("Tree")}, /* "U+1F333" */ + {"🌵", N_("Cactus")}, /* "U+1F335" */ + {"ðŸ„", N_("Mushroom")}, /* "U+1F344" */ + {"ðŸŒ", N_("Globe")}, /* "U+1F30F" */ + {"🌙", N_("Moon")}, /* "U+1F319" */ + {"â˜ï¸", N_("Cloud")}, /* "U+2601U+FE0F" */ + {"🔥", N_("Fire")}, /* "U+1F525" */ + {"ðŸŒ", N_("Banana")}, /* "U+1F34C" */ + {"ðŸŽ", N_("Apple")}, /* "U+1F34E" */ + {"ðŸ“", N_("Strawberry")}, /* "U+1F353" */ + {"🌽", N_("Corn")}, /* "U+1F33D" */ + {"ðŸ•", N_("Pizza")}, /* "U+1F355" */ + {"🎂", N_("Cake")}, /* "U+1F382" */ + {"â¤ï¸", N_("Heart")}, /* "U+2764U+FE0F" */ + {"😀", N_("Smiley")}, /* "U+1F600" */ + {"🤖", N_("Robot")}, /* "U+1F916" */ + {"🎩", N_("Hat")}, /* "U+1F3A9" */ + {"👓", N_("Glasses")}, /* "U+1F453" */ + {"🔧", N_("Spanner")}, /* "U+1F527" */ + {"🎅", N_("Santa")}, /* "U+1F385" */ + {"ðŸ‘", N_("Thumbs Up")}, /* "U+1F44D" */ + {"☂ï¸", N_("Umbrella")}, /* "U+2602U+FE0F" */ + {"⌛", N_("Hourglass")}, /* "U+231B" */ + {"â°", N_("Clock")}, /* "U+23F0" */ + {"ðŸŽ", N_("Gift")}, /* "U+1F381" */ + {"💡", N_("Light Bulb")}, /* "U+1F4A1" */ + {"📕", N_("Book")}, /* "U+1F4D5" */ + {"âœï¸", N_("Pencil")}, /* "U+270FU+FE0F" */ + {"📎", N_("Paperclip")}, /* "U+1F4CE" */ + {"✂ï¸", N_("Scissors")}, /* "U+2702U+FE0F" */ + {"🔒", N_("Lock")}, /* "U+1F512" */ + {"🔑", N_("Key")}, /* "U+1F511" */ + {"🔨", N_("Hammer")}, /* "U+1F528" */ + {"☎ï¸", N_("Telephone")}, /* "U+260EU+FE0F" */ + {"ðŸ", N_("Flag")}, /* "U+1F3C1" */ + {"🚂", N_("Train")}, /* "U+1F682" */ + {"🚲", N_("Bicycle")}, /* "U+1F6B2" */ + {"✈ï¸", N_("Aeroplane")}, /* "U+2708U+FE0F" */ + {"🚀", N_("Rocket")}, /* "U+1F680" */ + {"ðŸ†", N_("Trophy")}, /* "U+1F3C6" */ + {"âš½", N_("Ball")}, /* "U+26BD" */ + {"🎸", N_("Guitar")}, /* "U+1F3B8" */ + {"🎺", N_("Trumpet")}, /* "U+1F3BA" */ + {"🔔", N_("Bell")}, /* "U+1F514" */ + {"âš“", N_("Anchor")}, /* "U+2693" */ + {"🎧", N_("Headphones")}, /* "U+1F3A7" */ + {"ðŸ“", N_("Folder")}, /* "U+1F4C1" */ + {"📌", N_("Pin")}, /* "U+1F4CC" */ +}; + +struct _ChattyVerificationView +{ + GtkBox parent_instance; + + GtkWidget *user_avatar; + GtkWidget *name_label; + GtkWidget *username_label; + + GtkWidget *continue_button; + GtkWidget *continue_spinner; + GtkWidget *cancel_button; + GtkWidget *cancel_spinner; + + GtkWidget *verification_dialog; + GtkWidget *verification_type_button; + GtkWidget *content_stack; + + GtkWidget *decimal_content; + GtkWidget *decimal1_label; + GtkWidget *decimal2_label; + GtkWidget *decimal3_label; + + GtkWidget *emoji_content; + GtkWidget *emoji1_label; + GtkWidget *emoji1_title; + GtkWidget *emoji2_label; + GtkWidget *emoji2_title; + GtkWidget *emoji3_label; + GtkWidget *emoji3_title; + GtkWidget *emoji4_label; + GtkWidget *emoji4_title; + GtkWidget *emoji5_label; + GtkWidget *emoji5_title; + GtkWidget *emoji6_label; + GtkWidget *emoji6_title; + GtkWidget *emoji7_label; + GtkWidget *emoji7_title; + + ChattyMaKeyChat *item; + + GBinding *name_binding; + + gulong update_handler; + gulong delete_handler; + + gboolean emoji_set; + gboolean emoji_shown; +}; + +G_DEFINE_TYPE (ChattyVerificationView, chatty_verification_view, GTK_TYPE_BOX) + +static void +show_verification_dailog (ChattyVerificationView *self) +{ + GtkWidget *window; + + g_assert (CHATTY_IS_VERIFICATION_VIEW (self)); + + window = gtk_widget_get_ancestor (GTK_WIDGET (self), GTK_TYPE_WINDOW); + gtk_window_set_transient_for (GTK_WINDOW (self->verification_dialog), GTK_WINDOW (window)); + gtk_window_present (GTK_WINDOW (self->verification_dialog)); + gtk_spinner_stop (GTK_SPINNER (self->continue_spinner)); + + self->emoji_shown = TRUE; +} + +static void +verification_update_emoji (ChattyVerificationView *self) +{ + g_autoptr(GPtrArray) labels = NULL; + g_autoptr(GPtrArray) titles = NULL; + char *decimal_str; + GPtrArray *emoji; + guint16 *decimal; + + g_assert (CHATTY_IS_VERIFICATION_VIEW (self)); + + if (self->emoji_set) + return; + + emoji = chatty_ma_key_get_emoji (self->item); + decimal = chatty_ma_key_get_decimal (self->item); + + if (!emoji || !decimal) + return; + + self->emoji_set = TRUE; + labels = g_ptr_array_new (); + g_ptr_array_add (labels, self->emoji1_label); + g_ptr_array_add (labels, self->emoji2_label); + g_ptr_array_add (labels, self->emoji3_label); + g_ptr_array_add (labels, self->emoji4_label); + g_ptr_array_add (labels, self->emoji5_label); + g_ptr_array_add (labels, self->emoji6_label); + g_ptr_array_add (labels, self->emoji7_label); + + titles = g_ptr_array_new (); + g_ptr_array_add (titles, self->emoji1_title); + g_ptr_array_add (titles, self->emoji2_title); + g_ptr_array_add (titles, self->emoji3_title); + g_ptr_array_add (titles, self->emoji4_title); + g_ptr_array_add (titles, self->emoji5_title); + g_ptr_array_add (titles, self->emoji6_title); + g_ptr_array_add (titles, self->emoji7_title); + + for (guint i = 0; i < emoji->len; i++) { + GtkLabel *label_widget, *title_widget; + const char *title = ""; + char *label; + + label = emoji->pdata[i]; + + for (guint j = 0; j < G_N_ELEMENTS (emojis); j++) { + if (g_strcmp0 (label, emojis[j][0]) != 0) + continue; + + title = emojis[j][1]; + break; + } + + label_widget = labels->pdata[i]; + title_widget = titles->pdata[i]; + + gtk_label_set_label (label_widget, label); + gtk_label_set_label (title_widget, gettext (title)); + } + + decimal_str = g_strdup_printf ("%4u", (int)decimal[0]); + gtk_label_set_label (GTK_LABEL (self->decimal1_label), decimal_str); + g_free (decimal_str); + + decimal_str = g_strdup_printf ("%4u", (int)decimal[1]); + gtk_label_set_label (GTK_LABEL (self->decimal2_label), decimal_str); + g_free (decimal_str); + + decimal_str = g_strdup_printf ("%4u", (int)decimal[2]); + gtk_label_set_label (GTK_LABEL (self->decimal3_label), decimal_str); + g_free (decimal_str); +} + +static void +verification_item_updated_cb (ChattyVerificationView *self) +{ + CmEvent *event; + + g_assert (CHATTY_IS_VERIFICATION_VIEW (self)); + + event = chatty_ma_key_chat_get_event (self->item); + + if (g_object_get_data (G_OBJECT (event), "cancel") || + g_object_get_data (G_OBJECT (event), "completed")) { + ChattyManager *manager; + + manager = chatty_manager_get_default (); + g_signal_emit_by_name (manager, "chat-deleted", self->item); + return; + } + + if (!self->emoji_shown && + g_object_get_data (G_OBJECT (event), "key")) { + verification_update_emoji (self); + show_verification_dailog (self); + } +} + +static void +verification_item_deleted_cb (ChattyVerificationView *self) +{ + g_assert (CHATTY_IS_VERIFICATION_VIEW (self)); +} + +static void +verification_key_continue_cb (GObject *object, + GAsyncResult *result, + gpointer user_data) +{ + g_autoptr(ChattyVerificationView) self = user_data; + g_autoptr(GError) error = NULL; + + g_assert (CHATTY_IS_MA_KEY_CHAT (object)); + + chatty_ma_key_cancel_finish (CHATTY_MA_KEY_CHAT (object), result, &error); + + if (error) { + gtk_spinner_stop (GTK_SPINNER (self->continue_spinner)); + gtk_widget_set_sensitive (self->continue_button, TRUE); + g_warning ("Error: %s", error->message); + } +} + +static void +verification_continue_clicked_cb (ChattyVerificationView *self) +{ + g_assert (CHATTY_IS_VERIFICATION_VIEW (self)); + + gtk_spinner_start (GTK_SPINNER (self->continue_spinner)); + gtk_widget_set_sensitive (self->continue_button, FALSE); + + chatty_ma_key_accept_async (CHATTY_MA_KEY_CHAT (self->item), + verification_key_continue_cb, + g_object_ref (self)); +} + +static void +verification_key_cancel_cb (GObject *object, + GAsyncResult *result, + gpointer user_data) +{ + g_autoptr(ChattyVerificationView) self = user_data; + g_autoptr(GError) error = NULL; + + g_assert (CHATTY_IS_MA_KEY_CHAT (object)); + + gtk_spinner_stop (GTK_SPINNER (self->cancel_spinner)); + if (chatty_ma_key_cancel_finish (CHATTY_MA_KEY_CHAT (object), result, &error)) { + ChattyManager *manager; + + manager = chatty_manager_get_default (); + g_signal_emit_by_name (manager, "chat-deleted", object); + } + + if (error) + g_warning ("Error: %s", error->message); +} + +static void +verification_cancel_clicked_cb (ChattyVerificationView *self, + GtkWidget *widget) +{ + g_assert (CHATTY_IS_VERIFICATION_VIEW (self)); + + gtk_spinner_start (GTK_SPINNER (self->cancel_spinner)); + gtk_widget_set_sensitive (self->continue_button, FALSE); + gtk_widget_hide (self->verification_dialog); + + chatty_ma_key_cancel_async (CHATTY_MA_KEY_CHAT (self->item), + verification_key_cancel_cb, + g_object_ref (self)); +} + +static void +verification_key_match_cb (GObject *object, + GAsyncResult *result, + gpointer user_data) +{ + g_autoptr(ChattyVerificationView) self = user_data; + g_autoptr(GError) error = NULL; + + g_assert (CHATTY_IS_MA_KEY_CHAT (object)); + + gtk_widget_hide (self->verification_dialog); + chatty_ma_key_match_finish (CHATTY_MA_KEY_CHAT (object), result, &error); + + if (error) + g_warning ("Error: %s", error->message); +} + +static void +verification_type_clicked_cb (ChattyVerificationView *self) +{ + GtkWidget *visible_child; + + g_assert (CHATTY_IS_VERIFICATION_VIEW (self)); + + visible_child = gtk_stack_get_visible_child (GTK_STACK (self->content_stack)); + + if (visible_child == self->decimal_content) + visible_child = self->emoji_content; + else + visible_child = self->decimal_content; + + gtk_stack_set_visible_child (GTK_STACK (self->content_stack), visible_child); +} + +static void +verification_match_clicked_cb (ChattyVerificationView *self) +{ + g_assert (CHATTY_IS_VERIFICATION_VIEW (self)); + + gtk_widget_set_sensitive (self->continue_button, FALSE); + gtk_spinner_start (GTK_SPINNER (self->continue_spinner)); + chatty_ma_key_match_async (CHATTY_MA_KEY_CHAT (self->item), + verification_key_match_cb, + g_object_ref (self)); +} + +static void +verification_content_child_changed_cb (ChattyVerificationView *self) +{ + const char *button_label; + + g_assert (CHATTY_IS_VERIFICATION_VIEW (self)); + + if (gtk_stack_get_visible_child (GTK_STACK (self->content_stack)) == self->decimal_content) + button_label = _("Show Emojis"); + else + button_label = _("Show Numbers"); + + gtk_button_set_label (GTK_BUTTON (self->verification_type_button), button_label); +} + +static void +chatty_verification_view_dispose (GObject *object) +{ + ChattyVerificationView *self = (ChattyVerificationView *)object; + + g_clear_object (&self->item); + g_clear_object (&self->name_binding); + + G_OBJECT_CLASS (chatty_verification_view_parent_class)->dispose (object); +} + +static void +chatty_verification_view_class_init (ChattyVerificationViewClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass); + + object_class->dispose = chatty_verification_view_dispose; + + gtk_widget_class_set_template_from_resource (widget_class, + "/sm/puri/Chatty/" + "ui/chatty-verification-view.ui"); + + gtk_widget_class_bind_template_child (widget_class, ChattyVerificationView, user_avatar); + gtk_widget_class_bind_template_child (widget_class, ChattyVerificationView, name_label); + gtk_widget_class_bind_template_child (widget_class, ChattyVerificationView, username_label); + + gtk_widget_class_bind_template_child (widget_class, ChattyVerificationView, continue_button); + gtk_widget_class_bind_template_child (widget_class, ChattyVerificationView, continue_spinner); + gtk_widget_class_bind_template_child (widget_class, ChattyVerificationView, cancel_button); + gtk_widget_class_bind_template_child (widget_class, ChattyVerificationView, cancel_spinner); + + gtk_widget_class_bind_template_child (widget_class, ChattyVerificationView, verification_dialog); + gtk_widget_class_bind_template_child (widget_class, ChattyVerificationView, verification_type_button); + gtk_widget_class_bind_template_child (widget_class, ChattyVerificationView, content_stack); + + gtk_widget_class_bind_template_child (widget_class, ChattyVerificationView, decimal_content); + gtk_widget_class_bind_template_child (widget_class, ChattyVerificationView, decimal1_label); + gtk_widget_class_bind_template_child (widget_class, ChattyVerificationView, decimal2_label); + gtk_widget_class_bind_template_child (widget_class, ChattyVerificationView, decimal3_label); + + gtk_widget_class_bind_template_child (widget_class, ChattyVerificationView, emoji_content); + gtk_widget_class_bind_template_child (widget_class, ChattyVerificationView, emoji1_label); + gtk_widget_class_bind_template_child (widget_class, ChattyVerificationView, emoji1_title); + gtk_widget_class_bind_template_child (widget_class, ChattyVerificationView, emoji2_label); + gtk_widget_class_bind_template_child (widget_class, ChattyVerificationView, emoji2_title); + gtk_widget_class_bind_template_child (widget_class, ChattyVerificationView, emoji3_label); + gtk_widget_class_bind_template_child (widget_class, ChattyVerificationView, emoji3_title); + gtk_widget_class_bind_template_child (widget_class, ChattyVerificationView, emoji4_label); + gtk_widget_class_bind_template_child (widget_class, ChattyVerificationView, emoji4_title); + gtk_widget_class_bind_template_child (widget_class, ChattyVerificationView, emoji5_label); + gtk_widget_class_bind_template_child (widget_class, ChattyVerificationView, emoji5_title); + gtk_widget_class_bind_template_child (widget_class, ChattyVerificationView, emoji6_label); + gtk_widget_class_bind_template_child (widget_class, ChattyVerificationView, emoji6_title); + gtk_widget_class_bind_template_child (widget_class, ChattyVerificationView, emoji7_label); + gtk_widget_class_bind_template_child (widget_class, ChattyVerificationView, emoji7_title); + + gtk_widget_class_bind_template_callback (widget_class, verification_continue_clicked_cb); + gtk_widget_class_bind_template_callback (widget_class, verification_cancel_clicked_cb); + gtk_widget_class_bind_template_callback (widget_class, verification_type_clicked_cb); + gtk_widget_class_bind_template_callback (widget_class, verification_match_clicked_cb); + gtk_widget_class_bind_template_callback (widget_class, verification_content_child_changed_cb); +} + +static void +chatty_verification_view_init (ChattyVerificationView *self) +{ + gtk_widget_init_template (GTK_WIDGET (self)); + verification_content_child_changed_cb (self); +} + +void +chatty_verification_view_set_item (ChattyVerificationView *self, + ChattyItem *item) +{ + ChattyItem *sender; + + g_return_if_fail (CHATTY_IS_VERIFICATION_VIEW (self)); + + if (self->item) { + g_clear_object (&self->name_binding); + chatty_avatar_set_item (CHATTY_AVATAR (self->user_avatar), NULL); + g_clear_signal_handler (&self->update_handler, self->item); + g_clear_signal_handler (&self->delete_handler, self->item); + gtk_spinner_stop (GTK_SPINNER (self->continue_spinner)); + gtk_spinner_stop (GTK_SPINNER (self->cancel_spinner)); + gtk_widget_hide (self->verification_dialog); + g_clear_object (&self->item); + } + + if (!CHATTY_IS_MA_KEY_CHAT (item)) + return; + + g_set_object (&self->item, (ChattyMaKeyChat *)item); + + if (!item) + return; + + gtk_widget_set_sensitive (self->continue_button, TRUE); + self->update_handler = g_signal_connect_object (item, "changed", + G_CALLBACK (verification_item_updated_cb), + self, G_CONNECT_SWAPPED); + self->delete_handler = g_signal_connect_object (item, "deleted", + G_CALLBACK (verification_item_deleted_cb), + self, G_CONNECT_SWAPPED); + sender = chatty_ma_key_chat_get_sender (CHATTY_MA_KEY_CHAT (item)); + chatty_avatar_set_item (CHATTY_AVATAR (self->user_avatar), sender); + + self->name_binding = g_object_bind_property (sender, "name", + self->name_label, "label", + G_BINDING_SYNC_CREATE); + gtk_label_set_label (GTK_LABEL (self->username_label), + chatty_item_get_username (CHATTY_ITEM (sender))); +} diff --git a/src/chatty-verification-view.h b/src/chatty-verification-view.h new file mode 100644 index 0000000000000000000000000000000000000000..b6f8ab664ff4f7bc232ef28b842f22839f8ebfc6 --- /dev/null +++ b/src/chatty-verification-view.h @@ -0,0 +1,28 @@ +/* -*- mode: c; c-basic-offset: 2; indent-tabs-mode: nil; -*- */ +/* chatty-verification-view.h + * + * Copyright 2022 Purism SPC + * + * Author(s): + * Mohammed Sadiq <sadiq@sadiqpk.org> + * + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +#pragma once + +#include <gtk/gtk.h> + +#include "chatty-item.h" + +G_BEGIN_DECLS + +#define CHATTY_TYPE_VERIFICATION_VIEW (chatty_verification_view_get_type ()) + +G_DECLARE_FINAL_TYPE (ChattyVerificationView, chatty_verification_view, CHATTY, VERIFICATION_VIEW, GtkBox) + +void chatty_verification_view_set_item (ChattyVerificationView *self, + ChattyItem *item); + +G_END_DECLS + diff --git a/src/chatty-window.c b/src/chatty-window.c index 9e1439bb6cb2033c96024ffb2f99efa26b0742fb..be4328a0e60d37a34dd531d38c4e2acad904f8ae 100644 --- a/src/chatty-window.c +++ b/src/chatty-window.c @@ -19,6 +19,7 @@ #include <libgd/gd.h> #include "contrib/gtk.h" +#include "chatty-header-bar.h" #include "chatty-window.h" #include "chatty-history.h" #include "chatty-avatar.h" @@ -28,7 +29,7 @@ #include "chatty-settings.h" #include "chatty-mm-chat.h" #include "chatty-chat-list.h" -#include "chatty-chat-view.h" +#include "chatty-main-view.h" #include "chatty-manager.h" #include "chatty-utils.h" #include "chatty-selectable-row.h" @@ -48,46 +49,23 @@ struct _ChattyWindow GtkWidget *chat_list; + GtkWidget *header_bar; GtkWidget *content_box; + GtkWidget *content_view; GtkWidget *sidebar; - GtkWidget *header_box; - GtkWidget *header_group; - - GtkWidget *sub_header_icon; - GtkWidget *sub_header_label; GtkWidget *new_chat_dialog; GtkWidget *chat_info_dialog; - GtkWidget *search_button; GtkWidget *chats_search_bar; GtkWidget *chats_search_entry; - GtkWidget *header_chat_list_new_msg_popover; - - GtkWidget *menu_new_message_button; - GtkWidget *menu_new_sms_mms_message_button; - GtkWidget *menu_new_group_message_button; - GtkWidget *header_bar; - GtkWidget *header_back_button; - GtkWidget *header_add_chat_button; - GtkWidget *call_button; - GtkWidget *header_sub_menu_button; - GtkWidget *leave_button; - GtkWidget *delete_button; - GtkWidget *block_button; - GtkWidget *unblock_button; - GtkWidget *archive_button; - GtkWidget *unarchive_button; - - GtkWidget *chat_view; GtkWidget *settings_dialog; GtkWidget *protocol_list; GtkWidget *protocol_any_row; gulong chat_changed_handler; - GBinding *header_label_binding; GdTaggedEntryTag *protocol_tag; ChattyManager *manager; @@ -99,29 +77,6 @@ struct _ChattyWindow G_DEFINE_TYPE (ChattyWindow, chatty_window, HDY_TYPE_APPLICATION_WINDOW) -static void -window_update_item_state_button (ChattyWindow *self, - ChattyItem *item) -{ - ChattyItemState state; - - state = chatty_item_get_state (item); - - gtk_widget_hide (self->block_button); - gtk_widget_hide (self->unblock_button); - gtk_widget_hide (self->archive_button); - gtk_widget_hide (self->unarchive_button); - - if (state == CHATTY_ITEM_VISIBLE) { - gtk_widget_show (self->block_button); - gtk_widget_show (self->archive_button); - } else if (state == CHATTY_ITEM_ARCHIVED) { - gtk_widget_show (self->unarchive_button); - } else if (state == CHATTY_ITEM_BLOCKED) { - gtk_widget_show (self->unblock_button); - } -} - static void window_chat_changed_cb (ChattyWindow *self, ChattyChat *chat) @@ -132,8 +87,6 @@ window_chat_changed_cb (ChattyWindow *self, /* allow changing state only for 1:1 SMS/MMS chats */ if (CHATTY_IS_MM_CHAT (chat) && g_list_model_get_n_items (users) == 1) { - if (chat == chatty_chat_view_get_chat (CHATTY_CHAT_VIEW (self->chat_view))) - window_update_item_state_button (self, CHATTY_ITEM (chat)); chatty_chat_list_refilter (CHATTY_CHAT_LIST (self->chat_list)); } } @@ -144,16 +97,10 @@ window_set_item (ChattyWindow *self, { g_assert (CHATTY_IS_WINDOW (self)); - chatty_avatar_set_item (CHATTY_AVATAR (self->sub_header_icon), CHATTY_ITEM (chat)); - g_clear_object (&self->header_label_binding); - gtk_label_set_label (GTK_LABEL (self->sub_header_label), ""); g_clear_signal_handler (&self->chat_changed_handler, - chatty_chat_view_get_chat (CHATTY_CHAT_VIEW (self->chat_view))); + chatty_main_view_get_item (CHATTY_MAIN_VIEW (self->content_view))); if (CHATTY_IS_CHAT (chat)) { - self->header_label_binding = g_object_bind_property (chat, "name", - self->sub_header_label, "label", - G_BINDING_SYNC_CREATE); self->chat_changed_handler = g_signal_connect_object (chat, "changed", G_CALLBACK (window_chat_changed_cb), self, G_CONNECT_SWAPPED); @@ -162,8 +109,8 @@ window_set_item (ChattyWindow *self, if (!chat) hdy_leaflet_set_visible_child_name (HDY_LEAFLET (self->content_box), "sidebar"); - chatty_chat_view_set_chat (CHATTY_CHAT_VIEW (self->chat_view), chat); - gtk_widget_set_visible (self->header_sub_menu_button, !!chat); + chatty_header_bar_set_item (CHATTY_HEADER_BAR (self->header_bar), CHATTY_ITEM (chat)); + chatty_main_view_set_item (CHATTY_MAIN_VIEW (self->content_view), CHATTY_ITEM (chat)); } static void @@ -177,7 +124,7 @@ chatty_window_update_search_mode (ChattyWindow *self) model = chatty_chat_list_get_filter_model (CHATTY_CHAT_LIST (self->chat_list)); has_child = g_list_model_get_n_items (model) > 0; - gtk_widget_set_visible (self->search_button, has_child); + chatty_header_bar_set_can_search (CHATTY_HEADER_BAR (self->header_bar), has_child); if (!has_child) hdy_search_bar_set_search_mode (HDY_SEARCH_BAR (self->chats_search_bar), FALSE); @@ -213,22 +160,18 @@ chatty_window_open_item (ChattyWindow *self, } static void -window_call_button_clicked_cb (ChattyWindow *self) +window_search_enable_changed_cb (ChattyWindow *self) { - g_autoptr(GError) error = NULL; - g_autofree char *uri = NULL; - ChattyChat *chat; + gboolean enabled; g_assert (CHATTY_IS_WINDOW (self)); - chat = chatty_chat_view_get_chat (CHATTY_CHAT_VIEW (self->chat_view)); - g_return_if_fail (CHATTY_IS_MM_CHAT (chat)); + enabled = hdy_search_bar_get_search_mode (HDY_SEARCH_BAR (self->chats_search_bar)); - uri = g_strconcat ("tel://", chatty_chat_get_chat_name (chat), NULL); - - CHATTY_INFO (uri, "Calling uri:"); - if (!gtk_show_uri_on_window (NULL, uri, GDK_CURRENT_TIME, &error)) - g_warning ("Failed to launch call: %s", error->message); + /* Reset protocol filter */ + if (!enabled && + self->protocol_any_row != self->selected_protocol_row) + gtk_widget_activate (self->protocol_any_row); } static void @@ -253,17 +196,6 @@ search_tag_button_clicked_cb (ChattyWindow *self, gtk_widget_activate (self->protocol_any_row); } -static void -window_search_toggled_cb (ChattyWindow *self, - GtkToggleButton *button) -{ - g_assert (CHATTY_IS_WINDOW (self)); - - if (!gtk_toggle_button_get_active (button) && - self->protocol_any_row != self->selected_protocol_row) - gtk_widget_activate (self->protocol_any_row); -} - static void window_chat_list_selection_changed (ChattyWindow *self, ChattyChatList *list) @@ -281,8 +213,8 @@ window_chat_list_selection_changed (ChattyWindow *self, model = chatty_chat_list_get_filter_model (CHATTY_CHAT_LIST (self->chat_list)); if (g_list_model_get_n_items (model) == 0) { - chatty_chat_view_set_chat (CHATTY_CHAT_VIEW (self->chat_view), NULL); - gtk_widget_hide (self->header_sub_menu_button); + chatty_header_bar_set_item (CHATTY_HEADER_BAR (self->header_bar), NULL); + chatty_main_view_set_item (CHATTY_MAIN_VIEW (self->content_view), NULL); } return; @@ -291,7 +223,7 @@ window_chat_list_selection_changed (ChattyWindow *self, chat = chat_list->pdata[0]; g_return_if_fail (CHATTY_IS_CHAT (chat)); - if (chat == chatty_chat_view_get_chat (CHATTY_CHAT_VIEW (self->chat_view))) + if (chat == (gpointer)chatty_main_view_get_item (CHATTY_MAIN_VIEW (self->content_view))) return; #ifdef PURPLE_ENABLED @@ -315,7 +247,7 @@ notify_fold_cb (ChattyWindow *self) { gboolean folded; - folded = hdy_leaflet_get_folded (HDY_LEAFLET (self->header_box)); + folded = hdy_leaflet_get_folded (HDY_LEAFLET (self->content_box)); chatty_chat_list_set_selection_mode (CHATTY_CHAT_LIST (self->chat_list), !folded); if (folded) { @@ -331,17 +263,6 @@ window_content_box_changed (ChattyWindow *self) window_set_item (self, NULL); } } -static void -window_show_unarchived_clicked_cb (ChattyWindow *self) -{ - g_assert (CHATTY_IS_WINDOW (self)); - - hdy_header_bar_set_title (HDY_HEADER_BAR (self->header_bar), _("Chats")); - chatty_chat_list_show_archived (CHATTY_CHAT_LIST (self->chat_list), FALSE); - gtk_widget_hide (self->header_back_button); - gtk_widget_show (self->header_add_chat_button); - chatty_window_update_search_mode (self); -} static void window_show_new_chat_dialog (ChattyWindow *self, @@ -357,39 +278,225 @@ window_show_new_chat_dialog (ChattyWindow *self, } static void -window_new_message_clicked_cb (ChattyWindow *self) +window_back_clicked_cb (ChattyWindow *self) { - window_show_new_chat_dialog (self, FALSE); + g_assert (CHATTY_IS_WINDOW (self)); + + if (chatty_chat_list_is_archived (CHATTY_CHAT_LIST (self->chat_list))) { + chatty_header_bar_show_archived (CHATTY_HEADER_BAR (self->header_bar), FALSE); + chatty_chat_list_show_archived (CHATTY_CHAT_LIST (self->chat_list), FALSE); + chatty_window_update_search_mode (self); + } else { + window_set_item (self, NULL); + } } static void -window_new_sms_mms_message_clicked_cb (ChattyWindow *self) +window_search_protocol_changed_cb (ChattyWindow *self, + GtkWidget *selected_row, + GtkListBox *box) { - window_show_new_chat_dialog (self, TRUE); + GdTaggedEntry *entry; + GtkWidget *old_row; + + g_assert (CHATTY_IS_WINDOW (self)); + g_assert (GTK_IS_LIST_BOX (box)); + + entry = GD_TAGGED_ENTRY (self->chats_search_entry); + old_row = self->selected_protocol_row; + + if (old_row == selected_row) + return; + + self->protocol_filter = GPOINTER_TO_INT (g_object_get_data (G_OBJECT (selected_row), "protocol")); + chatty_chat_list_filter_protocol (CHATTY_CHAT_LIST (self->chat_list), self->protocol_filter); + chatty_selectable_row_set_selected (CHATTY_SELECTABLE_ROW (old_row), FALSE); + chatty_selectable_row_set_selected (CHATTY_SELECTABLE_ROW (selected_row), TRUE); + self->selected_protocol_row = selected_row; + + if (selected_row == self->protocol_any_row) { + gd_tagged_entry_remove_tag (entry, self->protocol_tag); + } else { + const char *title; + + gd_tagged_entry_add_tag (entry, self->protocol_tag); + title = chatty_selectable_row_get_title (CHATTY_SELECTABLE_ROW (selected_row)); + gd_tagged_entry_tag_set_label (self->protocol_tag, title); + } +} + +static void +window_chat_deleted_cb (ChattyWindow *self, + ChattyChat *chat) +{ + g_assert (CHATTY_IS_WINDOW (self)); + g_assert (CHATTY_IS_CHAT (chat)); + + if (chatty_main_view_get_item (CHATTY_MAIN_VIEW (self->content_view)) != (gpointer)chat) + return; + + window_set_item (self, NULL); } static void -window_new_muc_clicked_cb (ChattyWindow *self) +protocol_list_header_func (GtkListBoxRow *row, + GtkListBoxRow *before, + gpointer user_data) { - GtkWidget *dialog; + if (!before) { + gtk_list_box_row_set_header (row, NULL); + } else if (!gtk_list_box_row_get_header (row)) { + GtkWidget *separator; + + separator = gtk_separator_new (GTK_ORIENTATION_HORIZONTAL); + gtk_widget_show (separator); + gtk_list_box_row_set_header (row, separator); + } +} + +static void +new_chat_selection_changed_cb (ChattyWindow *self, + ChattyNewChatDialog *dialog) +{ + g_autoptr(GString) users = g_string_new (NULL); + GListModel *model; + guint n_items; + const char *name; g_assert (CHATTY_IS_WINDOW (self)); + g_assert (CHATTY_IS_NEW_CHAT_DIALOG (dialog)); - dialog = chatty_new_muc_dialog_new (GTK_WINDOW (self)); - gtk_window_present (GTK_WINDOW (dialog)); + model = chatty_new_chat_dialog_get_selected_items (dialog); + n_items = g_list_model_get_n_items (model); + + if (n_items == 0) + goto end; + + for (guint i = 0; i < n_items; i++) { + g_autoptr(ChattyItem) item = NULL; + const char *phone_number; + + item = g_list_model_get_item (model, i); + + if (CHATTY_IS_CONTACT (item)) { + phone_number = chatty_item_get_username (item); + g_string_append (users, phone_number); + g_string_append (users, ","); + } + } + + /* Remove the trailing "," */ + if (users->len >= 1) + g_string_truncate (users, users->len - 1); + + if (n_items == 1) { + g_autoptr(ChattyItem) item = NULL; + + item = g_list_model_get_item (model, 0); + + if (!CHATTY_IS_CONTACT (item) || + !chatty_contact_is_dummy (CHATTY_CONTACT (item))) { + chatty_window_open_item (self, item); + goto end; + } + } + + name = chatty_new_chat_dialog_get_chat_title (dialog); + chatty_window_set_uri (self, users->str, name); + + end: + gtk_widget_hide (GTK_WIDGET (dialog)); } static void -window_back_clicked_cb (ChattyWindow *self) +chatty_window_archive_chat (GSimpleAction *action, + GVariant *parameter, + gpointer user_data) { + ChattyWindow *self = user_data; + ChattyItem *item; + g_assert (CHATTY_IS_WINDOW (self)); - window_set_item (self, NULL); + item = chatty_main_view_get_item (CHATTY_MAIN_VIEW (self->content_view)); + g_return_if_fail (CHATTY_IS_MM_CHAT (item)); + g_return_if_fail (chatty_chat_is_im (CHATTY_CHAT (item))); + + chatty_item_set_state (item, CHATTY_ITEM_ARCHIVED); +} + +static void +chatty_window_unarchive_chat (GSimpleAction *action, + GVariant *parameter, + gpointer user_data) +{ + ChattyWindow *self = user_data; + ChattyItem *item; + + g_assert (CHATTY_IS_WINDOW (self)); + + item = chatty_main_view_get_item (CHATTY_MAIN_VIEW (self->content_view)); + g_return_if_fail (CHATTY_IS_MM_CHAT (item)); + g_return_if_fail (chatty_chat_is_im (CHATTY_CHAT (item))); + + chatty_item_set_state (item, CHATTY_ITEM_VISIBLE); +} + +static void +chatty_window_block_chat (GSimpleAction *action, + GVariant *parameter, + gpointer user_data) +{ + ChattyWindow *self = user_data; + GtkWidget *message; + ChattyItem *item; + int result; + + g_assert (CHATTY_IS_WINDOW (self)); + + item = chatty_main_view_get_item (CHATTY_MAIN_VIEW (self->content_view)); + g_return_if_fail (CHATTY_IS_MM_CHAT (item)); + g_return_if_fail (chatty_chat_is_im (CHATTY_CHAT (item))); + + message = gtk_message_dialog_new (GTK_WINDOW (self), + GTK_DIALOG_MODAL | GTK_DIALOG_USE_HEADER_BAR, + GTK_MESSAGE_INFO, + GTK_BUTTONS_OK_CANCEL, + _("You shall no longer be notified for new messages, continue?")); + + result = gtk_dialog_run (GTK_DIALOG (message)); + gtk_widget_destroy (message); + + if (result == GTK_RESPONSE_CANCEL) + return; + + chatty_item_set_state (item, CHATTY_ITEM_BLOCKED); +} + +static void +chatty_window_unblock_chat (GSimpleAction *action, + GVariant *parameter, + gpointer user_data) +{ + ChattyWindow *self = user_data; + ChattyItem *item; + + g_assert (CHATTY_IS_WINDOW (self)); + + item = chatty_main_view_get_item (CHATTY_MAIN_VIEW (self->content_view)); + g_return_if_fail (CHATTY_IS_MM_CHAT (item)); + g_return_if_fail (chatty_chat_is_im (CHATTY_CHAT (item))); + + chatty_item_set_state (item, CHATTY_ITEM_VISIBLE); + } static void -window_delete_buddy_clicked_cb (ChattyWindow *self) +chatty_window_delete_chat (GSimpleAction *action, + GVariant *parameter, + gpointer user_data) { + ChattyWindow *self = user_data; g_autofree char *text = NULL; GtkWidget *dialog; ChattyChat *chat; @@ -399,7 +506,7 @@ window_delete_buddy_clicked_cb (ChattyWindow *self) g_assert (CHATTY_IS_WINDOW (self)); - chat = chatty_chat_view_get_chat (CHATTY_CHAT_VIEW (self->chat_view)); + chat = (ChattyChat *)chatty_main_view_get_item (CHATTY_MAIN_VIEW (self->content_view)); g_return_if_fail (chat); name = chatty_item_get_name (CHATTY_ITEM (chat)); @@ -419,16 +526,12 @@ window_delete_buddy_clicked_cb (ChattyWindow *self) "%s", text); gtk_dialog_add_buttons (GTK_DIALOG (dialog), - _("Cancel"), - GTK_RESPONSE_CANCEL, - _("Delete"), - GTK_RESPONSE_OK, + _("Cancel"), GTK_RESPONSE_CANCEL, + _("Delete"), GTK_RESPONSE_OK, NULL); gtk_message_dialog_format_secondary_text (GTK_MESSAGE_DIALOG (dialog), - "%s", - sub_text); - + "%s", sub_text); gtk_dialog_set_default_response (GTK_DIALOG (dialog), GTK_RESPONSE_CANCEL); gtk_window_set_position (GTK_WINDOW (dialog), GTK_WIN_POS_CENTER_ON_PARENT); @@ -442,31 +545,32 @@ window_delete_buddy_clicked_cb (ChattyWindow *self) chatty_pp_chat_delete (CHATTY_PP_CHAT (chat)); } else #endif - if (CHATTY_IS_MM_CHAT (chat)) { - chatty_mm_chat_delete (CHATTY_MM_CHAT (chat)); - } else { - g_return_if_reached (); - } + if (CHATTY_IS_MM_CHAT (chat)) { + chatty_mm_chat_delete (CHATTY_MM_CHAT (chat)); + } else { + g_return_if_reached (); + } window_set_item (self, NULL); - gtk_widget_hide (self->call_button); - if (!hdy_leaflet_get_folded (HDY_LEAFLET (self->header_box))) + if (!hdy_leaflet_get_folded (HDY_LEAFLET (self->content_box))) chatty_chat_list_select_first (CHATTY_CHAT_LIST (self->chat_list)); } gtk_widget_destroy (dialog); } - static void -window_leave_chat_clicked_cb (ChattyWindow *self) +chatty_window_leave_chat (GSimpleAction *action, + GVariant *parameter, + gpointer user_data) { + ChattyWindow *self = user_data; ChattyChat *chat; g_assert (CHATTY_IS_WINDOW (self)); - chat = chatty_chat_view_get_chat (CHATTY_CHAT_VIEW (self->chat_view)); + chat = (ChattyChat *)chatty_main_view_get_item (CHATTY_MAIN_VIEW (self->content_view)); g_warn_if_fail (chat); if (chat) { @@ -478,89 +582,35 @@ window_leave_chat_clicked_cb (ChattyWindow *self) window_set_item (self, NULL); - if (!hdy_leaflet_get_folded (HDY_LEAFLET (self->header_box))) + if (!hdy_leaflet_get_folded (HDY_LEAFLET (self->content_box))) chatty_chat_list_select_first (CHATTY_CHAT_LIST (self->chat_list)); } static void -window_block_contact_clicked_cb (ChattyWindow *self) +chatty_window_show_archived (GSimpleAction *action, + GVariant *parameter, + gpointer user_data) { - GtkWidget *message; - ChattyChat *chat; - int result; - - g_assert (CHATTY_IS_WINDOW (self)); - - chat = chatty_chat_view_get_chat (CHATTY_CHAT_VIEW (self->chat_view)); - g_return_if_fail (CHATTY_IS_MM_CHAT (chat)); - g_return_if_fail (chatty_chat_is_im (chat)); - - message = gtk_message_dialog_new (GTK_WINDOW (self), - GTK_DIALOG_MODAL | GTK_DIALOG_USE_HEADER_BAR, - GTK_MESSAGE_INFO, - GTK_BUTTONS_OK_CANCEL, - _("You shall no longer be notified for new messages, continue?")); - - result = gtk_dialog_run (GTK_DIALOG (message)); - gtk_widget_destroy (message); - - if (result == GTK_RESPONSE_CANCEL) - return; - - chatty_item_set_state (CHATTY_ITEM (chat), CHATTY_ITEM_BLOCKED); -} - -static void -window_unblock_contact_clicked_cb (ChattyWindow *self) -{ - ChattyChat *chat; - - g_assert (CHATTY_IS_WINDOW (self)); - - chat = chatty_chat_view_get_chat (CHATTY_CHAT_VIEW (self->chat_view)); - g_return_if_fail (CHATTY_IS_MM_CHAT (chat)); - g_return_if_fail (chatty_chat_is_im (chat)); - - chatty_item_set_state (CHATTY_ITEM (chat), CHATTY_ITEM_VISIBLE); -} - -static void -window_archive_chat_clicked_cb (ChattyWindow *self) -{ - ChattyChat *chat; - - g_assert (CHATTY_IS_WINDOW (self)); - - chat = chatty_chat_view_get_chat (CHATTY_CHAT_VIEW (self->chat_view)); - g_return_if_fail (CHATTY_IS_MM_CHAT (chat)); - g_return_if_fail (chatty_chat_is_im (chat)); - - chatty_item_set_state (CHATTY_ITEM (chat), CHATTY_ITEM_ARCHIVED); -} - -static void -window_unarchive_chat_clicked_cb (ChattyWindow *self) -{ - ChattyChat *chat; + ChattyWindow *self = user_data; g_assert (CHATTY_IS_WINDOW (self)); - chat = chatty_chat_view_get_chat (CHATTY_CHAT_VIEW (self->chat_view)); - g_return_if_fail (CHATTY_IS_MM_CHAT (chat)); - g_return_if_fail (chatty_chat_is_im (chat)); - - chatty_item_set_state (CHATTY_ITEM (chat), CHATTY_ITEM_VISIBLE); + chatty_header_bar_show_archived (CHATTY_HEADER_BAR (self->header_bar), TRUE); + chatty_chat_list_show_archived (CHATTY_CHAT_LIST (self->chat_list), TRUE); } static void -window_show_chat_info_clicked_cb (ChattyWindow *self) +chatty_window_show_chat_details (GSimpleAction *action, + GVariant *parameter, + gpointer user_data) { + ChattyWindow *self = user_data; ChattyInfoDialog *dialog; ChattyChat *chat; g_assert (CHATTY_IS_WINDOW (self)); - chat = chatty_chat_view_get_chat (CHATTY_CHAT_VIEW (self->chat_view)); + chat = (ChattyChat *)chatty_main_view_get_item (CHATTY_MAIN_VIEW (self->content_view)); g_return_if_fail (CHATTY_IS_CHAT (chat)); dialog = CHATTY_INFO_DIALOG (self->chat_info_dialog); @@ -570,19 +620,12 @@ window_show_chat_info_clicked_cb (ChattyWindow *self) } static void -chatty_window_show_archived (ChattyWindow *self) +chatty_window_show_settings (GSimpleAction *action, + GVariant *parameter, + gpointer user_data) { - g_assert (CHATTY_IS_WINDOW (self)); - - hdy_header_bar_set_title (HDY_HEADER_BAR (self->header_bar), _("Archived")); - chatty_chat_list_show_archived (CHATTY_CHAT_LIST (self->chat_list), TRUE); - gtk_widget_show (self->header_back_button); - gtk_widget_hide (self->header_add_chat_button); -} + ChattyWindow *self = user_data; -static void -chatty_window_show_settings_dialog (ChattyWindow *self) -{ g_assert (CHATTY_IS_WINDOW (self)); if (!self->settings_dialog) @@ -590,191 +633,64 @@ chatty_window_show_settings_dialog (ChattyWindow *self) gtk_window_present (GTK_WINDOW (self->settings_dialog)); } -/* Copied from chatty-dialogs.c written by Andrea Schäfer <mosibasu@me.com> */ -static void -chatty_window_show_about_dialog (ChattyWindow *self) -{ - static const gchar *authors[] = { - "Adrien Plazas <kekun.plazas@laposte.net>", - "Andrea Schäfer <mosibasu@me.com>", - "Benedikt Wildenhain <benedikt.wildenhain@hs-bochum.de>", - "Chris Talbot (kop316) <chris@talbothome.com>", - "Guido Günther <agx@sigxcpu.org>", - "Julian Sparber <jsparber@gnome.org>", - "Leland Carlye <leland.carlye@protonmail.com>", - "Mohammed Sadiq https://www.sadiqpk.org/", - "Richard Bayerle (OMEMO Plugin) https://github.com/gkdr/lurch", - "Ruslan Marchenko <me@ruff.mobi>", - "and more...", - NULL - }; - - static const gchar *artists[] = { - "Tobias Bernard <tbernard@gnome.org>", - NULL - }; - - static const gchar *documenters[] = { - "Heather Ellsworth <heather.ellsworth@puri.sm>", - NULL - }; - - /* - * “program-name†defaults to g_get_application_name(). - * Don’t set it explicitly so that there is one less - * string to translate. - */ - gtk_show_about_dialog (GTK_WINDOW (self), - "logo-icon-name", CHATTY_APP_ID, - "version", GIT_VERSION, - "comments", _("An SMS and XMPP messaging client"), - "website", "https://source.puri.sm/Librem5/chatty", - "copyright", "© 2018–2022 Purism SPC", - "license-type", GTK_LICENSE_GPL_3_0, - "authors", authors, - "artists", artists, - "documenters", documenters, - "translator-credits", _("translator-credits"), - NULL); -} - static void -window_search_protocol_changed_cb (ChattyWindow *self, - GtkWidget *selected_row, - GtkListBox *box) +chatty_window_start_new_chat (GSimpleAction *action, + GVariant *parameter, + gpointer user_data) { - GdTaggedEntry *entry; - GtkWidget *old_row; + ChattyWindow *self = user_data; g_assert (CHATTY_IS_WINDOW (self)); - g_assert (GTK_IS_LIST_BOX (box)); - - entry = GD_TAGGED_ENTRY (self->chats_search_entry); - old_row = self->selected_protocol_row; - - if (old_row == selected_row) - return; - - self->protocol_filter = GPOINTER_TO_INT (g_object_get_data (G_OBJECT (selected_row), "protocol")); - chatty_chat_list_filter_protocol (CHATTY_CHAT_LIST (self->chat_list), self->protocol_filter); - chatty_selectable_row_set_selected (CHATTY_SELECTABLE_ROW (old_row), FALSE); - chatty_selectable_row_set_selected (CHATTY_SELECTABLE_ROW (selected_row), TRUE); - self->selected_protocol_row = selected_row; - - if (selected_row == self->protocol_any_row) { - gd_tagged_entry_remove_tag (entry, self->protocol_tag); - } else { - const char *title; - gd_tagged_entry_add_tag (entry, self->protocol_tag); - title = chatty_selectable_row_get_title (CHATTY_SELECTABLE_ROW (selected_row)); - gd_tagged_entry_tag_set_label (self->protocol_tag, title); - } + window_show_new_chat_dialog (self, FALSE); } static void -window_active_protocols_changed_cb (ChattyWindow *self) +chatty_window_start_sms_mms_chat (GSimpleAction *action, + GVariant *parameter, + gpointer user_data) { - ChattyAccount *mm_account; - ChattyProtocol protocols; - gboolean has_mms, has_sms, has_im; + ChattyWindow *self = user_data; g_assert (CHATTY_IS_WINDOW (self)); - mm_account = chatty_manager_get_mm_account (self->manager); - protocols = chatty_manager_get_active_protocols (self->manager); - has_mms = chatty_mm_account_has_mms_feature (CHATTY_MM_ACCOUNT (mm_account)); - has_sms = !!(protocols & CHATTY_PROTOCOL_MMS_SMS); - has_im = !!(protocols & ~CHATTY_PROTOCOL_MMS_SMS); - - gtk_widget_set_sensitive (self->header_add_chat_button, has_sms || has_im); - gtk_widget_set_sensitive (self->menu_new_group_message_button, has_im); - - gtk_widget_set_visible (self->menu_new_sms_mms_message_button, - has_mms && has_sms); + window_show_new_chat_dialog (self, TRUE); } static void -window_chat_deleted_cb (ChattyWindow *self, - ChattyChat *chat) +chatty_window_start_group_chat (GSimpleAction *action, + GVariant *parameter, + gpointer user_data) { - g_assert (CHATTY_IS_WINDOW (self)); - g_assert (CHATTY_IS_CHAT (chat)); - - if (chatty_chat_view_get_chat (CHATTY_CHAT_VIEW (self->chat_view)) != chat) - return; - - window_set_item (self, NULL); -} + ChattyWindow *self = user_data; + GtkWidget *dialog; -static void -protocol_list_header_func (GtkListBoxRow *row, - GtkListBoxRow *before, - gpointer user_data) -{ - if (!before) { - gtk_list_box_row_set_header (row, NULL); - } else if (!gtk_list_box_row_get_header (row)) { - GtkWidget *separator; + g_assert (CHATTY_IS_WINDOW (self)); - separator = gtk_separator_new (GTK_ORIENTATION_HORIZONTAL); - gtk_widget_show (separator); - gtk_list_box_row_set_header (row, separator); - } + dialog = chatty_new_muc_dialog_new (GTK_WINDOW (self)); + gtk_window_present (GTK_WINDOW (dialog)); } static void -new_chat_selection_changed_cb (ChattyWindow *self, - ChattyNewChatDialog *dialog) +chatty_window_call_user (GSimpleAction *action, + GVariant *parameter, + gpointer user_data) { - g_autoptr(GString) users = g_string_new (NULL); - GListModel *model; - guint n_items; - const char *name; + ChattyWindow *self = user_data; + g_autoptr(GError) error = NULL; + g_autofree char *uri = NULL; + ChattyChat *chat; g_assert (CHATTY_IS_WINDOW (self)); - g_assert (CHATTY_IS_NEW_CHAT_DIALOG (dialog)); - - model = chatty_new_chat_dialog_get_selected_items (dialog); - n_items = g_list_model_get_n_items (model); - - if (n_items == 0) - goto end; - - for (guint i = 0; i < n_items; i++) { - g_autoptr(ChattyItem) item = NULL; - const char *phone_number; - - item = g_list_model_get_item (model, i); - - if (CHATTY_IS_CONTACT (item)) { - phone_number = chatty_item_get_username (item); - g_string_append (users, phone_number); - g_string_append (users, ","); - } - } - - /* Remove the trailing "," */ - if (users->len >= 1) - g_string_truncate (users, users->len - 1); - - if (n_items == 1) { - g_autoptr(ChattyItem) item = NULL; - - item = g_list_model_get_item (model, 0); - if (!CHATTY_IS_CONTACT (item) || - !chatty_contact_is_dummy (CHATTY_CONTACT (item))) { - chatty_window_open_item (self, item); - goto end; - } - } + chat = (ChattyChat *)chatty_main_view_get_item (CHATTY_MAIN_VIEW (self->content_view)); + g_return_if_fail (CHATTY_IS_MM_CHAT (chat)); - name = chatty_new_chat_dialog_get_chat_title (dialog); - chatty_window_set_uri (self, users->str, name); + uri = g_strconcat ("tel://", chatty_chat_get_chat_name (chat), NULL); - end: - gtk_widget_hide (GTK_WIDGET (dialog)); + CHATTY_INFO (uri, "Calling uri:"); + if (!gtk_show_uri_on_window (NULL, uri, GDK_CURRENT_TIME, &error)) + g_warning ("Failed to launch call: %s", error->message); } static void @@ -806,17 +722,29 @@ chatty_window_map (GtkWidget *widget) "items-changed", G_CALLBACK (chatty_window_update_search_mode), self, G_CONNECT_SWAPPED); - g_signal_connect_object (self->manager, "notify::active-protocols", - G_CALLBACK (window_active_protocols_changed_cb), self, - G_CONNECT_SWAPPED); notify_fold_cb (self); - window_active_protocols_changed_cb (self); chatty_window_update_search_mode (self); GTK_WIDGET_CLASS (chatty_window_parent_class)->map (widget); } +static const GActionEntry win_entries[] = { + { "archive-chat", chatty_window_archive_chat }, + { "unarchive-chat", chatty_window_unarchive_chat }, + { "block-chat", chatty_window_block_chat }, + { "unblock-chat", chatty_window_unblock_chat }, + { "delete-chat", chatty_window_delete_chat }, + { "leave-chat", chatty_window_leave_chat }, + { "show-archived", chatty_window_show_archived }, + { "show-chat-details", chatty_window_show_chat_details }, + { "show-settings", chatty_window_show_settings }, + { "new-chat", chatty_window_start_new_chat }, + { "new-sms-mms", chatty_window_start_sms_mms_chat }, + { "new-group-chat", chatty_window_start_group_chat }, + { "call-user", chatty_window_call_user }, +}; + static void chatty_window_constructed (GObject *object) { @@ -824,6 +752,9 @@ chatty_window_constructed (GObject *object) GtkWindow *window = (GtkWindow *)object; GdkRectangle geometry; + g_action_map_add_action_entries (G_ACTION_MAP (self), win_entries, + G_N_ELEMENTS (win_entries), self); + self->settings = g_object_ref (chatty_settings_get_default ()); chatty_settings_get_window_geometry (self->settings, &geometry); gtk_window_set_default_size (window, geometry.width, geometry.height); @@ -860,9 +791,9 @@ chatty_window_dispose (GObject *object) ChattyWindow *self = (ChattyWindow *)object; /* XXX: Why it fails without the check? */ - if (CHATTY_IS_CHAT_VIEW (self->chat_view)) + if (CHATTY_IS_MAIN_VIEW (self->content_view)) g_clear_signal_handler (&self->chat_changed_handler, - chatty_chat_view_get_chat (CHATTY_CHAT_VIEW (self->chat_view))); + chatty_main_view_get_item (CHATTY_MAIN_VIEW (self->content_view))); g_clear_object (&self->manager); G_OBJECT_CLASS (chatty_window_parent_class)->dispose (object); @@ -886,62 +817,25 @@ chatty_window_class_init (ChattyWindowClass *klass) "/sm/puri/Chatty/" "ui/chatty-window.ui"); - gtk_widget_class_bind_template_child (widget_class, ChattyWindow, sub_header_label); - gtk_widget_class_bind_template_child (widget_class, ChattyWindow, sub_header_icon); - gtk_widget_class_bind_template_child (widget_class, ChattyWindow, menu_new_message_button); - gtk_widget_class_bind_template_child (widget_class, ChattyWindow, menu_new_sms_mms_message_button); - gtk_widget_class_bind_template_child (widget_class, ChattyWindow, menu_new_group_message_button); gtk_widget_class_bind_template_child (widget_class, ChattyWindow, header_bar); - gtk_widget_class_bind_template_child (widget_class, ChattyWindow, header_back_button); - gtk_widget_class_bind_template_child (widget_class, ChattyWindow, header_add_chat_button); - gtk_widget_class_bind_template_child (widget_class, ChattyWindow, call_button); - gtk_widget_class_bind_template_child (widget_class, ChattyWindow, header_sub_menu_button); - gtk_widget_class_bind_template_child (widget_class, ChattyWindow, leave_button); - gtk_widget_class_bind_template_child (widget_class, ChattyWindow, delete_button); - gtk_widget_class_bind_template_child (widget_class, ChattyWindow, block_button); - gtk_widget_class_bind_template_child (widget_class, ChattyWindow, unblock_button); - gtk_widget_class_bind_template_child (widget_class, ChattyWindow, archive_button); - gtk_widget_class_bind_template_child (widget_class, ChattyWindow, unarchive_button); - - gtk_widget_class_bind_template_child (widget_class, ChattyWindow, search_button); gtk_widget_class_bind_template_child (widget_class, ChattyWindow, chats_search_bar); gtk_widget_class_bind_template_child (widget_class, ChattyWindow, chats_search_entry); gtk_widget_class_bind_template_child (widget_class, ChattyWindow, content_box); + gtk_widget_class_bind_template_child (widget_class, ChattyWindow, content_view); gtk_widget_class_bind_template_child (widget_class, ChattyWindow, sidebar); - gtk_widget_class_bind_template_child (widget_class, ChattyWindow, header_box); - gtk_widget_class_bind_template_child (widget_class, ChattyWindow, header_group); gtk_widget_class_bind_template_child (widget_class, ChattyWindow, chat_list); - - gtk_widget_class_bind_template_child (widget_class, ChattyWindow, chat_view); - gtk_widget_class_bind_template_child (widget_class, ChattyWindow, header_chat_list_new_msg_popover); - gtk_widget_class_bind_template_child (widget_class, ChattyWindow, protocol_list); gtk_widget_class_bind_template_callback (widget_class, notify_fold_cb); gtk_widget_class_bind_template_callback (widget_class, window_content_box_changed); - gtk_widget_class_bind_template_callback (widget_class, window_show_unarchived_clicked_cb); - gtk_widget_class_bind_template_callback (widget_class, window_new_message_clicked_cb); - gtk_widget_class_bind_template_callback (widget_class, window_new_sms_mms_message_clicked_cb); - gtk_widget_class_bind_template_callback (widget_class, window_new_muc_clicked_cb); gtk_widget_class_bind_template_callback (widget_class, window_back_clicked_cb); - gtk_widget_class_bind_template_callback (widget_class, window_show_chat_info_clicked_cb); - gtk_widget_class_bind_template_callback (widget_class, window_leave_chat_clicked_cb); - gtk_widget_class_bind_template_callback (widget_class, window_block_contact_clicked_cb); - gtk_widget_class_bind_template_callback (widget_class, window_unblock_contact_clicked_cb); - gtk_widget_class_bind_template_callback (widget_class, window_archive_chat_clicked_cb); - gtk_widget_class_bind_template_callback (widget_class, window_unarchive_chat_clicked_cb); - gtk_widget_class_bind_template_callback (widget_class, window_delete_buddy_clicked_cb); - gtk_widget_class_bind_template_callback (widget_class, window_call_button_clicked_cb); + gtk_widget_class_bind_template_callback (widget_class, window_search_enable_changed_cb); gtk_widget_class_bind_template_callback (widget_class, window_search_changed_cb); gtk_widget_class_bind_template_callback (widget_class, search_tag_button_clicked_cb); - gtk_widget_class_bind_template_callback (widget_class, window_search_toggled_cb); gtk_widget_class_bind_template_callback (widget_class, window_search_entry_activated_cb); gtk_widget_class_bind_template_callback (widget_class, window_chat_list_selection_changed); - gtk_widget_class_bind_template_callback (widget_class, chatty_window_show_archived); - gtk_widget_class_bind_template_callback (widget_class, chatty_window_show_settings_dialog); - gtk_widget_class_bind_template_callback (widget_class, chatty_window_show_about_dialog); gtk_widget_class_bind_template_callback (widget_class, window_search_protocol_changed_cb); g_type_ensure (CHATTY_TYPE_SELECTABLE_ROW); @@ -969,11 +863,14 @@ chatty_window_init (ChattyWindow *self) { gtk_widget_init_template (GTK_WIDGET (self)); + chatty_header_bar_set_search_bar (CHATTY_HEADER_BAR (self->header_bar), self->chats_search_bar); + chatty_header_bar_set_content_box (CHATTY_HEADER_BAR (self->header_bar), self->content_box); + self->protocol_filter = CHATTY_PROTOCOL_ANY; hdy_search_bar_connect_entry (HDY_SEARCH_BAR (self->chats_search_bar), GTK_ENTRY (self->chats_search_entry)); self->manager = g_object_ref (chatty_manager_get_default ()); - chatty_chat_view_set_db (CHATTY_CHAT_VIEW (self->chat_view), + chatty_main_view_set_db (CHATTY_MAIN_VIEW (self->content_view), chatty_manager_get_history (self->manager)); g_signal_connect_object (self->manager, "chat-deleted", G_CALLBACK (window_chat_deleted_cb), self, @@ -1026,10 +923,15 @@ chatty_window_set_uri (ChattyWindow *self, ChattyChat * chatty_window_get_active_chat (ChattyWindow *self) { + ChattyItem *item = NULL; + g_return_val_if_fail (CHATTY_IS_WINDOW (self), NULL); if (gtk_window_has_toplevel_focus (GTK_WINDOW (self))) - return chatty_chat_view_get_chat (CHATTY_CHAT_VIEW (self->chat_view)); + item = chatty_main_view_get_item (CHATTY_MAIN_VIEW (self->content_view)); + + if (CHATTY_IS_CHAT (item)) + return CHATTY_CHAT (item); return NULL; } @@ -1038,8 +940,6 @@ void chatty_window_open_chat (ChattyWindow *self, ChattyChat *chat) { - gboolean can_delete; - g_return_if_fail (CHATTY_IS_WINDOW (self)); g_return_if_fail (CHATTY_IS_CHAT (chat)); @@ -1048,45 +948,8 @@ chatty_window_open_chat (ChattyWindow *self, window_set_item (self, chat); - gtk_widget_set_visible (self->leave_button, !CHATTY_IS_MM_CHAT (chat)); - /* We can't delete MaChat */ - can_delete = !CHATTY_IS_MA_CHAT (chat); - gtk_widget_set_visible (self->delete_button, can_delete); - - /* We shall update the ability to change state below */ - gtk_widget_hide (self->block_button); - gtk_widget_hide (self->unblock_button); - gtk_widget_hide (self->archive_button); - gtk_widget_hide (self->unarchive_button); - - hdy_leaflet_set_visible_child (HDY_LEAFLET (self->content_box), self->chat_view); - gtk_widget_hide (self->call_button); + hdy_leaflet_set_visible_child (HDY_LEAFLET (self->content_box), self->content_view); if (chatty_window_get_active_chat (self)) chatty_chat_set_unread_count (chat, 0); - - if (CHATTY_IS_MM_CHAT (chat)) { - GListModel *users; - const char *name; - - users = chatty_chat_get_users (chat); - name = chatty_chat_get_chat_name (chat); - - /* allow changing state only for 1:1 SMS/MMS chats */ - if (g_list_model_get_n_items (users) == 1) { - window_update_item_state_button (self, CHATTY_ITEM (chat)); - } - - if (g_list_model_get_n_items (users) == 1 && - chatty_utils_username_is_valid (name, CHATTY_PROTOCOL_MMS_SMS)) { - g_autoptr(ChattyMmBuddy) buddy = NULL; - g_autoptr(GAppInfo) app_info = NULL; - - app_info = g_app_info_get_default_for_uri_scheme ("tel"); - buddy = g_list_model_get_item (users, 0); - - if (app_info) - gtk_widget_show (self->call_button); - } - } } diff --git a/src/chatty.gresource.xml b/src/chatty.gresource.xml index b9f8a8db2cb9a35ecac1de54bc269ef83b652c21..b50cadea958d6740822c9efa31c6bee29dbe1b2b 100644 --- a/src/chatty.gresource.xml +++ b/src/chatty.gresource.xml @@ -1,13 +1,15 @@ <?xml version="1.0" encoding="UTF-8"?> <gresources> <gresource prefix="/sm/puri/Chatty"> - <file alias="matrix-filter.json">matrix/matrix-filter.json</file> <file preprocess="xml-stripblanks" alias="gtk/help-overlay.ui">ui/help-overlay.ui</file> <file preprocess="xml-stripblanks">ui/chatty-selectable-row.ui</file> <file preprocess="xml-stripblanks">ui/chatty-fp-row.ui</file> <file preprocess="xml-stripblanks">ui/chatty-list-row.ui</file> <file preprocess="xml-stripblanks">ui/chatty-chat-list.ui</file> <file preprocess="xml-stripblanks">ui/chatty-chat-view.ui</file> + <file preprocess="xml-stripblanks">ui/chatty-main-view.ui</file> + <file preprocess="xml-stripblanks">ui/chatty-verification-view.ui</file> + <file preprocess="xml-stripblanks">ui/chatty-invite-view.ui</file> <file preprocess="xml-stripblanks">ui/chatty-contact-list.ui</file> <file preprocess="xml-stripblanks">ui/chatty-contact-row.ui</file> <file preprocess="xml-stripblanks">ui/chatty-file-item.ui</file> @@ -22,6 +24,7 @@ <file preprocess="xml-stripblanks">ui/chatty-settings-dialog.ui</file> <file preprocess="xml-stripblanks">ui/chatty-dialog-new-chat.ui</file> <file preprocess="xml-stripblanks">ui/chatty-dialog-join-muc.ui</file> + <file preprocess="xml-stripblanks">ui/chatty-header-bar.ui</file> <file preprocess="xml-stripblanks">ui/chatty-window.ui</file> <file preprocess="xml-stripblanks">icons/eye-not-looking-symbolic.svg</file> <file preprocess="xml-stripblanks">icons/eye-open-negative-filled-symbolic.svg</file> diff --git a/src/dialogs/chatty-ma-chat-info.c b/src/dialogs/chatty-ma-chat-info.c index 7a690296f13ad800271af05114bb6c6766040461..5ab2f5c5abdf135b3013fc7a1de36e54368cc2e3 100644 --- a/src/dialogs/chatty-ma-chat-info.c +++ b/src/dialogs/chatty-ma-chat-info.c @@ -127,6 +127,8 @@ chatty_ma_chat_info_set_item (ChattyChatInfo *info, g_signal_connect_swapped (self->chat, "notify::encrypt", G_CALLBACK (ma_chat_encrypt_changed_cb), self); + gtk_widget_set_sensitive (self->encryption_switch, + chatty_ma_chat_can_set_encryption (CHATTY_MA_CHAT (self->chat))); ma_chat_encrypt_changed_cb (self); } diff --git a/src/dialogs/chatty-mm-chat-info.c b/src/dialogs/chatty-mm-chat-info.c index 806ae61ab79bef59dd175d0348ae440f07d93a59..870dcb42d9e73758e83775142c6003f8e8ee6eae 100644 --- a/src/dialogs/chatty-mm-chat-info.c +++ b/src/dialogs/chatty-mm-chat-info.c @@ -71,7 +71,7 @@ chatty_mm_chat_info_set_item (ChattyChatInfo *info, ChattyChat *chat) { ChattyMmChatInfo *self = (ChattyMmChatInfo *)info; - g_autoptr (ChattyContact) self_contact; + g_autoptr(ChattyContact) self_contact = NULL; GListModel *users; GtkWidget *contact_row; guint n_items = 0; @@ -112,7 +112,7 @@ chatty_mm_chat_info_set_item (ChattyChatInfo *info, gtk_list_box_prepend (GTK_LIST_BOX (self->contacts_list_box), GTK_WIDGET (contact_row)); } else { - g_autoptr (ChattyContact) new_contact; + g_autoptr(ChattyContact) new_contact = NULL; const char *phone; phone = chatty_mm_buddy_get_number (buddy); diff --git a/src/dialogs/chatty-settings-dialog.c b/src/dialogs/chatty-settings-dialog.c index bcd505e6a1adcf463e5d8ceae99d7afbc27d3edb..00986b72a2460f06ccce6f46aa97ff33ecfcda4b 100644 --- a/src/dialogs/chatty-settings-dialog.c +++ b/src/dialogs/chatty-settings-dialog.c @@ -30,6 +30,8 @@ #endif #include <glib/gi18n.h> +#define CMATRIX_USE_EXPERIMENTAL_API +#include "cmatrix.h" #include "chatty-utils.h" #include "matrix-utils.h" @@ -121,22 +123,6 @@ struct _ChattySettingsDialog G_DEFINE_TYPE (ChattySettingsDialog, chatty_settings_dialog, HDY_TYPE_WINDOW) -static void -finish_cb (GObject *object, - GAsyncResult *result, - gpointer user_data) -{ - GError *error = NULL; - gboolean status; - - status = g_task_propagate_boolean (G_TASK (result), &error); - - if (error) - g_task_return_error (user_data, error); - else - g_task_return_boolean (user_data, status); -} - static void settings_apply_style (GtkWidget *widget, const char *style) @@ -241,76 +227,49 @@ matrix_home_server_verify_cb (GObject *object, ChattySettingsDialog *self = user_data; g_autoptr(ChattyMaAccount) account = NULL; g_autoptr(GError) error = NULL; - const char *username, *password, *server; + const char *homeserver, *server; g_assert (CHATTY_IS_SETTINGS_DIALOG (self)); settings_dialog_set_save_state (self, FALSE); + /* verified homeserver URL */ + homeserver = cm_client_get_homeserver_finish (CM_CLIENT (object), result, &error); + /* homeserver URL entered by the user */ server = gtk_entry_get_text (GTK_ENTRY (self->matrix_homeserver_entry)); - if (!matrix_utils_verify_homeserver_finish (result, &error)) { + if (!homeserver) { gtk_widget_set_sensitive (self->add_button, FALSE); g_clear_handle_id (&self->revealer_timeout_id, g_source_remove); - gtk_label_set_text (GTK_LABEL (self->notification_label), - _("Failed to verify server")); - gtk_revealer_set_reveal_child (GTK_REVEALER (self->notification_revealer), TRUE); - self->revealer_timeout_id = g_timeout_add_seconds (5, dialog_notification_timeout_cb, self); - return; - } - username = gtk_entry_get_text (GTK_ENTRY (self->new_account_id_entry)); - password = gtk_entry_get_text (GTK_ENTRY (self->new_password_entry)); + if (error) + g_dbus_error_strip_remote_error (error); - account = chatty_ma_account_new (username, password); - chatty_ma_account_set_homeserver (account, server); - chatty_manager_save_account_async (chatty_manager_get_default (), CHATTY_ACCOUNT (account), - NULL, settings_save_account_cb, self); -} + if (server && *server) { + g_autofree char *label = NULL; -static void -chatty_settings_save_matrix (ChattySettingsDialog *self, - const char *user_id, - const char *password); -static void -matrix_home_server_got_cb (GObject *object, - GAsyncResult *result, - gpointer user_data) -{ - ChattySettingsDialog *self = user_data; - g_autoptr(ChattyMaAccount) account = NULL; - g_autofree char *home_server = NULL; - - g_assert (CHATTY_IS_SETTINGS_DIALOG (self)); - - home_server = matrix_utils_get_homeserver_finish (result, NULL); + if (error) + label = g_strdup_printf (_("Failed to verify server: %s"), error->message); + else + label = g_strdup (_("Failed to verify server")); - if (g_cancellable_is_cancelled (self->cancellable)) { - settings_dialog_set_save_state (self, FALSE); - return; - } - - if (home_server && *home_server) { - const char *username, *password; - - username = gtk_entry_get_text (GTK_ENTRY (self->new_account_id_entry)); - password = gtk_entry_get_text (GTK_ENTRY (self->new_password_entry)); - - gtk_entry_set_text (GTK_ENTRY (self->matrix_homeserver_entry), home_server); - chatty_settings_save_matrix (self, username, password); - } else { - settings_dialog_set_save_state (self, FALSE); - gtk_widget_set_sensitive (self->add_button, FALSE); + gtk_label_set_text (GTK_LABEL (self->notification_label), label); + } else { + settings_dialog_set_save_state (self, FALSE); + gtk_label_set_text (GTK_LABEL (self->notification_label), + _("Couldn't get Home server address")); + gtk_widget_show (self->matrix_homeserver_entry); + gtk_entry_grab_focus_without_selecting (GTK_ENTRY (self->matrix_homeserver_entry)); + gtk_editable_set_position (GTK_EDITABLE (self->matrix_homeserver_entry), -1); + } - g_clear_handle_id (&self->revealer_timeout_id, g_source_remove); - gtk_label_set_text (GTK_LABEL (self->notification_label), - _("Couldn't get Home server address")); gtk_revealer_set_reveal_child (GTK_REVEALER (self->notification_revealer), TRUE); self->revealer_timeout_id = g_timeout_add_seconds (5, dialog_notification_timeout_cb, self); - - gtk_widget_show (self->matrix_homeserver_entry); - gtk_entry_grab_focus_without_selecting (GTK_ENTRY (self->matrix_homeserver_entry)); - gtk_editable_set_position (GTK_EDITABLE (self->matrix_homeserver_entry), -1); + return; } + + account = chatty_ma_account_new_from_client (CM_CLIENT (object)); + chatty_manager_save_account_async (chatty_manager_get_default (), CHATTY_ACCOUNT (account), + NULL, settings_save_account_cb, self); } static void @@ -318,8 +277,12 @@ chatty_settings_save_matrix (ChattySettingsDialog *self, const char *user_id, const char *password) { + g_autoptr(CmClient) cm_client = NULL; + g_autofree char *uri_prefixed = NULL; + CmAccount *cm_account; GtkEntry *entry; const char *uri; + gboolean uri_has_prefix; g_assert (CHATTY_IS_SETTINGS_DIALOG (self)); g_return_if_fail (user_id && *user_id); @@ -332,12 +295,20 @@ chatty_settings_save_matrix (ChattySettingsDialog *self, entry = GTK_ENTRY (self->matrix_homeserver_entry); uri = gtk_entry_get_text (entry); - if (uri && *uri) - matrix_utils_verify_homeserver_async (uri, 30, self->cancellable, - matrix_home_server_verify_cb, self); - else - matrix_utils_get_homeserver_async (user_id, 10, self->cancellable, - matrix_home_server_got_cb, self); + uri_has_prefix = g_str_has_prefix (uri, "http"); + + /* Assume https by default */ + if (!uri_has_prefix) + uri_prefixed = g_strdup_printf ("https://%s", uri); + + cm_client = chatty_manager_matrix_client_new (chatty_manager_get_default ()); + cm_account = cm_client_get_account (cm_client); + cm_account_set_login_id (cm_account, user_id); + cm_client_set_homeserver (cm_client, uri_has_prefix ? uri : uri_prefixed); + cm_client_set_password (cm_client, password); + cm_client_get_homeserver_async (cm_client, self->cancellable, + matrix_home_server_verify_cb, + self); } static void @@ -360,56 +331,13 @@ chatty_settings_add_clicked_cb (ChattySettingsDialog *self) if (is_matrix) settings_check_librem_one (self); - if (is_matrix && chatty_settings_get_experimental_features (chatty_settings_get_default ())) { + if (is_matrix) { chatty_settings_save_matrix (self, user_id, password); return; } #ifdef PURPLE_ENABLED - if (is_matrix) { - g_autoptr(GTask) task = NULL; - GtkEntry *entry; - const char *server_url; - - entry = GTK_ENTRY (self->matrix_homeserver_entry); - server_url = gtk_entry_get_text (entry); - - if (!server_url || !*server_url) { - gtk_widget_show (GTK_WIDGET (entry)); - gtk_entry_grab_focus_without_selecting (entry); - gtk_editable_set_position (GTK_EDITABLE (entry), -1); - - gtk_widget_set_sensitive (self->add_button, FALSE); - g_clear_handle_id (&self->revealer_timeout_id, g_source_remove); - gtk_label_set_text (GTK_LABEL (self->notification_label), - _("Couldn't get Home server address")); - gtk_revealer_set_reveal_child (GTK_REVEALER (self->notification_revealer), TRUE); - self->revealer_timeout_id = g_timeout_add_seconds (5, dialog_notification_timeout_cb, self); - return; - } - - task = g_task_new (self, NULL, NULL, NULL); - settings_dialog_set_save_state (self, TRUE); - matrix_utils_verify_homeserver_async (server_url, 10, self->cancellable, - finish_cb, task); - - while (!g_task_get_completed (task)) - g_main_context_iteration (NULL, TRUE); - - settings_dialog_set_save_state (self, FALSE); - - if (!g_task_propagate_boolean (task, NULL)) { - gtk_widget_set_sensitive (self->add_button, FALSE); - g_clear_handle_id (&self->revealer_timeout_id, g_source_remove); - gtk_label_set_text (GTK_LABEL (self->notification_label), - _("Failed to verify server")); - gtk_revealer_set_reveal_child (GTK_REVEALER (self->notification_revealer), TRUE); - self->revealer_timeout_id = g_timeout_add_seconds (5, dialog_notification_timeout_cb, self); - return; - } - - account = (ChattyAccount *)chatty_pp_account_new (CHATTY_PROTOCOL_MATRIX, user_id, server_url, FALSE); - } else if (is_telegram) { + if (is_telegram) { account = (ChattyAccount *)chatty_pp_account_new (CHATTY_PROTOCOL_TELEGRAM, user_id, NULL, FALSE); } else {/* XMPP */ gboolean has_encryption; @@ -619,13 +547,29 @@ settings_homeserver_entry_changed (ChattySettingsDialog *self, server = gtk_entry_get_text (entry); if (server && *server) { - g_autoptr(SoupURI) uri = NULL; - - uri = soup_uri_new (gtk_entry_get_text (entry)); + g_autoptr(GUri) uri = NULL; + g_autofree char *server_prefixed = NULL; + const char *scheme = NULL; + const char *path = NULL; + const char *host = NULL; + gboolean server_has_prefix = g_str_has_prefix (server, "http"); + + /* Assume https by default */ + if (!server_has_prefix) + server_prefixed = g_strdup_printf ("https://%s", server); + + uri = g_uri_parse (server_has_prefix ? server : server_prefixed, G_URI_FLAGS_NONE, NULL); + if (uri) { + scheme = g_uri_get_scheme (uri); + path = g_uri_get_path (uri); + host = g_uri_get_host (uri); + } - valid = SOUP_URI_VALID_FOR_HTTP (uri); - /* We need an absolute path URI */ - valid = valid && *uri->host && g_str_equal (soup_uri_get_path (uri), "/"); + valid = scheme && *scheme; + valid = valid && (g_str_equal (scheme, "http") || g_str_equal (scheme, "https")); + valid = valid && host && *host; + valid = valid && !g_str_has_suffix (host, "."); + valid = valid && (!path || !*path); } if (valid) @@ -693,12 +637,11 @@ settings_update_new_account_view (ChattySettingsDialog *self) gtk_widget_grab_focus (self->new_account_id_entry); gtk_widget_show (self->add_button); - if (chatty_settings_get_experimental_features (chatty_settings_get_default ())) - gtk_widget_set_visible (self->matrix_row, TRUE); + gtk_widget_set_visible (self->matrix_row, TRUE); #ifdef PURPLE_ENABLED - if (purple_find_prpl ("prpl-matrix")) - gtk_widget_set_visible (self->matrix_row, TRUE); + /* if (purple_find_prpl ("prpl-matrix")) */ + /* gtk_widget_set_visible (self->matrix_row, TRUE); */ gtk_widget_set_visible (self->telegram_row, purple_find_prpl ("prpl-telegram") != NULL); #endif @@ -898,13 +841,16 @@ settings_new_detail_changed_cb (ChattySettingsDialog *self) else protocol = CHATTY_PROTOCOL_XMPP; - if (chatty_settings_get_experimental_features (chatty_settings_get_default ()) && - gtk_toggle_button_get_active (GTK_TOGGLE_BUTTON (self->matrix_radio_button))) + if (gtk_toggle_button_get_active (GTK_TOGGLE_BUTTON (self->matrix_radio_button))) protocol = CHATTY_PROTOCOL_MATRIX | CHATTY_PROTOCOL_EMAIL; valid_protocol = chatty_utils_username_is_valid (id, protocol); valid = valid && valid_protocol; + if (valid_protocol && + gtk_toggle_button_get_active (GTK_TOGGLE_BUTTON (self->matrix_radio_button))) + valid_protocol = !chatty_manager_has_matrix_with_id (chatty_manager_get_default (), id); + if (valid_protocol) settings_remove_style (GTK_WIDGET (self->new_account_id_entry), "error"); else @@ -1269,8 +1215,7 @@ chatty_settings_dialog_init (ChattySettingsDialog *self) gtk_widget_hide (self->xmpp_radio_button); #endif - if (chatty_settings_get_experimental_features (chatty_settings_get_default ())) - show_account_box = TRUE; + show_account_box = TRUE; gtk_widget_set_visible (self->accounts_list_box, show_account_box); diff --git a/src/library.c b/src/library.c new file mode 100644 index 0000000000000000000000000000000000000000..c6cd526c2b46935a3ea8d9e38e25e4dde25a5f76 --- /dev/null +++ b/src/library.c @@ -0,0 +1,27 @@ +/* -*- mode: c; c-basic-offset: 2; indent-tabs-mode: nil; -*- */ +/* library.h + * + * Copyright 2022 Mohammed Sadiq <sadiq@sadiqpk.org> + * + * Author(s): + * Mohammed Sadiq <sadiq@sadiqpk.org> + * + * SPDX-License-Identifier: GPL-2-or-later OR CC0-1.0 + */ + +#include <handy.h> +#include <libgd/gd.h> + +#include "dialogs/chatty-chat-info.h" + +void __attribute__((constructor)) initialize_libraries (void); + +void +__attribute__((constructor)) initialize_libraries (void) +{ + gtk_init (NULL, NULL); + hdy_init (); + gd_ensure_types (); + + g_type_ensure (CHATTY_TYPE_CHAT_INFO); +} diff --git a/src/main.c b/src/main.c index cf341f92135bdf5614d573f2544b40a0ce5e5e85..438dc376c099709913ab5124b3d2a53b219a2077 100644 --- a/src/main.c +++ b/src/main.c @@ -17,6 +17,45 @@ #include "chatty-manager.h" #include "chatty-log.h" +static void +show_backtrace (int signum) +{ + g_on_error_stack_trace (g_get_prgname ()); + g_print ("signum %d: %s\n", signum, g_strsignal (signum)); + + exit (128 + signum); +} + +static void +enable_backtrace (void) +{ + const char *env; + + env = g_getenv ("LD_PRELOAD"); + + /* Don't log backtrace if run inside valgrind */ + if (env && (strstr (env, "/valgrind/") || strstr (env, "/vgpreload"))) + return; + + signal (SIGABRT, show_backtrace); + signal (SIGTRAP, show_backtrace); + +#ifndef __has_feature +# define __has_feature(x) (0) +#endif + +#if __has_feature (address_sanitizer) || \ + defined(__SANITIZE_ADDRESS__) || \ + defined(__SANITIZE_THREAD__) + return; +#endif + + /* Trap SIGSEGV only if not compiled with sanitizers */ + /* as sanitizers shall handle this better. */ + /* fixme: How to check if leak sanitizer is enabled? */ + signal (SIGSEGV, show_backtrace); +} + int main (int argc, @@ -24,6 +63,10 @@ main (int argc, { g_autoptr(ChattyApplication) application = NULL; + g_setenv ("SOUP_FORCE_HTTP1", "1", FALSE); + + g_set_prgname (CHATTY_APP_ID); + enable_backtrace (); chatty_log_init (); gd_ensure_types (); @@ -31,7 +74,6 @@ main (int argc, bindtextdomain (GETTEXT_PACKAGE, LOCALEDIR); bind_textdomain_codeset (GETTEXT_PACKAGE, "UTF-8"); - g_set_prgname (CHATTY_APP_ID); application = chatty_application_new (); return g_application_run (G_APPLICATION (application), argc, argv); diff --git a/src/matrix/chatty-ma-account.c b/src/matrix/chatty-ma-account.c index 54cc9a611bc05901f22e1bebf88bbe282404ec7b..2cda52f41fe5f1b66c151b8fbd480a77821ef3b0 100644 --- a/src/matrix/chatty-ma-account.c +++ b/src/matrix/chatty-ma-account.c @@ -1,7 +1,7 @@ /* -*- mode: c; c-basic-offset: 2; indent-tabs-mode: nil; -*- */ /* chatty-ma-account.c * - * Copyright 2020 Purism SPC + * Copyright 2020, 2022 Purism SPC * * Author(s): * Mohammed Sadiq <sadiq@sadiqpk.org> @@ -11,19 +11,15 @@ #define G_LOG_DOMAIN "chatty-ma-account" -#include <json-glib/json-glib.h> #include <libsecret/secret.h> -#include <libsoup/soup.h> #include <glib/gi18n.h> #include "chatty-secret-store.h" #include "chatty-history.h" -#include "matrix-api.h" -#include "matrix-enc.h" -#include "matrix-db.h" #include "matrix-utils.h" #include "chatty-utils.h" #include "chatty-ma-chat.h" +#include "chatty-ma-key-chat.h" #include "chatty-ma-account.h" #include "chatty-log.h" @@ -34,44 +30,23 @@ * @include: "chatty-mat-account.h" */ -#define SYNC_TIMEOUT 30000 /* milliseconds */ - struct _ChattyMaAccount { ChattyAccount parent_instance; char *name; - MatrixApi *matrix_api; - MatrixEnc *matrix_enc; - MatrixDb *matrix_db; - HdyValueObject *device_fp; - - ChattyHistory *history_db; + CmMatrix *cm_matrix; + CmClient *cm_client; - char *pickle_key; - char *next_batch; + HdyValueObject *device_fp; GListStore *chat_list; - /* this will be moved to chat_list after login succeeds */ - GPtrArray *db_chat_list; GdkPixbuf *avatar; ChattyFileInfo *avatar_file; ChattyStatus status; - gboolean homeserver_valid; - gboolean account_enabled; - gboolean avatar_is_loading; - /* @is_loading is set when the account is loading - * from db and set to not save the change to db. - */ - gboolean is_loading; - gboolean save_account_pending; - gboolean save_password_pending; - - /* for sending events, incremented for each event */ - int event_id; guint connect_id; }; @@ -83,7 +58,7 @@ G_DEFINE_TYPE (ChattyMaAccount, chatty_ma_account, CHATTY_TYPE_ACCOUNT) if (self->status != _status) { \ self->status = _status; \ g_object_notify (G_OBJECT (self), "status"); \ - CHATTY_TRACE (matrix_api_get_username (self->matrix_api), \ + CHATTY_TRACE (cm_client_get_user_id (self->cm_client), \ "status changed, connected: %s, user:", \ _status == CHATTY_CONNECTING ? "connecting" : \ CHATTY_LOG_BOOL (_status == CHATTY_CONNECTED)); \ @@ -113,184 +88,20 @@ ma_account_get_avatar_pixbuf_cb (GObject *object, } } -static void -ma_account_get_avatar_cb (GObject *object, - GAsyncResult *result, - gpointer user_data) -{ - g_autoptr(ChattyMaAccount) self = user_data; - - self->avatar_is_loading = FALSE; - - if (matrix_api_get_file_finish (self->matrix_api, result, NULL)) { - g_clear_object (&self->avatar); - g_signal_emit_by_name (self, "avatar-changed"); - chatty_history_update_user (self->history_db, CHATTY_ACCOUNT (self)); - } -} - -static ChattyMaChat * -matrix_find_chat_with_id (ChattyMaAccount *self, - const char *room_id, - guint *index) -{ - guint n_items; - - g_assert (CHATTY_IS_MA_ACCOUNT (self)); - - if (!room_id || !*room_id) - return NULL; - - n_items = g_list_model_get_n_items (G_LIST_MODEL (self->chat_list)); - for (guint i = 0; i < n_items; i++) { - g_autoptr(ChattyMaChat) chat = NULL; - - chat = g_list_model_get_item (G_LIST_MODEL (self->chat_list), i); - if (chatty_ma_chat_matches_id (chat, room_id)) { - if (index) - *index = i; - - return chat; - } - } - - return NULL; -} - -static void -matrix_parse_device_data (ChattyMaAccount *self, - JsonObject *to_device) -{ - JsonObject *object; - JsonArray *array; - guint length = 0; - - g_assert (CHATTY_IS_MA_ACCOUNT (self)); - g_assert (to_device); - - array = matrix_utils_json_object_get_array (to_device, "events"); - if (array) - length = json_array_get_length (array); - - if (length) - CHATTY_TRACE_MSG ("Got %d to-device events", length); - - for (guint i = 0; i < length; i++) { - const char *type; - - object = json_array_get_object_element (array, i); - type = matrix_utils_json_object_get_string (object, "type"); - - CHATTY_TRACE_MSG ("parsing to-device event, type: %s", type); - - if (g_strcmp0 (type, "m.room.encrypted") == 0) - matrix_enc_handle_room_encrypted (self->matrix_enc, object); - } -} - -static void -matrix_parse_room_data (ChattyMaAccount *self, - JsonObject *rooms) -{ - JsonObject *joined_rooms, *left_rooms; - ChattyMaChat *chat; - - g_assert (CHATTY_IS_MA_ACCOUNT (self)); - g_assert (rooms); - - joined_rooms = matrix_utils_json_object_get_object (rooms, "join"); - - if (joined_rooms) { - g_autoptr(GList) joined_room_ids = NULL; - JsonObject *room_data; - - joined_room_ids = json_object_get_members (joined_rooms); - - for (GList *room_id = joined_room_ids; room_id; room_id = room_id->next) { - guint index = 0; - - chat = matrix_find_chat_with_id (self, room_id->data, &index); - room_data = matrix_utils_json_object_get_object (joined_rooms, room_id->data); - - CHATTY_TRACE (room_id->data, "joined room, new: %d, room:", !!chat); - - if (!chat) { - chat = g_object_new (CHATTY_TYPE_MA_CHAT, "room-id", room_id->data, NULL); - chatty_ma_chat_set_matrix_db (chat, self->matrix_db); - chatty_ma_chat_set_history_db (chat, self->history_db); - /* TODO */ - /* chatty_ma_chat_set_last_batch (chat, self->next_batch); */ - chatty_ma_chat_set_data (chat, CHATTY_ACCOUNT (self), self->matrix_api, self->matrix_enc); - g_object_set (chat, "json-data", room_data, NULL); - g_list_store_append (self->chat_list, chat); - g_object_unref (chat); - } else if (room_data) { - g_object_set (chat, "json-data", room_data, NULL); - g_list_model_items_changed (G_LIST_MODEL (self->chat_list), index, 1, 1); - } - } - } - - left_rooms = matrix_utils_json_object_get_object (rooms, "leave"); - - if (left_rooms) { - g_autoptr(GList) left_room_ids = NULL; - - left_room_ids = json_object_get_members (left_rooms); - - for (GList *room_id = left_room_ids; room_id; room_id = room_id->next) { - chat = matrix_find_chat_with_id (self, room_id->data, NULL); - - if (chat) { - chatty_item_set_state (CHATTY_ITEM (chat), CHATTY_ITEM_HIDDEN); - chatty_history_update_chat (self->history_db, CHATTY_CHAT (chat)); - chatty_utils_remove_list_item (self->chat_list, chat); - } - } - } -} - -static void -handle_get_homeserver (ChattyMaAccount *self, - JsonObject *object, - GError *error) -{ - g_assert (CHATTY_IS_MA_ACCOUNT (self)); - - if (error) - ma_account_update_status (self, CHATTY_DISCONNECTED); - - if (g_error_matches (error, G_IO_ERROR, G_IO_ERROR_NOT_FOUND)) { - g_warning ("Couldn't connect to ‘/.well-known/matrix/client’ "); - matrix_api_set_homeserver (self->matrix_api, "https://chat.librem.one"); - } -} - -static void -handle_verify_homeserver (ChattyMaAccount *self, - JsonObject *object, - GError *error) -{ - g_assert (CHATTY_IS_MA_ACCOUNT (self)); - - if (error) - ma_account_update_status (self, CHATTY_DISCONNECTED); -} - static void handle_password_login (ChattyMaAccount *self, - JsonObject *object, GError *error) { g_assert (CHATTY_IS_MA_ACCOUNT (self)); /* If no error, Api is informing us that logging in succeeded. * Let’s update matrix_enc & set device keys to upload */ - if (g_error_matches (error, MATRIX_ERROR, M_BAD_PASSWORD)) { - GtkWidget *dialog, *content, *header_bar; + if (g_error_matches (error, CM_ERROR, CM_ERROR_BAD_PASSWORD)) { + GtkWidget *dialog, *content, *header_bar, *label; GtkWidget *cancel_btn, *ok_btn, *entry; - g_autofree char *label = NULL; + g_autofree char *message = NULL; const char *password; + CmAccount *cm_account; int response; dialog = gtk_dialog_new_with_buttons (_("Incorrect password"), @@ -303,9 +114,13 @@ handle_password_login (ChattyMaAccount *self, content = gtk_dialog_get_content_area (GTK_DIALOG (dialog)); gtk_container_set_border_width (GTK_CONTAINER (content), 18); gtk_box_set_spacing (GTK_BOX (content), 12); - label = g_strdup_printf (_("Please enter password for “%sâ€"), - matrix_api_get_login_username (self->matrix_api)); - gtk_container_add (GTK_CONTAINER (content), gtk_label_new (label)); + cm_account = cm_client_get_account (self->cm_client); + message = g_strdup_printf (_("Please enter password for “%sâ€, homeserver: %s"), + cm_account_get_login_id (cm_account), + cm_client_get_homeserver (self->cm_client)); + label = gtk_label_new (message); + gtk_label_set_line_wrap (GTK_LABEL (label), TRUE); + gtk_container_add (GTK_CONTAINER (content), label); entry = gtk_entry_new (); gtk_entry_set_activates_default (GTK_ENTRY (entry), TRUE); gtk_entry_set_visibility (GTK_ENTRY (entry), FALSE); @@ -334,130 +149,30 @@ handle_password_login (ChattyMaAccount *self, if (response != GTK_RESPONSE_ACCEPT || !password || !*password) { chatty_account_set_enabled (CHATTY_ACCOUNT (self), FALSE); } else { - matrix_api_set_password (self->matrix_api, password); - self->is_loading = TRUE; + cm_client_set_password (self->cm_client, password); chatty_account_set_enabled (CHATTY_ACCOUNT (self), FALSE); - self->is_loading = FALSE; chatty_account_set_enabled (CHATTY_ACCOUNT (self), TRUE); } gtk_widget_destroy (dialog); } - if (!error) { - self->save_password_pending = TRUE; - chatty_account_save (CHATTY_ACCOUNT (self)); - + if (!error) ma_account_update_status (self, CHATTY_CONNECTED); - } } static void -handle_upload_key (ChattyMaAccount *self, - JsonObject *object, - GError *error) -{ - g_assert (CHATTY_IS_MA_ACCOUNT (self)); - - if (object) { - /* XXX: check later */ - matrix_enc_publish_one_time_keys (self->matrix_enc); - - self->save_account_pending = TRUE; - chatty_account_save (CHATTY_ACCOUNT (self)); - } -} - -static ChattyMaChat * -ma_account_find_chat (ChattyMaAccount *self, - const char *room_id) +cm_account_sync_cb (gpointer user_data, + CmClient *cm_client, + CmRoom *cm_room, + GPtrArray *events, + GError *error) { - GPtrArray *chats = self->db_chat_list; - - g_assert (CHATTY_IS_MA_ACCOUNT (self)); - - if (!room_id || !*room_id || !chats) - return NULL; - - for (guint i = 0; i < chats->len; i++) { - const char *chat_name; - - chat_name = chatty_chat_get_chat_name (chats->pdata[i]); - if (g_strcmp0 (chat_name, room_id) == 0) - return g_object_ref (chats->pdata[i]); - } - - return NULL; -} - -static void -handle_get_joined_rooms (ChattyMaAccount *self, - JsonObject *object, - GError *error) -{ - JsonArray *array; - guint length = 0; - - g_assert (CHATTY_IS_MA_ACCOUNT (self)); - - array = matrix_utils_json_object_get_array (object, "joined_rooms"); - - if (array) - length = json_array_get_length (array); - - for (guint i = 0; i < length; i++) { - g_autoptr(ChattyMaChat) chat = NULL; - const char *room_id; - - room_id = json_array_get_string_element (array, i); - chat = ma_account_find_chat (self, room_id); - if (!chat) - chat = g_object_new (CHATTY_TYPE_MA_CHAT, "room-id", room_id, NULL); - chatty_ma_chat_set_matrix_db (chat, self->matrix_db); - chatty_ma_chat_set_history_db (chat, self->history_db); - chatty_ma_chat_set_data (chat, CHATTY_ACCOUNT (self), self->matrix_api, self->matrix_enc); - g_list_store_append (self->chat_list, chat); - } - - g_clear_pointer (&self->db_chat_list, g_ptr_array_unref); -} - -static void -handle_red_pill (ChattyMaAccount *self, - JsonObject *root, - GError *error) -{ - JsonObject *object; - - g_assert (CHATTY_IS_MA_ACCOUNT (self)); - - if (error) - return; - - ma_account_update_status (self, CHATTY_CONNECTED); - - object = matrix_utils_json_object_get_object (root, "to_device"); - if (object) - matrix_parse_device_data (self, object); - - object = matrix_utils_json_object_get_object (root, "rooms"); - if (object) - matrix_parse_room_data (self, object); - - self->save_account_pending = TRUE; - chatty_account_save (CHATTY_ACCOUNT (self)); -} + ChattyMaAccount *self = user_data; -static void -matrix_account_sync_cb (ChattyMaAccount *self, - MatrixApi *api, - MatrixAction action, - JsonObject *object, - GError *error) -{ g_assert (CHATTY_IS_MA_ACCOUNT (self)); - g_assert (MATRIX_IS_API (api)); - g_assert (self->matrix_api == api); + g_assert (CM_IS_CLIENT (self->cm_client)); + g_assert (self->cm_client == cm_client); if (g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CANCELLED)) return; @@ -466,61 +181,11 @@ matrix_account_sync_cb (ChattyMaAccount *self, g_debug ("%s Error %d: %s", g_quark_to_string (error->domain), error->code, error->message); - if (error && - ((error->domain == SOUP_HTTP_ERROR && - error->code <= SOUP_STATUS_TLS_FAILED && - error->code > SOUP_STATUS_CANCELLED) || - g_error_matches (error, G_IO_ERROR, G_IO_ERROR_NETWORK_UNREACHABLE) || - g_error_matches (error, G_IO_ERROR, G_IO_ERROR_TIMED_OUT) || - error->domain == G_RESOLVER_ERROR || - error->domain == JSON_PARSER_ERROR)) { - ma_account_update_status (self, CHATTY_DISCONNECTED); - return; - } - - if (!error && !matrix_api_is_sync (self->matrix_api) && - action != MATRIX_GET_JOINED_ROOMS) { - ma_account_update_status (self, CHATTY_DISCONNECTED); - return; - } - - switch (action) { - case MATRIX_BLUE_PILL: - return; - - case MATRIX_GET_HOMESERVER: - handle_get_homeserver (self, object, error); - return; - - case MATRIX_VERIFY_HOMESERVER: - handle_verify_homeserver (self, object, error); - return; - - case MATRIX_PASSWORD_LOGIN: - handle_password_login (self, object, error); - return; - - case MATRIX_UPLOAD_KEY: - handle_upload_key (self, object, error); - return; - - case MATRIX_GET_JOINED_ROOMS: - handle_get_joined_rooms (self, object, error); - return; - - case MATRIX_RED_PILL: - handle_red_pill (self, object, error); - return; - - case MATRIX_ACCESS_TOKEN_LOGIN: - case MATRIX_SET_TYPING: - case MATRIX_SEND_MESSAGE: - case MATRIX_SEND_IMAGE: - case MATRIX_SEND_VIDEO: - case MATRIX_SEND_FILE: - default: - break; - } + if (g_error_matches (error, CM_ERROR, CM_ERROR_BAD_PASSWORD)) + { + handle_password_login (self, error); + return; + } } static const char * @@ -546,7 +211,7 @@ chatty_ma_account_get_enabled (ChattyAccount *account) g_assert (CHATTY_IS_MA_ACCOUNT (self)); - return self->account_enabled; + return cm_client_get_enabled (self->cm_client); } static void @@ -556,40 +221,12 @@ chatty_ma_account_set_enabled (ChattyAccount *account, ChattyMaAccount *self = (ChattyMaAccount *)account; g_assert (CHATTY_IS_MA_ACCOUNT (self)); + g_assert (self->cm_client); - if (self->account_enabled == enable) - return; - - g_clear_handle_id (&self->connect_id, g_source_remove); - - if (!self->matrix_enc && enable) { - CHATTY_TRACE_MSG ("Create new enc. user: %s has pickle: %d, has key: %d", - chatty_item_get_username (CHATTY_ITEM (account)), FALSE, FALSE); - self->matrix_enc = matrix_enc_new (self->matrix_db, NULL, NULL); - matrix_api_set_enc (self->matrix_api, self->matrix_enc); - } - - self->account_enabled = enable; - CHATTY_TRACE (chatty_item_get_username (CHATTY_ITEM (account)), - "Enable account: %d, is loading: %d, user:", - enable, self->is_loading); - - if (self->account_enabled && - chatty_ma_account_can_connect (self)) { - ma_account_update_status (self, CHATTY_CONNECTING); - matrix_api_start_sync (self->matrix_api); - } else if (!self->account_enabled){ - ma_account_update_status (self, CHATTY_DISCONNECTED); - matrix_api_stop_sync (self->matrix_api); - } + cm_client_set_enabled (self->cm_client, enable); g_object_notify (G_OBJECT (self), "enabled"); g_object_notify (G_OBJECT (self), "status"); - - if (!self->is_loading) { - self->save_account_pending = TRUE; - chatty_account_save (account); - } } static const char * @@ -600,7 +237,7 @@ chatty_ma_account_get_password (ChattyAccount *account) g_assert (CHATTY_IS_MA_ACCOUNT (self)); - password = matrix_api_get_password (self->matrix_api); + password = cm_client_get_password (self->cm_client); if (password) return password; @@ -616,15 +253,18 @@ chatty_ma_account_set_password (ChattyAccount *account, g_assert (CHATTY_IS_MA_ACCOUNT (self)); - if (g_strcmp0 (password, matrix_api_get_password (self->matrix_api)) == 0) - return; - matrix_api_set_password (self->matrix_api, password); + if (self->cm_client) + { + if (cm_client_get_logging_in (self->cm_client) || + cm_client_get_logged_in (self->cm_client)) + return; + } - if (matrix_api_get_homeserver (self->matrix_api)) { - self->save_password_pending = TRUE; - chatty_account_save (account); - } + if (g_strcmp0 (password, cm_client_get_password (self->cm_client)) == 0) + return; + + cm_client_set_password (self->cm_client, password); } static gboolean @@ -635,8 +275,7 @@ account_connect (gpointer user_data) g_assert (CHATTY_IS_MA_ACCOUNT (self)); self->connect_id = 0; - matrix_api_start_sync (self->matrix_api); - ma_account_update_status (self, CHATTY_CONNECTING); + cm_client_start_sync (self->cm_client); return G_SOURCE_REMOVE; } @@ -652,7 +291,10 @@ chatty_ma_account_connect (ChattyAccount *account, g_assert (CHATTY_IS_MA_ACCOUNT (self)); if (!chatty_account_get_enabled (account)) { - CHATTY_TRACE (matrix_api_get_login_username (self->matrix_api), + CmAccount *cm_account; + + cm_account = cm_client_get_account (self->cm_client); + CHATTY_TRACE (cm_account_get_login_id (cm_account), "Trying to connect disabled account, username:"); return; } @@ -675,7 +317,7 @@ chatty_ma_account_disconnect (ChattyAccount *account) g_assert (CHATTY_IS_MA_ACCOUNT (self)); - matrix_api_stop_sync (self->matrix_api); + cm_client_stop_sync (self->cm_client); ma_account_update_status (self, CHATTY_DISCONNECTED); } @@ -686,17 +328,6 @@ chatty_ma_account_get_remember_password (ChattyAccount *self) return TRUE; } -static void -chatty_ma_account_save (ChattyAccount *account) -{ - ChattyMaAccount *self = (ChattyMaAccount *)account; - - g_assert (CHATTY_IS_MA_ACCOUNT (self)); - g_return_if_fail (matrix_api_get_login_username (self->matrix_api)); - - chatty_ma_account_save_async (self, FALSE, NULL, NULL, NULL); -} - static void chatty_ma_account_delete (ChattyAccount *account) { @@ -713,15 +344,15 @@ chatty_ma_account_get_device_fp (ChattyAccount *account) g_assert (CHATTY_IS_MA_ACCOUNT (self)); - device_id = matrix_api_get_device_id (self->matrix_api); + device_id = cm_client_get_device_id (self->cm_client); g_clear_object (&self->device_fp); - if (!self->device_fp && device_id) { + if (device_id) { g_autoptr(GString) fp = NULL; const char *str; fp = g_string_new (NULL); - str = matrix_enc_get_ed25519_key (self->matrix_enc); + str = cm_client_get_ed25519_key (self->cm_client); while (str && *str) { g_autofree char *chunk = g_strndup (str, 4); @@ -755,7 +386,7 @@ ma_account_leave_chat_cb (GObject *object, chat = g_task_get_task_data (task); g_assert (CHATTY_IS_MA_ACCOUNT (self)); - success = matrix_api_leave_chat_finish (self->matrix_api, result, &error); + success = cm_room_leave_finish (CM_ROOM (object), result, &error); CHATTY_TRACE_MSG ("Leaving chat: %s(%s), success: %d", chatty_item_get_name (CHATTY_ITEM (chat)), chatty_chat_get_chat_name (chat), @@ -773,7 +404,6 @@ ma_account_leave_chat_cb (GObject *object, old_state = GPOINTER_TO_INT (g_object_get_data (G_OBJECT (task), "state")); chatty_item_set_state (CHATTY_ITEM (chat), old_state); - chatty_history_update_chat (self->history_db, chat); } if (error) @@ -809,11 +439,10 @@ chatty_ma_account_leave_chat_async (ChattyAccount *account, g_object_set_data (G_OBJECT (task), "state", GINT_TO_POINTER (chatty_item_get_state (CHATTY_ITEM (chat)))); chatty_item_set_state (CHATTY_ITEM (chat), CHATTY_ITEM_HIDDEN); - chatty_history_update_chat (self->history_db, chat); - matrix_api_leave_chat_async (self->matrix_api, - chatty_chat_get_chat_name (chat), - ma_account_leave_chat_cb, - g_steal_pointer (&task)); + cm_room_leave_async (chatty_ma_chat_get_cm_room (CHATTY_MA_CHAT (chat)), + NULL, + ma_account_leave_chat_cb, + g_steal_pointer (&task)); } static ChattyProtocol @@ -854,27 +483,12 @@ chatty_ma_account_get_username (ChattyItem *item) g_assert (CHATTY_IS_MA_ACCOUNT (self)); - if (matrix_api_get_username (self->matrix_api)) - return matrix_api_get_username (self->matrix_api); + if (self->cm_client && cm_client_get_user_id (self->cm_client)) + return cm_client_get_user_id (self->cm_client); return ""; } -static void -chatty_ma_account_set_username (ChattyItem *item, - const char *username) -{ - ChattyMaAccount *self = (ChattyMaAccount *)item; - - g_assert (CHATTY_IS_MA_ACCOUNT (self)); - - matrix_api_set_login_username (self->matrix_api, username); - - /* If in test, also set username */ - if (g_test_initialized ()) - matrix_api_set_username (self->matrix_api, username); -} - static ChattyFileInfo * chatty_ma_account_get_avatar_file (ChattyItem *item) { @@ -913,11 +527,6 @@ chatty_ma_account_get_avatar (ChattyItem *item) ma_account_get_avatar_pixbuf_cb, g_object_ref (self)); - } else { - matrix_api_get_file_async (self->matrix_api, NULL, self->avatar_file, - NULL, NULL, - ma_account_get_avatar_cb, - g_object_ref (self)); } return NULL; @@ -937,7 +546,7 @@ ma_account_set_user_avatar_cb (GObject *object, self = g_task_get_source_object (task); g_assert (CHATTY_MA_ACCOUNT (self)); - matrix_api_set_user_avatar_finish (self->matrix_api, result, &error); + cm_account_set_user_avatar_finish (CM_ACCOUNT (object), result, &error); if (error) g_task_return_error (task, error); @@ -957,7 +566,6 @@ ma_account_set_user_avatar_cb (GObject *object, g_clear_pointer (&self->avatar_file, chatty_file_info_free); g_clear_object (&self->avatar); - chatty_history_update_user (self->history_db, CHATTY_ACCOUNT (self)); g_signal_emit_by_name (self, "avatar-changed"); } } @@ -971,6 +579,7 @@ chatty_ma_account_set_avatar_async (ChattyItem *item, { ChattyMaAccount *self = (ChattyMaAccount *)item; g_autoptr(GTask) task = NULL; + GFile *file; g_assert (CHATTY_IS_MA_ACCOUNT (self)); @@ -982,7 +591,9 @@ chatty_ma_account_set_avatar_async (ChattyItem *item, return; } - matrix_api_set_user_avatar_async (self->matrix_api, file_name, cancellable, + file = g_file_new_for_path (file_name); + cm_account_set_user_avatar_async (cm_client_get_account (self->cm_client), + file, cancellable, ma_account_set_user_avatar_cb, g_steal_pointer (&task)); } @@ -995,19 +606,12 @@ chatty_ma_account_finalize (GObject *object) g_clear_handle_id (&self->connect_id, g_source_remove); g_list_store_remove_all (self->chat_list); - g_clear_object (&self->matrix_api); - g_clear_object (&self->matrix_enc); g_clear_object (&self->device_fp); g_clear_object (&self->chat_list); g_clear_object (&self->avatar); - g_clear_object (&self->matrix_db); - g_clear_object (&self->history_db); - g_clear_pointer (&self->db_chat_list, g_ptr_array_unref); g_clear_pointer (&self->avatar_file, chatty_file_info_free); g_free (self->name); - g_free (self->pickle_key); - g_free (self->next_batch); G_OBJECT_CLASS (chatty_ma_account_parent_class)->finalize (object); } @@ -1025,7 +629,6 @@ chatty_ma_account_class_init (ChattyMaAccountClass *klass) item_class->get_name = chatty_ma_account_get_name; item_class->set_name = chatty_ma_account_set_name; item_class->get_username = chatty_ma_account_get_username; - item_class->set_username = chatty_ma_account_set_username; item_class->get_avatar_file = chatty_ma_account_get_avatar_file; item_class->get_avatar = chatty_ma_account_get_avatar; item_class->set_avatar_async = chatty_ma_account_set_avatar_async; @@ -1039,7 +642,6 @@ chatty_ma_account_class_init (ChattyMaAccountClass *klass) account_class->connect = chatty_ma_account_connect; account_class->disconnect = chatty_ma_account_disconnect; account_class->get_remember_password = chatty_ma_account_get_remember_password; - account_class->save = chatty_ma_account_save; account_class->delete = chatty_ma_account_delete; account_class->get_device_fp = chatty_ma_account_get_device_fp; account_class->leave_chat_async = chatty_ma_account_leave_chat_async; @@ -1048,399 +650,176 @@ chatty_ma_account_class_init (ChattyMaAccountClass *klass) static void chatty_ma_account_init (ChattyMaAccount *self) { - self->chat_list = g_list_store_new (CHATTY_TYPE_MA_CHAT); - - self->matrix_api = matrix_api_new (NULL); - matrix_api_set_sync_callback (self->matrix_api, - (MatrixCallback)matrix_account_sync_cb, self); -} - -ChattyMaAccount * -chatty_ma_account_new (const char *username, - const char *password) -{ - ChattyMaAccount *self; - - g_return_val_if_fail (username, NULL); - - self = g_object_new (CHATTY_TYPE_MA_ACCOUNT, NULL); - - chatty_item_set_username (CHATTY_ITEM (self), username); - chatty_account_set_password (CHATTY_ACCOUNT (self), password); - CHATTY_DEBUG_DETAILED (username, "New Matrix account"); - - return self; -} - -gboolean -chatty_ma_account_can_connect (ChattyMaAccount *self) -{ - g_return_val_if_fail (CHATTY_IS_MA_ACCOUNT (self), FALSE); - - return matrix_api_can_connect (self->matrix_api); -} - -/** - * chatty_ma_account_get_login_username: - * @self: A #ChattyMaAccount - * - * Get the username set when @self was created. This - * can be different from chatty_item_get_username(). - * - * Say for example the user may have logged in using - * an email address. So If you want to get the original - * username (which is the mail) which was used for login, - * use this method. - */ - -const char * -chatty_ma_account_get_login_username (ChattyMaAccount *self) -{ - g_return_val_if_fail (CHATTY_IS_MA_ACCOUNT (self), ""); - - return matrix_api_get_login_username (self->matrix_api); -} - -static char * -ma_account_get_value (const char *str, - const char *key) -{ - const char *start, *end; - - if (!str || !*str) - return NULL; - - g_assert (key && *key); - - start = strstr (str, key); - if (start) { - start = start + strlen (key); - while (*start && *start++ != '"') - ; - - end = start - 1; - do { - end++; - end = strchr (end, '"'); - } while (end && *(end - 1) == '\\' && *(end - 2) != '\\'); - - if (end && end > start) - return g_strndup (start, end - start); - } - - return NULL; -} - -ChattyMaAccount * -chatty_ma_account_new_secret (gpointer secret_retrievable) -{ - ChattyMaAccount *self = NULL; - g_autoptr(GHashTable) attributes = NULL; - SecretRetrievable *item = secret_retrievable; - g_autoptr(SecretValue) value = NULL; - const char *homeserver, *credentials = NULL; - const char *username, *login_username; - char *password, *token, *device_id; - char *password_str, *token_str = NULL; - - g_return_val_if_fail (SECRET_IS_RETRIEVABLE (item), NULL); - - value = secret_retrievable_retrieve_secret_sync (item, NULL, NULL); - - if (value) - credentials = secret_value_get_text (value); - - if (!credentials) - return NULL; - - attributes = secret_retrievable_get_attributes (item); - login_username = g_hash_table_lookup (attributes, CHATTY_USERNAME_ATTRIBUTE); - homeserver = g_hash_table_lookup (attributes, CHATTY_SERVER_ATTRIBUTE); - - password = ma_account_get_value (credentials, "\"password\""); - g_return_val_if_fail (password, NULL); - password_str = g_strcompress (password); - - self = chatty_ma_account_new (login_username, password_str); - token = ma_account_get_value (credentials, "\"access-token\""); - device_id = ma_account_get_value (credentials, "\"device-id\""); - username = ma_account_get_value (credentials, "\"username\""); - - if (username && *username) - matrix_api_set_username (self->matrix_api, username); - chatty_ma_account_set_homeserver (self, homeserver); - - if (token) - token_str = g_strcompress (token); - - if (token && device_id) { - self->pickle_key = ma_account_get_value (credentials, "\"pickle-key\""); - matrix_api_set_access_token (self->matrix_api, token_str, device_id); - } - - matrix_utils_free_buffer (device_id); - matrix_utils_free_buffer (password); - matrix_utils_free_buffer (password_str); - matrix_utils_free_buffer (token); - matrix_utils_free_buffer (token_str); - - return self; + self->chat_list = g_list_store_new (CHATTY_TYPE_CHAT); } static void -db_load_account_cb (GObject *object, - GAsyncResult *result, - gpointer user_data) +joined_rooms_changed (ChattyMaAccount *self, + int position, + int removed, + int added, + GListModel *model) { - ChattyMaAccount *self = user_data; - GTask *task = (GTask *)result; - g_autoptr(GError) error = NULL; - gboolean enabled; + g_autoptr(GPtrArray) items = NULL; g_assert (CHATTY_IS_MA_ACCOUNT (self)); - g_assert (G_IS_TASK (task)); + g_assert (G_IS_LIST_MODEL (model)); - if (!self->matrix_enc) { - const char *pickle; - - pickle = g_object_get_data (G_OBJECT (task), "pickle"); - CHATTY_TRACE (chatty_item_get_username (CHATTY_ITEM (self)), - "Create new enc. has pickle: %d, has key: %d, user:", - !!pickle, !!self->pickle_key); - self->matrix_enc = matrix_enc_new (self->matrix_db, pickle, self->pickle_key); - matrix_api_set_enc (self->matrix_api, self->matrix_enc); - if (!pickle) - matrix_api_set_access_token (self->matrix_api, NULL, NULL); - g_clear_pointer (&self->pickle_key, matrix_utils_free_buffer); - } - - if (!matrix_db_load_account_finish (self->matrix_db, result, &error)) { - if (error && !g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CANCELLED)) - g_warning ("Error loading account %s: %s", - chatty_item_get_username (CHATTY_ITEM (self)), - error->message); - return; - } + for (guint i = position; i < position + added; i++) + { + g_autoptr(CmRoom) room = NULL; + ChattyMaChat *chat; - enabled = GPOINTER_TO_INT (g_object_get_data (G_OBJECT (task), "enabled")); - self->next_batch = g_strdup (g_object_get_data (G_OBJECT (task), "batch")); - CHATTY_TRACE (chatty_item_get_username (CHATTY_ITEM (self)), - "Loaded from db. enabled: %d, has next-batch: %d, user:", - !!enabled, !!self->next_batch); + if (!items) + items = g_ptr_array_new_with_free_func (g_object_unref); - self->is_loading = TRUE; + room = g_list_model_get_item (model, i); + chat = chatty_ma_chat_new_with_room (room); + chatty_ma_chat_set_data (chat, CHATTY_ACCOUNT (self), self->cm_client); + g_ptr_array_add (items, chat); + } - matrix_api_set_next_batch (self->matrix_api, self->next_batch); - chatty_account_set_enabled (CHATTY_ACCOUNT (self), enabled); - self->is_loading = FALSE; + g_list_store_splice (self->chat_list, position, removed, + items ? items->pdata : NULL, added); } static void -db_load_chats_cb (GObject *object, - GAsyncResult *result, - gpointer user_data) +key_verifications_changed (ChattyMaAccount *self, + int position, + int removed, + int added, + GListModel *model) { - ChattyMaAccount *self = user_data; - GTask *task = (GTask *)result; - GPtrArray *chats = NULL; - g_autoptr(GError) error = NULL; + g_autoptr(GPtrArray) items = NULL; g_assert (CHATTY_IS_MA_ACCOUNT (self)); - g_assert (G_IS_TASK (task)); + g_assert (G_IS_LIST_MODEL (model)); - chats = chatty_history_get_chats_finish (self->history_db, result, &error); - self->db_chat_list = chats; - CHATTY_TRACE (chatty_item_get_username (CHATTY_ITEM (self)), - "Loaded %u chats from db, user:", - !chats ? 0 : chats->len); + for (guint i = position; i < position + added; i++) { + g_autoptr(CmEvent) event = NULL; + ChattyMaKeyChat *chat; - if (error) - g_warning ("Error getting chats: %s", error->message); + if (!items) + items = g_ptr_array_new_with_free_func (g_object_unref); + event = g_list_model_get_item (model, i); + chat = chatty_ma_key_chat_new (self, event); + g_ptr_array_add (items, chat); + } - matrix_db_load_account_async (self->matrix_db, CHATTY_ACCOUNT (self), - matrix_api_get_device_id (self->matrix_api), - db_load_account_cb, self); + g_list_store_splice (self->chat_list, position, removed, + items ? items->pdata : NULL, added); } static void -history_db_load_account_cb (GObject *object, - GAsyncResult *result, - gpointer user_data) +client_status_changed_cb (ChattyMaAccount *self) { - g_autoptr(ChattyMaAccount) self = user_data; - const char *name, *avatar_url, *avatar_path; - g_autoptr(GError) error = NULL; - ChattyFileInfo *file; + ChattyStatus status = CHATTY_DISCONNECTED; g_assert (CHATTY_IS_MA_ACCOUNT (self)); - chatty_history_load_account_finish (self->history_db, result, &error); - - if (error) - g_warning ("error loading account: %s", error->message); - - name = g_object_get_data (G_OBJECT (result), "name"); - avatar_url = g_object_get_data (G_OBJECT (result), "avatar-url"); - avatar_path = g_object_get_data (G_OBJECT (result), "avatar-path"); - - self->name = g_strdup (name); - g_object_notify (G_OBJECT (self), "name"); + if (!cm_client_get_enabled (self->cm_client)) + status = CHATTY_DISCONNECTED; + else if (cm_client_is_sync (self->cm_client)) + status = CHATTY_CONNECTED; + else if (cm_client_get_logging_in (self->cm_client) || + cm_client_get_logged_in (self->cm_client)) + status = CHATTY_CONNECTING; - file = g_new0 (ChattyFileInfo, 1); - file->url = g_strdup (avatar_url); - file->path = g_strdup (avatar_path); - self->avatar_file = file; - - chatty_history_get_chats_async (self->history_db, CHATTY_ACCOUNT (self), - db_load_chats_cb, self); -} - -void -chatty_ma_account_set_db (ChattyMaAccount *self, - gpointer matrix_db, - gpointer history_db) -{ - g_return_if_fail (CHATTY_IS_MA_ACCOUNT (self)); - g_return_if_fail (MATRIX_IS_DB (matrix_db)); - g_return_if_fail (CHATTY_IS_HISTORY (history_db)); - g_return_if_fail (!self->matrix_db); - g_return_if_fail (!self->history_db); - - self->matrix_db = g_object_ref (matrix_db); - self->history_db = g_object_ref (history_db); - chatty_history_load_account_async (self->history_db, CHATTY_ACCOUNT (self), - history_db_load_account_cb, - g_object_ref (self)); + ma_account_update_status (self, status); } static void -ma_account_db_save_cb (GObject *object, - GAsyncResult *result, - gpointer user_data) +ma_account_set_client (ChattyMaAccount *self, + CmClient *client) { - ChattyMaAccount *self; - g_autoptr(GTask) task = user_data; - GError *error = NULL; - gboolean status; - - g_assert (G_IS_ASYNC_RESULT (result)); - g_assert (G_IS_TASK (task)); + GListModel *joined_rooms, *invited_rooms, *key_verifications; - self = g_task_get_source_object (task); g_assert (CHATTY_IS_MA_ACCOUNT (self)); - - status = matrix_db_save_account_finish (self->matrix_db, result, &error); - if (error || !status) - CHATTY_TRACE_MSG ("Saving %s failed", - chatty_item_get_username (CHATTY_ITEM (self))); - - if (error || !status) - self->save_account_pending = TRUE; - - if (error) - g_task_return_error (task, error); - else - g_task_return_boolean (task, status); + g_assert (CM_IS_CLIENT (client)); + g_assert (!self->cm_client); + + self->cm_client = client; + cm_client_set_device_name (client, "Chatty"); + cm_client_set_sync_callback (client, + cm_account_sync_cb, + self, NULL); + + g_signal_connect_object (self->cm_client, "status-changed", + G_CALLBACK (client_status_changed_cb), + self, G_CONNECT_SWAPPED); + + joined_rooms = cm_client_get_joined_rooms (client); + g_signal_connect_object (joined_rooms, "items-changed", + G_CALLBACK (joined_rooms_changed), self, + G_CONNECT_SWAPPED); + joined_rooms_changed (self, 0, 0, g_list_model_get_n_items (joined_rooms), joined_rooms); + + invited_rooms = cm_client_get_invited_rooms (client); + g_signal_connect_object (invited_rooms, "items-changed", + G_CALLBACK (joined_rooms_changed), self, + G_CONNECT_SWAPPED); + joined_rooms_changed (self, 0, 0, g_list_model_get_n_items (invited_rooms), invited_rooms); + + key_verifications = cm_client_get_key_verifications (client); + g_signal_connect_object (key_verifications, "items-changed", + G_CALLBACK (key_verifications_changed), self, + G_CONNECT_SWAPPED); + key_verifications_changed (self, 0, 0, g_list_model_get_n_items (key_verifications), key_verifications); } -static void -ma_account_save_cb (GObject *object, - GAsyncResult *result, - gpointer user_data) +ChattyMaAccount * +chatty_ma_account_new_from_client (CmClient *cm_client) { ChattyMaAccount *self; - g_autoptr(GTask) task = user_data; - GError *error = NULL; - gboolean status; - g_assert (G_IS_ASYNC_RESULT (result)); - g_assert (G_IS_TASK (task)); + g_return_val_if_fail (CM_IS_CLIENT (cm_client), NULL); - self = g_task_get_source_object (task); - g_assert (CHATTY_IS_MA_ACCOUNT (self)); + self = g_object_new (CHATTY_TYPE_MA_ACCOUNT, NULL); + ma_account_set_client (self, g_object_ref (cm_client)); - status = chatty_secret_store_save_finish (result, &error); + return self; +} - if (error || !status) - self->save_password_pending = TRUE; +CmClient * +chatty_ma_account_get_cm_client (ChattyMaAccount *self) +{ + g_return_val_if_fail (CHATTY_IS_MA_ACCOUNT (self), NULL); - if (error) { - g_task_return_error (task, error); - } else if (self->save_account_pending) { - char *pickle = NULL; - - if (matrix_api_get_access_token (self->matrix_api)) - pickle = matrix_enc_get_account_pickle (self->matrix_enc); - - self->save_account_pending = FALSE; - matrix_db_save_account_async (self->matrix_db, CHATTY_ACCOUNT (self), - chatty_account_get_enabled (CHATTY_ACCOUNT (self)), - pickle, - matrix_api_get_device_id (self->matrix_api), - matrix_api_get_next_batch (self->matrix_api), - ma_account_db_save_cb, g_steal_pointer (&task)); - } else { - g_task_return_boolean (task, status); - } + return self->cm_client; } -void -chatty_ma_account_save_async (ChattyMaAccount *self, - gboolean force, - GCancellable *cancellable, - GAsyncReadyCallback callback, - gpointer user_data) +gboolean +chatty_ma_account_can_connect (ChattyMaAccount *self) { - GTask *task; + g_return_val_if_fail (CHATTY_IS_MA_ACCOUNT (self), FALSE); - g_return_if_fail (CHATTY_IS_MA_ACCOUNT (self)); - g_return_if_fail (!cancellable || G_IS_CANCELLABLE (cancellable)); - g_return_if_fail (*chatty_ma_account_get_login_username (self)); + return cm_client_can_connect (self->cm_client); +} - if (!*chatty_account_get_password (CHATTY_ACCOUNT (self))) - return; +/** + * chatty_ma_account_get_login_username: + * @self: A #ChattyMaAccount + * + * Get the username set when @self was created. This + * can be different from chatty_item_get_username(). + * + * Say for example the user may have logged in using + * an email address. So If you want to get the original + * username (which is the mail) which was used for login, + * use this method. + */ - g_return_if_fail (*chatty_ma_account_get_homeserver (self)); +const char * +chatty_ma_account_get_login_username (ChattyMaAccount *self) +{ + CmAccount *cm_account; - task = g_task_new (self, cancellable, callback, user_data); - if (self->save_password_pending || force) { - char *key = NULL; - - if (self->matrix_enc && matrix_api_get_access_token (self->matrix_api)) - key = matrix_enc_get_pickle_key (self->matrix_enc); - - self->save_password_pending = FALSE; - chatty_secret_store_save_async (CHATTY_ACCOUNT (self), - g_strdup (matrix_api_get_access_token (self->matrix_api)), - matrix_api_get_device_id (self->matrix_api), - key, cancellable, - ma_account_save_cb, task); - } else if (self->save_account_pending) { - char *pickle = NULL; - - if (matrix_api_get_access_token (self->matrix_api)) - pickle = matrix_enc_get_account_pickle (self->matrix_enc); - - self->save_account_pending = FALSE; - matrix_db_save_account_async (self->matrix_db, CHATTY_ACCOUNT (self), - chatty_account_get_enabled (CHATTY_ACCOUNT (self)), - pickle, - matrix_api_get_device_id (self->matrix_api), - matrix_api_get_next_batch (self->matrix_api), - ma_account_db_save_cb, task); - } -} + g_return_val_if_fail (CHATTY_IS_MA_ACCOUNT (self), ""); -gboolean -chatty_ma_account_save_finish (ChattyMaAccount *self, - GAsyncResult *result, - GError **error) -{ - g_return_val_if_fail (CHATTY_IS_MA_ACCOUNT (self), FALSE); - g_return_val_if_fail (G_IS_TASK (result), FALSE); + cm_account = cm_client_get_account (self->cm_client); - return g_task_propagate_boolean (G_TASK (result), error); + return cm_account_get_login_id (cm_account); } const char * @@ -1450,7 +829,7 @@ chatty_ma_account_get_homeserver (ChattyMaAccount *self) g_return_val_if_fail (CHATTY_IS_MA_ACCOUNT (self), ""); - homeserver = matrix_api_get_homeserver (self->matrix_api); + homeserver = cm_client_get_homeserver (self->cm_client); if (homeserver) return homeserver; @@ -1464,7 +843,7 @@ chatty_ma_account_set_homeserver (ChattyMaAccount *self, { g_return_if_fail (CHATTY_IS_MA_ACCOUNT (self)); - matrix_api_set_homeserver (self->matrix_api, server_url); + cm_client_set_homeserver (self->cm_client, server_url); } const char * @@ -1473,7 +852,7 @@ chatty_ma_account_get_device_id (ChattyMaAccount *self) const char *device_id; g_return_val_if_fail (CHATTY_IS_MA_ACCOUNT (self), ""); - device_id = matrix_api_get_device_id (self->matrix_api); + device_id = cm_client_get_device_id (self->cm_client); if (device_id) return device_id; @@ -1504,7 +883,6 @@ ma_get_details_cb (GObject *object, { ChattyMaAccount *self; g_autoptr(GTask) task = user_data; - char *name, *avatar_url; GError *error = NULL; g_assert (G_IS_TASK (task)); @@ -1512,9 +890,8 @@ ma_get_details_cb (GObject *object, self = g_task_get_source_object (task); g_assert (CHATTY_IS_MA_ACCOUNT (self)); - matrix_api_get_user_info_finish (self->matrix_api, - &name, &avatar_url, - result, &error); + cm_user_load_info_finish (CM_USER (object), + result, &error); if (error) g_task_return_error (task, error); @@ -1522,19 +899,18 @@ ma_get_details_cb (GObject *object, ChattyFileInfo *file; CHATTY_TRACE_MSG ("Got user info for %s", - matrix_api_get_username (self->matrix_api)); + cm_client_get_user_id (self->cm_client)); g_free (self->name); - self->name = name; + self->name = g_strdup (cm_user_get_display_name (CM_USER (object))); file = self->avatar_file; - if (g_strcmp0 (file->url, avatar_url) != 0) { + if (file && g_strcmp0 (file->url, cm_user_get_avatar_url (CM_USER (object))) != 0) { g_clear_pointer (&file->path, g_free); g_free (file->url); - file->url = avatar_url; + file->url = g_strdup (cm_user_get_avatar_url (CM_USER (object))); } - chatty_history_update_user (self->history_db, CHATTY_ACCOUNT (self)); g_object_notify (G_OBJECT (self), "name"); g_task_return_boolean (task, TRUE); } @@ -1547,18 +923,20 @@ chatty_ma_account_get_details_async (ChattyMaAccount *self, gpointer user_data) { g_autoptr(GTask) task = NULL; + CmAccount *account; g_return_if_fail (CHATTY_IS_MA_ACCOUNT (self)); g_return_if_fail (!cancellable || G_IS_CANCELLABLE (cancellable)); task = g_task_new (self, cancellable, callback, user_data); + account = cm_client_get_account (self->cm_client); if (self->name) g_task_return_boolean (task, TRUE); else - matrix_api_get_user_info_async (self->matrix_api, NULL, cancellable, - ma_get_details_cb, - g_steal_pointer (&task)); + cm_user_load_info_async (CM_USER (account), cancellable, + ma_get_details_cb, + g_steal_pointer (&task)); } gboolean @@ -1586,7 +964,9 @@ ma_set_name_cb (GObject *object, self = g_task_get_source_object (task); g_assert (CHATTY_IS_MA_ACCOUNT (self)); - matrix_api_set_name_finish (self->matrix_api, result, &error); + cm_account_set_display_name_finish (CM_ACCOUNT (object), result, &error); + CHATTY_TRACE (chatty_item_get_username (CHATTY_ITEM (self)), + "Setting name %s user:", CHATTY_LOG_SUCESS (!error)); if (error) g_task_return_error (task, error); @@ -1597,7 +977,6 @@ ma_set_name_cb (GObject *object, g_free (self->name); self->name = g_strdup (name); - chatty_history_update_user (self->history_db, CHATTY_ACCOUNT (self)); g_object_notify (G_OBJECT (self), "name"); g_task_return_boolean (task, TRUE); } @@ -1618,8 +997,9 @@ chatty_ma_account_set_name_async (ChattyMaAccount *self, task = g_task_new (self, cancellable, callback, user_data); g_task_set_task_data (task, g_strdup (name), g_free); - matrix_api_set_name_async (self->matrix_api, name, cancellable, - ma_set_name_cb, task); + cm_account_set_display_name_async (cm_client_get_account (self->cm_client), + name, cancellable, + ma_set_name_cb, task); } gboolean @@ -1648,9 +1028,9 @@ ma_get_3pid_cb (GObject *object, self = g_task_get_source_object (task); g_assert (CHATTY_IS_MA_ACCOUNT (self)); - matrix_api_get_3pid_finish (self->matrix_api, - &emails, &phones, - result, &error); + cm_account_get_3pids_finish (CM_ACCOUNT (object), + &emails, &phones, + result, &error); if (error) g_task_return_error (task, error); @@ -1677,9 +1057,10 @@ chatty_ma_account_get_3pid_async (ChattyMaAccount *self, task = g_task_new (self, cancellable, callback, user_data); - matrix_api_get_3pid_async (self->matrix_api, cancellable, - ma_get_3pid_cb, - g_steal_pointer (&task)); + cm_account_get_3pids_async (cm_client_get_account (self->cm_client), + cancellable, + ma_get_3pid_cb, + g_steal_pointer (&task)); } gboolean @@ -1714,7 +1095,7 @@ ma_delete_3pid_cb (GObject *object, self = g_task_get_source_object (task); g_assert (CHATTY_IS_MA_ACCOUNT (self)); - matrix_api_delete_3pid_finish (self->matrix_api, result, &error); + cm_account_delete_3pid_finish (CM_ACCOUNT (object), result, &error); if (error) g_task_return_error (task, error); @@ -1731,14 +1112,19 @@ chatty_ma_account_delete_3pid_async (ChattyMaAccount *self, gpointer user_data) { GTask *task = NULL; + const char *type_str = NULL; g_return_if_fail (CHATTY_IS_MA_ACCOUNT (self)); g_return_if_fail (!cancellable || G_IS_CANCELLABLE (cancellable)); task = g_task_new (self, cancellable, callback, user_data); - matrix_api_delete_3pid_async (self->matrix_api, - value, type, cancellable, + if (type == CHATTY_ID_PHONE) + type_str = "msisdn"; + else + type_str = "email"; + cm_account_delete_3pid_async (cm_client_get_account (self->cm_client), + value, type_str, cancellable, ma_delete_3pid_cb, task); } @@ -1761,6 +1147,6 @@ chatty_ma_account_add_chat (ChattyMaAccount *self, g_return_if_fail (CHATTY_IS_MA_CHAT (chat)); chatty_ma_chat_set_data (CHATTY_MA_CHAT (chat), CHATTY_ACCOUNT (self), - self->matrix_api, self->matrix_enc); + self->cm_client); g_list_store_append (self->chat_list, chat); } diff --git a/src/matrix/chatty-ma-account.h b/src/matrix/chatty-ma-account.h index 310bb61e90fc0c1ca5a019dfb7a647b7c051c046..73fba4bc29b2cb3b894e7594c7411bdac922add7 100644 --- a/src/matrix/chatty-ma-account.h +++ b/src/matrix/chatty-ma-account.h @@ -12,6 +12,8 @@ #pragma once #include <glib-object.h> +#define CMATRIX_USE_EXPERIMENTAL_API +#include "cmatrix.h" #include "chatty-chat.h" #include "chatty-enums.h" @@ -23,22 +25,10 @@ G_BEGIN_DECLS G_DECLARE_FINAL_TYPE (ChattyMaAccount, chatty_ma_account, CHATTY, MA_ACCOUNT, ChattyAccount) -ChattyMaAccount *chatty_ma_account_new (const char *username, - const char *password); -gboolean chatty_ma_account_can_connect (ChattyMaAccount *self); +ChattyMaAccount *chatty_ma_account_new_from_client (CmClient *cm_client); +CmClient *chatty_ma_account_get_cm_client (ChattyMaAccount *self); +gboolean chatty_ma_account_can_connect (ChattyMaAccount *self); const char *chatty_ma_account_get_login_username (ChattyMaAccount *self); -ChattyMaAccount *chatty_ma_account_new_secret (gpointer secret_retrievable); -void chatty_ma_account_set_db (ChattyMaAccount *self, - gpointer matrix_db, - gpointer history_db); -void chatty_ma_account_save_async (ChattyMaAccount *self, - gboolean force, - GCancellable *cancellable, - GAsyncReadyCallback callback, - gpointer user_data); -gboolean chatty_ma_account_save_finish (ChattyMaAccount *self, - GAsyncResult *result, - GError **error); const char *chatty_ma_account_get_homeserver (ChattyMaAccount *self); void chatty_ma_account_set_homeserver (ChattyMaAccount *self, const char *server_url); diff --git a/src/matrix/chatty-ma-buddy.c b/src/matrix/chatty-ma-buddy.c index 81b5567416c4dff198e058ac795386c0b064e1fe..cb8aa3709e40f31e960e5c503f17bad9e783c538 100644 --- a/src/matrix/chatty-ma-buddy.c +++ b/src/matrix/chatty-ma-buddy.c @@ -1,7 +1,7 @@ /* -*- mode: c; c-basic-offset: 2; indent-tabs-mode: nil; -*- */ /* chatty-ma-buddy.c * - * Copyright 2020 Purism SPC + * Copyright 2020, 2022 Purism SPC * * Author(s): * Mohammed Sadiq <sadiq@sadiqpk.org> @@ -22,32 +22,13 @@ struct _ChattyMaBuddy { ChattyItem parent_instance; - char *matrix_id; - char *name; - GList *devices; - - MatrixApi *matrix_api; - MatrixEnc *matrix_enc; + CmUser *cm_user; /* generated using g_str_hash for faster comparison */ guint id_hash; gboolean is_self; }; -struct _BuddyDevice -{ - - char *device_id; - char *device_name; - char *curve_key; /* Public part Curve25519 identity key pair */ - char *ed_key; /* Public part of Ed25519 fingerprint key pair */ - char *one_time_key; - - gboolean meagolm_v1; - gboolean olm_v1; -}; - - G_DEFINE_TYPE (ChattyMaBuddy, chatty_ma_buddy, CHATTY_TYPE_ITEM) enum { @@ -57,20 +38,6 @@ enum { static guint signals[N_SIGNALS]; -static void -chatty_ma_device_free (BuddyDevice *device) -{ - if (!device) - return; - - g_free (device->device_id); - g_free (device->device_name); - g_free (device->curve_key); - g_free (device->ed_key); - g_free (device->one_time_key); - g_free (device); -} - static ChattyProtocol chatty_ma_buddy_get_protocols (ChattyItem *item) { @@ -85,45 +52,32 @@ chatty_ma_buddy_matches (ChattyItem *item, { ChattyMaBuddy *self = (ChattyMaBuddy *)item; - if (needle == self->matrix_id) - return TRUE; - - if (!needle || !self->matrix_id) + if (!self->cm_user || !cm_user_get_id (self->cm_user)) return FALSE; - return strcasestr (needle, self->matrix_id) != NULL; + if (needle == cm_user_get_id (self->cm_user)) + return TRUE; + + return strcasestr (needle, cm_user_get_id (self->cm_user)) != NULL; } static const char * chatty_ma_buddy_get_name (ChattyItem *item) { ChattyMaBuddy *self = (ChattyMaBuddy *)item; + const char *name = NULL; g_assert (CHATTY_IS_MA_BUDDY (self)); - if (self->name) - return self->name; - - if (self->matrix_id) - return self->matrix_id; - - return ""; -} - -static void -chatty_ma_buddy_set_name (ChattyItem *item, - const char *name) -{ - ChattyMaBuddy *self = (ChattyMaBuddy *)item; + if (!self->cm_user) + return ""; - g_assert (CHATTY_IS_MA_BUDDY (self)); + name = cm_user_get_display_name (self->cm_user); - g_free (self->name); + if (name) + return name; - if (!name || !*name) - self->name = NULL; - else - self->name = g_strdup (name); + return cm_user_get_id (self->cm_user); } /* @@ -143,8 +97,8 @@ chatty_ma_buddy_get_username (ChattyItem *item) g_assert (CHATTY_IS_MA_BUDDY (self)); - if (self->matrix_id) - return self->matrix_id; + if (self->cm_user) + return cm_user_get_id (self->cm_user); return ""; } @@ -164,14 +118,7 @@ chatty_ma_buddy_dispose (GObject *object) { ChattyMaBuddy *self = (ChattyMaBuddy *)object; - g_clear_pointer (&self->matrix_id, g_free); - g_clear_pointer (&self->name, g_free); - - g_clear_object (&self->matrix_api); - g_clear_object (&self->matrix_enc); - - g_list_free_full (self->devices, (GDestroyNotify)chatty_ma_device_free); - self->devices = NULL; + g_clear_object (&self->cm_user); G_OBJECT_CLASS (chatty_ma_buddy_parent_class)->dispose (object); } @@ -187,7 +134,6 @@ chatty_ma_buddy_class_init (ChattyMaBuddyClass *klass) item_class->get_protocols = chatty_ma_buddy_get_protocols; item_class->matches = chatty_ma_buddy_matches; item_class->get_name = chatty_ma_buddy_get_name; - item_class->set_name = chatty_ma_buddy_set_name; item_class->get_username = chatty_ma_buddy_get_username; item_class->get_avatar = chatty_ma_buddy_get_avatar; @@ -212,228 +158,35 @@ chatty_ma_buddy_init (ChattyMaBuddy *self) } ChattyMaBuddy * -chatty_ma_buddy_new (const char *matrix_id, - MatrixApi *api, - MatrixEnc *enc) +chatty_ma_buddy_new_with_user (CmUser *user) { ChattyMaBuddy *self; - g_return_val_if_fail (matrix_id && *matrix_id == '@', NULL); - g_return_val_if_fail (MATRIX_IS_API (api), NULL); - g_return_val_if_fail (MATRIX_IS_ENC (enc), NULL); + g_return_val_if_fail (CM_IS_USER (user), NULL); self = g_object_new (CHATTY_TYPE_MA_BUDDY, NULL); - self->matrix_id = g_strdup (matrix_id); - self->matrix_api = g_object_ref (api); - self->matrix_enc = g_object_ref (enc); - - if (g_str_equal (matrix_id, matrix_api_get_username (api))) - self->is_self = TRUE; + self->cm_user = g_object_ref (user); return self; } -guint -chatty_ma_buddy_get_id_hash (ChattyMaBuddy *self) -{ - g_return_val_if_fail (CHATTY_IS_MA_BUDDY (self), 0); - - if (!self->id_hash && self->matrix_id) - self->id_hash = g_str_hash (self->matrix_id); - - return self->id_hash; -} - -void -chatty_ma_buddy_add_devices (ChattyMaBuddy *self, - JsonObject *root) +gboolean +chatty_ma_buddy_matches_cm_user (ChattyMaBuddy *self, + CmUser *user) { - g_autoptr(GList) members = NULL; - JsonObject *object, *child; - BuddyDevice *device; - - g_return_if_fail (CHATTY_IS_MA_BUDDY (self)); - g_return_if_fail (root); - - members = json_object_get_members (root); - - for (GList *member = members; member; member = member->next) { - g_autofree char *device_name = NULL; - const char *device_id, *user, *key; - JsonArray *array; - char *key_name; - - child = matrix_utils_json_object_get_object (root, member->data); - device_id = matrix_utils_json_object_get_string (child, "device_id"); - user = matrix_utils_json_object_get_string (child, "user_id"); - - if (g_strcmp0 (user, self->matrix_id) != 0) { - g_warning ("‘%s’ and ‘%s’ are not the same users", user, self->matrix_id); - continue; - } - - if (self->is_self && - g_strcmp0 (device_id, matrix_api_get_device_id (self->matrix_api)) == 0) - continue; - - if (g_strcmp0 (member->data, device_id) != 0) { - g_warning ("‘%s’ and ‘%s’ are not the same device", (char *)member->data, device_id); - continue; - } - - object = matrix_utils_json_object_get_object (child, "unsigned"); - device_name = g_strdup (matrix_utils_json_object_get_string (object, "device_display_name")); - - key_name = g_strconcat ("ed25519:", device_id, NULL); - object = matrix_utils_json_object_get_object (child, "keys"); - key = matrix_utils_json_object_get_string (object, key_name); - g_free (key_name); - - if (!matrix_enc_verify (self->matrix_enc, child, self->matrix_id, device_id, key)) { - g_warning ("failed to verify signature for %s with device %s", self->matrix_id, device_id); - continue; - } - - device = g_new0 (BuddyDevice, 1); - device->device_id = g_strdup (device_id); - device->device_name = g_steal_pointer (&device_name); - device->ed_key = g_strdup (key); - - key_name = g_strconcat ("curve25519:", device_id, NULL); - object = matrix_utils_json_object_get_object (child, "keys"); - key = matrix_utils_json_object_get_string (object, key_name); - device->curve_key = g_strdup (key); - g_free (key_name); - - array = matrix_utils_json_object_get_array (child, "algorithms"); - for (guint i = 0; array && i < json_array_get_length (array); i++) { - const char *algorithm; - - algorithm = json_array_get_string_element (array, i); - if (g_strcmp0 (algorithm, ALGORITHM_MEGOLM) == 0) - device->meagolm_v1 = TRUE; - else if (g_strcmp0 (algorithm, ALGORITHM_OLM) == 0) - device->olm_v1 = TRUE; - } - - self->devices = g_list_prepend (self->devices, device); - } -} + g_return_val_if_fail (CHATTY_IS_MA_BUDDY (self), FALSE); + g_return_val_if_fail (CM_IS_USER (user), FALSE); -GList * -chatty_ma_buddy_get_devices (ChattyMaBuddy *self) -{ - g_return_val_if_fail (CHATTY_IS_MA_BUDDY (self), NULL); - - return g_list_copy (self->devices); + return user == self->cm_user; } -/** - * chatty_ma_buddy_device_key_json: - * @self: A #ChattyMaBuddy - * - * Get A JSON object with all the devices - * that we don't have an one time key for. - * - * The JSON created will have the following format: - * - * { - * "@alice:example.com": { - * "JLAFKJWSCS": "signed_curve25519" - * }, - * "@bob:example.com": { - * "JOJOAEWBZY": "signed_curve25519" - * } - * - * Returns: (transfer full): A #JsonObject - */ -JsonObject * -chatty_ma_buddy_device_key_json (ChattyMaBuddy *self) -{ - JsonObject *object; - - g_return_val_if_fail (CHATTY_IS_MA_BUDDY (self), NULL); - - if (!self->devices) - return NULL; - - object = json_object_new (); - - for (GList *node = self->devices; node; node = node->next) { - BuddyDevice *device = node->data; - - if (!device->one_time_key) - json_object_set_string_member (object, device->device_id, "signed_curve25519"); - } - - return object; -} - -void -chatty_ma_buddy_add_one_time_keys (ChattyMaBuddy *self, - JsonObject *root) -{ - JsonObject *object, *child; - - g_return_if_fail (CHATTY_IS_MA_BUDDY (self)); - g_return_if_fail (root); - - for (GList *item = self->devices; item; item = item->next) { - g_autoptr(GList) members = NULL; - BuddyDevice *device = item->data; - - child = matrix_utils_json_object_get_object (root, device->device_id); - - if (!child) { - g_warning ("device '%s' not found", device->device_id); - continue; - } - - members = json_object_get_members (child); - - for (GList *node = members; node; node = node->next) { - object = matrix_utils_json_object_get_object (child, node->data); - - if (matrix_enc_verify (self->matrix_enc, object, self->matrix_id, - device->device_id, device->ed_key)) { - const char *key; - - key = matrix_utils_json_object_get_string (object, "key"); - g_free (device->one_time_key); - device->one_time_key = g_strdup (key); - } - } - } -} - -const char * -chatty_ma_device_get_id (BuddyDevice *device) -{ - g_return_val_if_fail (device, ""); - - return device->device_id; -} - -const char * -chatty_ma_device_get_ed_key (BuddyDevice *device) -{ - g_return_val_if_fail (device, ""); - - return device->ed_key; -} - -const char * -chatty_ma_device_get_curve_key (BuddyDevice *device) +guint +chatty_ma_buddy_get_id_hash (ChattyMaBuddy *self) { - g_return_val_if_fail (device, ""); - - return device->curve_key; -} + g_return_val_if_fail (CHATTY_IS_MA_BUDDY (self), 0); -char * -chatty_ma_device_get_one_time_key (BuddyDevice *device) -{ - g_return_val_if_fail (device, g_strdup ("")); + if (!self->id_hash && self->cm_user) + self->id_hash = g_str_hash (cm_user_get_id (self->cm_user)); - return g_steal_pointer (&device->one_time_key); + return self->id_hash; } diff --git a/src/matrix/chatty-ma-buddy.h b/src/matrix/chatty-ma-buddy.h index fc0e65f2026ef5d1f5d212067217d09186fb210a..3a5dfa086053120a47f2fe87145c116ee4522130 100644 --- a/src/matrix/chatty-ma-buddy.h +++ b/src/matrix/chatty-ma-buddy.h @@ -12,35 +12,20 @@ #pragma once #include <glib-object.h> -#include <json-glib/json-glib.h> +#define CMATRIX_USE_EXPERIMENTAL_API +#include "cmatrix.h" #include "chatty-item.h" -#include "matrix-api.h" -#include "matrix-enc.h" G_BEGIN_DECLS -typedef struct _BuddyDevice BuddyDevice; - #define CHATTY_TYPE_MA_BUDDY (chatty_ma_buddy_get_type ()) G_DECLARE_FINAL_TYPE (ChattyMaBuddy, chatty_ma_buddy, CHATTY, MA_BUDDY, ChattyItem) -ChattyMaBuddy *chatty_ma_buddy_new (const char *matrix_id, - MatrixApi *api, - MatrixEnc *enc); -const char *chatty_ma_buddy_get_id (ChattyMaBuddy *self); +ChattyMaBuddy *chatty_ma_buddy_new_with_user (CmUser *user); +gboolean chatty_ma_buddy_matches_cm_user (ChattyMaBuddy *self, + CmUser *user); guint chatty_ma_buddy_get_id_hash (ChattyMaBuddy *self); -void chatty_ma_buddy_add_devices (ChattyMaBuddy *self, - JsonObject *root); -GList *chatty_ma_buddy_get_devices (ChattyMaBuddy *self); -JsonObject *chatty_ma_buddy_device_key_json (ChattyMaBuddy *self); -void chatty_ma_buddy_add_one_time_keys (ChattyMaBuddy *self, - JsonObject *root); - -const char *chatty_ma_device_get_id (BuddyDevice *device); -const char *chatty_ma_device_get_ed_key (BuddyDevice *device); -const char *chatty_ma_device_get_curve_key (BuddyDevice *device); -char *chatty_ma_device_get_one_time_key (BuddyDevice *device); G_END_DECLS diff --git a/src/matrix/chatty-ma-chat.c b/src/matrix/chatty-ma-chat.c index e6bd91a77e8d13a43f9e3d2ee98f05db71b674f9..f7004128a209085da244491f27bd56705e794c8e 100644 --- a/src/matrix/chatty-ma-chat.c +++ b/src/matrix/chatty-ma-chat.c @@ -16,20 +16,16 @@ #endif #include <glib/gi18n.h> +#define CMATRIX_USE_EXPERIMENTAL_API +#include "cmatrix.h" #include "contrib/gtk.h" -#include "chatty-history.h" #include "chatty-utils.h" -#include "matrix-api.h" -#include "matrix-db.h" -#include "matrix-enc.h" #include "matrix-utils.h" #include "chatty-ma-buddy.h" #include "chatty-ma-chat.h" #include "chatty-log.h" -#define CHATTY_COLOR_BLUE "4A8FD9" - /** * SECTION: chatty-chat * @title: ChattyChat @@ -45,152 +41,81 @@ struct _ChattyMaChat ChattyChat parent_instance; char *room_name; - char *generated_name; char *room_id; - char *encryption; - char *prev_batch; - char *last_batch; GdkPixbuf *avatar; ChattyFileInfo *avatar_file; GCancellable *avatar_cancellable; - ChattyMaBuddy *self_buddy; GListStore *buddy_list; GListStore *message_list; - GtkSortListModel *sorted_message_list; - - /* Pending messages to be sent. Queue messages here when - @self is busy (eg: claiming keys for encrypted chat) */ - GQueue *message_queue; + GtkFilterListModel *filtered_event_list; - JsonObject *json_data; ChattyAccount *account; - MatrixApi *matrix_api; - MatrixEnc *matrix_enc; - MatrixDb *matrix_db; - ChattyHistory *history_db; + CmClient *cm_client; + CmRoom *cm_room; ChattyItemState visibility_state; - gint64 highlight_count; int unread_count; - int room_name_update_ts; - int message_timeout_id; - guint is_sending_message : 1; guint notification_shown : 1; - guint user_list_loading : 1; - guint user_list_loaded : 1; - guint state_is_sync : 1; - guint state_is_syncing : 1; - /* Set if the complete buddy list is loaded */ - guint claiming_keys : 1; - guint keys_claimed : 1; - guint prev_batch_loading : 1; guint history_is_loading : 1; guint avatar_is_loading : 1; - guint saving_room_to_db : 1; - guint room_db_loaded : 1; - guint room_name_loaded : 1; guint buddy_typing : 1; - /* Set if server says we are typing */ - guint self_typing : 1; - /* The time when self_typing was updated */ - /* Uses g_get_monotonic_time(), we only need the interval */ - gint64 self_typing_set_time; }; G_DEFINE_TYPE (ChattyMaChat, chatty_ma_chat, CHATTY_TYPE_CHAT) -enum { - PROP_0, - PROP_JSON_DATA, - PROP_ROOM_ID, - N_PROPS -}; - -static GParamSpec *properties[N_PROPS]; - -static void matrix_send_message_from_queue (ChattyMaChat *self); - -static int -sort_message (gconstpointer a, - gconstpointer b, - gpointer user_data) -{ - time_t time_a, time_b; - - time_a = chatty_message_get_time ((gpointer)a); - time_b = chatty_message_get_time ((gpointer)b); +/* Private */ +CmStatus cm_room_get_status (CmRoom *self); - return time_a - time_b; -} - -static void -chatty_mat_chat_update_name (ChattyMaChat *self) +static gboolean +ma_chat_filter_event_list (ChattyMessage *message, + ChattyMaChat *self) { + g_assert (CHATTY_IS_MESSAGE (message)); g_assert (CHATTY_IS_MA_CHAT (self)); - if (self->room_name) - return; - - g_free (self->generated_name); - self->generated_name = chatty_chat_generate_name (CHATTY_CHAT (self), - G_LIST_MODEL (self->buddy_list)); - g_signal_emit_by_name (self, "avatar-changed"); - chatty_history_update_chat (self->history_db, CHATTY_CHAT (self)); + return !!chatty_message_get_cm_event (message); } static ChattyMaBuddy * -ma_chat_find_buddy (ChattyMaChat *self, - GListModel *model, - const char *matrix_id, - guint *index) +ma_chat_find_cm_user (ChattyMaChat *self, + GListModel *model, + CmUser *user, + guint *index, + gboolean add_if_missing) { guint n_items; - guint id_hash; g_assert (CHATTY_IS_MA_CHAT (self)); g_assert (G_IS_LIST_MODEL (model)); - g_return_val_if_fail (matrix_id && *matrix_id, NULL); n_items = g_list_model_get_n_items (model); - id_hash = g_str_hash (matrix_id); for (guint i = 0; i < n_items; i++) { g_autoptr(ChattyMaBuddy) buddy = NULL; buddy = g_list_model_get_item (model, i); - if (id_hash == chatty_ma_buddy_get_id_hash (buddy) && - g_str_equal (chatty_item_get_username (CHATTY_ITEM (buddy)), matrix_id)) { + if (chatty_ma_buddy_matches_cm_user (buddy, user)) { if (index) *index = i; - return buddy; + return g_steal_pointer (&buddy); } } - return NULL; -} - -static ChattyMaBuddy * -ma_chat_add_buddy (ChattyMaChat *self, - GListStore *store, - const char *matrix_id) -{ - g_autoptr(ChattyMaBuddy) buddy = NULL; - - g_assert (CHATTY_IS_MA_CHAT (self)); + if (add_if_missing) + { + ChattyMaBuddy *buddy; - if (!matrix_id || *matrix_id != '@') - g_return_val_if_reached (NULL); + buddy = chatty_ma_buddy_new_with_user (user); + g_list_store_append (self->buddy_list, buddy); - buddy = chatty_ma_buddy_new (matrix_id, - self->matrix_api, - self->matrix_enc); - g_list_store_append (store, buddy); + return buddy; + } - return buddy; + return NULL; } static void @@ -209,1100 +134,55 @@ ma_chat_get_avatar_pixbuf_cb (GObject *object, if (error && !g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CANCELLED)) g_warning ("Error loading avatar file: %s", error->message); - if (!error) { - g_set_object (&self->avatar, pixbuf); - g_signal_emit_by_name (self, "avatar-changed"); - } - - self->avatar_is_loading = FALSE; -} - -#if 0 -static void -chat_got_room_avatar_cb (GObject *object, - GAsyncResult *result, - gpointer user_data) -{ - g_autoptr(ChattyMaChat) self = user_data; - - if (matrix_api_get_file_finish (self->matrix_api, result, NULL)) { - g_clear_object (&self->avatar); - g_signal_emit_by_name (self, "avatar-changed"); - chatty_history_update_chat (self->history_db, CHATTY_CHAT (self)); - } -} -#endif - -static ChattyFileInfo * -ma_chat_new_file (ChattyMaChat *self, - JsonObject *object, - JsonObject *content) -{ - ChattyFileInfo *file = NULL; - const char *url; - - g_assert (CHATTY_IS_MA_CHAT (self)); - - url = matrix_utils_json_object_get_string (object, "url"); - - if (url && g_str_has_prefix (url, "mxc://")) { - file = g_new0 (ChattyFileInfo, 1); - - url = url + strlen ("mxc://"); - file->url = g_strconcat (matrix_api_get_homeserver (self->matrix_api), - "/_matrix/media/r0/download/", url, NULL); - file->file_name = g_strdup (matrix_utils_json_object_get_string (object, "body")); - object = matrix_utils_json_object_get_object (content, "info"); - file->mime_type = g_strdup (matrix_utils_json_object_get_string (object, "mimetype")); - file->height = matrix_utils_json_object_get_int (object, "h"); - file->width = matrix_utils_json_object_get_int (object, "w"); - file->size = matrix_utils_json_object_get_int (object, "size"); - } - - return file; -} - -static void -handle_m_room_member (ChattyMaChat *self, - JsonObject *object) -{ - GListModel *model; - ChattyMaBuddy *buddy; - JsonObject *content; - const char *membership, *sender, *name; - - g_assert (CHATTY_IS_MA_CHAT (self)); - g_assert (object); - - sender = matrix_utils_json_object_get_string (object, "sender"); - content = matrix_utils_json_object_get_object (object, "content"); - membership = matrix_utils_json_object_get_string (content, "membership"); - - model = G_LIST_MODEL (self->buddy_list); - buddy = ma_chat_find_buddy (self, model, sender, NULL); - name = matrix_utils_json_object_get_string (content, "displayname"); - - if (g_strcmp0 (membership, "join") == 0) { - if (!buddy) - buddy = ma_chat_add_buddy (self, self->buddy_list, sender); - chatty_item_set_name (CHATTY_ITEM (buddy), name); - self->keys_claimed = FALSE; - } else if (buddy && g_strcmp0 (membership, "leave") == 0) { - chatty_utils_remove_list_item (self->buddy_list, buddy); - g_clear_pointer (&self->generated_name, g_free); - self->keys_claimed = FALSE; - } -} - -static void -handle_m_room_name (ChattyMaChat *self, - JsonObject *root) -{ - JsonObject *content; - const char *name; - - g_assert (CHATTY_MA_CHAT (self)); - - content = matrix_utils_json_object_get_object (root, "content"); - name = matrix_utils_json_object_get_string (content, "name"); - - if (name && g_strcmp0 (name, self->room_name) != 0) { - g_free (self->room_name); - self->room_name = g_strdup (name); - g_object_notify (G_OBJECT (self), "name"); - g_signal_emit_by_name (self, "avatar-changed"); - } -} - -static void -handle_m_room_encryption (ChattyMaChat *self, - JsonObject *root) -{ - JsonObject *content; - - g_assert (CHATTY_IS_MA_CHAT (self)); - - if (!root || self->encryption) - return; - - content = matrix_utils_json_object_get_object (root, "content"); - self->encryption = g_strdup (matrix_utils_json_object_get_string (content, "algorithm")); - - if (self->encryption) - g_object_notify (G_OBJECT (self), "encrypt"); -} - -#if 0 -static void -handle_m_room_avatar (ChattyMaChat *self, - JsonObject *root) -{ - JsonObject *content; - - g_assert (CHATTY_IS_MA_CHAT (self)); - - g_clear_pointer (&self->avatar_file, chatty_file_info_free); - content = matrix_utils_json_object_get_object (root, "content"); - self->avatar_file = ma_chat_new_file (self, content, content); - - g_cancellable_cancel (self->avatar_cancellable); - g_clear_object (&self->avatar_cancellable); - self->avatar_cancellable = g_cancellable_new (); - - matrix_api_get_file_async (self->matrix_api, NULL, self->avatar_file, - NULL, NULL, - chat_got_room_avatar_cb, - g_object_ref (self)); -} -#endif - -static void -ma_chat_download_cb (GObject *object, - GAsyncResult *result, - gpointer user_data) -{ - g_autoptr(GError) error = NULL; - ChattyMaChat *self = user_data; - GTask *task = G_TASK (result); - ChattyMessage *message; - ChattyFileInfo *file; - - g_assert (CHATTY_IS_MA_CHAT (self)); - - file = g_object_get_data (G_OBJECT (task), "file"); - message = g_object_get_data (G_OBJECT (task), "message"); - g_return_if_fail (file); - g_return_if_fail (message); - - if (matrix_api_get_file_finish (self->matrix_api, result, &error)) - file->status = CHATTY_FILE_DOWNLOADED; - else - file->status = CHATTY_FILE_ERROR; - - chatty_history_add_message (self->history_db, CHATTY_CHAT (self), message); - chatty_message_emit_updated (message); -} - -static void -ma_chat_parse_base64_value (guchar **out, - gsize *out_len, - const char *value) -{ - g_autofree char *base64 = NULL; - gsize len, padded_len; - - g_assert (out); - g_assert (out_len); - - if (!value) - return; - - len = strlen (value); - /* base64 is always multiple of 4, so add space for padding */ - if (len % 4) - padded_len = len + 4 - len % 4; - else - padded_len = len; - base64 = malloc (padded_len + 1); - strcpy (base64, value); - memset (base64 + len, '=', padded_len - len); - base64[padded_len] = '\0'; - - *out = g_base64_decode (base64, out_len); -} - -static void -chat_handle_m_media (ChattyMaChat *self, - ChattyMessage *message, - JsonObject *content, - const char *type, - gboolean encrypted) -{ - ChattyFileInfo *file = NULL; - JsonObject *object; - - g_assert (CHATTY_IS_MA_CHAT (self)); - g_assert (CHATTY_IS_MESSAGE (message)); - g_assert (content); - g_assert (type); - - CHATTY_TRACE_MSG ("Got media, type: %s, encrypted: %d", type, !!encrypted); - - if (encrypted) - object = matrix_utils_json_object_get_object (content, "file"); - else - object = content; - - if (!matrix_utils_json_object_get_string (object, "url")) - return; - - if (!g_str_equal (type, "m.image") && - !g_str_equal (type, "m.video") && - !g_str_equal (type, "m.file") && - !g_str_equal (type, "m.audio")) - return; - - file = ma_chat_new_file (self, object, content); - - if (encrypted && file) { - g_autoptr(MatrixFileEncInfo) info = NULL; - JsonObject *json_key; - - object = matrix_utils_json_object_get_object (content, "file"); - json_key = matrix_utils_json_object_get_object (object, "key"); - - if (g_strcmp0 (matrix_utils_json_object_get_string (object, "v"), "v2") != 0 || - g_strcmp0 (matrix_utils_json_object_get_string (json_key, "alg"), "A256CTR") != 0 || - !matrix_utils_json_object_get_bool (json_key, "ext") || - g_strcmp0 (matrix_utils_json_object_get_string (json_key, "kty"), "oct") != 0) - return; - - info = g_new0 (MatrixFileEncInfo, 1); - info->aes_iv_base64 = g_strdup (matrix_utils_json_object_get_string (object, "iv")); - info->aes_key_base64 = g_strdup (matrix_utils_json_object_get_string (json_key, "k")); - /* XXX: update doc: uses basae64url */ - g_strdelimit (info->aes_key_base64, "_", '/'); - g_strdelimit (info->aes_key_base64, "-", '+'); - - object = matrix_utils_json_object_get_object (object, "hashes"); - info->sha256_base64 = g_strdup (matrix_utils_json_object_get_string (object, "sha256")); - - ma_chat_parse_base64_value (&info->aes_iv, &info->aes_iv_len, info->aes_iv_base64); - ma_chat_parse_base64_value (&info->aes_key, &info->aes_key_len, info->aes_key_base64); - ma_chat_parse_base64_value (&info->sha256, &info->sha256_len, info->sha256_base64); - - if (info->aes_iv_len == 16 && info->aes_key_len == 32 && info->sha256_len == 32) - file->user_data = g_steal_pointer (&info); - - if (file->user_data) - matrix_db_save_file_url_async (self->matrix_db, message, file, 2, - CHATTY_ALGORITHM_A256CTR, CHATTY_KEY_TYPE_OCT, TRUE, - NULL, NULL); - } - - if (!file) - return; - - g_object_set_data_full (G_OBJECT (message), "file-url", g_strdup (file->url), g_free); - chatty_message_set_files (message, g_list_append (NULL, file)); - return; -} - -static void -matrix_add_message_from_data (ChattyMaChat *self, - ChattyMaBuddy *buddy, - JsonObject *root, - JsonObject *object, - gboolean encrypted) -{ - g_autoptr(ChattyMessage) message = NULL; - JsonObject *content; - const char *body, *type; - ChattyMsgDirection direction = CHATTY_DIRECTION_IN; - ChattyMsgType msg_type; - const char *uuid; - time_t ts; - - g_assert (CHATTY_IS_MA_CHAT (self)); - g_assert (object); - - content = matrix_utils_json_object_get_object (object, "content"); - type = matrix_utils_json_object_get_string (content, "msgtype"); - - if (!type) - return; - - if (g_str_equal (type, "m.image")) - msg_type = CHATTY_MESSAGE_IMAGE; - else if (g_str_equal (type, "m.video")) - msg_type = CHATTY_MESSAGE_VIDEO; - else if (g_str_equal (type, "m.file")) - msg_type = CHATTY_MESSAGE_FILE; - else if (g_str_equal (type, "m.audio")) - msg_type = CHATTY_MESSAGE_AUDIO; - else if (g_str_equal (type, "m.location")) - msg_type = CHATTY_MESSAGE_LOCATION; - else - msg_type = CHATTY_MESSAGE_TEXT; - - body = matrix_utils_json_object_get_string (content, "body"); - if (root) - uuid = matrix_utils_json_object_get_string (root, "event_id"); - else - uuid = matrix_utils_json_object_get_string (object, "event_id"); - - /* timestamp is in milliseconds */ - ts = matrix_utils_json_object_get_int (object, "origin_server_ts"); - ts = ts / 1000; - - if (buddy == self->self_buddy) - direction = CHATTY_DIRECTION_OUT; - - CHATTY_TRACE_MSG ("Got message, direction: %s, type %s", - direction == CHATTY_DIRECTION_OUT ? "out" : "in", type); - - if (direction == CHATTY_DIRECTION_OUT && uuid) { - JsonObject *data_unsigned; - const char *transaction_id; - guint n_items = 0, limit; - - if (root) - data_unsigned = matrix_utils_json_object_get_object (root, "unsigned"); - else - data_unsigned = matrix_utils_json_object_get_object (object, "unsigned"); - transaction_id = matrix_utils_json_object_get_string (data_unsigned, "transaction_id"); - - if (transaction_id) - n_items = g_list_model_get_n_items (G_LIST_MODEL (self->message_list)); - - if (n_items > 50) - limit = n_items - 50; - else - limit = 0; - - /* Note: i, limit and n_items are unsigned */ - for (guint i = n_items - 1; i + 1 > limit; i--) { - g_autoptr(ChattyMessage) msg = NULL; - const char *event_id; - - msg = g_list_model_get_item (G_LIST_MODEL (self->message_list), i); - event_id = g_object_get_data (G_OBJECT (msg), "event-id"); - - if (event_id && g_str_equal (event_id, transaction_id)) { - chatty_message_set_uid (msg, uuid); - chatty_history_add_message (self->history_db, CHATTY_CHAT (self), msg); - return; - } - } - } - - /* We should move to more precise time (ie, time in ms) as it is already provided */ - message = chatty_message_new (CHATTY_ITEM (buddy), body, uuid, ts, msg_type, direction, 0); - chatty_message_set_encrypted (message, encrypted); - - if (msg_type != CHATTY_MESSAGE_TEXT) - chat_handle_m_media (self, message, content, type, encrypted); - - g_list_store_append (self->message_list, message); - chatty_history_add_message (self->history_db, CHATTY_CHAT (self), message); -} - -static void -handle_m_room_encrypted (ChattyMaChat *self, - ChattyMaBuddy *buddy, - JsonObject *root) -{ - g_autofree char *plaintext = NULL; - JsonObject *content; - - g_assert (CHATTY_IS_MA_CHAT (self)); - - if (!root) - return; - - content = matrix_utils_json_object_get_object (root, "content"); - if (content) - plaintext = matrix_enc_handle_join_room_encrypted (self->matrix_enc, - self->room_id, - content); - - if (plaintext) { - g_autoptr(JsonObject) message = NULL; - - message = matrix_utils_string_to_json_object (plaintext); - matrix_add_message_from_data (self, buddy, root, message, TRUE); - } -} - -static gboolean -chat_resend_message (gpointer user_data) -{ - ChattyMaChat *self = user_data; - - g_assert (CHATTY_IS_MA_CHAT (self)); - - self->message_timeout_id = 0; - matrix_send_message_from_queue (self); - - return G_SOURCE_REMOVE; -} - -static void -ma_chat_send_message_cb (GObject *object, - GAsyncResult *result, - gpointer user_data) -{ - g_autoptr(ChattyMaChat) self = user_data; - g_autoptr(GError) error = NULL; - ChattyMessage *message; - - g_assert (CHATTY_IS_MA_CHAT (self)); - - self->is_sending_message = FALSE; - - matrix_api_send_message_finish (self->matrix_api, result, &error); - message = g_object_get_data (G_OBJECT (result), "message"); - - if (error) { - if (!g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CANCELLED)) - g_warning ("Error sending message: %s", error->message); - - if (g_error_matches (error, MATRIX_ERROR, M_LIMIT_EXCEEDED) && - !self->message_timeout_id) { - int timeout; - - timeout = GPOINTER_TO_INT (g_object_get_data (G_OBJECT (result), "retry-after")); - - if (!timeout) - timeout = 2000; - - self->message_timeout_id = g_timeout_add (timeout, chat_resend_message, self); - } - - g_queue_push_head (self->message_queue, g_object_ref (message)); - return; - } - - matrix_send_message_from_queue (self); -} - -static void -matrix_send_message_from_queue (ChattyMaChat *self) -{ - g_autoptr(ChattyMessage) message = NULL; - - g_assert (CHATTY_IS_MA_CHAT (self)); - - if (self->is_sending_message || - !self->message_queue || - !self->message_queue->length || - self->message_timeout_id) - return; - - message = g_queue_pop_head (self->message_queue); - self->is_sending_message = TRUE; - matrix_api_send_message_async (self->matrix_api, CHATTY_CHAT (self), - self->room_id, message, - ma_chat_send_message_cb, - g_object_ref (self)); -} - -static void -ma_chat_handle_ephemeral (ChattyMaChat *self, - JsonObject *root) -{ - JsonObject *object; - JsonArray *array; - - g_assert (CHATTY_IS_MA_CHAT (self)); - g_assert (root); - - array = matrix_utils_json_object_get_array (root, "events"); - - if (array) { - g_autoptr(GList) elements = NULL; - - elements = json_array_get_elements (array); - - for (GList *node = elements; node; node = node->next) { - const char *type; - - object = json_node_get_object (node->data); - type = matrix_utils_json_object_get_string (object, "type"); - object = matrix_utils_json_object_get_object (object, "content"); - - if (g_strcmp0 (type, "m.typing") == 0) { - array = matrix_utils_json_object_get_array (object, "user_ids"); - - if (array) { - const char *username, *name = NULL; - guint typing_count = 0; - gboolean buddy_typing = FALSE; - gboolean self_typing = FALSE; - - typing_count = json_array_get_length (array); - buddy_typing = typing_count >= 2; - - /* Handle the first item so that we don’t have to - handle buddy_typing in the loop */ - username = matrix_api_get_username (self->matrix_api); - if (typing_count) - name = json_array_get_string_element (array, 0); - - if (g_strcmp0 (name, username) == 0) - self_typing = TRUE; - else if (typing_count) - buddy_typing = TRUE; - - /* Check if the server says we are typing too */ - for (guint i = 0; !self_typing && i < typing_count; i++) - if (g_str_equal (json_array_get_string_element (array, i), username)) - self_typing = TRUE; - - if (self->self_typing != self_typing) { - self->self_typing = self_typing; - self->self_typing_set_time = g_get_monotonic_time (); - } - - if (self->buddy_typing != buddy_typing) { - self->buddy_typing = buddy_typing; - g_object_notify (G_OBJECT (self), "buddy-typing"); - } - } - } - } - } -} - -static void -upload_out_group_key_cb (GObject *obj, - GAsyncResult *result, - gpointer user_data) -{ - ChattyMaChat *self = user_data; - g_autoptr(GError) error = NULL; - - g_assert (CHATTY_IS_MA_CHAT (self)); - - self->claiming_keys = FALSE; - if (matrix_api_upload_group_keys_finish (self->matrix_api, result, &error)) - self->keys_claimed = TRUE; - - if (error) { - if (!g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CANCELLED)) - g_warning ("error uploading group keys: %s", error->message); - return; - } - - matrix_send_message_from_queue (self); -} - -static void -claim_key_cb (GObject *obj, - GAsyncResult *result, - gpointer user_data) -{ - ChattyMaChat *self = user_data; - g_autoptr(JsonObject) root = NULL; - g_autoptr(GList) members = NULL; - g_autoptr(GError) error = NULL; - JsonObject *object; - - g_assert (CHATTY_IS_MA_CHAT (self)); - - root = matrix_api_claim_keys_finish (self->matrix_api, result, &error); - - if (error) { - self->claiming_keys = FALSE; - if (!g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CANCELLED)) - g_warning ("error: %s", error->message); - return; - } - - object = matrix_utils_json_object_get_object (root, "one_time_keys"); - if (object) - members = json_object_get_members (object); - - for (GList *member = members; member; member = member->next) { - ChattyMaBuddy *buddy; - JsonObject *keys; - - buddy = ma_chat_find_buddy (self, G_LIST_MODEL (self->buddy_list), - member->data, NULL); - - if (!buddy) { - g_warning ("‘%s’ not found in buddy list", (char *)member->data); - continue; - } - - keys = matrix_utils_json_object_get_object (object, member->data); - chatty_ma_buddy_add_one_time_keys (buddy, keys); - } - - matrix_api_upload_group_keys_async (self->matrix_api, - self->room_id, - G_LIST_MODEL (self->buddy_list), - upload_out_group_key_cb, - self); -} - -static void -query_key_cb (GObject *obj, - GAsyncResult *result, - gpointer user_data) -{ - ChattyMaChat *self = user_data; - g_autoptr(JsonObject) root = NULL; - g_autoptr(GList) members = NULL; - g_autoptr(GError) error = NULL; - JsonObject *object; - - g_assert (CHATTY_IS_MA_CHAT (self)); - g_return_if_fail (!self->keys_claimed); - - root = matrix_api_query_keys_finish (self->matrix_api, result, &error); - - if (error) { - self->claiming_keys = FALSE; - if (!g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CANCELLED)) - g_warning ("error: %s", error->message); - return; - } - - object = matrix_utils_json_object_get_object (root, "device_keys"); - if (object) - members = json_object_get_members (object); - - /* TODO: avoid blocked devices (once we implement blocking) */ - for (GList *member = members; member; member = member->next) { - ChattyMaBuddy *buddy; - JsonObject *device; - - buddy = ma_chat_find_buddy (self, G_LIST_MODEL (self->buddy_list), - member->data, NULL); - - if (!buddy) { - g_warning ("‘%s’ not found in buddy list", (char *)member->data); - continue; - } - - device = matrix_utils_json_object_get_object (object, member->data); - chatty_ma_buddy_add_devices (buddy, device); - } - - matrix_api_claim_keys_async (self->matrix_api, - G_LIST_MODEL (self->buddy_list), - claim_key_cb, self); -} - -static void -get_chat_users_cb (GObject *obj, - GAsyncResult *result, - gpointer user_data) -{ - g_autoptr(ChattyMaChat) self = user_data; - g_autoptr(JsonObject) object = NULL; - g_autoptr(GList) members = NULL; - g_autoptr(GError) error = NULL; - JsonObject *joined; - - g_assert (CHATTY_IS_MA_CHAT (self)); - - object = matrix_api_get_room_users_finish (self->matrix_api, result, &error); - - self->user_list_loading = FALSE; - self->user_list_loaded = !error; - - if (error) { - if (!g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CANCELLED)) - g_warning ("Error loading chat members: %s", error->message); - - return; - } - - joined = matrix_utils_json_object_get_object (object, "joined"); - members = json_object_get_members (joined); - - for (GList *member = members; member; member = member->next) { - ChattyMaBuddy *buddy; - JsonObject *data; - const char *name; - - buddy = ma_chat_find_buddy (self, G_LIST_MODEL (self->buddy_list), member->data, NULL); - if (!buddy) - buddy = ma_chat_add_buddy (self, self->buddy_list, member->data); - - data = json_object_get_object_member (joined, member->data); - name = matrix_utils_json_object_get_string (data, "display_name"); - chatty_item_set_name (CHATTY_ITEM (buddy), name); - }; - - self->claiming_keys = TRUE; - matrix_api_query_keys_async (self->matrix_api, - G_LIST_MODEL (self->buddy_list), - NULL, query_key_cb, self); -} - -#if 0 -static void -get_room_state_cb (GObject *obj, - GAsyncResult *result, - gpointer user_data) -{ - ChattyMaChat *self = user_data; - g_autoptr(GError) error = NULL; - g_autoptr(JsonArray) array = NULL; - - g_assert (CHATTY_IS_MA_CHAT (self)); - - array = matrix_api_get_room_state_finish (self->matrix_api, result, &error); - self->state_is_syncing = FALSE; - - CHATTY_TRACE_MSG ("Got room state, room: %s (%s), success: %d", - self->room_id, chatty_item_get_name (CHATTY_ITEM (self)), !error); - if (error) { - if (!g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CANCELLED)) - g_warning ("error: %s", error->message); - return; - } - - if (json_array_get_length (array) == 0) - return; - - self->state_is_sync = TRUE; - for (guint i = 0; i < json_array_get_length (array); i++) { - JsonObject *object; - const char *type; - - object = json_array_get_object_element (array, i); - type = matrix_utils_json_object_get_string (object, "type"); - - if (!type || !*type) - continue; - - if (g_str_equal (type, "m.room.member")) - handle_m_room_member (self, object, self->buddy_list); - else if (g_str_equal (type, "m.room.name")) - handle_m_room_name (self, object); - else if (g_str_equal (type, "m.room.encryption")) - handle_m_room_encryption (self, object); - else if (g_str_equal (type, "m.room.avatar")) - handle_m_room_avatar (self, object); - /* TODO */ - /* else if (g_str_equal (type, "m.room.power_levels")) */ - /* handle_m_room_power_levels (self, object); */ - /* else if (g_str_equal (type, "m.room.guest_access")) */ - /* handle_m_room_guest_access (self, object); */ - /* else if (g_str_equal (type, "m.room.create")) */ - /* handle_m_room_create (self, object); */ - /* else if (g_str_equal (type, "m.room.history_visibility")) */ - /* handle_m_room_history_visibility (self, object); */ - /* else if (g_str_equal (type, "m.room.join_rules")) */ - /* handle_m_room_join_rules (self, object); */ - /* else */ - /* g_warn_if_reached (); */ - } - - /* Clear pointer so that it’s generated when requested */ - g_clear_pointer (&self->generated_name, g_free); - g_object_notify (G_OBJECT (self), "name"); - - if (self->message_queue->length > 0) { - if (!self->claiming_keys) - matrix_api_query_keys_async (self->matrix_api, - G_LIST_MODEL (self->buddy_list), - NULL, query_key_cb, self); - else if (self->keys_claimed) - matrix_send_message_from_queue (self); - } - - g_object_notify (G_OBJECT (self), "name"); - g_signal_emit_by_name (self, "avatar-changed"); - - chatty_history_update_chat (self->history_db, CHATTY_CHAT (self)); -} -#endif - -static void -get_room_name_cb (GObject *obj, - GAsyncResult *result, - gpointer user_data) -{ - ChattyMaChat *self = user_data; - g_autoptr(GError) error = NULL; - g_autoptr(JsonObject) object = NULL; - const char *name; - - g_assert (CHATTY_IS_MA_CHAT (self)); - self->state_is_sync = TRUE; - self->state_is_syncing = FALSE; - - object = matrix_api_get_room_name_finish (self->matrix_api, result, &error); - - if (error) { - if (!g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CANCELLED) && - !g_error_matches (error, MATRIX_ERROR, M_NOT_FOUND)) - g_warning ("error getting room name: %s", error->message); - return; - } - - name = matrix_utils_json_object_get_string (object, "name"); - g_free (self->room_name); - self->room_name = g_strdup (name); - chatty_history_update_chat (self->history_db, CHATTY_CHAT (self)); - - CHATTY_TRACE (self->room_id, "Got room name, room:"); - - g_object_notify (G_OBJECT (self), "name"); -} - -static void -get_room_encryption_cb (GObject *obj, - GAsyncResult *result, - gpointer user_data) -{ - ChattyMaChat *self = user_data; - g_autoptr(GError) error = NULL; - - g_assert (CHATTY_IS_MA_CHAT (self)); - - self->encryption = matrix_api_get_room_encryption_finish (self->matrix_api, result, &error); - - CHATTY_TRACE (self->room_id, "Got room encryption state: encrypted: %d, room:", - !!self->encryption); - g_object_notify (G_OBJECT (self), "encrypt"); - - CHATTY_TRACE (self->room_id, "Getting room name for"); - matrix_api_get_room_name_async (self->matrix_api, - self->room_id, - get_room_name_cb, - self); -} - -static void -parse_chat_array (ChattyMaChat *self, - JsonArray *array) -{ - g_autoptr(GList) events = NULL; - - if (!array) - return; - - events = json_array_get_elements (array); - CHATTY_TRACE_MSG ("Got %u events", json_array_get_length (array)); - - for (GList *event = events; event; event = event->next) { - ChattyMaBuddy *buddy; - JsonObject *object; - const char *type, *sender; - - object = json_node_get_object (event->data); - type = matrix_utils_json_object_get_string (object, "type"); - sender = matrix_utils_json_object_get_string (object, "sender"); - - if (!type || !*type || !sender || !*sender) - continue; - - buddy = ma_chat_find_buddy (self, G_LIST_MODEL (self->buddy_list), sender, NULL); - - if (!buddy) - buddy = ma_chat_add_buddy (self, self->buddy_list, sender); - - if (!self->self_buddy && - g_strcmp0 (sender, chatty_item_get_username (CHATTY_ITEM (self))) == 0) - g_set_object (&self->self_buddy, buddy); - - if (g_str_equal (type, "m.room.name")) { - handle_m_room_name (self, object); - } else if (g_str_equal (type, "m.room.message")) { - matrix_add_message_from_data (self, buddy, NULL, object, FALSE); - } else if (g_str_equal (type, "m.room.encryption")) { - handle_m_room_encryption (self, object); - } else if (g_str_equal (type, "m.room.encrypted")) { - handle_m_room_encrypted (self, buddy, object); - } else if (g_str_equal (type, "m.room.member")) { - handle_m_room_member (self, object); - } - } - - if (!self->room_name_loaded) { - self->room_name_loaded = TRUE; - g_object_notify (G_OBJECT (self), "name"); - g_signal_emit_by_name (self, "avatar-changed"); - } -} - -static void -matrix_chat_set_json_data (ChattyMaChat *self, - JsonObject *object) -{ - g_assert (CHATTY_IS_MA_CHAT (self)); - - g_clear_pointer (&self->json_data, json_object_unref); - self->json_data = object; - - if (!object) - return; - - object = matrix_utils_json_object_get_object (self->json_data, "ephemeral"); - if (object) - ma_chat_handle_ephemeral (self, object); - - object = matrix_utils_json_object_get_object (self->json_data, "unread_notifications"); - if (object) - self->highlight_count = matrix_utils_json_object_get_int (object, "highlight_count"); - - object = matrix_utils_json_object_get_object (self->json_data, "timeline"); - parse_chat_array (self, matrix_utils_json_object_get_array (object, "events")); - - if (object && matrix_utils_json_object_get_bool (object, "limited")) - chatty_ma_chat_set_prev_batch (self, g_strdup (matrix_utils_json_object_get_string (object, "prev_batch"))); - - object = matrix_utils_json_object_get_object (self->json_data, "unread_notifications"); - if (object) { - guint old_count; - - old_count = self->unread_count; - self->unread_count = matrix_utils_json_object_get_int (object, "notification_count"); - chatty_chat_show_notification (CHATTY_CHAT (self), NULL); - g_signal_emit_by_name (self, "changed", 0); - - /* Reset notification state on new messages */ - if (self->unread_count > old_count) - self->notification_shown = FALSE; - } -} - -static void -get_messages_cb (GObject *obj, - GAsyncResult *result, - gpointer user_data) -{ - ChattyMaChat *self; - g_autoptr(GTask) task = user_data; - g_autoptr(JsonObject) root = NULL; - g_autoptr(GError) error = NULL; - - g_assert (G_IS_TASK (task)); - - self = g_task_get_source_object (task); - g_assert (CHATTY_IS_MA_CHAT (self)); - - root = matrix_api_load_prev_batch_finish (self->matrix_api, result, &error); - self->prev_batch_loading = FALSE; - self->history_is_loading = FALSE; - g_object_notify (G_OBJECT (self), "loading-history"); - - if (error) { - if (!g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CANCELLED)) - g_warning ("error: %s", error->message); - g_task_return_boolean (task, FALSE); - return; - } - - parse_chat_array (self, matrix_utils_json_object_get_array (root, "chunk")); - - /* If start and end are same, we no longer have events to load */ - if (g_strcmp0 (matrix_utils_json_object_get_string (root, "end"), - matrix_utils_json_object_get_string (root, "start")) == 0) - chatty_ma_chat_set_prev_batch (self, NULL); - else - chatty_ma_chat_set_prev_batch (self, g_strdup (matrix_utils_json_object_get_string (root, "end"))); - - if (self->prev_batch) { - GListModel *model; - guint message_count; - - model = G_LIST_MODEL (chatty_chat_get_messages (CHATTY_CHAT (self))); - message_count = GPOINTER_TO_UINT(g_object_get_data (G_OBJECT (task), "count")); - - /* Load more items if no message was loaded. This can happen - * when no event in the loaded events was a room message. - */ - if (chatty_chat_get_encryption (CHATTY_CHAT (self)) != CHATTY_ENCRYPTION_ENABLED && - g_list_model_get_n_items (model) == message_count) - chatty_chat_load_past_messages (CHATTY_CHAT (self), -1); - } -} - -static void -db_room_room_cb (GObject *object, - GAsyncResult *result, - gpointer user_data) -{ - ChattyMaChat *self; - g_autoptr(GTask) task = user_data; - g_autoptr(GError) error = NULL; - - g_assert (G_IS_TASK (task)); - - self = g_task_get_source_object (task); - g_assert (CHATTY_IS_MA_CHAT (self)); - - g_object_freeze_notify (G_OBJECT (self)); - - self->room_db_loaded = TRUE; - self->history_is_loading = FALSE; - g_object_notify (G_OBJECT (self), "loading-history"); - - g_free (self->prev_batch); - self->prev_batch = matrix_db_load_room_finish (self->matrix_db, result, &error); - CHATTY_TRACE (self->room_id, "Load from db, success: %d, has prev-batch: %d, chat:", - !error, !!self->prev_batch); - - if (error) - g_warning ("Error loading prev batch: %s", error->message); - - if (self->prev_batch) { - self->history_is_loading = TRUE; - g_object_notify (G_OBJECT (self), "loading-history"); - matrix_api_load_prev_batch_async (self->matrix_api, - self->room_id, - self->prev_batch, - self->last_batch, - get_messages_cb, - g_steal_pointer (&task)); + if (!error) { + g_set_object (&self->avatar, pixbuf); + g_signal_emit_by_name (self, "avatar-changed"); } - g_object_thaw_notify (G_OBJECT (self)); + self->avatar_is_loading = FALSE; } static void -ma_chat_load_db_messages_cb (GObject *object, - GAsyncResult *result, - gpointer user_data) +ma_chat_get_past_events_cb (GObject *object, + GAsyncResult *result, + gpointer user_data) { - ChattyMaChat *self; - g_autoptr(GTask) task = user_data; - g_autoptr(GPtrArray) messages = NULL; + g_autoptr(ChattyMaChat) self = user_data; + /* g_autoptr(GPtrArray) events = NULL; */ g_autoptr(GError) error = NULL; - g_assert (G_IS_TASK (task)); - - self = g_task_get_source_object (task); g_assert (CHATTY_IS_MA_CHAT (self)); - g_object_freeze_notify (G_OBJECT (self)); + cm_room_load_past_events_finish (CM_ROOM (object), result, &error); - messages = chatty_history_get_messages_finish (self->history_db, result, &error); self->history_is_loading = FALSE; g_object_notify (G_OBJECT (self), "loading-history"); - if (error && !g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CANCELLED)) - g_warning ("Error fetching messages from db: %s,", error->message); + if (error) + g_warning ("Error: %s", error->message); +} + +static gboolean +chatty_ma_chat_is_im (ChattyChat *chat) +{ + return TRUE; +} - CHATTY_TRACE_MSG ("Messages loaded from db: %u", !messages ? 0 : messages->len); +static ChattyChatState +chatty_ma_chat_get_chat_state (ChattyChat *chat) +{ + ChattyMaChat *self = (ChattyMaChat *)chat; - if (messages && messages->len) { - g_list_store_splice (self->message_list, 0, 0, messages->pdata, messages->len); - chatty_chat_show_notification (CHATTY_CHAT (self), NULL); - g_signal_emit_by_name (self, "changed", 0); - g_task_return_boolean (task, TRUE); - } else if (!messages && self->prev_batch) { - self->history_is_loading = TRUE; - g_object_notify (G_OBJECT (self), "loading-history"); - matrix_api_load_prev_batch_async (self->matrix_api, - self->room_id, - self->prev_batch, - self->last_batch, - get_messages_cb, - g_steal_pointer (&task)); - } else if (!self->room_db_loaded && - matrix_api_get_device_id (self->matrix_api)) { - self->history_is_loading = TRUE; - g_object_notify (G_OBJECT (self), "loading-history"); - matrix_db_load_room_async (self->matrix_db, self->account, - matrix_api_get_device_id (self->matrix_api), - self->room_id, - db_room_room_cb, - g_steal_pointer (&task)); - } + g_assert (CHATTY_IS_MA_CHAT (self)); + + if (cm_room_get_status (self->cm_room) == CM_STATUS_INVITE) + return CHATTY_CHAT_INVITED; - g_object_thaw_notify (G_OBJECT (self)); + return CHATTY_CHAT_JOINED; } static gboolean -chatty_ma_chat_is_im (ChattyChat *chat) +chatty_ma_chat_has_file_upload (ChattyChat *chat) { return TRUE; } @@ -1322,31 +202,18 @@ chatty_ma_chat_real_past_messages (ChattyChat *chat, int count) { ChattyMaChat *self = (ChattyMaChat *)chat; - GListModel *model; - GTask *task; - guint n_items; g_assert (CHATTY_IS_MA_CHAT (self)); g_assert (count > 0); - if (self->history_is_loading) - return; - CHATTY_TRACE (self->room_id, "Loading %d past messages from", count); self->history_is_loading = TRUE; g_object_notify (G_OBJECT (self), "loading-history"); - model = chatty_chat_get_messages (chat); - n_items = g_list_model_get_n_items (model); - - task = g_task_new (self, NULL, NULL, NULL); - g_object_set_data (G_OBJECT (task), "count", GUINT_TO_POINTER (n_items)); - - chatty_history_get_messages_async (self->history_db, chat, - g_list_model_get_item (model, 0), - count, ma_chat_load_db_messages_cb, - task); + cm_room_load_past_events_async (self->cm_room, + ma_chat_get_past_events_cb, + g_object_ref (self)); } static gboolean @@ -1366,7 +233,7 @@ chatty_ma_chat_get_messages (ChattyChat *chat) g_assert (CHATTY_IS_MA_CHAT (self)); - return G_LIST_MODEL (self->sorted_message_list); + return G_LIST_MODEL (self->filtered_event_list); } static ChattyAccount * @@ -1387,7 +254,7 @@ chatty_ma_chat_get_encryption (ChattyChat *chat) g_assert (CHATTY_IS_MA_CHAT (self)); - if (self->encryption) + if (self->cm_room && cm_room_is_encrypted (self->cm_room)) return CHATTY_ENCRYPTION_ENABLED; return CHATTY_ENCRYPTION_DISABLED; @@ -1408,16 +275,13 @@ ma_chat_set_encryption_cb (GObject *object, self = g_task_get_source_object (task); g_assert (CHATTY_IS_MA_CHAT (self)); - ret = matrix_api_set_room_encryption_finish (self->matrix_api, result, &error); + ret = cm_room_enable_encryption_finish (self->cm_room, result, &error); CHATTY_DEBUG (self->room_id, "Setting encryption, success: %d, room:", ret); if (error) { g_warning ("Failed to set encryption: %s", error->message); g_task_return_error (task, error); } else { - if (ret) - self->encryption = g_strdup ("encrypted"); - g_object_notify (G_OBJECT (self), "encrypt"); g_task_return_boolean (task, ret); } @@ -1433,7 +297,7 @@ chatty_ma_chat_set_encryption_async (ChattyChat *chat, g_autoptr(GTask) task = NULL; g_assert (CHATTY_IS_MA_CHAT (self)); - g_assert (self->matrix_api); + g_assert (self->cm_client); task = g_task_new (self, NULL, callback, user_data); @@ -1445,18 +309,80 @@ chatty_ma_chat_set_encryption_async (ChattyChat *chat, return; } - if (enable && - chatty_chat_get_encryption (chat) == CHATTY_ENCRYPTION_ENABLED) { - g_debug ("Encryption is already enabled"); - g_task_return_boolean (task, TRUE); + CHATTY_DEBUG (self->room_id, "Setting encryption for room:"); + cm_room_enable_encryption_async (self->cm_room, NULL, + ma_chat_set_encryption_cb, + g_steal_pointer (&task)); +} - return; - } +static void +ma_chat_accept_invite_cb (GObject *object, + GAsyncResult *result, + gpointer user_data) +{ + ChattyMaChat *self; + g_autoptr(GTask) task = user_data; + GError *error = NULL; + gboolean success; - CHATTY_DEBUG (self->room_id, "Setting encryption for room:"); - matrix_api_set_room_encryption_async (self->matrix_api, self->room_id, - ma_chat_set_encryption_cb, - g_steal_pointer (&task)); + self = g_task_get_source_object (task); + success = cm_room_accept_invite_finish (self->cm_room, result, &error); + + if (error) + g_task_return_error (task, error); + else + g_task_return_boolean (task, success); +} + +static void +chatty_ma_chat_accept_invite_async (ChattyChat *chat, + GAsyncReadyCallback callback, + gpointer user_data) +{ + ChattyMaChat *self = (ChattyMaChat *)chat; + GTask *task; + + g_assert (CHATTY_IS_MA_CHAT (chat)); + + task = g_task_new (self, NULL, callback, user_data); + cm_room_accept_invite_async (self->cm_room, NULL, + ma_chat_accept_invite_cb, + task); +} + +static void +ma_chat_reject_invite_cb (GObject *object, + GAsyncResult *result, + gpointer user_data) +{ + ChattyMaChat *self; + g_autoptr(GTask) task = user_data; + g_autoptr(GError) error = NULL; + gboolean success; + + self = g_task_get_source_object (task); + success = cm_room_reject_invite_finish (self->cm_room, result, &error); + + if (error) + g_task_return_error (task, error); + else + g_task_return_boolean (task, success); +} + +static void +chatty_ma_chat_reject_invite_async (ChattyChat *chat, + GAsyncReadyCallback callback, + gpointer user_data) +{ + ChattyMaChat *self = (ChattyMaChat *)chat; + GTask *task; + + g_assert (CHATTY_IS_MA_CHAT (chat)); + + task = g_task_new (self, NULL, callback, user_data); + cm_room_reject_invite_async (self->cm_room, NULL, + ma_chat_reject_invite_cb, + task); } static const char * @@ -1469,7 +395,7 @@ chatty_ma_chat_get_last_message (ChattyChat *chat) g_assert (CHATTY_IS_MA_CHAT (self)); - model = G_LIST_MODEL (self->message_list); + model = G_LIST_MODEL (self->filtered_event_list); n_items = g_list_model_get_n_items (model); if (n_items == 0) @@ -1487,7 +413,7 @@ chatty_ma_chat_get_unread_count (ChattyChat *chat) g_assert (CHATTY_IS_MA_CHAT (self)); - return self->unread_count; + return cm_room_get_unread_notification_counts (self->cm_room); } static void @@ -1500,8 +426,7 @@ chat_set_read_marker_cb (GObject *object, g_assert (CHATTY_IS_MA_CHAT (self)); - if (matrix_api_set_read_marker_finish (self->matrix_api, result, &error)) { - self->unread_count = 0; + if (cm_room_set_read_marker_finish (self->cm_room, result, &error)) { g_signal_emit_by_name (self, "changed", 0); } else if (error && !g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CANCELLED)) g_warning ("Error updating read marker: %s", error->message); @@ -1515,27 +440,49 @@ chatty_ma_chat_set_unread_count (ChattyChat *chat, g_assert (CHATTY_IS_MA_CHAT (self)); - if (self->unread_count == unread_count) - return; - if (unread_count == 0) { - g_autoptr(ChattyMessage) message = NULL; + g_autoptr(CmEvent) event = NULL; GListModel *model; guint n_items; - model = G_LIST_MODEL (self->message_list); + model = cm_room_get_events_list (self->cm_room); n_items = g_list_model_get_n_items (model); if (n_items == 0) return; - message = g_list_model_get_item (model, n_items - 1); - matrix_api_set_read_marker_async (self->matrix_api, self->room_id, message, - chat_set_read_marker_cb, self); + event = g_list_model_get_item (model, n_items - 1); + cm_room_set_read_marker_async (self->cm_room, + event, event, + chat_set_read_marker_cb, self); + } +} + +static void +ma_chage_send_message_cb (GObject *object, + GAsyncResult *result, + gpointer user_data) +{ + ChattyMaChat *self; + g_autoptr(GTask) task = user_data; + g_autofree char *event_id = NULL; + ChattyMessage *message; + + g_assert (G_IS_TASK (task)); + + self = g_task_get_source_object (task); + message = g_task_get_task_data (task); + g_assert (CHATTY_IS_MA_CHAT (self)); + g_assert (CHATTY_IS_MESSAGE (message)); + + event_id = cm_room_send_text_finish (self->cm_room, result, NULL); + + /* We add only failed to send messages. If sending succeeded, + * we shall get the same via the /sync responses */ + if (event_id) { + chatty_message_set_status (message, CHATTY_STATUS_SENT, 0); } else { - self->unread_count = unread_count; - chatty_chat_show_notification (CHATTY_CHAT (chat), NULL); - g_signal_emit_by_name (self, "changed", 0); + chatty_message_set_status (message, CHATTY_STATUS_SENDING_FAILED, 0); } } @@ -1546,30 +493,59 @@ chatty_ma_chat_send_message_async (ChattyChat *chat, gpointer user_data) { ChattyMaChat *self = (ChattyMaChat *)chat; + const char *event_id; + GList *files = NULL; + GFile *file = NULL; + GTask *task; g_assert (CHATTY_IS_MA_CHAT (self)); g_assert (CHATTY_IS_MESSAGE (message)); - chatty_message_set_user (message, CHATTY_ITEM (self->self_buddy)); chatty_message_set_status (message, CHATTY_STATUS_SENDING, 0); + task = g_task_new (self, NULL, callback, user_data); + g_task_set_task_data (task, g_object_ref (message), g_object_unref); g_list_store_append (self->message_list, message); - g_queue_push_tail (self->message_queue, g_object_ref (message)); - - if (chatty_chat_get_encryption (chat) != CHATTY_ENCRYPTION_ENABLED || - self->keys_claimed) - matrix_send_message_from_queue (self); - else if (!self->user_list_loaded && !self->user_list_loading) { - self->user_list_loading = TRUE; - matrix_api_get_room_users_async (self->matrix_api, self->room_id, - get_chat_users_cb, - g_object_ref (self)); - } else if (!self->state_is_syncing && !self->claiming_keys) { - self->claiming_keys = TRUE; - matrix_api_query_keys_async (self->matrix_api, - G_LIST_MODEL (self->buddy_list), - NULL, query_key_cb, self); + + files = chatty_message_get_files (message); + + if (files) { + ChattyFileInfo *file_info; + file_info = files->data; + + if (file_info && file_info->path) + file = g_file_new_for_path (file_info->path); + } + + if (file) + event_id = cm_room_send_file_async (self->cm_room, file, NULL, + NULL, NULL, NULL, + ma_chage_send_message_cb, task); + else + event_id = cm_room_send_text_async (self->cm_room, + chatty_message_get_text (message), + NULL, + ma_chage_send_message_cb, task); + g_object_set_data_full (G_OBJECT (message), "event-id", g_strdup (event_id), g_free); +} + +static void +ma_chat_download_cb (GObject *object, + GAsyncResult *result, + gpointer user_data) +{ + ChattyMaChat *self; + g_autoptr(GTask) task = user_data; + g_autoptr(GInputStream) istream = NULL; + GError *error = NULL; + + g_assert (G_IS_TASK (task)); + + self = g_task_get_source_object (task); + g_assert (CHATTY_IS_MA_CHAT (self)); + + istream = chatty_message_get_file_stream_finish (CHATTY_MESSAGE (object), result, &error); } static void @@ -1578,15 +554,38 @@ chatty_ma_chat_get_files_async (ChattyChat *chat, GAsyncReadyCallback callback, gpointer user_data) { + g_autoptr(GTask) task = NULL; ChattyMaChat *self = CHATTY_MA_CHAT (chat); + ChattyFileInfo *file; GList *files; - g_assert (CHATTY_IS_MA_CHAT (self)); g_assert (CHATTY_IS_MESSAGE (message)); + task = g_task_new (self, NULL, callback, user_data); + g_object_set_data_full (G_OBJECT (task), "message", g_object_ref (message), g_object_unref); + files = chatty_message_get_files (message); - matrix_api_get_file_async (self->matrix_api, message, files->data, NULL, NULL, - ma_chat_download_cb, self); + if (!files) + { + g_task_return_new_error (task, G_IO_ERROR, G_IO_ERROR_FAILED, + "No file found in message"); + return; + } + + + file = files->data; + if (file->status != CHATTY_FILE_UNKNOWN) + { + g_task_return_new_error (task, G_IO_ERROR, G_IO_ERROR_FAILED, + "File URL missing or invalid file state"); + return; + } + + g_object_set_data (G_OBJECT (task), "file", file); + + chatty_message_get_file_stream_async (message, NULL, CHATTY_PROTOCOL_MATRIX, NULL, + ma_chat_download_cb, + g_steal_pointer (&task)); } static gboolean @@ -1607,13 +606,7 @@ chatty_ma_chat_set_typing (ChattyChat *chat, g_assert (CHATTY_IS_MA_CHAT (self)); - if (self->self_typing == is_typing && - (g_get_monotonic_time () - self->self_typing_set_time) < G_TIME_SPAN_SECOND * 5) - return; - - self->self_typing = is_typing; - self->self_typing_set_time = g_get_monotonic_time (); - matrix_api_set_typing (self->matrix_api, self->room_id, is_typing); + cm_room_set_typing_notice_async (self->cm_room, is_typing, NULL, NULL, NULL); } static void @@ -1634,20 +627,23 @@ static const char * chatty_ma_chat_get_name (ChattyItem *item) { ChattyMaChat *self = (ChattyMaChat *)item; + const char *name; g_assert (CHATTY_IS_MA_CHAT (self)); - if (self->room_name) - return self->room_name; + name = cm_room_get_name (self->cm_room); - if (!self->room_name_loaded) - return self->room_id; + if (name && cm_room_get_past_name (self->cm_room)) { + g_free (self->room_name); + self->room_name = g_strdup_printf ("%s (was %s)", name, + cm_room_get_past_name (self->cm_room)); + } - if (!self->generated_name) - chatty_mat_chat_update_name (self); + if (self->room_name) + return self->room_name; - if (self->generated_name) - return self->generated_name; + if (name && *name) + return name; if (self->room_id) return self->room_id; @@ -1662,7 +658,7 @@ chatty_ma_chat_get_username (ChattyItem *item) g_assert (CHATTY_IS_MA_CHAT (self)); - return matrix_api_get_username (self->matrix_api); + return cm_client_get_user_id (self->cm_client); } static ChattyItemState @@ -1727,36 +723,11 @@ chatty_ma_chat_get_avatar (ChattyItem *item) return NULL; } -static void -chatty_ma_chat_set_property (GObject *object, - guint prop_id, - const GValue *value, - GParamSpec *pspec) -{ - ChattyMaChat *self = (ChattyMaChat *)object; - - switch (prop_id) - { - case PROP_JSON_DATA: - matrix_chat_set_json_data (self, g_value_dup_boxed (value)); - break; - - case PROP_ROOM_ID: - self->room_id = g_value_dup_string (value); - break; - - default: - G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); - } -} - static void chatty_ma_chat_finalize (GObject *object) { ChattyMaChat *self = (ChattyMaChat *)object; - g_clear_handle_id (&self->message_timeout_id, g_source_remove); - if (self->avatar_cancellable) g_cancellable_cancel (self->avatar_cancellable); g_clear_object (&self->avatar_cancellable); @@ -1764,26 +735,12 @@ chatty_ma_chat_finalize (GObject *object) g_list_store_remove_all (self->message_list); g_list_store_remove_all (self->buddy_list); g_clear_object (&self->message_list); - g_clear_object (&self->sorted_message_list); g_clear_object (&self->buddy_list); - g_clear_object (&self->matrix_api); - g_clear_object (&self->matrix_enc); - g_clear_object (&self->self_buddy); g_clear_pointer (&self->avatar_file, chatty_file_info_free); g_clear_object (&self->avatar); - g_clear_object (&self->matrix_db); - g_clear_object (&self->history_db); - - g_clear_pointer (&self->json_data, json_object_unref); - g_queue_free_full (self->message_queue, g_object_unref); - g_free (self->room_name); - g_free (self->generated_name); g_free (self->room_id); - g_free (self->encryption); - g_free (self->prev_batch); - g_free (self->last_batch); G_OBJECT_CLASS (chatty_ma_chat_parent_class)->finalize (object); } @@ -1795,7 +752,6 @@ chatty_ma_chat_class_init (ChattyMaChatClass *klass) ChattyItemClass *item_class = CHATTY_ITEM_CLASS (klass); ChattyChatClass *chat_class = CHATTY_CHAT_CLASS (klass); - object_class->set_property = chatty_ma_chat_set_property; object_class->finalize = chatty_ma_chat_finalize; item_class->get_name = chatty_ma_chat_get_name; @@ -1807,6 +763,8 @@ chatty_ma_chat_class_init (ChattyMaChatClass *klass) item_class->get_avatar = chatty_ma_chat_get_avatar; chat_class->is_im = chatty_ma_chat_is_im; + chat_class->get_chat_state = chatty_ma_chat_get_chat_state; + chat_class->has_file_upload = chatty_ma_chat_has_file_upload; chat_class->get_chat_name = chatty_ma_chat_get_chat_name; chat_class->load_past_messages = chatty_ma_chat_real_past_messages; chat_class->is_loading_history = chatty_ma_chat_is_loading_history; @@ -1814,6 +772,8 @@ chatty_ma_chat_class_init (ChattyMaChatClass *klass) chat_class->get_account = chatty_ma_chat_get_account; chat_class->get_encryption = chatty_ma_chat_get_encryption; chat_class->set_encryption_async = chatty_ma_chat_set_encryption_async; + chat_class->accept_invite_async = chatty_ma_chat_accept_invite_async; + chat_class->reject_invite_async = chatty_ma_chat_reject_invite_async; chat_class->get_last_message = chatty_ma_chat_get_last_message; chat_class->get_unread_count = chatty_ma_chat_get_unread_count; chat_class->set_unread_count = chatty_ma_chat_set_unread_count; @@ -1822,86 +782,173 @@ chatty_ma_chat_class_init (ChattyMaChatClass *klass) chat_class->get_buddy_typing = chatty_ma_chat_get_buddy_typing; chat_class->set_typing = chatty_ma_chat_set_typing; chat_class->show_notification = chatty_ma_chat_show_notification; - - properties[PROP_JSON_DATA] = - g_param_spec_boxed ("json-data", - "json-data", - "json-data for the room", - JSON_TYPE_OBJECT, - G_PARAM_WRITABLE | G_PARAM_STATIC_STRINGS); - - properties[PROP_ROOM_ID] = - g_param_spec_string ("room-id", - "json-data", - "json-data for the room", - NULL, - G_PARAM_WRITABLE | G_PARAM_STATIC_STRINGS | G_PARAM_CONSTRUCT_ONLY); - - g_object_class_install_properties (object_class, N_PROPS, properties); } static void chatty_ma_chat_init (ChattyMaChat *self) { - g_autoptr(GtkSorter) sorter = NULL; - - sorter = gtk_custom_sorter_new (sort_message, NULL, NULL); + g_autoptr(GtkFilter) filter = NULL; self->message_list = g_list_store_new (CHATTY_TYPE_MESSAGE); - self->sorted_message_list = gtk_sort_list_model_new (G_LIST_MODEL (self->message_list), sorter); + + filter = gtk_custom_filter_new ((GtkCustomFilterFunc)ma_chat_filter_event_list, + g_object_ref (self), + g_object_unref); + self->filtered_event_list = gtk_filter_list_model_new (G_LIST_MODEL (self->message_list), + filter); self->buddy_list = g_list_store_new (CHATTY_TYPE_MA_BUDDY); - self->message_queue = g_queue_new (); self->avatar_cancellable = g_cancellable_new (); } -ChattyMaChat * -chatty_ma_chat_new (const char *room_id, - const char *name, - ChattyFileInfo *avatar, - gboolean encrypted) +static void +ma_chat_name_changed_cb (ChattyMaChat *self) { - ChattyMaChat *self; + g_assert (CHATTY_IS_MA_CHAT (self)); + + g_object_notify (G_OBJECT (self), "name"); + g_signal_emit_by_name (self, "avatar-changed"); +} + +static void +ma_chat_encryption_changed_cb (ChattyMaChat *self) +{ + g_assert (CHATTY_IS_MA_CHAT (self)); + + g_object_notify (G_OBJECT (self), "encrypt"); +} + +static void +joined_members_changed_cb (ChattyMaChat *self, + guint position, + guint removed, + guint added, + GListModel *model) +{ + g_autoptr(GPtrArray) items = NULL; + + g_assert (CHATTY_IS_CHAT (self)); + g_assert (G_IS_LIST_MODEL (model)); + + for (guint i = position; i < position + added; i++) + { + g_autoptr(CmUser) user = NULL; + ChattyMaBuddy *buddy; + + if (!items) + items = g_ptr_array_new_with_free_func (g_object_unref); - g_return_val_if_fail (room_id && *room_id, NULL); + user = g_list_model_get_item (model, i); + buddy = chatty_ma_buddy_new_with_user (user); + g_ptr_array_add (items, buddy); + } + + g_list_store_splice (self->buddy_list, position, removed, + items ? items->pdata : NULL, added); +} + +static void +events_list_changed_cb (ChattyMaChat *self, + guint position, + guint removed, + guint added, + GListModel *model) +{ + g_autoptr(GPtrArray) items = NULL; + + g_assert (CHATTY_IS_CHAT (self)); + g_assert (G_IS_LIST_MODEL (model)); + + for (guint i = position; i < position + added; i++) + { + g_autoptr(CmEvent) event = NULL; + ChattyMessage *message; + + if (!items) + items = g_ptr_array_new_with_free_func (g_object_unref); + + event = g_list_model_get_item (model, i); + + if (CM_IS_ROOM_MESSAGE_EVENT (event) || + cm_event_is_encrypted (event)) { + g_autoptr(ChattyMaBuddy) user = NULL; + + user = ma_chat_find_cm_user (self, G_LIST_MODEL (self->buddy_list), + cm_event_get_sender (event), NULL, TRUE); + message = chatty_message_new_from_event (CHATTY_ITEM (user), event); + } else { + message = g_object_new (CHATTY_TYPE_MESSAGE, NULL); + } + + g_object_set_data_full (G_OBJECT (message), "cm-event", g_object_ref (event), g_object_unref); + g_ptr_array_add (items, message); + } + + g_list_store_splice (self->message_list, position, removed, + items ? items->pdata : NULL, added); + g_signal_emit_by_name (self, "changed", 0); +} - self = g_object_new (CHATTY_TYPE_MA_CHAT, - "room-id", room_id, NULL); - self->room_name = g_strdup (name); - self->avatar_file = avatar; - if (encrypted) - self->encryption = g_strdup ("encrypted"); +ChattyMaChat * +chatty_ma_chat_new_with_room (CmRoom *room) +{ + ChattyMaChat *self; + GListModel *members, *events; + + g_return_val_if_fail (CM_IS_ROOM (room), NULL); + + self = g_object_new (CHATTY_TYPE_MA_CHAT, NULL); + self->room_id = g_strdup (cm_room_get_id (room)); + self->cm_room = g_object_ref (room); + g_signal_connect_object (room, "notify::name", + G_CALLBACK (ma_chat_name_changed_cb), + self, G_CONNECT_SWAPPED); + g_signal_connect_object (room, "notify::encrypted", + G_CALLBACK (ma_chat_encryption_changed_cb), + self, G_CONNECT_SWAPPED); + + members = cm_room_get_joined_members (room); + g_signal_connect_object (members, "items-changed", + G_CALLBACK (joined_members_changed_cb), + self, G_CONNECT_SWAPPED); + joined_members_changed_cb (self, 0, 0, + g_list_model_get_n_items (members), + members); + + events = cm_room_get_events_list (room); + g_signal_connect_object (events, "items-changed", + G_CALLBACK (events_list_changed_cb), + self, G_CONNECT_SWAPPED); + events_list_changed_cb (self, 0, 0, + g_list_model_get_n_items (events), + events); return self; } -void -chatty_ma_chat_set_history_db (ChattyMaChat *self, - gpointer history_db) +CmRoom * +chatty_ma_chat_get_cm_room (ChattyMaChat *self) { - g_return_if_fail (CHATTY_IS_MA_CHAT (self)); - g_return_if_fail (CHATTY_IS_HISTORY (history_db)); - g_return_if_fail (!self->history_db); + g_return_val_if_fail (CHATTY_IS_MA_CHAT (self), FALSE); - self->history_db = g_object_ref (history_db); + return self->cm_room; } -void -chatty_ma_chat_set_matrix_db (ChattyMaChat *self, - gpointer matrix_db) +gboolean +chatty_ma_chat_can_set_encryption (ChattyMaChat *self) { - g_return_if_fail (CHATTY_IS_MA_CHAT (self)); - g_return_if_fail (MATRIX_IS_DB (matrix_db)); - g_return_if_fail (!self->matrix_db); + g_return_val_if_fail (CHATTY_IS_MA_CHAT (self), FALSE); + + if (!self->cm_room) + return FALSE; - self->matrix_db = g_object_ref (matrix_db); + return cm_room_self_has_power_for_event (self->cm_room, CM_M_ROOM_ENCRYPTION); } /** * chatty_ma_chat_set_data: * @self: a #ChattyMaChat * @account: A #ChattyMaAccount - * @api: A #MatrixApi - * @enc: A #MatrixEnc for E2E + * @client: A #CmClient * * Use this function to set internal data required * to connect to a matrix server. @@ -1909,25 +956,13 @@ chatty_ma_chat_set_matrix_db (ChattyMaChat *self, void chatty_ma_chat_set_data (ChattyMaChat *self, ChattyAccount *account, - gpointer api, - gpointer enc) + gpointer client) { g_return_if_fail (CHATTY_IS_MA_CHAT (self)); - g_return_if_fail (MATRIX_IS_API (api)); + g_return_if_fail (CM_IS_CLIENT (client)); g_set_weak_pointer (&self->account, account); - g_set_object (&self->matrix_api, api); - g_set_object (&self->matrix_enc, enc); - - if (!self->state_is_sync && !self->state_is_syncing && - self->matrix_api && self->matrix_enc) { - self->state_is_syncing = TRUE; - CHATTY_TRACE (self->room_id, "Getting encryption state for"); - matrix_api_get_room_encryption_async (self->matrix_api, - self->room_id, - get_room_encryption_cb, - self); - } + g_set_object (&self->cm_client, client); } gboolean @@ -1941,66 +976,3 @@ chatty_ma_chat_matches_id (ChattyMaChat *self, return g_strcmp0 (self->room_id, room_id) == 0; } - -static void -db_room_saved_cb (GObject *object, - GAsyncResult *result, - gpointer user_data) -{ - g_autoptr(ChattyMaChat) self = user_data; - - g_assert (CHATTY_IS_MA_CHAT (self)); - - self->saving_room_to_db = FALSE; -} - -void -chatty_ma_chat_set_prev_batch (ChattyMaChat *self, - char *prev_batch) -{ - g_return_if_fail (CHATTY_IS_MA_CHAT (self)); - - g_clear_pointer (&self->prev_batch, g_free); - self->prev_batch = prev_batch; - - if (self->saving_room_to_db) - return; - - self->saving_room_to_db = TRUE; - matrix_db_save_room_async (self->matrix_db, self->account, - matrix_api_get_device_id (self->matrix_api), - self->room_id, prev_batch, - db_room_saved_cb, g_object_ref (self)); -} - -/** - * chatty_ma_chat_set_last_batch: - * @self: A #ChattyMaChat - * @last_batch: A string representing time - * - * The batch which we have already loaded chat - * up to. When history is loaded from server, - * only items after @last_batch shall be loaded. - * This limits only loading history from server - * not from database. - */ -void -chatty_ma_chat_set_last_batch (ChattyMaChat *self, - const char *last_batch) -{ - g_return_if_fail (CHATTY_IS_MA_CHAT (self)); - - g_free (self->last_batch); - self->last_batch = g_strdup (last_batch); -} - -void -chatty_ma_chat_add_messages (ChattyMaChat *self, - GPtrArray *messages) -{ - g_return_if_fail (CHATTY_IS_MA_CHAT (self)); - - if (messages && messages->len) - g_list_store_splice (self->message_list, 0, 0, - messages->pdata, messages->len); -} diff --git a/src/matrix/chatty-ma-chat.h b/src/matrix/chatty-ma-chat.h index 1f6e147b55c88dd6c87cfcbbc484083f3aeebc1d..74f1d5c738fdf00d2b47dde4417ce768e47bbda9 100644 --- a/src/matrix/chatty-ma-chat.h +++ b/src/matrix/chatty-ma-chat.h @@ -12,12 +12,10 @@ #pragma once #include <glib-object.h> +#define CMATRIX_USE_EXPERIMENTAL_API +#include "cmatrix.h" -#include "chatty-item.h" -#include "chatty-account.h" -#include "chatty-message.h" #include "chatty-chat.h" -#include "chatty-enums.h" G_BEGIN_DECLS @@ -25,25 +23,13 @@ G_BEGIN_DECLS G_DECLARE_FINAL_TYPE (ChattyMaChat, chatty_ma_chat, CHATTY, MA_CHAT, ChattyChat) -ChattyMaChat *chatty_ma_chat_new (const char *room_id, - const char *name, - ChattyFileInfo *avatar, - gboolean encrypted); -void chatty_ma_chat_set_history_db (ChattyMaChat *self, - gpointer history_db); -void chatty_ma_chat_set_matrix_db (ChattyMaChat *self, - gpointer matrix_db); +ChattyMaChat *chatty_ma_chat_new_with_room (CmRoom *room); +CmRoom *chatty_ma_chat_get_cm_room (ChattyMaChat *self); +gboolean chatty_ma_chat_can_set_encryption (ChattyMaChat *self); void chatty_ma_chat_set_data (ChattyMaChat *self, ChattyAccount *account, - gpointer api, - gpointer enc); + gpointer client); gboolean chatty_ma_chat_matches_id (ChattyMaChat *self, const char *room_id); -void chatty_ma_chat_set_prev_batch (ChattyMaChat *self, - char *prev_batch); -void chatty_ma_chat_set_last_batch (ChattyMaChat *self, - const char *last_batch); -void chatty_ma_chat_add_messages (ChattyMaChat *self, - GPtrArray *messages); G_END_DECLS diff --git a/src/matrix/chatty-ma-key-chat.c b/src/matrix/chatty-ma-key-chat.c new file mode 100644 index 0000000000000000000000000000000000000000..b6e44a2c8d2c78f143d88f875037e9a8a846d028 --- /dev/null +++ b/src/matrix/chatty-ma-key-chat.c @@ -0,0 +1,310 @@ +/* -*- mode: c; c-basic-offset: 2; indent-tabs-mode: nil; -*- */ +/* chatty-ma-key-chat.c + * + * Copyright 2022 Purism SPC + * + * Author(s): + * Mohammed Sadiq <sadiq@sadiqpk.org> + * + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +#define G_LOG_DOMAIN "chatty-ma-key-chat" + +#ifdef HAVE_CONFIG_H +# include "config.h" +#endif + +#include <glib/gi18n.h> +#define CMATRIX_USE_EXPERIMENTAL_API +#include <cmatrix.h> + +#include "chatty-ma-buddy.h" +#include "chatty-ma-account.h" +#include "chatty-ma-key-chat.h" + +struct _ChattyMaKeyChat +{ + ChattyChat parent_instance; + + ChattyMaAccount *ma_account; + CmClient *cm_client; + ChattyMaBuddy *buddy; + + CmEvent *key_event; + ChattyChatState chat_state; +}; + +G_DEFINE_TYPE (ChattyMaKeyChat, chatty_ma_key_chat, CHATTY_TYPE_CHAT) + +static ChattyChatState +chatty_ma_key_chat_get_chat_state (ChattyChat *chat) +{ + ChattyMaKeyChat *self = (ChattyMaKeyChat *)chat; + + g_assert (CHATTY_IS_MA_KEY_CHAT (self)); + + return self->chat_state; +} + +static ChattyAccount * +chatty_ma_key_chat_get_account (ChattyChat *chat) +{ + ChattyMaKeyChat *self = (ChattyMaKeyChat *)chat; + + g_assert (CHATTY_IS_MA_KEY_CHAT (self)); + + return CHATTY_ACCOUNT (self->ma_account); +} + +static const char * +chatty_ma_key_chat_get_name (ChattyItem *item) +{ + return _("Key Verification"); +} + +static ChattyProtocol +chatty_ma_key_chat_get_protocols (ChattyItem *item) +{ + return CHATTY_PROTOCOL_MATRIX; +} + +static void +chatty_ma_key_chat_finalize (GObject *object) +{ + ChattyMaKeyChat *self = (ChattyMaKeyChat *)object; + + g_clear_object (&self->key_event); + g_clear_object (&self->cm_client); + g_clear_object (&self->buddy); + + G_OBJECT_CLASS (chatty_ma_key_chat_parent_class)->finalize (object); +} + +static void +chatty_ma_key_chat_class_init (ChattyMaKeyChatClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + ChattyItemClass *item_class = CHATTY_ITEM_CLASS (klass); + ChattyChatClass *chat_class = CHATTY_CHAT_CLASS (klass); + + object_class->finalize = chatty_ma_key_chat_finalize; + + item_class->get_name = chatty_ma_key_chat_get_name; + item_class->get_protocols = chatty_ma_key_chat_get_protocols; + + chat_class->get_chat_state = chatty_ma_key_chat_get_chat_state; + chat_class->get_account = chatty_ma_key_chat_get_account; +} + +static void +chatty_ma_key_chat_init (ChattyMaKeyChat *self) +{ + self->chat_state = CHATTY_CHAT_VERIFICATION; +} + +static void +key_event_updated_cb (ChattyMaKeyChat *self) +{ + g_assert (CHATTY_IS_MA_KEY_CHAT (self)); + + g_signal_emit_by_name (self, "changed", 0); +} + +ChattyMaKeyChat * +chatty_ma_key_chat_new (gpointer ma_account, + CmEvent *key_event) +{ + ChattyMaKeyChat *self; + CmEventType event_type; + + g_return_val_if_fail (CHATTY_MA_ACCOUNT (ma_account), NULL); + g_return_val_if_fail (CM_IS_EVENT (key_event), NULL); + + event_type = cm_event_get_m_type (key_event); + g_return_val_if_fail (event_type == CM_M_KEY_VERIFICATION_START || + event_type == CM_M_KEY_VERIFICATION_REQUEST, NULL); + + self = g_object_new (CHATTY_TYPE_MA_KEY_CHAT, NULL); + g_set_weak_pointer (&self->ma_account, ma_account); + self->cm_client = g_object_ref (chatty_ma_account_get_cm_client (ma_account)); + self->key_event = g_object_ref (key_event); + self->buddy = chatty_ma_buddy_new_with_user (cm_event_get_sender (key_event)); + + g_signal_connect_object (self->key_event, "updated", + G_CALLBACK (key_event_updated_cb), + self, + G_CONNECT_SWAPPED); + + return self; +} + +CmEvent * +chatty_ma_key_chat_get_event (ChattyMaKeyChat *self) +{ + g_return_val_if_fail (CHATTY_IS_MA_KEY_CHAT (self), NULL); + + return self->key_event; +} + +ChattyItem * +chatty_ma_key_chat_get_sender (ChattyMaKeyChat *self) +{ + g_return_val_if_fail (CHATTY_IS_MA_KEY_CHAT (self), NULL); + + return CHATTY_ITEM (self->buddy); +} + +GPtrArray * +chatty_ma_key_get_emoji (ChattyMaKeyChat *self) +{ + g_return_val_if_fail (CHATTY_IS_MA_KEY_CHAT (self), NULL); + + return g_object_get_data (G_OBJECT (self->key_event), "emoji"); +} + +/* decimal is an array of 3 */ +guint16 * +chatty_ma_key_get_decimal (ChattyMaKeyChat *self) +{ + g_return_val_if_fail (CHATTY_IS_MA_KEY_CHAT (self), NULL); + + return g_object_get_data (G_OBJECT (self->key_event), "decimal"); +} + +static void +ma_key_cancel_cb (GObject *object, + GAsyncResult *result, + gpointer user_data) +{ + ChattyMaKeyChat *self; + g_autoptr(GTask) task = user_data; + GError *error = NULL; + gboolean success; + + self = g_task_get_source_object (task); + success = cm_client_key_verification_cancel_finish (CM_CLIENT (object), result, &error); + + if (success) + self->chat_state = CHATTY_CHAT_LEFT; + + if (error) + g_task_return_error (task, error); + else + g_task_return_boolean (task, success); +} + +void +chatty_ma_key_cancel_async (ChattyMaKeyChat *self, + GAsyncReadyCallback callback, + gpointer user_data) +{ + GTask *task; + + g_return_if_fail (CHATTY_IS_MA_KEY_CHAT (self)); + + task = g_task_new (self, NULL, callback, user_data); + + cm_client_key_verification_cancel_async (self->cm_client, self->key_event, NULL, + ma_key_cancel_cb, + task); +} + +gboolean +chatty_ma_key_cancel_finish (ChattyMaKeyChat *self, + GAsyncResult *result, + GError **error) +{ + g_return_val_if_fail (CHATTY_IS_MA_KEY_CHAT (self), FALSE); + g_return_val_if_fail (G_IS_TASK (result), FALSE); + + return g_task_propagate_boolean (G_TASK (result), error); +} + +static void +ma_key_accept_cb (GObject *object, + GAsyncResult *result, + gpointer user_data) +{ + g_autoptr(GTask) task = user_data; + GError *error = NULL; + gboolean success; + + success = cm_client_key_verification_continue_finish (CM_CLIENT (object), result, &error); + + if (error) + g_task_return_error (task, error); + else + g_task_return_boolean (task, success); +} + +void +chatty_ma_key_accept_async (ChattyMaKeyChat *self, + GAsyncReadyCallback callback, + gpointer user_data) +{ + GTask *task; + + g_return_if_fail (CHATTY_IS_MA_KEY_CHAT (self)); + + task = g_task_new (self, NULL, callback, user_data); + + cm_client_key_verification_continue_async (self->cm_client, self->key_event, NULL, + ma_key_accept_cb, + task); +} + +gboolean +chatty_ma_key_accept_finish (ChattyMaKeyChat *self, + GAsyncResult *result, + GError **error) +{ + g_return_val_if_fail (CHATTY_IS_MA_KEY_CHAT (self), FALSE); + g_return_val_if_fail (G_IS_TASK (result), FALSE); + + return g_task_propagate_boolean (G_TASK (result), error); +} + +static void +ma_key_match_cb (GObject *object, + GAsyncResult *result, + gpointer user_data) +{ + g_autoptr(GTask) task = user_data; + GError *error = NULL; + gboolean success; + + success = cm_client_key_verification_match_finish (CM_CLIENT (object), result, &error); + + if (error) + g_task_return_error (task, error); + else + g_task_return_boolean (task, success); +} + +void +chatty_ma_key_match_async (ChattyMaKeyChat *self, + GAsyncReadyCallback callback, + gpointer user_data) +{ + GTask *task; + + g_return_if_fail (CHATTY_IS_MA_KEY_CHAT (self)); + + task = g_task_new (self, NULL, callback, user_data); + + cm_client_key_verification_match_async (self->cm_client, self->key_event, NULL, + ma_key_match_cb, + task); +} + +gboolean +chatty_ma_key_match_finish (ChattyMaKeyChat *self, + GAsyncResult *result, + GError **error) +{ + g_return_val_if_fail (CHATTY_IS_MA_KEY_CHAT (self), FALSE); + g_return_val_if_fail (G_IS_TASK (result), FALSE); + + return g_task_propagate_boolean (G_TASK (result), error); +} diff --git a/src/matrix/chatty-ma-key-chat.h b/src/matrix/chatty-ma-key-chat.h new file mode 100644 index 0000000000000000000000000000000000000000..1c5d2318bde3aeefece4fcc8bf7b782927646174 --- /dev/null +++ b/src/matrix/chatty-ma-key-chat.h @@ -0,0 +1,51 @@ +/* -*- mode: c; c-basic-offset: 2; indent-tabs-mode: nil; -*- */ +/* chatty-ma-key-chat.c + * + * Copyright 2022 Purism SPC + * + * Author(s): + * Mohammed Sadiq <sadiq@sadiqpk.org> + * + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +#pragma once + +#include <gtk/gtk.h> +#include <glib-object.h> + +#include "chatty-chat.h" + +G_BEGIN_DECLS + +#define CHATTY_TYPE_MA_KEY_CHAT (chatty_ma_key_chat_get_type ()) + +G_DECLARE_FINAL_TYPE (ChattyMaKeyChat, chatty_ma_key_chat, CHATTY, MA_KEY_CHAT, ChattyChat) + +ChattyMaKeyChat *chatty_ma_key_chat_new (gpointer ma_account, + CmEvent *key_event); +CmEvent *chatty_ma_key_chat_get_event (ChattyMaKeyChat *self); +ChattyItem *chatty_ma_key_chat_get_sender (ChattyMaKeyChat *self); +GPtrArray *chatty_ma_key_get_emoji (ChattyMaKeyChat *self); +guint16 *chatty_ma_key_get_decimal (ChattyMaKeyChat *self); +void chatty_ma_key_cancel_async (ChattyMaKeyChat *self, + GAsyncReadyCallback callback, + gpointer user_data); +gboolean chatty_ma_key_cancel_finish (ChattyMaKeyChat *self, + GAsyncResult *result, + GError **error); +void chatty_ma_key_accept_async (ChattyMaKeyChat *self, + GAsyncReadyCallback callback, + gpointer user_data); +gboolean chatty_ma_key_accept_finish (ChattyMaKeyChat *self, + GAsyncResult *result, + GError **error); +void chatty_ma_key_match_async (ChattyMaKeyChat *self, + GAsyncReadyCallback callback, + gpointer user_data); +gboolean chatty_ma_key_match_finish (ChattyMaKeyChat *self, + GAsyncResult *result, + GError **error); + +G_END_DECLS + diff --git a/src/matrix/chatty-matrix.c b/src/matrix/chatty-matrix.c index 25c2507105f979475826b95f0d1a90c9a40bb91c..6f997bd4a4495a0698601f5b732d217f24b4f537 100644 --- a/src/matrix/chatty-matrix.c +++ b/src/matrix/chatty-matrix.c @@ -16,13 +16,14 @@ #endif #include "contrib/gtk.h" +#define CMATRIX_USE_EXPERIMENTAL_API +#include "cmatrix.h" #include "chatty-secret-store.h" #include "chatty-settings.h" #include "chatty-ma-account.h" #include "chatty-ma-buddy.h" #include "chatty-ma-chat.h" -#include "matrix-db.h" #include "chatty-matrix.h" #include "chatty-log.h" @@ -35,65 +36,21 @@ struct _ChattyMatrix GListStore *list_of_chat_list; GtkFlattenListModel *chat_list; - ChattyHistory *history; - MatrixDb *matrix_db; + CmMatrix *cm_matrix; - /* The timeout id for the callback on network changes */ - guint network_change_id; - - gboolean has_loaded; gboolean disable_auto_login; - gboolean network_available; + gboolean is_ready; }; G_DEFINE_TYPE (ChattyMatrix, chatty_matrix, G_TYPE_OBJECT) -#define RECONNECT_TIMEOUT 1000 /* milliseconds */ - -static gboolean -matrix_reconnect (gpointer user_data) -{ - ChattyMatrix *self = user_data; - GListModel *list; - guint n_items; - - self->network_change_id = 0; - - list = G_LIST_MODEL (self->account_list); - n_items = g_list_model_get_n_items (list); - - for (guint i = 0; i < n_items; i++) { - g_autoptr(ChattyAccount) account = NULL; - - account = g_list_model_get_item (list, i); - - if (chatty_ma_account_can_connect (CHATTY_MA_ACCOUNT (account))) - chatty_account_connect (account, FALSE); - else - chatty_account_disconnect (account); - } - - return G_SOURCE_REMOVE; -} - -static void -matrix_network_changed_cb (ChattyMatrix *self, - gboolean network_available, - GNetworkMonitor *network_monitor) -{ - g_assert (CHATTY_IS_MATRIX (self)); - g_assert (G_IS_NETWORK_MONITOR (network_monitor)); - - if (!self->has_loaded) - return; - - g_clear_handle_id (&self->network_change_id, g_source_remove); - self->network_change_id = g_timeout_add (RECONNECT_TIMEOUT, - matrix_reconnect, self); +enum { + PROP_0, + PROP_ENABLED, + N_PROPS +}; - g_log (G_LOG_DOMAIN, CHATTY_LOG_LEVEL_TRACE, - "Network changed, has network: %s", CHATTY_LOG_BOOL (network_available)); -} +static GParamSpec *properties[N_PROPS]; static void matrix_ma_account_changed_cb (ChattyMatrix *self, @@ -111,6 +68,20 @@ matrix_ma_account_changed_cb (ChattyMatrix *self, g_list_model_items_changed (G_LIST_MODEL (self->list_of_chat_list), position, 1, 1); } +static void +matrix_add_clients_cb (GObject *object, + GAsyncResult *result, + gpointer user_data) +{ + g_autoptr(ChattyMatrix) self = user_data; + g_autoptr(GError) error = NULL; + + cm_matrix_add_clients_finish (self->cm_matrix, result, &error); + + if (error) + g_warning ("Error saving accounts: %s", error->message); +} + static void matrix_secret_load_cb (GObject *object, GAsyncResult *result, @@ -118,50 +89,44 @@ matrix_secret_load_cb (GObject *object, { ChattyMatrix *self = user_data; g_autoptr(GPtrArray) accounts = NULL; + g_autoptr(GPtrArray) secrets = NULL; g_autoptr(GError) error = NULL; g_assert (CHATTY_IS_MATRIX (self)); - accounts = chatty_secret_load_finish (result, &error); + secrets = chatty_secret_load_finish (result, &error); g_info ("Loading accounts from secrets %s", CHATTY_LOG_SUCESS (!error)); - if (error) - g_warning ("Error loading secret accounts: %s", error->message); + if (error && + !g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CANCELLED)) + g_warning ("Error loading secrets: %s", error->message); - if (!accounts) + if (!secrets || !secrets->len) return; - g_info ("Loaded %d matrix accounts", accounts ? accounts->len : 0); - - for (guint i = 0; i < accounts->len; i++) { - g_signal_connect_object (accounts->pdata[i], "notify::status", - G_CALLBACK (matrix_ma_account_changed_cb), - self, G_CONNECT_SWAPPED); - - chatty_ma_account_set_db (accounts->pdata[i], self->matrix_db, self->history); - g_list_store_append (self->list_of_chat_list, - chatty_ma_account_get_chat_list (accounts->pdata[i])); - } - - g_list_store_splice (self->account_list, 0, 0, accounts->pdata, accounts->len); + /* ... store the locally loaded accounts to cmatrix store */ + cm_matrix_add_clients_async (self->cm_matrix, secrets, + matrix_add_clients_cb, + g_object_ref (self)); } static void -matrix_db_open_cb (GObject *object, - GAsyncResult *result, - gpointer user_data) +chatty_matrix_get_property (GObject *object, + guint prop_id, + GValue *value, + GParamSpec *pspec) { - ChattyMatrix *self = user_data; - g_autoptr(GError) error = NULL; - - g_assert (CHATTY_IS_MATRIX (self)); + ChattyMatrix *self = (ChattyMatrix *)object; - if (matrix_db_open_finish (self->matrix_db, result, &error)) - chatty_secret_load_async (NULL, matrix_secret_load_cb, self); - else if (error && !g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CANCELLED)) - g_warning ("Failed to open Matrix DB: %s", error->message); + switch (prop_id) + { + case PROP_ENABLED: + g_value_set_boolean (value, chatty_matrix_is_enabled (self)); + break; - g_info ("Opening matrix db %s", CHATTY_LOG_SUCESS (!error)); + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + } } static void @@ -169,8 +134,6 @@ chatty_matrix_finalize (GObject *object) { ChattyMatrix *self = (ChattyMatrix *)object; - g_clear_handle_id (&self->network_change_id, g_source_remove); - g_list_store_remove_all (self->list_of_chat_list); g_list_store_remove_all (self->account_list); @@ -178,9 +141,6 @@ chatty_matrix_finalize (GObject *object) g_clear_object (&self->list_of_chat_list); g_clear_object (&self->chat_list); - g_clear_object (&self->history); - g_clear_object (&self->matrix_db); - G_OBJECT_CLASS (chatty_matrix_parent_class)->finalize (object); } @@ -190,12 +150,26 @@ chatty_matrix_class_init (ChattyMatrixClass *klass) GObjectClass *object_class = G_OBJECT_CLASS (klass); object_class->finalize = chatty_matrix_finalize; + object_class->get_property = chatty_matrix_get_property; + + /** + * ChattyMatrix:enabled: + * + * Whether matrix is enabled and usable + */ + properties[PROP_ENABLED] = + g_param_spec_boolean ("enabled", + "matrix enabled", + "matrix enabled", + FALSE, + G_PARAM_READABLE | G_PARAM_STATIC_STRINGS); + + g_object_class_install_properties (object_class, N_PROPS, properties); } static void chatty_matrix_init (ChattyMatrix *self) { - GNetworkMonitor *network_monitor; GListModel *model; self->account_list = g_list_store_new (CHATTY_TYPE_ACCOUNT); @@ -203,49 +177,116 @@ chatty_matrix_init (ChattyMatrix *self) model = G_LIST_MODEL (self->list_of_chat_list); self->chat_list = gtk_flatten_list_model_new (CHATTY_TYPE_CHAT, model); - - network_monitor = g_network_monitor_get_default (); - self->network_available = g_network_monitor_get_network_available (network_monitor); - - g_signal_connect_object (network_monitor, "network-changed", - G_CALLBACK (matrix_network_changed_cb), self, - G_CONNECT_AFTER | G_CONNECT_SWAPPED); - } ChattyMatrix * -chatty_matrix_new (ChattyHistory *history, - gboolean disable_auto_login) +chatty_matrix_new (gboolean disable_auto_login) { ChattyMatrix *self; - g_return_val_if_fail (CHATTY_IS_HISTORY (history), NULL); - self = g_object_new (CHATTY_TYPE_MATRIX, NULL); - self->history = g_object_ref (history); self->disable_auto_login = !!disable_auto_login; return self; } -void -chatty_matrix_load (ChattyMatrix *self) +gboolean +chatty_matrix_is_enabled (ChattyMatrix *self) { - char *db_path; + g_return_val_if_fail (CHATTY_IS_MATRIX (self), FALSE); - if (self->has_loaded) + return self->is_ready; +} + +static void +matrix_open_cb (GObject *obj, + GAsyncResult *result, + gpointer user_data) +{ + g_autoptr(ChattyMatrix) self = user_data; + g_autoptr(GError) error = NULL; + GListModel *accounts; + gboolean success; + + g_assert (CHATTY_IS_MATRIX (self)); + g_assert (CM_IS_MATRIX (obj)); + + success = cm_matrix_open_finish (CM_MATRIX (obj), result, &error); + g_object_notify_by_pspec (G_OBJECT (self), properties[PROP_ENABLED]); + + if (error) { + g_warning ("Error plugging in to the matrix: %s", error->message); return; + } + + if (success) + { + accounts = cm_matrix_get_clients_list (self->cm_matrix); + /* If we have no items loaded, load the items stored in chatty secret store... */ + if (g_list_model_get_n_items (accounts) == 0) + chatty_secret_load_async (NULL, matrix_secret_load_cb, self); + } +} + +static void +matrix_client_list_changed_cb (ChattyMatrix *self, + int position, + int removed, + int added, + GListModel *model) +{ + g_autoptr(GPtrArray) items = NULL; + + g_assert (CHATTY_IS_MATRIX (self)); + g_assert (G_IS_LIST_MODEL (model)); + + for (guint i = position; i < position + added; i++) + { + g_autoptr(CmClient) client = NULL; + ChattyMaAccount *account; + + if (!items) + items = g_ptr_array_new_with_free_func (g_object_unref); + + client = g_list_model_get_item (model, i); + account = chatty_ma_account_new_from_client (client); + g_list_store_append (self->list_of_chat_list, + chatty_ma_account_get_chat_list (account)); + g_signal_connect_object (account, "notify::status", + G_CALLBACK (matrix_ma_account_changed_cb), + self, G_CONNECT_SWAPPED); + if (g_object_get_data (G_OBJECT (client), "enable")) + cm_client_set_enabled (client, TRUE); + g_ptr_array_add (items, account); + } - self->has_loaded = TRUE; + g_list_store_splice (self->account_list, position, removed, + items ? items->pdata : NULL, added); +} - if (!chatty_settings_get_experimental_features (chatty_settings_get_default ())) +void +chatty_matrix_load (ChattyMatrix *self) +{ + g_autofree char *db_path = NULL; + g_autofree char *data_path = NULL; + g_autofree char *cache_path = NULL; + GListModel *client_list; + + if (self->cm_matrix) return; - self->matrix_db = matrix_db_new (); - db_path = g_build_filename (chatty_utils_get_purple_dir (), "chatty", "db", NULL); - matrix_db_open_async (self->matrix_db, db_path, "matrix.db", - matrix_db_open_cb, self); - g_info ("Opening matrix db"); + data_path = g_build_filename (g_get_user_data_dir (), "chatty", NULL); + cache_path = g_build_filename (g_get_user_cache_dir (), "chatty", NULL); + self->cm_matrix = cm_matrix_new (data_path, cache_path, "sm.puri.Chatty", + self->disable_auto_login); + client_list = cm_matrix_get_clients_list (self->cm_matrix); + g_signal_connect_object (client_list, "items-changed", + G_CALLBACK (matrix_client_list_changed_cb), + self, G_CONNECT_SWAPPED); + + db_path = g_build_filename (chatty_utils_get_purple_dir (), "chatty", "db", NULL); + cm_matrix_open_async (self->cm_matrix, db_path, "matrix.db", NULL, + matrix_open_cb, g_object_ref (self)); } GListModel * @@ -265,34 +306,6 @@ chatty_matrix_get_chat_list (ChattyMatrix *self) } -static void -matrix_db_account_delete_cb (GObject *object, - GAsyncResult *result, - gpointer user_data) -{ - g_autoptr(GTask) task = user_data; - ChattyMaAccount *account; - GError *error = NULL; - const char *username; - gboolean status; - - g_assert (G_IS_TASK (task)); - - status = matrix_db_delete_account_finish (MATRIX_DB (object), - result, &error); - - account = g_task_get_task_data (task); - username = chatty_item_get_username (CHATTY_ITEM (account)); - if (!username || !*username) - username = chatty_ma_account_get_login_username (account); - CHATTY_DEBUG_DETAILED (username, "Deleting %s, account:", CHATTY_LOG_SUCESS (!error)); - - if (error) - g_task_return_error (task, error); - else - g_task_return_boolean (task, status); -} - static void matrix_account_delete_cb (GObject *object, GAsyncResult *result, @@ -312,7 +325,7 @@ matrix_account_delete_cb (GObject *object, g_assert (CHATTY_IS_MATRIX (self)); g_assert (CHATTY_IS_MA_ACCOUNT (account)); - chatty_secret_delete_finish (result, &error); + cm_matrix_delete_client_finish (self->cm_matrix, result, &error); username = chatty_item_get_username (CHATTY_ITEM (account)); if (!username || !*username) @@ -322,9 +335,7 @@ matrix_account_delete_cb (GObject *object, if (error) g_task_return_error (task, error); else - matrix_db_delete_account_async (self->matrix_db, CHATTY_ACCOUNT (account), - matrix_db_account_delete_cb, - g_steal_pointer (&task)); + chatty_account_delete (CHATTY_ACCOUNT (account)); } void @@ -334,7 +345,6 @@ chatty_matrix_delete_account_async (ChattyMatrix *self, GAsyncReadyCallback callback, gpointer user_data) { - GListModel *chat_list; GTask *task; g_return_if_fail (CHATTY_IS_MATRIX (self)); @@ -343,14 +353,12 @@ chatty_matrix_delete_account_async (ChattyMatrix *self, task = g_task_new (self, cancellable, callback, user_data); g_task_set_task_data (task, g_object_ref (account), g_object_unref); - chat_list = chatty_ma_account_get_chat_list (CHATTY_MA_ACCOUNT (account)); - chatty_utils_remove_list_item (self->list_of_chat_list, chat_list); - chatty_utils_remove_list_item (self->account_list, account); chatty_account_set_enabled (account, FALSE); CHATTY_DEBUG (chatty_item_get_username (CHATTY_ITEM (account)), "Deleting account"); - - chatty_secret_delete_async (account, NULL, matrix_account_delete_cb, task); + cm_matrix_delete_client_async (self->cm_matrix, + chatty_ma_account_get_cm_client (CHATTY_MA_ACCOUNT (account)), + matrix_account_delete_cb, task); } gboolean @@ -371,19 +379,20 @@ matrix_save_account_cb (GObject *object, gpointer user_data) { ChattyMatrix *self; - ChattyMaAccount *account = (gpointer)object; + ChattyMaAccount *account; g_autoptr(GTask) task = user_data; g_autoptr(GError) error = NULL; const char *username; gboolean saved; g_assert (G_IS_TASK (task)); - g_assert (CHATTY_IS_MA_ACCOUNT (account)); self = g_task_get_source_object (task); + account = g_task_get_task_data (task); g_assert (CHATTY_IS_MATRIX (self)); + g_assert (CHATTY_IS_MA_ACCOUNT (account)); - saved = chatty_ma_account_save_finish (account, result, &error); + saved = cm_matrix_save_client_finish (self->cm_matrix, result, &error); username = chatty_item_get_username (CHATTY_ITEM (account)); if (!username || !*username) @@ -395,17 +404,6 @@ matrix_save_account_cb (GObject *object, return; } - if (saved) { - g_list_store_append (self->account_list, account); - - g_signal_connect_object (account, "notify::status", - G_CALLBACK (matrix_ma_account_changed_cb), - self, G_CONNECT_SWAPPED); - - if (!self->disable_auto_login) - chatty_account_set_enabled (CHATTY_ACCOUNT (account), TRUE); - } - g_task_return_boolean (task, saved); } @@ -416,6 +414,7 @@ chatty_matrix_save_account_async (ChattyMatrix *self, GAsyncReadyCallback callback, gpointer user_data) { + CmClient *cm_client; GTask *task; g_return_if_fail (CHATTY_IS_MATRIX (self)); @@ -423,10 +422,13 @@ chatty_matrix_save_account_async (ChattyMatrix *self, CHATTY_DEBUG (chatty_item_get_username (CHATTY_ITEM (account)), "Saving account"); + cm_client = chatty_ma_account_get_cm_client (CHATTY_MA_ACCOUNT (account)); + g_return_if_fail (CM_IS_CLIENT (cm_client)); + task = g_task_new (self, cancellable, callback, user_data); - chatty_ma_account_set_db (CHATTY_MA_ACCOUNT (account), self->matrix_db, self->history); - chatty_ma_account_save_async (CHATTY_MA_ACCOUNT (account), TRUE, NULL, - matrix_save_account_cb, task); + g_task_set_task_data (task, g_object_ref (account), g_object_unref); + cm_matrix_save_client_async (self->cm_matrix, cm_client, + matrix_save_account_cb, task); } gboolean @@ -480,8 +482,7 @@ chatty_matrix_find_chat_with_name (ChattyMatrix *self, g_return_val_if_fail (account_id && *account_id, NULL); g_return_val_if_fail (chat_id && *chat_id, NULL); - if (!chatty_settings_get_experimental_features (chatty_settings_get_default ()) || - protocol != CHATTY_PROTOCOL_MATRIX) + if (protocol != CHATTY_PROTOCOL_MATRIX) return NULL; accounts = G_LIST_MODEL (self->account_list); @@ -510,3 +511,20 @@ chatty_matrix_find_chat_with_name (ChattyMatrix *self, return NULL; } + +CmClient * +chatty_matrix_client_new (ChattyMatrix *self) +{ + g_return_val_if_fail (CHATTY_IS_MATRIX (self), NULL); + + return cm_matrix_client_new (self->cm_matrix); +} + +gboolean +chatty_matrix_has_user_id (ChattyMatrix *self, + const char *user_id) +{ + g_return_val_if_fail (CHATTY_IS_MATRIX (self), FALSE); + + return cm_matrix_has_client_with_id (self->cm_matrix, user_id); +} diff --git a/src/matrix/chatty-matrix.h b/src/matrix/chatty-matrix.h index 3cbe9acc779eadcb02a097a5cbf549792149eceb..7ec4bc529967b20d830d18ea485063b673a012cc 100644 --- a/src/matrix/chatty-matrix.h +++ b/src/matrix/chatty-matrix.h @@ -12,6 +12,8 @@ #pragma once #include <glib-object.h> +#define CMATRIX_USE_EXPERIMENTAL_API +#include "cmatrix.h" #include "chatty-history.h" @@ -21,9 +23,9 @@ G_BEGIN_DECLS G_DECLARE_FINAL_TYPE (ChattyMatrix, chatty_matrix, CHATTY, MATRIX, GObject) -ChattyMatrix *chatty_matrix_new (ChattyHistory *history, - gboolean disable_auto_login); -void chatty_matrix_load (ChattyMatrix *self); +ChattyMatrix *chatty_matrix_new (gboolean disable_auto_login); +gboolean chatty_matrix_is_enabled (ChattyMatrix *self); +void chatty_matrix_load (ChattyMatrix *self); GListModel *chatty_matrix_get_account_list (ChattyMatrix *self); GListModel *chatty_matrix_get_chat_list (ChattyMatrix *self); void chatty_matrix_delete_account_async (ChattyMatrix *self, @@ -48,5 +50,8 @@ ChattyChat *chatty_matrix_find_chat_with_name (ChattyMatrix *self, ChattyProtocol protocol, const char *account_id, const char *chat_id); +CmClient *chatty_matrix_client_new (ChattyMatrix *self); +gboolean chatty_matrix_has_user_id (ChattyMatrix *self, + const char *user_id); G_END_DECLS diff --git a/src/matrix/matrix-api.c b/src/matrix/matrix-api.c deleted file mode 100644 index 2a37adab8f322be95d95d05966432fddcad05325..0000000000000000000000000000000000000000 --- a/src/matrix/matrix-api.c +++ /dev/null @@ -1,2561 +0,0 @@ -/* -*- mode: c; c-basic-offset: 2; indent-tabs-mode: nil; -*- */ -/* matrix-api.c - * - * Copyright 2020 Purism SPC - * - * Author(s): - * Mohammed Sadiq <sadiq@sadiqpk.org> - * - * SPDX-License-Identifier: GPL-3.0-or-later - */ - -#define G_LOG_DOMAIN "chatty-matrix-api" - -#ifdef HAVE_CONFIG_H -# include "config.h" -#endif - -#include <libsoup/soup.h> -#include <json-glib/json-glib.h> -#include <olm/olm.h> -#include <sys/random.h> - -#include "chatty-chat.h" -#include "chatty-ma-buddy.h" -#include "matrix-enums.h" -#include "matrix-utils.h" -#include "matrix-api.h" -#include "matrix-net.h" -#include "chatty-log.h" - -/** - * SECTION: chatty-api - * @title: MatrixApi - * @short_description: The Matrix HTTP API. - * @include: "chatty-api.h" - * - * This class handles all communications with Matrix server - * user REST APIs. - */ - -#define URI_REQUEST_TIMEOUT 60 /* seconds */ -#define SYNC_TIMEOUT 30000 /* milliseconds */ -#define TYPING_TIMEOUT 10000 /* milliseconds */ -#define KEY_TIMEOUT 10000 /* milliseconds */ - -struct _MatrixApi -{ - GObject parent_instance; - - /* The username used to log in. This can be different from - * the @username as this can be an email, phone number, etc. */ - char *login_username; - char *username; - char *password; - char *homeserver; - char *device_id; - char *access_token; - char *key; - - MatrixEnc *matrix_enc; - MatrixNet *matrix_net; - GSocketAddress *gaddress; - - /* Executed for every request response */ - MatrixCallback callback; - gpointer cb_object; - GCancellable *cancellable; - char *next_batch; - char *filter_id; - MatrixAction action; - - /* for sending events, incremented for each event */ - int event_id; - - guint full_state_loaded : 1; - guint is_sync : 1; - /* Set when error occurs with sync enabled */ - guint sync_failed : 1; - guint homeserver_verified : 1; - guint login_success : 1; - guint room_list_loaded : 1; - /* Set when @self has tried connecting the network atleast once */ - guint has_tried_connecting : 1; - - guint resync_id; -}; - -G_DEFINE_TYPE (MatrixApi, matrix_api, G_TYPE_OBJECT) - -static void matrix_verify_homeserver (MatrixApi *self); -static void matrix_login (MatrixApi *self); -static void matrix_upload_key (MatrixApi *self); -static void matrix_start_sync (MatrixApi *self); -static void matrix_take_red_pill (MatrixApi *self); -static gboolean handle_common_errors (MatrixApi *self, - GError *error); - -static void -api_set_string_value (char **strp, - const char *value) -{ - g_assert (strp); - - if (value) { - g_free (*strp); - *strp = g_strdup (value); - } -} - -static void -api_verify_homeserver_cb (GObject *obj, - GAsyncResult *result, - gpointer user_data) -{ - MatrixApi *self = user_data; - g_autoptr(GError) error = NULL; - gboolean success; - - g_assert (MATRIX_IS_API (self)); - - success = matrix_utils_verify_homeserver_finish (result, &error); - - if (g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CANCELLED)) - return; - - if (!self->gaddress) - self->gaddress = g_object_steal_data (G_OBJECT (result), "address"); - self->has_tried_connecting = TRUE; - - /* Since GTask can't have timeout, We cancel the cancellable to fake timeout */ - if (g_error_matches (error, G_IO_ERROR, G_IO_ERROR_TIMED_OUT)) { - g_clear_object (&self->cancellable); - self->cancellable = g_cancellable_new (); - } - - if (handle_common_errors (self, error)) - return; - - if (error) { - CHATTY_TRACE_MSG ("Error verifying home server: %s", error->message); - self->callback (self->cb_object, self, MATRIX_VERIFY_HOMESERVER, NULL, error); - return; - } - - if (success) { - self->homeserver_verified = TRUE; - matrix_start_sync (self); - } else { - error = g_error_new (G_IO_ERROR, G_IO_ERROR_FAILED, "Failed to verify homeserver"); - self->callback (self->cb_object, self, self->action, NULL, error); - } -} - -static void -api_get_homeserver_cb (gpointer object, - GAsyncResult *result, - gpointer user_data) -{ - MatrixApi *self = user_data; - g_autoptr(GError) error = NULL; - char *homeserver; - - g_assert (MATRIX_IS_API (self)); - - homeserver = matrix_utils_get_homeserver_finish (result, &error); - - if (g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CANCELLED)) - return; - - CHATTY_TRACE_MSG ("Get home server, has-error: %d, home server: %s", - !error, homeserver); - - if (!self->gaddress) - self->gaddress = g_object_steal_data (G_OBJECT (result), "address"); - self->has_tried_connecting = TRUE; - - if (!homeserver) { - self->sync_failed = TRUE; - self->callback (self->cb_object, self, self->action, NULL, error); - - return; - } - - matrix_api_set_homeserver (self, homeserver); - matrix_verify_homeserver (self); -} - -static void -api_send_message_cb (GObject *obj, - GAsyncResult *result, - gpointer user_data) -{ - g_autoptr(GTask) task = user_data; - g_autoptr(JsonObject) object = NULL; - ChattyMessage *message; - GError *error = NULL; - char *event_id; - int retry_after; - - g_assert (G_IS_TASK (task)); - - object = g_task_propagate_pointer (G_TASK (result), &error); - retry_after = GPOINTER_TO_INT (g_object_get_data (G_OBJECT (result), "retry-after")); - g_object_set_data (G_OBJECT (task), "retry-after", GINT_TO_POINTER (retry_after)); - - message = g_object_get_data (G_OBJECT (task), "message"); - event_id = g_object_get_data (G_OBJECT (task), "event-id"); - - g_debug ("Sending message %s. event-id: %s, retry-after: %d", - CHATTY_LOG_SUCESS (!error), event_id, retry_after); - - if (error) { - g_debug ("Error sending message: %s", error->message); - chatty_message_set_status (message, CHATTY_STATUS_SENDING_FAILED, 0); - g_task_return_error (task, error); - } else { - chatty_message_set_status (message, CHATTY_STATUS_SENT, 0); - g_task_return_boolean (task, !!object); - } -} - -static void -api_get_file_cb (GObject *object, - GAsyncResult *result, - gpointer user_data) -{ - MatrixApi *self; - g_autoptr(GTask) task = user_data; - GError *error = NULL; - gboolean status; - - g_assert (G_IS_TASK (task)); - - self = g_task_get_source_object (task); - g_assert (MATRIX_IS_API (self)); - - status = matrix_net_get_file_finish (self->matrix_net, result, &error); - - if (error) { - g_task_return_error (task, error); - - return; - } - - g_task_return_boolean (task, status); -} - -static void -matrix_send_typing_cb (GObject *obj, - GAsyncResult *result, - gpointer user_data) -{ - g_autoptr(JsonObject) object = NULL; - g_autoptr(GError) error = NULL; - - object = g_task_propagate_pointer (G_TASK (result), &error); - - if (error) - g_warning ("Error set typing: %s", error->message); -} - - -static void -api_set_read_marker_cb (GObject *obj, - GAsyncResult *result, - gpointer user_data) -{ - g_autoptr(GTask) task = user_data; - g_autoptr(JsonObject) object = NULL; - GError *error = NULL; - - object = g_task_propagate_pointer (G_TASK (result), &error); - - CHATTY_TRACE_MSG ("Mark as read. success: %d", !error); - - if (error) { - g_debug ("Error setting read marker: %s", error->message); - g_task_return_error (task, error); - } else { - g_task_return_boolean (task, TRUE); - } -} - -static void -api_upload_group_keys_cb (GObject *obj, - GAsyncResult *result, - gpointer user_data) -{ - g_autoptr(GTask) task = user_data; - g_autoptr(JsonObject) object = NULL; - GError *error = NULL; - - object = g_task_propagate_pointer (G_TASK (result), &error); - - if (error) { - g_debug ("Error uploading group keys: %s", error->message); - g_task_return_error (task, error); - } else { - g_task_return_boolean (task, TRUE); - } -} - -static void -matrix_get_room_state_cb (GObject *obj, - GAsyncResult *result, - gpointer user_data) -{ - g_autoptr(GTask) task = user_data; - JsonArray *array; - GError *error = NULL; - - g_assert (G_IS_TASK (task)); - - array = g_task_propagate_pointer (G_TASK (result), &error); - - if (error) { - g_debug ("Error getting room state: %s", error->message); - g_task_return_error (task, error); - } else { - g_task_return_pointer (task, array, (GDestroyNotify)json_array_unref); - } -} - -static void -matrix_get_room_name_cb (GObject *obj, - GAsyncResult *result, - gpointer user_data) -{ - g_autoptr(GTask) task = user_data; - JsonObject *object = NULL; - GError *error = NULL; - - object = g_task_propagate_pointer (G_TASK (result), &error); - - if (error) { - if (!g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CANCELLED)) - CHATTY_TRACE_MSG ("Error getting room name: %s", error->message); - - g_task_return_error (task, error); - } else { - g_task_return_pointer (task, object, (GDestroyNotify)json_object_unref); - } -} - -static void -matrix_get_room_encryption_cb (GObject *obj, - GAsyncResult *result, - gpointer user_data) -{ - g_autoptr(GTask) task = user_data; - g_autoptr(JsonObject) object = NULL; - const char *encryption; - GError *error = NULL; - - object = g_task_propagate_pointer (G_TASK (result), &error); - - if (error && - !g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CANCELLED) && - !g_error_matches (error, MATRIX_ERROR, M_NOT_FOUND)) - g_warning ("Error loading encryption state: %s", error->message); - - encryption = matrix_utils_json_object_get_string (object, "algorithm"); - g_task_return_pointer (task, g_strdup (encryption), g_free); -} - -static void -matrix_set_room_encryption_cb (GObject *obj, - GAsyncResult *result, - gpointer user_data) -{ - g_autoptr(GTask) task = user_data; - g_autoptr(JsonObject) object = NULL; - const char *event; - GError *error = NULL; - - object = g_task_propagate_pointer (G_TASK (result), &error); - - if (error && - !g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CANCELLED) && - !g_error_matches (error, MATRIX_ERROR, M_NOT_FOUND)) - g_warning ("Error setting encryption: %s", error->message); - - event = matrix_utils_json_object_get_string (object, "event_id"); - g_task_return_boolean (task, !!event); -} - -static void -matrix_get_members_cb (GObject *obj, - GAsyncResult *result, - gpointer user_data) -{ - g_autoptr(GTask) task = user_data; - JsonObject *object = NULL; - GError *error = NULL; - - object = g_task_propagate_pointer (G_TASK (result), &error); - - if (error) { - g_debug ("Error getting members: %s", error->message); - g_task_return_error (task, error); - } else { - g_task_return_pointer (task, object, (GDestroyNotify)json_object_unref); - } -} - -static void -matrix_get_messages_cb (GObject *obj, - GAsyncResult *result, - gpointer user_data) -{ - g_autoptr(GTask) task = user_data; - JsonObject *object = NULL; - GError *error = NULL; - - object = g_task_propagate_pointer (G_TASK (result), &error); - - if (error) { - g_warning ("Error getting members: %s", error->message); - g_task_return_error (task, error); - } else { - g_task_return_pointer (task, object, (GDestroyNotify)json_object_unref); - } -} - -static void -matrix_keys_query_cb (GObject *obj, - GAsyncResult *result, - gpointer user_data) -{ - g_autoptr(GTask) task = user_data; - JsonObject *object = NULL; - GError *error = NULL; - - object = g_task_propagate_pointer (G_TASK (result), &error); - - CHATTY_TRACE_MSG ("Query key complete. success: %d", !error); - - if (error) { - g_debug ("Error key query: %s", error->message); - g_task_return_error (task, error); - } else { - g_task_return_pointer (task, object, (GDestroyNotify)json_object_unref); - } -} - -static void -matrix_keys_claim_cb (GObject *obj, - GAsyncResult *result, - gpointer user_data) -{ - g_autoptr(GTask) task = user_data; - JsonObject *object = NULL; - GError *error = NULL; - - object = g_task_propagate_pointer (G_TASK (result), &error); - - if (error) { - g_debug ("Error key query: %s", error->message); - g_task_return_error (task, error); - } else { - g_task_return_pointer (task, object, (GDestroyNotify)json_object_unref); - } -} - -static gboolean -schedule_resync (gpointer user_data) -{ - MatrixApi *self = user_data; - gboolean sync_now; - - g_assert (MATRIX_IS_API (self)); - self->resync_id = 0; - - sync_now = matrix_api_can_connect (self); - CHATTY_TRACE (self->username, "Schedule sync. sync now: %d, user: ", sync_now); - - if (sync_now) - matrix_start_sync (self); - - return G_SOURCE_REMOVE; -} - -/* - * Handle Self fixable errors. - * - * Returns: %TRUE if @error was handled. - * %FALSE otherwise - */ -static gboolean -handle_common_errors (MatrixApi *self, - GError *error) -{ - if (!error) - return FALSE; - - CHATTY_TRACE_MSG ("Error: %s", error->message); - - if (g_error_matches (error, MATRIX_ERROR, M_UNKNOWN_TOKEN) - && self->password) { - CHATTY_TRACE (self->username ? self->username : self->login_username, "Re-logging in "); - self->login_success = FALSE; - self->room_list_loaded = FALSE; - g_clear_pointer (&self->access_token, matrix_utils_free_buffer); - matrix_enc_set_details (self->matrix_enc, NULL, NULL); - self->callback (self->cb_object, self, MATRIX_ACCESS_TOKEN_LOGIN, NULL, NULL); - matrix_start_sync (self); - - return TRUE; - } - - /* - * The G_RESOLVER_ERROR may be suggesting that the hostname is wrong, but we don't - * know if it's network/DNS/Proxy error. So keep retrying. - */ - if ((error->domain == SOUP_HTTP_ERROR && - error->code <= SOUP_STATUS_TLS_FAILED && - error->code > SOUP_STATUS_CANCELLED) || - g_error_matches (error, G_IO_ERROR, G_IO_ERROR_NETWORK_UNREACHABLE) || - g_error_matches (error, G_IO_ERROR, G_IO_ERROR_TIMED_OUT) || - error->domain == G_RESOLVER_ERROR || - error->domain == JSON_PARSER_ERROR) { - - if (matrix_api_can_connect (self)) { - g_clear_handle_id (&self->resync_id, g_source_remove); - - self->sync_failed = TRUE; - self->callback (self->cb_object, self, MATRIX_RED_PILL, NULL, NULL); - CHATTY_TRACE (self->username, "Schedule sync for user "); - self->resync_id = g_timeout_add_seconds (URI_REQUEST_TIMEOUT, - schedule_resync, self); - return TRUE; - } - } - - return FALSE; -} - -static gboolean -handle_one_time_keys (MatrixApi *self, - JsonObject *object) -{ - size_t count, limit; - - g_assert (MATRIX_IS_API (self)); - - if (!object) - return FALSE; - - count = matrix_utils_json_object_get_int (object, "signed_curve25519"); - limit = matrix_enc_max_one_time_keys (self->matrix_enc) / 2; - - /* If we don't have enough onetime keys add some */ - if (count < limit) { - CHATTY_TRACE_MSG ("generating %" G_GSIZE_FORMAT " onetime keys", limit - count); - matrix_enc_create_one_time_keys (self->matrix_enc, limit - count); - - if (!self->key) - self->key = matrix_enc_get_one_time_keys_json (self->matrix_enc); - matrix_upload_key (self); - - return TRUE; - } - - return FALSE; -} - -static void -api_upload_filter_cb (GObject *object, - GAsyncResult *result, - gpointer user_data) -{ - g_autoptr(MatrixApi) self = user_data; - g_autoptr(JsonObject) root = NULL; - g_autoptr(GError) error = NULL; - - g_assert (MATRIX_IS_API (self)); - g_assert (G_IS_TASK (result)); - - root = g_task_propagate_pointer (G_TASK (result), &error); - - if (g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CANCELLED)) - return; - - CHATTY_DEBUG (self->username, "Uploading filter %s, user", - CHATTY_LOG_SUCESS (!error)); - - if (handle_common_errors (self, error)) - return; - - self->filter_id = g_strdup (matrix_utils_json_object_get_string (root, "filter_id")); - - if (!self->filter_id) - self->filter_id = g_strdup (""); - matrix_start_sync (self); -} - -static void -matrix_upload_filter (MatrixApi *self) -{ - g_autoptr(GError) error = NULL; - g_autoptr(GBytes) data = NULL; - const char *data_str = NULL; - gsize size; - - data = g_resources_lookup_data ("/sm/puri/Chatty/matrix-filter.json", 0, &error); - - if (error) - g_warning ("Error getting filter file: %s", error->message); - else if (data) - data_str = g_bytes_get_data (data, &size); - - if (!data || !data_str || !size) { - self->filter_id = g_strdup (""); - matrix_start_sync (self); - } else { - g_autofree char *uri = NULL; - g_autoptr(JsonParser) parser = NULL; - JsonObject *filter = NULL; - JsonNode *root = NULL; - - CHATTY_DEBUG (self->username, "Uploading filter, user:"); - - parser = json_parser_new (); - json_parser_load_from_data (parser, data_str, size, &error); - - if (error) - g_warning ("Error parsing filter file: %s", error->message); - - if (!error) - root = json_parser_get_root (parser); - - if (root) - filter = json_node_get_object (root); - - if (error || !root || !filter) { - if (error) - g_warning ("Error getting filter file: %s", error->message); - - self->filter_id = g_strdup (""); - matrix_start_sync (self); - - return; - } - - uri = g_strconcat ("/_matrix/client/r0/user/", self->username, "/filter", NULL); - matrix_net_send_json_async (self->matrix_net, 2, json_object_ref (filter), - uri, SOUP_METHOD_POST, - NULL, self->cancellable, api_upload_filter_cb, - g_object_ref (self)); - } -} - -static void -matrix_login_cb (GObject *obj, - GAsyncResult *result, - gpointer user_data) -{ - g_autoptr(MatrixApi) self = user_data; - g_autoptr(JsonObject) root = NULL; - g_autoptr(GError) error = NULL; - JsonObject *object = NULL; - const char *value; - - g_assert (MATRIX_IS_API (self)); - g_assert (G_IS_TASK (result)); - - root = g_task_propagate_pointer (G_TASK (result), &error); - - if (g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CANCELLED)) - return; - - CHATTY_TRACE_MSG ("login %s", CHATTY_LOG_SUCESS (!error)); - - if (error) { - self->sync_failed = TRUE; - /* use a better code to inform invalid password */ - if (error->code == M_FORBIDDEN) - error->code = M_BAD_PASSWORD; - self->callback (self->cb_object, self, MATRIX_PASSWORD_LOGIN, NULL, error); - g_debug ("Error logging in: %s", error->message); - return; - } - - self->login_success = TRUE; - - /* https://matrix.org/docs/spec/client_server/r0.6.1#post-matrix-client-r0-login */ - value = matrix_utils_json_object_get_string (root, "user_id"); - api_set_string_value (&self->username, value); - - value = matrix_utils_json_object_get_string (root, "access_token"); - matrix_utils_free_buffer (self->access_token); - self->access_token = g_strdup (value); - matrix_net_set_access_token (self->matrix_net, self->access_token); - - value = matrix_utils_json_object_get_string (root, "device_id"); - api_set_string_value (&self->device_id, value); - - object = matrix_utils_json_object_get_object (root, "well_known"); - object = matrix_utils_json_object_get_object (object, "m.homeserver"); - value = matrix_utils_json_object_get_string (object, "base_url"); - matrix_api_set_homeserver (self, value); - - matrix_enc_set_details (self->matrix_enc, self->username, self->device_id); - g_free (self->key); - self->key = matrix_enc_get_device_keys_json (self->matrix_enc); - - self->callback (self->cb_object, self, MATRIX_PASSWORD_LOGIN, NULL, NULL); - matrix_start_sync (self); -} - -static void -matrix_upload_key_cb (GObject *obj, - GAsyncResult *result, - gpointer user_data) -{ - g_autoptr(MatrixApi) self = user_data; - g_autoptr(JsonObject) root = NULL; - g_autoptr(GError) error = NULL; - JsonObject *object = NULL; - - g_assert (MATRIX_IS_API (self)); - g_assert (G_IS_TASK (result)); - - root = g_task_propagate_pointer (G_TASK (result), &error); - - if (error) { - self->sync_failed = TRUE; - self->callback (self->cb_object, self, MATRIX_UPLOAD_KEY, NULL, error); - g_debug ("Error uploading key: %s", error->message); - return; - } - - self->callback (self->cb_object, self, MATRIX_UPLOAD_KEY, root, NULL); - - object = matrix_utils_json_object_get_object (root, "one_time_key_counts"); - CHATTY_TRACE_MSG ("Uploaded %ld keys", - matrix_utils_json_object_get_int (object, "signed_curve25519")); - - if (!handle_one_time_keys (self, object) && - self->action != MATRIX_RED_PILL) - matrix_take_red_pill (self); -} - -/* sync callback */ -static void -matrix_take_red_pill_cb (GObject *obj, - GAsyncResult *result, - gpointer user_data) -{ - g_autoptr(MatrixApi) self = user_data; - g_autoptr(JsonObject) root = NULL; - g_autoptr(GError) error = NULL; - JsonObject *object = NULL; - - g_assert (MATRIX_IS_API (self)); - g_assert (G_IS_TASK (result)); - - root = g_task_propagate_pointer (G_TASK (result), &error); - - if (g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CANCELLED)) - return; - - if (!self->next_batch || error || !self->full_state_loaded) - g_log (G_LOG_DOMAIN, CHATTY_LOG_LEVEL_TRACE, "sync %s, full-state: %d, next-batch: %s", - CHATTY_LOG_SUCESS (!error), !self->full_state_loaded, self->next_batch); - - if (handle_common_errors (self, error)) - return; - - if (error) { - self->sync_failed = TRUE; - self->callback (self->cb_object, self, self->action, NULL, error); - g_debug ("Error syncing with time %s: %s", self->next_batch, error->message); - return; - } - - self->login_success = TRUE; - - object = matrix_utils_json_object_get_object (root, "device_one_time_keys_count"); - handle_one_time_keys (self, object); - - /* XXX: For some reason full state isn't loaded unless we have passed “next_batchâ€. - * So, if we haven’t, don’t mark so. - */ - if (self->next_batch) - self->full_state_loaded = TRUE; - - g_free (self->next_batch); - self->next_batch = g_strdup (matrix_utils_json_object_get_string (root, "next_batch")); - - self->callback (self->cb_object, self, self->action, root, NULL); - - /* Repeat */ - matrix_take_red_pill (self); -} - -static void -matrix_verify_homeserver (MatrixApi *self) -{ - g_assert (MATRIX_IS_API (self)); - g_log (G_LOG_DOMAIN, CHATTY_LOG_LEVEL_TRACE, - "verifying homeserver %s", self->homeserver); - - self->action = MATRIX_VERIFY_HOMESERVER; - matrix_utils_verify_homeserver_async (self->homeserver, URI_REQUEST_TIMEOUT, - self->cancellable, - api_verify_homeserver_cb, self); -} - -static void -matrix_login (MatrixApi *self) -{ - JsonObject *object, *child; - - g_assert (MATRIX_IS_API (self)); - g_assert (self->login_username); - g_assert (self->homeserver); - g_assert (!self->access_token); - g_assert (self->password && *self->password); - - CHATTY_TRACE (self->login_username, "logging on server %s, account", self->homeserver); - - /* https://matrix.org/docs/spec/client_server/r0.6.1#post-matrix-client-r0-login */ - object = json_object_new (); - json_object_set_string_member (object, "type", "m.login.password"); - json_object_set_string_member (object, "password", self->password); - json_object_set_string_member (object, "initial_device_display_name", "Chatty"); - - child = json_object_new (); - - if (chatty_utils_username_is_valid (self->login_username, CHATTY_PROTOCOL_EMAIL)) { - json_object_set_string_member (child, "type", "m.id.thirdparty"); - json_object_set_string_member (child, "medium", "email"); - json_object_set_string_member (child, "address", self->login_username); - } else { - json_object_set_string_member (child, "type", "m.id.user"); - json_object_set_string_member (child, "user", self->login_username); - } - - json_object_set_object_member (object, "identifier", child); - - matrix_net_send_json_async (self->matrix_net, 2, object, - "/_matrix/client/r0/login", SOUP_METHOD_POST, - NULL, self->cancellable, matrix_login_cb, - g_object_ref (self)); -} - -static void -matrix_upload_key (MatrixApi *self) -{ - char *key; - - g_assert (MATRIX_IS_API (self)); - g_assert (self->key); - - key = g_steal_pointer (&self->key); - - matrix_net_send_data_async (self->matrix_net, 2, key, strlen (key), - "/_matrix/client/r0/keys/upload", SOUP_METHOD_POST, - NULL, self->cancellable, matrix_upload_key_cb, - g_object_ref (self)); -} - -static void -get_joined_rooms_cb (GObject *obj, - GAsyncResult *result, - gpointer user_data) -{ - g_autoptr(MatrixApi) self = user_data; - g_autoptr(JsonObject) root = NULL; - g_autoptr(GError) error = NULL; - - g_assert (MATRIX_IS_API (self)); - g_assert (G_IS_TASK (result)); - - root = g_task_propagate_pointer (G_TASK (result), &error); - - if (g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CANCELLED)) - return; - - CHATTY_TRACE_MSG ("Getting joined rooms %s", CHATTY_LOG_SUCESS (!error)); - - if (handle_common_errors (self, error)) - return; - - self->callback (self->cb_object, self, MATRIX_GET_JOINED_ROOMS, root, error); - - if (!error) { - self->room_list_loaded = TRUE; - matrix_start_sync (self); - } -} - -static void -matrix_get_joined_rooms (MatrixApi *self) -{ - g_assert (MATRIX_IS_API (self)); - g_assert (!self->room_list_loaded); - - CHATTY_TRACE_MSG ("Getting joined rooms"); - matrix_net_send_json_async (self->matrix_net, 0, NULL, - "/_matrix/client/r0/joined_rooms", SOUP_METHOD_GET, - NULL, self->cancellable, get_joined_rooms_cb, - g_object_ref (self)); -} - -static void -matrix_start_sync (MatrixApi *self) -{ - g_assert (MATRIX_IS_API (self)); - - self->is_sync = TRUE; - self->sync_failed = FALSE; - g_clear_handle_id (&self->resync_id, g_source_remove); - - if (!self->homeserver) { - self->action = MATRIX_GET_HOMESERVER; - if (!matrix_utils_username_is_complete (self->login_username)) { - g_autoptr(GError) error = NULL; - - g_debug ("Error: No Homeserver provided"); - self->sync_failed = TRUE; - error = g_error_new (MATRIX_ERROR, M_NO_HOME_SERVER, "No Homeserver provided"); - self->callback (self->cb_object, self, self->action, NULL, error); - } else { - g_debug ("Fetching home server details from username"); - matrix_utils_get_homeserver_async (self->login_username, URI_REQUEST_TIMEOUT, self->cancellable, - (GAsyncReadyCallback)api_get_homeserver_cb, - self); - } - } else if (!self->homeserver_verified) { - matrix_verify_homeserver (self); - } else if (!self->password){ - g_autoptr(GError) error = NULL; - - error = g_error_new (MATRIX_ERROR, M_BAD_PASSWORD, "Empty password"); - self->callback (self->cb_object, self, MATRIX_PASSWORD_LOGIN, NULL, error); - } else if (!self->access_token) { - matrix_login (self); - } else if (!self->filter_id){ - matrix_upload_filter (self); - } else if (!self->room_list_loaded) { - matrix_get_joined_rooms (self); - } else { - matrix_take_red_pill (self); - } -} - -static void -matrix_take_red_pill (MatrixApi *self) -{ - GHashTable *query; - - g_assert (MATRIX_IS_API (self)); - - self->action = MATRIX_RED_PILL; - query = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, g_free); - - if (self->login_success) - g_hash_table_insert (query, g_strdup ("timeout"), g_strdup_printf ("%u", SYNC_TIMEOUT)); - else - g_hash_table_insert (query, g_strdup ("timeout"), g_strdup_printf ("%u", SYNC_TIMEOUT / 1000)); - - if (self->next_batch) - g_hash_table_insert (query, g_strdup ("since"), g_strdup (self->next_batch)); - if (!self->full_state_loaded) - g_hash_table_insert (query, g_strdup ("full_state"), g_strdup ("true")); - - matrix_net_send_json_async (self->matrix_net, 2, NULL, - "/_matrix/client/r0/sync", SOUP_METHOD_GET, - query, self->cancellable, matrix_take_red_pill_cb, - g_object_ref (self)); -} - -static void -matrix_api_finalize (GObject *object) -{ - MatrixApi *self = (MatrixApi *)object; - - if (self->cancellable) - g_cancellable_cancel (self->cancellable); - g_clear_object (&self->cancellable); - - g_clear_object (&self->matrix_enc); - g_clear_object (&self->matrix_net); - g_clear_object (&self->gaddress); - - g_clear_handle_id (&self->resync_id, g_source_remove); - - g_free (self->username); - g_free (self->login_username); - g_free (self->homeserver); - g_free (self->device_id); - g_free (self->filter_id); - matrix_utils_free_buffer (self->password); - matrix_utils_free_buffer (self->access_token); - - g_free (self->next_batch); - - G_OBJECT_CLASS (matrix_api_parent_class)->finalize (object); -} - -static void -matrix_api_class_init (MatrixApiClass *klass) -{ - GObjectClass *object_class = G_OBJECT_CLASS (klass); - - object_class->finalize = matrix_api_finalize; -} - - -static void -matrix_api_init (MatrixApi *self) -{ - self->cancellable = g_cancellable_new (); - self->matrix_net = matrix_net_new (); -} - -/** - * matrix_api_new: - * @username: (nullable): A valid matrix user id - * - * Create a new #MatrixApi for @username. For the - * #MatrixApi to be usable password/access token - * and sync_callback should be set. - * - * If @username is not in full form (ie, - * @user:example.com) or an email or phone - * number, homeserver should be set with - * matrix_api_set_homeserver() - * - * Returns: (transfer full): A new #MatrixApi. - * Free with g_object_unref(). - */ -MatrixApi * -matrix_api_new (const char *username) -{ - MatrixApi *self; - - self = g_object_new (MATRIX_TYPE_API, NULL); - self->login_username = g_strdup (username); - - return self; -} - -void -matrix_api_set_enc (MatrixApi *self, - MatrixEnc *enc) -{ - g_return_if_fail (MATRIX_IS_API (self)); - g_return_if_fail (MATRIX_IS_ENC (enc)); - g_return_if_fail (!self->matrix_enc); - - g_set_object (&self->matrix_enc, enc); - - if (self->username && self->device_id) - matrix_enc_set_details (self->matrix_enc, self->username, self->device_id); -} - -/** - * matrix_api_can_connect: - * @self: A #MatrixApi - * - * Check if @self can be connected to homeserver with current - * network state. This function is a bit dumb: returning - * %TRUE shall not ensure that the @self is connectable. - * But if %FALSE is returned, @self shall not be - * able to connect. - */ -gboolean -matrix_api_can_connect (MatrixApi *self) -{ - GNetworkMonitor *nm; - GInetAddress *inet; - - g_return_val_if_fail (MATRIX_IS_API (self), FALSE); - - /* If never tried, assume we can connect */ - if (!self->has_tried_connecting) - return TRUE; - - nm = g_network_monitor_get_default (); - - if (!self->gaddress || !G_IS_INET_SOCKET_ADDRESS (self->gaddress)) - goto end; - - inet = g_inet_socket_address_get_address ((GInetSocketAddress *)self->gaddress); - - if (g_inet_address_get_is_loopback (inet) || - g_inet_address_get_is_site_local (inet)) - return g_network_monitor_can_reach (nm, G_SOCKET_CONNECTABLE (self->gaddress), NULL, NULL); - - end: - /* Distributions may advertise to have full network support event - * when connected only to local network, so this isn't always right */ - return g_network_monitor_get_connectivity (nm) == G_NETWORK_CONNECTIVITY_FULL; -} - -/** - * matrix_api_get_username: - * @self: A #MatrixApi - * - * Get the username of @self. This will be a fully - * qualified Matrix ID (eg: @user:example.com) if - * @self has succeeded in synchronizing with the - * server, otherwise NULL is returned - * - * Please note that this can return a different value - * than the one set with matrix_api_set_login_username(). - * - * Returns: (nullable): The matrix username. - */ -const char * -matrix_api_get_username (MatrixApi *self) -{ - g_return_val_if_fail (MATRIX_IS_API (self), NULL); - - return self->username; -} - -void -matrix_api_set_username (MatrixApi *self, - const char *username) -{ - g_return_if_fail (MATRIX_IS_API (self)); - g_return_if_fail (!self->username); - - if (chatty_utils_username_is_valid (username, CHATTY_PROTOCOL_MATRIX)) - self->username = g_strdup (username); -} - -/** - * matrix_api_get_login_username: - * @self: A #MatrixApi - * - * Get the username as set with - * matrix_api_set_login_username(). - * - * Returns: The matrix username. - */ -const char * -matrix_api_get_login_username (MatrixApi *self) -{ - g_return_val_if_fail (MATRIX_IS_API (self), ""); - - return self->login_username; -} - -/** - * matrix_api_set_login_username: - * @self: A #MatrixApi - * @userame: The usernamed to use for login - * - * Set the username of @self. This is not required to - * be a fully qualified Matrix ID like @user:example.com - * and can also be an email ID or phone number. - * - * username can be set only once. - */ -void -matrix_api_set_login_username (MatrixApi *self, - const char *username) -{ - g_return_if_fail (MATRIX_IS_API (self)); - g_return_if_fail (!self->login_username); - - self->login_username = g_strdup (username); -} - -/** - * matrix_api_get_password: - * @self: A #MatrixApi - * - * Get the password of @self. - * - * Returns: (nullable): The matrix username. - */ -const char * -matrix_api_get_password (MatrixApi *self) -{ - g_return_val_if_fail (MATRIX_IS_API (self), NULL); - - return self->password; -} - -/** - * matrix_api_set_password: - * @self: A #MatrixApi - * @password: A valid password string - * - * Set the password for @self. - */ -void -matrix_api_set_password (MatrixApi *self, - const char *password) -{ - g_return_if_fail (MATRIX_IS_API (self)); - - if (!password || !*password) - return; - - matrix_utils_free_buffer (self->password); - self->password = g_strdup (password); -} - -/** - * matrix_api_set_sync_callback: - * @self: A #MatrixApi - * @callback: A #MatriCallback - * @object: A #GObject - * - * Set sync callback. It’s allowed to set callback - * only once. - * - * @object should be a #GObject (derived) object. - * - * callback shall run as `callback(@object, ...)` - * - * @callback shall be run for every event that’s worth - * informing (Say, the callback won’t be run if the - * sync response is empty). - * - * The @callback may run with a %NULL #GAsyncResult - * argument. Check the sync state before handling - * the #GAsyncResult. See matrix_api_get_sync_state(). - */ -void -matrix_api_set_sync_callback (MatrixApi *self, - MatrixCallback callback, - gpointer object) -{ - g_return_if_fail (MATRIX_IS_API (self)); - g_return_if_fail (callback); - g_return_if_fail (G_IS_OBJECT (object)); - g_return_if_fail (!self->callback); - - self->callback = callback; - g_set_weak_pointer (&self->cb_object, object); -} - -const char * -matrix_api_get_homeserver (MatrixApi *self) -{ - g_return_val_if_fail (MATRIX_IS_API (self), NULL); - - return self->homeserver; -} - -void -matrix_api_set_homeserver (MatrixApi *self, - const char *homeserver) -{ - g_autoptr(SoupURI) uri = NULL; - GString *host; - - g_return_if_fail (MATRIX_IS_API (self)); - - uri = soup_uri_new (homeserver); - if (!homeserver || !uri || - !SOUP_URI_VALID_FOR_HTTP (uri)) - return; - - host = g_string_new (NULL); - g_string_append (host, soup_uri_get_scheme (uri)); - g_string_append (host, "://"); - g_string_append (host, uri->host); - if (!soup_uri_uses_default_port (uri)) - g_string_append_printf (host, ":%d", soup_uri_get_port (uri)); - - g_free (self->homeserver); - self->homeserver = g_string_free (host, FALSE); - - matrix_net_set_homeserver (self->matrix_net, self->homeserver); - - if (self->is_sync && - self->sync_failed && - self->action == MATRIX_GET_HOMESERVER) { - self->sync_failed = FALSE; - matrix_verify_homeserver (self); - } -} - -/** - * matrix_api_get_device_id: - * @self: A #MatrixApi - * - * Get the device ID of @self. If the - * account login succeeded, the device - * ID provided by the server is returned. - * Otherwise, the one set with @self is - * returned. - * - * Please not that the user login is done - * only if @self has no access-token set, - * or if the acces-token is invalid. - * - * Returns: (nullable): The Device ID. - */ -const char * -matrix_api_get_device_id (MatrixApi *self) -{ - g_return_val_if_fail (MATRIX_IS_API (self), NULL); - - return self->device_id; -} - -const char * -matrix_api_get_access_token (MatrixApi *self) -{ - g_return_val_if_fail (MATRIX_IS_API (self), NULL); - - return self->access_token; -} - -void -matrix_api_set_access_token (MatrixApi *self, - const char *access_token, - const char *device_id) -{ - g_return_if_fail (MATRIX_IS_API (self)); - - g_clear_pointer (&self->access_token, matrix_utils_free_buffer); - g_clear_pointer (&self->device_id, g_free); - - if (!access_token || !device_id) - return; - - self->access_token = g_strdup (access_token); - self->device_id = g_strdup (device_id); - matrix_net_set_access_token (self->matrix_net, self->access_token); - - if (self->matrix_enc && self->username) - matrix_enc_set_details (self->matrix_enc, self->username, self->device_id); -} - -const char * -matrix_api_get_next_batch (MatrixApi *self) -{ - g_return_val_if_fail (MATRIX_IS_API (self), NULL); - - return self->next_batch; -} - -void -matrix_api_set_next_batch (MatrixApi *self, - const char *next_batch) -{ - g_return_if_fail (MATRIX_IS_API (self)); - g_return_if_fail (!self->next_batch); - - if (next_batch) - self->full_state_loaded = TRUE; - - self->next_batch = g_strdup (next_batch); -} - -/** - * matrix_api_start_sync: - * @self: A #MatrixApi - * - * Start synchronizing with the matrix server. - * - * If a sync process is already in progress - * this function simply returns. - * - * The process is: - * 1. Get home server (if required) - * 2. Verify homeserver Server-Client API - * 3. If access token set, start sync - * 4. Else login with password - */ -void -matrix_api_start_sync (MatrixApi *self) -{ - g_return_if_fail (MATRIX_IS_API (self)); - g_return_if_fail (self->callback); - g_return_if_fail (self->login_username); - - if (self->is_sync && !self->sync_failed) - return; - - if (g_cancellable_is_cancelled (self->cancellable)) { - g_object_unref (self->cancellable); - self->cancellable = g_cancellable_new (); - } - - matrix_start_sync (self); -} - -gboolean -matrix_api_is_sync (MatrixApi *self) -{ - g_return_val_if_fail (MATRIX_IS_API (self), FALSE); - - return self->access_token && self->login_success && - self->is_sync && !self->sync_failed; -} - -void -matrix_api_stop_sync (MatrixApi *self) -{ - g_return_if_fail (MATRIX_IS_API (self)); - - g_clear_handle_id (&self->resync_id, g_source_remove); - g_cancellable_cancel (self->cancellable); - self->is_sync = FALSE; - self->sync_failed = FALSE; -} - -void -matrix_api_set_upload_key (MatrixApi *self, - char *key) -{ - g_return_if_fail (MATRIX_IS_API (self)); - g_return_if_fail (key && *key); - - g_free (self->key); - self->key = key; - - if (self->is_sync && self->action == MATRIX_RED_PILL) - matrix_upload_key (self); -} - -void -matrix_api_set_typing (MatrixApi *self, - const char *room_id, - gboolean is_typing) -{ - g_autofree char *uri = NULL; - JsonObject *object; - - g_return_if_fail (MATRIX_IS_API (self)); - - CHATTY_TRACE_MSG ("Update typing: %d", !!is_typing); - /* https://matrix.org/docs/spec/client_server/r0.6.1#put-matrix-client-r0-rooms-roomid-typing-userid */ - object = json_object_new (); - json_object_set_boolean_member (object, "typing", !!is_typing); - if (is_typing) - json_object_set_int_member (object, "timeout", TYPING_TIMEOUT); - - uri = g_strconcat (self->homeserver, "/_matrix/client/r0/rooms/", - room_id, "/typing/", self->username, NULL); - - matrix_net_send_json_async (self->matrix_net, 0, object, uri, SOUP_METHOD_PUT, - NULL, self->cancellable, matrix_send_typing_cb, NULL); -} - -void -matrix_api_get_room_state_async (MatrixApi *self, - const char *room_id, - GAsyncReadyCallback callback, - gpointer user_data) -{ - g_autofree char *uri = NULL; - GTask *task; - - g_return_if_fail (MATRIX_IS_API (self)); - g_return_if_fail (room_id && *room_id); - - task = g_task_new (self, self->cancellable, callback, user_data); - - uri = g_strconcat ("/_matrix/client/r0/rooms/", room_id, "/state", NULL); - matrix_net_send_json_async (self->matrix_net, -1, NULL, uri, SOUP_METHOD_GET, - NULL, self->cancellable, matrix_get_room_state_cb, task); -} - -JsonArray * -matrix_api_get_room_state_finish (MatrixApi *self, - GAsyncResult *result, - GError **error) -{ - g_return_val_if_fail (MATRIX_IS_API (self), NULL); - g_return_val_if_fail (G_IS_TASK (result), NULL); - g_return_val_if_fail (!error || !*error, NULL); - - return g_task_propagate_pointer (G_TASK (result), error); -} - -static void -matrix_get_room_users_cb (GObject *obj, - GAsyncResult *result, - gpointer user_data) -{ - g_autoptr(GTask) task = user_data; - JsonObject *object; - GError *error = NULL; - - g_assert (G_IS_TASK (task)); - - object = g_task_propagate_pointer (G_TASK (result), &error); - - if (error) { - if (!g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CANCELLED)) - g_debug ("Error getting room members: %s", error->message); - g_task_return_error (task, error); - } else { - g_task_return_pointer (task, object, (GDestroyNotify)json_object_unref); - } -} - -void -matrix_api_get_room_users_async (MatrixApi *self, - const char *room_id, - GAsyncReadyCallback callback, - gpointer user_data) -{ - g_autofree char *uri = NULL; - GTask *task; - - g_return_if_fail (MATRIX_IS_API (self)); - g_return_if_fail (room_id && *room_id); - - task = g_task_new (self, self->cancellable, callback, user_data); - - uri = g_strconcat ("/_matrix/client/r0/rooms/", room_id, "/joined_members", NULL); - matrix_net_send_json_async (self->matrix_net, -1, NULL, uri, SOUP_METHOD_GET, - NULL, self->cancellable, matrix_get_room_users_cb, task); -} - -JsonObject * -matrix_api_get_room_users_finish (MatrixApi *self, - GAsyncResult *result, - GError **error) -{ - g_return_val_if_fail (MATRIX_IS_API (self), NULL); - g_return_val_if_fail (G_IS_TASK (result), NULL); - g_return_val_if_fail (!error || !*error, NULL); - - return g_task_propagate_pointer (G_TASK (result), error); -} - -void -matrix_api_get_room_name_async (MatrixApi *self, - const char *room_id, - GAsyncReadyCallback callback, - gpointer user_data) -{ - g_autofree char *uri = NULL; - GTask *task; - - g_return_if_fail (MATRIX_IS_API (self)); - g_return_if_fail (room_id && *room_id); - - task = g_task_new (self, self->cancellable, callback, user_data); - uri = g_strconcat ("/_matrix/client/r0/rooms/", room_id, "/state/m.room.name", NULL); - matrix_net_send_json_async (self->matrix_net, -1, NULL, uri, SOUP_METHOD_GET, - NULL, self->cancellable, matrix_get_room_name_cb, task); -} - -JsonObject * -matrix_api_get_room_name_finish (MatrixApi *self, - GAsyncResult *result, - GError **error) -{ - g_return_val_if_fail (MATRIX_IS_API (self), NULL); - g_return_val_if_fail (G_IS_TASK (result), NULL); - g_return_val_if_fail (!error || !*error, NULL); - - return g_task_propagate_pointer (G_TASK (result), error); -} - -void -matrix_api_get_room_encryption_async (MatrixApi *self, - const char *room_id, - GAsyncReadyCallback callback, - gpointer user_data) -{ - g_autofree char *uri = NULL; - GTask *task; - - g_return_if_fail (MATRIX_IS_API (self)); - g_return_if_fail (room_id && *room_id); - - task = g_task_new (self, self->cancellable, callback, user_data); - uri = g_strconcat ("/_matrix/client/r0/rooms/", room_id, "/state/m.room.encryption", NULL); - matrix_net_send_json_async (self->matrix_net, -1, NULL, uri, SOUP_METHOD_GET, - NULL, self->cancellable, matrix_get_room_encryption_cb, task); -} - -char * -matrix_api_get_room_encryption_finish (MatrixApi *self, - GAsyncResult *result, - GError **error) -{ - g_return_val_if_fail (MATRIX_IS_API (self), NULL); - g_return_val_if_fail (G_IS_TASK (result), NULL); - g_return_val_if_fail (!error || !*error, NULL); - - return g_task_propagate_pointer (G_TASK (result), error); -} - -/** - * matrix_api_set_room_encryption_async: - * @self: A #MatrixApi - * @room_id: The room id to set encryption for - * @callback: A #GAsyncReadyCallback - * @user_data: user data passed to @callback - * - * Calling this method shall enable encryption. - * There is no way to disable encryption once - * enabled. - * - * To get the result, finish the call with - * matrix_api_set_room_encryption_finish() - */ -void -matrix_api_set_room_encryption_async (MatrixApi *self, - const char *room_id, - GAsyncReadyCallback callback, - gpointer user_data) -{ - g_autofree char *uri = NULL; - JsonObject *object; - GTask *task; - - g_return_if_fail (MATRIX_IS_API (self)); - g_return_if_fail (room_id && *room_id); - - task = g_task_new (self, self->cancellable, callback, user_data); - object = json_object_new (); - json_object_set_string_member (object, "algorithm", ALGORITHM_MEGOLM); - uri = g_strconcat ("/_matrix/client/r0/rooms/", room_id, "/state/m.room.encryption", NULL); - matrix_net_send_json_async (self->matrix_net, 2, object, uri, SOUP_METHOD_PUT, - NULL, self->cancellable, matrix_set_room_encryption_cb, task); -} - -gboolean -matrix_api_set_room_encryption_finish (MatrixApi *self, - GAsyncResult *result, - GError **error) -{ - g_return_val_if_fail (MATRIX_IS_API (self), FALSE); - g_return_val_if_fail (G_IS_TASK (result), FALSE); - g_return_val_if_fail (!error || !*error, FALSE); - - return g_task_propagate_boolean (G_TASK (result), error); -} - -void -matrix_api_get_members_async (MatrixApi *self, - const char *room_id, - GAsyncReadyCallback callback, - gpointer user_data) -{ - g_autofree char *uri = NULL; - GHashTable *query; - GTask *task; - - g_return_if_fail (MATRIX_IS_API (self)); - - task = g_task_new (self, self->cancellable, callback, user_data); - - query = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, - (GDestroyNotify)matrix_utils_free_buffer); - g_hash_table_insert (query, g_strdup ("membership"), g_strdup ("join")); - - if (self->next_batch) - g_hash_table_insert (query, g_strdup ("since"), g_strdup (self->next_batch)); - - /* https://matrix.org/docs/spec/client_server/r0.6.1#get-matrix-client-r0-rooms-roomid-members */ - uri = g_strconcat ("/_matrix/client/r0/rooms/", room_id, "/members", NULL); - matrix_net_send_json_async (self->matrix_net, -1, NULL, uri, SOUP_METHOD_GET, - query, self->cancellable, matrix_get_members_cb, task); -} - -JsonObject * -matrix_api_get_members_finish (MatrixApi *self, - GAsyncResult *result, - GError **error) -{ - g_return_val_if_fail (MATRIX_IS_API (self), NULL); - g_return_val_if_fail (G_IS_TASK (result), NULL); - g_return_val_if_fail (!error || !*error, NULL); - - return g_task_propagate_pointer (G_TASK (result), error); -} - -void -matrix_api_load_prev_batch_async (MatrixApi *self, - const char *room_id, - char *prev_batch, - char *last_batch, - GAsyncReadyCallback callback, - gpointer user_data) -{ - g_autofree char *uri = NULL; - GHashTable *query; - GTask *task; - - if (!prev_batch) - return; - - g_return_if_fail (MATRIX_IS_API (self)); - - task = g_task_new (self, self->cancellable, callback, user_data); - - /* Create a query to get past 30 messages */ - query = g_hash_table_new_full (g_str_hash, g_str_equal, free, - (GDestroyNotify)matrix_utils_free_buffer); - g_hash_table_insert (query, g_strdup ("from"), g_strdup (prev_batch)); - g_hash_table_insert (query, g_strdup ("dir"), g_strdup ("b")); - g_hash_table_insert (query, g_strdup ("limit"), g_strdup ("30")); - if (last_batch) - g_hash_table_insert (query, g_strdup ("to"), g_strdup (last_batch)); - - CHATTY_TRACE_MSG ("Load prev-batch"); - /* https://matrix.org/docs/spec/client_server/r0.6.1#get-matrix-client-r0-rooms-roomid-messages */ - uri = g_strconcat ("/_matrix/client/r0/rooms/", room_id, "/messages", NULL); - matrix_net_send_json_async (self->matrix_net, 0, NULL, uri, SOUP_METHOD_GET, - query, self->cancellable, matrix_get_messages_cb, task); -} - -JsonObject * -matrix_api_load_prev_batch_finish (MatrixApi *self, - GAsyncResult *result, - GError **error) -{ - g_return_val_if_fail (MATRIX_IS_API (self), NULL); - g_return_val_if_fail (G_IS_TASK (result), NULL); - g_return_val_if_fail (!error || !*error, NULL); - - return g_task_propagate_pointer (G_TASK (result), error); -} - -/** - * matrix_api_query_keys_async: - * @self: A #MatrixApi - * @member_list: A #GListModel of #ChattyMaBuddy - * @token: (nullable): A 'since' token string - * @callback: A #GAsyncReadyCallback - * @user_data: user data passed to @callback - * - * Get identity keys of all devices in @member_list. - * Pass in @token (obtained via the "since" in /sync) - * if only the device changes since the corresponding - * /sync is needed. - * - * Finish the call with matrix_api_query_keys_finish() - * to get the result. - */ -void -matrix_api_query_keys_async (MatrixApi *self, - GListModel *member_list, - const char *token, - GAsyncReadyCallback callback, - gpointer user_data) -{ - JsonObject *object, *child; - GTask *task; - guint n_items; - - g_return_if_fail (MATRIX_IS_API (self)); - g_return_if_fail (G_IS_LIST_MODEL (member_list)); - g_return_if_fail (g_list_model_get_item_type (member_list) == CHATTY_TYPE_MA_BUDDY); - g_return_if_fail (g_list_model_get_n_items (member_list) > 0); - - /* https://matrix.org/docs/spec/client_server/r0.6.1#post-matrix-client-r0-keys-query */ - object = json_object_new (); - json_object_set_int_member (object, "timeout", KEY_TIMEOUT); - if (token) - json_object_set_string_member (object, "token", token); - - n_items = g_list_model_get_n_items (member_list); - child = json_object_new (); - - for (guint i = 0; i < n_items; i++) { - g_autoptr(ChattyMaBuddy) buddy = NULL; - - buddy = g_list_model_get_item (member_list, i); - json_object_set_array_member (child, - chatty_item_get_username (CHATTY_ITEM (buddy)), - json_array_new ()); - } - - json_object_set_object_member (object, "device_keys", child); - CHATTY_TRACE_MSG ("Query keys of %u members", n_items); - - task = g_task_new (self, self->cancellable, callback, user_data); - - matrix_net_send_json_async (self->matrix_net, 0, object, - "/_matrix/client/r0/keys/query", SOUP_METHOD_POST, - NULL, self->cancellable, matrix_keys_query_cb, task); -} - -JsonObject * -matrix_api_query_keys_finish (MatrixApi *self, - GAsyncResult *result, - GError **error) -{ - g_return_val_if_fail (MATRIX_IS_API (self), NULL); - g_return_val_if_fail (G_IS_TASK (result), NULL); - g_return_val_if_fail (!error || !*error, NULL); - - return g_task_propagate_pointer (G_TASK (result), error); -} - -/** - * matrix_api_claim_keys_async: - * @self: A #MatrixApi - * @member_list: A #GListModel of #ChattyMaBuddy - * @callback: A #GAsyncReadyCallback - * @user_data: user data passed to @callback - * - * Claim a key for all devices of @members_list - */ -void -matrix_api_claim_keys_async (MatrixApi *self, - GListModel *member_list, - GAsyncReadyCallback callback, - gpointer user_data) -{ - JsonObject *object, *child; - GTask *task; - - g_return_if_fail (MATRIX_IS_API (self)); - g_return_if_fail (G_IS_LIST_MODEL (member_list)); - g_return_if_fail (g_list_model_get_item_type (member_list) == CHATTY_TYPE_MA_BUDDY); - g_return_if_fail (g_list_model_get_n_items (member_list) > 0); - - /* https://matrix.org/docs/spec/client_server/r0.6.1#post-matrix-client-r0-keys-claim */ - object = json_object_new (); - json_object_set_int_member (object, "timeout", KEY_TIMEOUT); - - child = json_object_new (); - - for (guint i = 0; i < g_list_model_get_n_items (member_list); i++) { - g_autoptr(ChattyMaBuddy) buddy = NULL; - JsonObject *key_json; - - buddy = g_list_model_get_item (member_list, i); - key_json = chatty_ma_buddy_device_key_json (buddy); - - if (key_json) - json_object_set_object_member (child, - chatty_item_get_username (CHATTY_ITEM (buddy)), - key_json); - } - - json_object_set_object_member (object, "one_time_keys", child); - CHATTY_TRACE_MSG ("Claiming keys"); - - task = g_task_new (self, self->cancellable, callback, user_data); - - matrix_net_send_json_async (self->matrix_net, 0, object, - "/_matrix/client/r0/keys/claim", SOUP_METHOD_POST, - NULL, self->cancellable, matrix_keys_claim_cb, task); -} - -JsonObject * -matrix_api_claim_keys_finish (MatrixApi *self, - GAsyncResult *result, - GError **error) -{ - g_return_val_if_fail (MATRIX_IS_API (self), NULL); - g_return_val_if_fail (G_IS_TASK (result), NULL); - g_return_val_if_fail (!error || !*error, NULL); - - return g_task_propagate_pointer (G_TASK (result), error); -} - -void -matrix_api_get_file_async (MatrixApi *self, - ChattyMessage *message, - ChattyFileInfo *file, - GCancellable *cancellable, - GFileProgressCallback progress_callback, - GAsyncReadyCallback callback, - gpointer user_data) -{ - g_autoptr(GTask) task = NULL; - - g_return_if_fail (MATRIX_IS_API (self)); - g_return_if_fail (!message || CHATTY_IS_MESSAGE (message)); - - if (message) - g_object_ref (message); - - task = g_task_new (self, cancellable, callback, user_data); - - g_object_set_data (G_OBJECT (task), "file", file); - g_object_set_data_full (G_OBJECT (task), "message", message, g_object_unref); - - if (file->status != CHATTY_FILE_UNKNOWN) { - g_task_return_new_error (task, G_IO_ERROR, G_IO_ERROR_FAILED, - "Download not required"); - return; - } - - matrix_net_get_file_async (self->matrix_net, message, - file, cancellable, - progress_callback, - api_get_file_cb, - g_steal_pointer (&task)); -} - -gboolean -matrix_api_get_file_finish (MatrixApi *self, - GAsyncResult *result, - GError **error) -{ - g_return_val_if_fail (MATRIX_IS_API (self), FALSE); - g_return_val_if_fail (G_IS_TASK (result), FALSE); - g_return_val_if_fail (!error || !*error, FALSE); - - return g_task_propagate_boolean (G_TASK (result), error); -} - -static void -api_send_message_encrypted (MatrixApi *self, - const char *room_id, - ChattyMessage *message, - JsonObject *content, - GTask *task) -{ - g_autofree char *text = NULL; - g_autofree char *uri = NULL; - JsonObject *root; - char *id; - - g_assert (MATRIX_IS_API (self)); - g_assert (content); - - root = json_object_new (); - json_object_set_string_member (root, "type", "m.room.message"); - json_object_set_string_member (root, "room_id", room_id); - json_object_set_object_member (root, "content", content); - - text = matrix_utils_json_object_to_string (root, FALSE); - json_object_unref (root); - root = matrix_enc_encrypt_for_chat (self->matrix_enc, room_id, text); - - self->event_id++; - id = g_strdup_printf ("m%"G_GINT64_FORMAT".%d", - g_get_real_time () / G_TIME_SPAN_MILLISECOND, - self->event_id); - g_object_set_data_full (G_OBJECT (message), "event-id", id, g_free); - - g_object_set_data_full (G_OBJECT (task), "message", g_object_ref (message), - g_object_unref); - CHATTY_DEBUG (room_id, "Sending encrypted message. event id: %s, room:", id); - - uri = g_strdup_printf ("/_matrix/client/r0/rooms/%s/send/m.room.encrypted/%s", room_id, id); - - matrix_net_send_json_async (self->matrix_net, 0, root, uri, SOUP_METHOD_PUT, - NULL, self->cancellable, api_send_message_cb, task); -} - -static void -api_send_message (MatrixApi *self, - const char *room_id, - ChattyMessage *message, - JsonObject *content, - GTask *task) -{ - g_autofree char *uri = NULL; - char *id; - - g_assert (MATRIX_IS_API (self)); - g_assert (content); - - self->event_id++; - id = g_strdup_printf ("m%"G_GINT64_FORMAT".%d", - g_get_real_time () / G_TIME_SPAN_MILLISECOND, - self->event_id); - g_object_set_data_full (G_OBJECT (message), "event-id", id, g_free); - - g_object_set_data_full (G_OBJECT (task), "message", g_object_ref (message), - g_object_unref); - - CHATTY_DEBUG (room_id, "Sending message. event id: %s, room:", id); - - /* https://matrix.org/docs/spec/client_server/r0.6.1#put-matrix-client-r0-rooms-roomid-send-eventtype-txnid */ - uri = g_strdup_printf ("/_matrix/client/r0/rooms/%s/send/m.room.message/%s", room_id, id); - matrix_net_send_json_async (self->matrix_net, 0, content, uri, SOUP_METHOD_PUT, - NULL, self->cancellable, api_send_message_cb, task); -} - -void -matrix_api_send_message_async (MatrixApi *self, - ChattyChat *chat, - const char *room_id, - ChattyMessage *message, - GAsyncReadyCallback callback, - gpointer user_data) -{ - JsonObject *object; - GTask *task; - - g_return_if_fail (MATRIX_IS_API (self)); - g_return_if_fail (CHATTY_IS_MESSAGE (message)); - - task = g_task_new (self, self->cancellable, callback, user_data); - object = json_object_new (); - json_object_set_string_member (object, "msgtype", "m.text"); - json_object_set_string_member (object, "body", chatty_message_get_text (message)); - - if (chatty_chat_get_encryption (chat) == CHATTY_ENCRYPTION_ENABLED) - api_send_message_encrypted (self, room_id, message, object, task); - else - api_send_message (self, room_id, message, object, task); -} - -gboolean -matrix_api_send_message_finish (MatrixApi *self, - GAsyncResult *result, - GError **error) -{ - g_return_val_if_fail (MATRIX_IS_API (self), FALSE); - g_return_val_if_fail (G_IS_TASK (result), FALSE); - g_return_val_if_fail (!error || !*error, FALSE); - - return g_task_propagate_boolean (G_TASK (result), error); -} - -void -matrix_api_set_read_marker_async (MatrixApi *self, - const char *room_id, - ChattyMessage *message, - GAsyncReadyCallback callback, - gpointer user_data) -{ - g_autofree char *uri = NULL; - JsonObject *root; - const char *id; - GTask *task; - - g_return_if_fail (MATRIX_IS_API (self)); - g_return_if_fail (CHATTY_IS_MESSAGE (message)); - - id = chatty_message_get_uid (message); - root = json_object_new (); - json_object_set_string_member (root, "m.fully_read", id); - json_object_set_string_member (root, "m.read", id); - - CHATTY_TRACE_MSG ("Marking is read, message id: %s", id); - - task = g_task_new (self, self->cancellable, callback, user_data); - - uri = g_strdup_printf ("/_matrix/client/r0/rooms/%s/read_markers", room_id); - matrix_net_send_json_async (self->matrix_net, 0, root, uri, SOUP_METHOD_POST, - NULL, self->cancellable, api_set_read_marker_cb, task); -} - -gboolean -matrix_api_set_read_marker_finish (MatrixApi *self, - GAsyncResult *result, - GError **error) -{ - g_return_val_if_fail (MATRIX_IS_API (self), FALSE); - g_return_val_if_fail (G_IS_TASK (result), FALSE); - g_return_val_if_fail (!error || !*error, FALSE); - - return g_task_propagate_boolean (G_TASK (result), error); -} - -void -matrix_api_upload_group_keys_async (MatrixApi *self, - const char *room_id, - GListModel *member_list, - GAsyncReadyCallback callback, - gpointer user_data) -{ - g_autofree char *uri = NULL; - JsonObject *root, *object; - GTask *task; - - g_return_if_fail (MATRIX_IS_API (self)); - g_return_if_fail (G_IS_LIST_MODEL (member_list)); - g_return_if_fail (g_list_model_get_item_type (member_list) == CHATTY_TYPE_MA_BUDDY); - g_return_if_fail (g_list_model_get_n_items (member_list) > 0); - - root = json_object_new (); - object = matrix_enc_create_out_group_keys (self->matrix_enc, room_id, member_list); - json_object_set_object_member (root, "messages", object); - - task = g_task_new (self, self->cancellable, callback, user_data); - - self->event_id++; - uri = g_strdup_printf ("/_matrix/client/r0/sendToDevice/m.room.encrypted/m%"G_GINT64_FORMAT".%d", - g_get_real_time () / G_TIME_SPAN_MILLISECOND, - self->event_id); - matrix_net_send_json_async (self->matrix_net, 0, root, uri, SOUP_METHOD_PUT, - NULL, self->cancellable, api_upload_group_keys_cb, task); -} - -gboolean -matrix_api_upload_group_keys_finish (MatrixApi *self, - GAsyncResult *result, - GError **error) -{ - g_return_val_if_fail (MATRIX_IS_API (self), FALSE); - g_return_val_if_fail (G_IS_TASK (result), FALSE); - g_return_val_if_fail (!error || !*error, FALSE); - - return g_task_propagate_boolean (G_TASK (result), error); -} - -static void -api_leave_room_cb (GObject *obj, - GAsyncResult *result, - gpointer user_data) -{ - MatrixApi *self; - g_autoptr(GTask) task = user_data; - g_autoptr(JsonObject) object = NULL; - GError *error = NULL; - - g_assert (G_IS_TASK (task)); - - self = g_task_get_source_object (task); - g_assert (MATRIX_IS_API (self)); - - object = g_task_propagate_pointer (G_TASK (result), &error); - - CHATTY_DEBUG (g_task_get_task_data (G_TASK (task)), - "Leaving room %s", CHATTY_LOG_SUCESS (!error)); - - if (error && !g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CANCELLED)) - g_debug ("Error leaving room: %s", error->message); - - if (error) - g_task_return_error (task, error); - else - g_task_return_boolean (task, TRUE); -} - -void -matrix_api_leave_chat_async (MatrixApi *self, - const char *room_id, - GAsyncReadyCallback callback, - gpointer user_data) -{ - GTask *task; - g_autofree char *uri = NULL; - - g_return_if_fail (MATRIX_IS_API (self)); - g_return_if_fail (room_id && *room_id == '!'); - - CHATTY_DEBUG (room_id, "Leaving room"); - - task = g_task_new (self, self->cancellable, callback, user_data); - g_task_set_task_data (task, g_strdup (room_id), g_free); - uri = g_strdup_printf ("/_matrix/client/r0/rooms/%s/leave", room_id); - matrix_net_send_json_async (self->matrix_net, 1, NULL, uri, SOUP_METHOD_POST, - NULL, self->cancellable, api_leave_room_cb, task); -} - -gboolean -matrix_api_leave_chat_finish (MatrixApi *self, - GAsyncResult *result, - GError **error) -{ - g_return_val_if_fail (MATRIX_IS_API (self), FALSE); - g_return_val_if_fail (G_IS_TASK (result), FALSE); - g_return_val_if_fail (!error || !*error, FALSE); - - return g_task_propagate_boolean (G_TASK (result), error); -} - -static void -api_get_user_info_cb (GObject *obj, - GAsyncResult *result, - gpointer user_data) -{ - MatrixApi *self; - g_autoptr(GTask) task = user_data; - const char *name, *avatar_url; - GError *error = NULL; - g_autoptr(JsonObject) object = NULL; - - g_assert (G_IS_TASK (task)); - - self = g_task_get_source_object (task); - g_assert (MATRIX_IS_API (self)); - - object = g_task_propagate_pointer (G_TASK (result), &error); - - CHATTY_TRACE_MSG ("Getting user info. success: %d", !error); - - if (error) { - g_task_return_error (task, error); - return; - } - - name = matrix_utils_json_object_get_string (object, "displayname"); - avatar_url = matrix_utils_json_object_get_string (object, "avatar_url"); - - g_object_set_data_full (G_OBJECT (task), "name", g_strdup (name), g_free); - g_object_set_data_full (G_OBJECT (task), "avatar-url", g_strdup (avatar_url), g_free); - - g_task_return_boolean (task, TRUE); -} - -void -matrix_api_get_user_info_async (MatrixApi *self, - const char *user_id, - GCancellable *cancellable, - GAsyncReadyCallback callback, - gpointer user_data) -{ - g_autofree char *uri = NULL; - GTask *task; - - g_return_if_fail (MATRIX_IS_API (self)); - g_return_if_fail (!cancellable || G_IS_CANCELLABLE (cancellable)); - - if (!user_id) - user_id = self->username; - - task = g_task_new (self, cancellable, callback, user_data); - - if (!user_id) { - g_task_return_new_error (task, G_IO_ERROR, G_IO_ERROR_INVALID_ARGUMENT, - "No user id provided"); - return; - } - - CHATTY_TRACE (user_id, "Getting user info: "); - - g_task_set_task_data (task, g_strdup (user_id), g_free); - uri = g_strdup_printf ("/_matrix/client/r0/profile/%s", user_id); - matrix_net_send_json_async (self->matrix_net, 1, NULL, uri, SOUP_METHOD_GET, - NULL, self->cancellable, api_get_user_info_cb, task); -} - -gboolean -matrix_api_get_user_info_finish (MatrixApi *self, - char **name, - char **avatar_url, - GAsyncResult *result, - GError **error) -{ - g_return_val_if_fail (MATRIX_IS_API (self), FALSE); - g_return_val_if_fail (G_IS_TASK (result), FALSE); - g_return_val_if_fail (!error || !*error, FALSE); - - *name = g_strdup (g_object_get_data (G_OBJECT (result), "name")); - *avatar_url = g_strdup (g_object_get_data (G_OBJECT (result), "avatar-url")); - - return g_task_propagate_boolean (G_TASK (result), error); -} - -static void -api_set_name_cb (GObject *obj, - GAsyncResult *result, - gpointer user_data) -{ - MatrixApi *self; - g_autoptr(GTask) task = user_data; - g_autoptr(JsonObject) object = NULL; - GError *error = NULL; - - g_assert (G_IS_TASK (task)); - - self = g_task_get_source_object (task); - g_assert (MATRIX_IS_API (self)); - - object = g_task_propagate_pointer (G_TASK (result), &error); - - CHATTY_DEBUG (self->username, "Setting name to '%s' %s for user", - (char *)g_task_get_task_data (task), CHATTY_LOG_SUCESS (!error)); - - if (error) - g_task_return_error (task, error); - else - g_task_return_boolean (task, TRUE); -} - -void -matrix_api_set_name_async (MatrixApi *self, - const char *name, - GCancellable *cancellable, - GAsyncReadyCallback callback, - gpointer user_data) -{ - g_autofree char *uri = NULL; - JsonObject *root = NULL; - GTask *task; - - g_return_if_fail (MATRIX_IS_API (self)); - g_return_if_fail (!cancellable || G_IS_CANCELLABLE (cancellable)); - - CHATTY_DEBUG (self->username, "Setting name to '%s' for user", name); - - if (name && *name) { - root = json_object_new (); - json_object_set_string_member (root, "displayname", name); - } - - task = g_task_new (self, cancellable, callback, user_data); - g_task_set_task_data (task, g_strdup (name), g_free); - - uri = g_strdup_printf ("/_matrix/client/r0/profile/%s/displayname", self->username); - matrix_net_send_json_async (self->matrix_net, 1, root, uri, SOUP_METHOD_PUT, - NULL, self->cancellable, api_set_name_cb, task); -} - -gboolean -matrix_api_set_name_finish (MatrixApi *self, - GAsyncResult *result, - GError **error) -{ - g_return_val_if_fail (MATRIX_IS_API (self), FALSE); - g_return_val_if_fail (G_IS_TASK (result), FALSE); - g_return_val_if_fail (!error || !*error, FALSE); - - return g_task_propagate_boolean (G_TASK (result), error); -} - -static void -api_set_user_avatar_cb (GObject *obj, - GAsyncResult *result, - gpointer user_data) -{ - MatrixApi *self; - g_autoptr(GTask) task = user_data; - g_autoptr(JsonObject) object = NULL; - GError *error = NULL; - - g_assert (G_IS_TASK (task)); - - self = g_task_get_source_object (task); - g_assert (MATRIX_IS_API (self)); - - object = g_task_propagate_pointer (G_TASK (result), &error); - - CHATTY_DEBUG (self->username, "Setting avatar %s, user:", CHATTY_LOG_SUCESS (!error)); - - if (error) - g_task_return_error (task, error); - else - g_task_return_boolean (task, TRUE); -} - -void -matrix_api_set_user_avatar_async (MatrixApi *self, - const char *file_name, - GCancellable *cancellable, - GAsyncReadyCallback callback, - gpointer user_data) -{ - g_autoptr(GTask) task = NULL; - - g_return_if_fail (MATRIX_IS_API (self)); - g_return_if_fail (!cancellable || G_IS_CANCELLABLE (cancellable)); - - task = g_task_new (self, cancellable, callback, user_data); - - if (!file_name) { - g_autofree char *uri = NULL; - const char *data; - - data = "{\"avatar_url\":\"\"}"; - uri = g_strdup_printf ("/_matrix/client/r0/profile/%s/avatar_url", self->username); - matrix_net_send_data_async (self->matrix_net, 2, g_strdup (data), strlen (data), - uri, SOUP_METHOD_PUT, NULL, self->cancellable, - api_set_user_avatar_cb, g_steal_pointer (&task)); - } else { - g_task_return_new_error (task, G_IO_ERROR, G_IO_ERROR_NOT_SUPPORTED, - "Setting new user avatar not implemented"); - } -} - -gboolean -matrix_api_set_user_avatar_finish (MatrixApi *self, - GAsyncResult *result, - GError **error) -{ - g_return_val_if_fail (MATRIX_IS_API (self), FALSE); - g_return_val_if_fail (G_IS_TASK (result), FALSE); - g_return_val_if_fail (!error || !*error, FALSE); - - return g_task_propagate_boolean (G_TASK (result), error); -} - -static void -api_get_3pid_cb (GObject *obj, - GAsyncResult *result, - gpointer user_data) -{ - MatrixApi *self; - g_autoptr(GTask) task = user_data; - GPtrArray *emails, *phones; - GError *error = NULL; - g_autoptr(JsonObject) object = NULL; - JsonArray *array; - guint length; - - g_assert (G_IS_TASK (task)); - - self = g_task_get_source_object (task); - g_assert (MATRIX_IS_API (self)); - - object = g_task_propagate_pointer (G_TASK (result), &error); - array = matrix_utils_json_object_get_array (object, "threepids"); - - CHATTY_DEBUG (self->username, "Getting 3pid %s, user:", CHATTY_LOG_SUCESS (!error)); - - if (!array) { - if (error) - g_task_return_error (task, error); - else - g_task_return_boolean (task, FALSE); - - return; - } - - emails = g_ptr_array_new_full (1, g_free); - phones = g_ptr_array_new_full (1, g_free); - - length = json_array_get_length (array); - - for (guint i = 0; i < length; i++) { - const char *type, *value; - - object = json_array_get_object_element (array, i); - value = matrix_utils_json_object_get_string (object, "address"); - type = matrix_utils_json_object_get_string (object, "medium"); - - if (g_strcmp0 (type, "email") == 0) - g_ptr_array_add (emails, g_strdup (value)); - else if (g_strcmp0 (type, "msisdn") == 0) - g_ptr_array_add (phones, g_strdup (value)); - } - - g_object_set_data_full (G_OBJECT (task), "email", emails, - (GDestroyNotify)g_ptr_array_unref); - g_object_set_data_full (G_OBJECT (task), "phone", phones, - (GDestroyNotify)g_ptr_array_unref); - - g_task_return_boolean (task, TRUE); -} - -void -matrix_api_get_3pid_async (MatrixApi *self, - GCancellable *cancellable, - GAsyncReadyCallback callback, - gpointer user_data) -{ - GTask *task; - - g_return_if_fail (MATRIX_IS_API (self)); - g_return_if_fail (!cancellable || G_IS_CANCELLABLE (cancellable)); - - task = g_task_new (self, cancellable, callback, user_data); - - if (!self->username) { - g_task_return_new_error (task, G_IO_ERROR, G_IO_ERROR_INVALID_ARGUMENT, - "user hasn't logged in yet"); - return; - } - - CHATTY_DEBUG (self->username, "Getting 3pid of user"); - - matrix_net_send_json_async (self->matrix_net, 1, NULL, - "/_matrix/client/r0/account/3pid", SOUP_METHOD_GET, - NULL, self->cancellable, api_get_3pid_cb, task); -} - -gboolean -matrix_api_get_3pid_finish (MatrixApi *self, - GPtrArray **emails, - GPtrArray **phones, - GAsyncResult *result, - GError **error) -{ - g_return_val_if_fail (MATRIX_IS_API (self), FALSE); - g_return_val_if_fail (G_IS_TASK (result), FALSE); - g_return_val_if_fail (!error || !*error, FALSE); - - if (emails) - *emails = g_object_steal_data (G_OBJECT (result), "email"); - if (phones) - *phones = g_object_steal_data (G_OBJECT (result), "phone"); - - return g_task_propagate_boolean (G_TASK (result), error); -} - -static void -api_delete_3pid_cb (GObject *obj, - GAsyncResult *result, - gpointer user_data) -{ - MatrixApi *self; - g_autoptr(GTask) task = user_data; - GError *error = NULL; - g_autoptr(JsonObject) object = NULL; - - g_assert (G_IS_TASK (task)); - - self = g_task_get_source_object (task); - g_assert (MATRIX_IS_API (self)); - - object = g_task_propagate_pointer (G_TASK (result), &error); - - CHATTY_DEBUG (self->username, "Deleting 3pid %s", CHATTY_LOG_SUCESS (!error)); - - if (error) { - g_task_return_error (task, error); - return; - } - - g_task_return_boolean (task, TRUE); -} - -void -matrix_api_delete_3pid_async (MatrixApi *self, - const char *value, - ChattyIdType type, - GCancellable *cancellable, - GAsyncReadyCallback callback, - gpointer user_data) -{ - JsonObject *root; - GTask *task; - - g_return_if_fail (MATRIX_IS_API (self)); - g_return_if_fail (value && *value); - g_return_if_fail (type == CHATTY_ID_EMAIL || type == CHATTY_ID_PHONE); - g_return_if_fail (!cancellable || G_IS_CANCELLABLE (cancellable)); - - if (chatty_log_get_verbosity () > 2) { - g_autoptr(GString) str = NULL; - - str = g_string_new (NULL); - g_string_append (str, "user: "); - chatty_log_anonymize_value (str, self->username); - g_string_append (str, " value: "); - chatty_log_anonymize_value (str, value); - - g_debug ("Deleting 3pid, %s", str->str); - } - - root = json_object_new (); - json_object_set_string_member (root, "address", value); - if (type == CHATTY_ID_PHONE) - json_object_set_string_member (root, "medium", "msisdn"); - else - json_object_set_string_member (root, "medium", "email"); - - task = g_task_new (self, cancellable, callback, user_data); - g_object_set_data (G_OBJECT (task), "type", GINT_TO_POINTER (type)); - g_object_set_data_full (G_OBJECT (task), "value", - g_strdup (value), g_free); - - matrix_net_send_json_async (self->matrix_net, 2, root, - "/_matrix/client/r0/account/3pid/delete", SOUP_METHOD_POST, - NULL, cancellable, api_delete_3pid_cb, task); -} - -gboolean -matrix_api_delete_3pid_finish (MatrixApi *self, - GAsyncResult *result, - GError **error) -{ - g_return_val_if_fail (MATRIX_IS_API (self), FALSE); - g_return_val_if_fail (G_IS_TASK (result), FALSE); - g_return_val_if_fail (!error || !*error, FALSE); - - return g_task_propagate_boolean (G_TASK (result), error); -} diff --git a/src/matrix/matrix-api.h b/src/matrix/matrix-api.h deleted file mode 100644 index 6d0039716d5bc5e18f3e5d7c35c1acbdbdc97eda..0000000000000000000000000000000000000000 --- a/src/matrix/matrix-api.h +++ /dev/null @@ -1,235 +0,0 @@ -/* -*- mode: c; c-basic-offset: 2; indent-tabs-mode: nil; -*- */ -/* matrix-api.h - * - * Copyright 2020 Purism SPC - * - * Author(s): - * Mohammed Sadiq <sadiq@sadiqpk.org> - * - * SPDX-License-Identifier: GPL-3.0-or-later - */ - -#pragma once - -#include <glib-object.h> - -#include "chatty-chat.h" -#include "chatty-account.h" -#include "chatty-message.h" -#include "matrix-enc.h" -#include "matrix-enums.h" - -G_BEGIN_DECLS - -#define MATRIX_TYPE_API (matrix_api_get_type ()) - -G_DECLARE_FINAL_TYPE (MatrixApi, matrix_api, MATRIX, API, GObject) - -typedef void (*MatrixCallback) (gpointer object, - MatrixApi *self, - MatrixAction action, - JsonObject *jobject, - GError *err); - -MatrixApi *matrix_api_new (const char *username); -void matrix_api_set_enc (MatrixApi *self, - MatrixEnc *enc); -gboolean matrix_api_can_connect (MatrixApi *self); - -const char *matrix_api_get_username (MatrixApi *self); -void matrix_api_set_username (MatrixApi *self, - const char *username); -const char *matrix_api_get_login_username (MatrixApi *self); -void matrix_api_set_login_username (MatrixApi *self, - const char *username); - -const char *matrix_api_get_password (MatrixApi *self); -void matrix_api_set_password (MatrixApi *self, - const char *password); - -const char *matrix_api_get_homeserver (MatrixApi *self); -void matrix_api_set_homeserver (MatrixApi *self, - const char *homeserver); -const char *matrix_api_get_device_id (MatrixApi *self); -const char *matrix_api_get_access_token (MatrixApi *self); -void matrix_api_set_access_token (MatrixApi *self, - const char *access_token, - const char *device_id); -const char *matrix_api_get_next_batch (MatrixApi *self); -void matrix_api_set_next_batch (MatrixApi *self, - const char *next_batch); -void matrix_api_set_sync_callback (MatrixApi *self, - MatrixCallback callback, - gpointer object); -gboolean matrix_api_is_sync (MatrixApi *self); -void matrix_api_start_sync (MatrixApi *self); -void matrix_api_stop_sync (MatrixApi *self); -void matrix_api_set_upload_key (MatrixApi *self, - char *key); -void matrix_api_set_typing (MatrixApi *self, - const char *room_id, - gboolean is_typing); -void matrix_api_get_room_state_async (MatrixApi *self, - const char *room_id, - GAsyncReadyCallback callback, - gpointer user_data); -JsonArray *matrix_api_get_room_state_finish (MatrixApi *self, - GAsyncResult *result, - GError **error); -void matrix_api_get_room_users_async (MatrixApi *self, - const char *room_id, - GAsyncReadyCallback callback, - gpointer user_data); -JsonObject *matrix_api_get_room_users_finish (MatrixApi *self, - GAsyncResult *result, - GError **error); -void matrix_api_get_room_name_async (MatrixApi *self, - const char *room_id, - GAsyncReadyCallback callback, - gpointer user_data); -JsonObject *matrix_api_get_room_name_finish (MatrixApi *self, - GAsyncResult *result, - GError **error); -void matrix_api_get_room_encryption_async (MatrixApi *self, - const char *room_id, - GAsyncReadyCallback callback, - gpointer user_data); -char *matrix_api_get_room_encryption_finish (MatrixApi *self, - GAsyncResult *result, - GError **error); -void matrix_api_set_room_encryption_async (MatrixApi *self, - const char *room_id, - GAsyncReadyCallback callback, - gpointer user_data); -gboolean matrix_api_set_room_encryption_finish (MatrixApi *self, - GAsyncResult *result, - GError **error); - -void matrix_api_get_members_async (MatrixApi *self, - const char *room_id, - GAsyncReadyCallback callback, - gpointer user_data); -JsonObject *matrix_api_get_members_finish (MatrixApi *self, - GAsyncResult *result, - GError **error); -void matrix_api_load_prev_batch_async (MatrixApi *self, - const char *room_id, - char *prev_batch, - char *last_batch, - GAsyncReadyCallback callback, - gpointer user_data); -JsonObject *matrix_api_load_prev_batch_finish (MatrixApi *self, - GAsyncResult *result, - GError **error); -void matrix_api_query_keys_async (MatrixApi *self, - GListModel *members_list, - const char *token, - GAsyncReadyCallback callback, - gpointer user_data); -JsonObject *matrix_api_query_keys_finish (MatrixApi *self, - GAsyncResult *result, - GError **error); -void matrix_api_claim_keys_async (MatrixApi *self, - GListModel *member_list, - GAsyncReadyCallback callback, - gpointer user_data); -JsonObject *matrix_api_claim_keys_finish (MatrixApi *self, - GAsyncResult *result, - GError **error); -void matrix_api_get_file_async (MatrixApi *self, - ChattyMessage *message, - ChattyFileInfo *file, - GCancellable *cancellable, - GFileProgressCallback progress_callback, - GAsyncReadyCallback callback, - gpointer user_data); -gboolean matrix_api_get_file_finish (MatrixApi *self, - GAsyncResult *result, - GError **error); -void matrix_api_send_file_async (MatrixApi *self, - ChattyChat *chat, - const char *file_name, - GCancellable *cancellable, - GAsyncReadyCallback callback, - gpointer user_data); -gboolean matrix_api_send_file_finish (MatrixApi *self, - GAsyncResult *result, - GError **error); -void matrix_api_send_message_async (MatrixApi *self, - ChattyChat *chat, - const char *room_id, - ChattyMessage *message, - GAsyncReadyCallback callback, - gpointer user_data); -gboolean matrix_api_send_message_finish (MatrixApi *self, - GAsyncResult *result, - GError **error); -void matrix_api_set_read_marker_async (MatrixApi *self, - const char *room_id, - ChattyMessage *message, - GAsyncReadyCallback callback, - gpointer user_data); -gboolean matrix_api_set_read_marker_finish (MatrixApi *self, - GAsyncResult *result, - GError **error); -void matrix_api_upload_group_keys_async (MatrixApi *self, - const char *room_id, - GListModel *member_list, - GAsyncReadyCallback callback, - gpointer user_data); -gboolean matrix_api_upload_group_keys_finish (MatrixApi *self, - GAsyncResult *result, - GError **error); -void matrix_api_leave_chat_async (MatrixApi *self, - const char *room_id, - GAsyncReadyCallback callback, - gpointer user_data); -gboolean matrix_api_leave_chat_finish (MatrixApi *self, - GAsyncResult *result, - GError **error); -void matrix_api_get_user_info_async (MatrixApi *self, - const char *user_id, - GCancellable *cancellable, - GAsyncReadyCallback callback, - gpointer user_data); -gboolean matrix_api_get_user_info_finish (MatrixApi *self, - char **name, - char **avatar_url, - GAsyncResult *result, - GError **error); -void matrix_api_set_name_async (MatrixApi *self, - const char *name, - GCancellable *cancellable, - GAsyncReadyCallback callback, - gpointer user_data); -gboolean matrix_api_set_name_finish (MatrixApi *self, - GAsyncResult *result, - GError **error); -void matrix_api_set_user_avatar_async (MatrixApi *self, - const char *file_name, - GCancellable *cancellable, - GAsyncReadyCallback callback, - gpointer user_data); -gboolean matrix_api_set_user_avatar_finish (MatrixApi *self, - GAsyncResult *result, - GError **error); -void matrix_api_get_3pid_async (MatrixApi *self, - GCancellable *cancellable, - GAsyncReadyCallback callback, - gpointer user_data); -gboolean matrix_api_get_3pid_finish (MatrixApi *self, - GPtrArray **emails, - GPtrArray **phones, - GAsyncResult *result, - GError **error); -void matrix_api_delete_3pid_async (MatrixApi *self, - const char *value, - ChattyIdType type, - GCancellable *cancellable, - GAsyncReadyCallback callback, - gpointer user_data); -gboolean matrix_api_delete_3pid_finish (MatrixApi *self, - GAsyncResult *result, - GError **error); - -G_END_DECLS diff --git a/src/matrix/matrix-db.c b/src/matrix/matrix-db.c deleted file mode 100644 index d0e57319929fa6484fbde49f445c5c6fa6e567f8..0000000000000000000000000000000000000000 --- a/src/matrix/matrix-db.c +++ /dev/null @@ -1,1320 +0,0 @@ -/* -*- mode: c; c-basic-offset: 2; indent-tabs-mode: nil; -*- */ -/* matrix-db.c - * - * Copyright 2020 Purism SPC - * - * Author(s): - * Mohammed Sadiq <sadiq@sadiqpk.org> - * - * SPDX-License-Identifier: GPL-3.0-or-later - */ - -/* This is heavily based on chatty-history */ - -#define G_LOG_DOMAIN "matrix-db" - -#ifdef HAVE_CONFIG_H -# include "config.h" -#endif - -#include <glib.h> -#include <fcntl.h> -#include <sqlite3.h> - -#include "chatty-utils.h" -#include "matrix-enc.h" -#include "matrix-db.h" - -struct _MatrixDb -{ - GObject parent_instance; - - GAsyncQueue *queue; - GThread *worker_thread; - sqlite3 *db; -}; - -/* - * MatrixDb->db should never be accessed nor modified in main thread - * except for checking if it’s %NULL. Any operation should be done only - * in @worker_thread. Don't reuse the same #MatrixDb once closed. - */ - -typedef void (*MatrixDbCallback) (MatrixDb *self, - GTask *task); - -G_DEFINE_TYPE (MatrixDb, matrix_db, G_TYPE_OBJECT) - -static void -warn_if_sql_error (int status, - const char *message) -{ - if (status == SQLITE_OK || status == SQLITE_ROW || status == SQLITE_DONE) - return; - - g_warning ("Error %s. errno: %d, message: %s", message, status, sqlite3_errstr (status)); -} - -static void -matrix_bind_text (sqlite3_stmt *statement, - guint position, - const char *bind_value, - const char *message) -{ - guint status; - - status = sqlite3_bind_text (statement, position, bind_value, -1, SQLITE_TRANSIENT); - warn_if_sql_error (status, message); -} - -static void -matrix_bind_int (sqlite3_stmt *statement, - guint position, - int bind_value, - const char *message) -{ - guint status; - - status = sqlite3_bind_int (statement, position, bind_value); - warn_if_sql_error (status, message); -} - -static int -matrix_db_create_schema (MatrixDb *self, - GTask *task) -{ - const char *sql; - char *error = NULL; - int status; - - g_assert (MATRIX_IS_DB (self)); - g_assert (G_IS_TASK (task)); - g_assert (g_thread_self () == self->worker_thread); - g_assert (self->db); - - sql = "BEGIN TRANSACTION;" - - "CREATE TABLE IF NOT EXISTS devices (" - "id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, " - "device TEXT NOT NULL UNIQUE);" - - "CREATE TABLE IF NOT EXISTS users (" - "id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, " - "username TEXT NOT NULL, " - "device_id INTEGER REFERENCES devices(id), " - "UNIQUE (username, device_id));" - - "CREATE TABLE IF NOT EXISTS accounts (" - "id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, " - "user_id INTEGER NOT NULL REFERENCES users(id), " - "next_batch TEXT, " - "pickle TEXT, " - "enabled INTEGER DEFAULT 0, " - "UNIQUE (user_id));" - - "CREATE TABLE IF NOT EXISTS rooms (" - "id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, " - "account_id INTEGER NOT NULL REFERENCES accounts(id), " - "room_name TEXT NOT NULL, " - "prev_batch TEXT, " - "UNIQUE (account_id, room_name));" - - "CREATE TABLE IF NOT EXISTS encryption_keys (" - "id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, " - "file_url TEXT NOT NULL, " - "file_sha256 TEXT, " - /* Initialization vector: iv in JSON */ - "iv TEXT NOT NULL, " - /* v in JSON */ - "version INT DEFAULT 2 NOT NULL, " - /* alg in JSON */ - "algorithm INT NOT NULL, " - /* k in JSON */ - "key TEXT NOT NULL, " - /* kty in JSON */ - "type INT NOT NULL, " - /* ext in JSON */ - "extractable INT DEFAULT 1 NOT NULL, " - "UNIQUE (file_url));" - - /* TODO: Someday */ - "CREATE TABLE IF NOT EXISTS session (" - "id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, " - "account_id INTEGER NOT NULL REFERENCES accounts(id), " - "sender_key TEXT NOT NULL, " - "session_id TEXT NOT NULL, " - "type INTEGER NOT NULL, " - "pickle TEXT NOT NULL, " - "time INT, " - "UNIQUE (account_id, sender_key, session_id));" - - "COMMIT;"; - - status = sqlite3_exec (self->db, sql, NULL, NULL, &error); - - if (status != SQLITE_OK) { - g_task_return_new_error (task, - G_IO_ERROR, - G_IO_ERROR_FAILED, - "Error creating chatty_im table. errno: %d, desc: %s. %s", - status, sqlite3_errmsg (self->db), error); - return status; - } - - return status; -} - - -static void -matrix_open_db (MatrixDb *self, - GTask *task) -{ - const char *dir, *file_name; - g_autofree char *db_path = NULL; - sqlite3 *db; - int status; - - g_assert (MATRIX_IS_DB (self)); - g_assert (G_IS_TASK (task)); - g_assert (g_thread_self () == self->worker_thread); - g_assert (!self->db); - - dir = g_object_get_data (G_OBJECT (task), "dir"); - file_name = g_object_get_data (G_OBJECT (task), "file-name"); - g_assert (dir && *dir); - g_assert (file_name && *file_name); - - g_mkdir_with_parents (dir, S_IRWXU); - db_path = g_build_filename (dir, file_name, NULL); - - status = sqlite3_open (db_path, &db); - - if (status == SQLITE_OK) { - self->db = db; - status = matrix_db_create_schema (self, task); - - if (status == SQLITE_OK) - g_task_return_boolean (task, TRUE); - } else { - g_task_return_new_error (task, - G_IO_ERROR, - G_IO_ERROR_FAILED, - "Database could not be opened. errno: %d, desc: %s", - status, sqlite3_errmsg (db)); - sqlite3_close (db); - } -} - -static void -matrix_close_db (MatrixDb *self, - GTask *task) -{ - sqlite3 *db; - int status; - - g_assert (MATRIX_IS_DB (self)); - g_assert (G_IS_TASK (task)); - g_assert (g_thread_self () == self->worker_thread); - g_assert (self->db); - - db = self->db; - self->db = NULL; - status = sqlite3_close (db); - - if (status == SQLITE_OK) { - /* - * We can’t know when will @self associated with the task will - * be unref. So matrix_db_get_default() called immediately - * after this may return the @self that is yet to be free. But - * as the worker_thread is exited after closing the database, any - * actions with the same @self will not execute, and so the tasks - * will take ∞ time to complete. - * - * So Instead of relying on GObject to free the object, Let’s - * explicitly run dispose - */ - g_object_run_dispose (G_OBJECT (self)); - g_debug ("Database closed successfully"); - g_task_return_boolean (task, TRUE); - } else { - g_task_return_new_error (task, - G_IO_ERROR, - G_IO_ERROR_FAILED, - "Database could not be closed. errno: %d, desc: %s", - status, sqlite3_errmsg (db)); - } -} - -static void -matrix_db_save_account (MatrixDb *self, - GTask *task) -{ - sqlite3_stmt *stmt; - const char *device, *pickle, *username, *batch; - int status, device_id = 0, user_id = 0; - gboolean enabled; - - g_assert (MATRIX_IS_DB (self)); - g_assert (G_IS_TASK (task)); - g_assert (g_thread_self () == self->worker_thread); - g_assert (self->db); - - batch = g_object_get_data (G_OBJECT (task), "batch"); - pickle = g_object_get_data (G_OBJECT (task), "pickle"); - device = g_object_get_data (G_OBJECT (task), "device"); - enabled = GPOINTER_TO_INT (g_object_get_data (G_OBJECT (task), "enabled")); - username = g_object_get_data (G_OBJECT (task), "username"); - - if (device) { - sqlite3_prepare_v2 (self->db, - "INSERT OR IGNORE INTO devices(device) VALUES(?1)", - -1, &stmt, NULL); - - matrix_bind_text (stmt, 1, device, "binding when updating device"); - sqlite3_step (stmt); - sqlite3_finalize (stmt); - - sqlite3_prepare_v2 (self->db, - "SELECT id FROM devices WHERE device=?", - -1, &stmt, NULL); - matrix_bind_text (stmt, 1, device, "binding when getting device id"); - - if (sqlite3_step (stmt) == SQLITE_ROW) - device_id = sqlite3_column_int (stmt, 0); - sqlite3_finalize (stmt); - } - - - /* Find user id with no device id set and have an associated account */ - sqlite3_prepare_v2 (self->db, - "SELECT users.id FROM users " - "INNER JOIN accounts ON user_id=users.id " - "WHERE username=?1 AND device_id IS NULL LIMIT 1", - -1, &stmt, NULL); - matrix_bind_text (stmt, 1, username, "binding when getting user"); - - if (sqlite3_step (stmt) == SQLITE_ROW) - user_id = sqlite3_column_int (stmt, 0); - - if (user_id) { - sqlite3_prepare_v2 (self->db, - "UPDATE users SET device_id=? WHERE users.id=?", - -1, &stmt, NULL); - if (device_id) - matrix_bind_int (stmt, 1, device_id, "binding when updating existing user"); - matrix_bind_int (stmt, 2, user_id, "binding when updating existing user"); - user_id = 0; - } else { - sqlite3_prepare_v2 (self->db, - "INSERT OR IGNORE INTO users(username,device_id) VALUES(?1,?2)", - -1, &stmt, NULL); - matrix_bind_text (stmt, 1, username, "binding when updating user"); - if (device_id) - matrix_bind_int (stmt, 2, device_id, "binding when updating user"); - } - - sqlite3_step (stmt); - sqlite3_finalize (stmt); - - sqlite3_prepare_v2 (self->db, - "SELECT id FROM users WHERE username=?1 AND (device_id=?2 " - "OR (device_id IS NULL AND ?2 IS NULL)) LIMIT 1", - -1, &stmt, NULL); - matrix_bind_text (stmt, 1, username, "binding when getting user id"); - if (device_id) - matrix_bind_int (stmt, 2, device_id, "binding when getting user id"); - - if (sqlite3_step (stmt) == SQLITE_ROW) - user_id = sqlite3_column_int (stmt, 0); - sqlite3_finalize (stmt); - - sqlite3_prepare_v2 (self->db, - "INSERT INTO accounts(user_id,pickle,next_batch,enabled) " - "VALUES(?1,?2,?3,?4) " - "ON CONFLICT(user_id) " - "DO UPDATE SET pickle=?2, next_batch=?3, enabled=?4", - -1, &stmt, NULL); - - matrix_bind_int (stmt, 1, user_id, "binding when updating account"); - if (pickle && *pickle) - matrix_bind_text (stmt, 2, pickle, "binding when updating account"); - matrix_bind_text (stmt, 3, batch, "binding when updating account"); - matrix_bind_int (stmt, 4, enabled, "binding when updating account"); - - status = sqlite3_step (stmt); - sqlite3_finalize (stmt); - - if (status == SQLITE_DONE) - g_task_return_boolean (task, TRUE); - else - g_task_return_new_error (task, - G_IO_ERROR, - G_IO_ERROR_FAILED, - "Error saving account. errno: %d, desc: %s", - status, sqlite3_errmsg (self->db)); -} - -/* Find the account id of matching @username and @device_id, - * The item should already be in the db, otherwise it's an - * error. - */ -static int -matrix_db_get_account_id (MatrixDb *self, - GTask *task, - const char *username, - const char *device_name) -{ - sqlite3_stmt *stmt; - const char *error; - int status; - - g_assert (username && *username); - g_assert (device_name && *device_name); - - sqlite3_prepare_v2 (self->db, - "SELECT accounts.id FROM accounts " - "INNER JOIN users ON username=? " - "INNER JOIN devices ON device_id=users.device_id AND devices.device=? " - "WHERE accounts.user_id=users.id", - -1, &stmt, NULL); - matrix_bind_text (stmt, 1, username, "binding when getting account id"); - matrix_bind_text (stmt, 2, device_name, "binding when getting account id"); - - status = sqlite3_step (stmt); - if (status == SQLITE_ROW) - return sqlite3_column_int (stmt, 0); - - if (status == SQLITE_DONE) - error = "Account not found in db"; - else - error = sqlite3_errmsg (self->db); - - g_task_return_new_error (task, - G_IO_ERROR, - G_IO_ERROR_FAILED, - "Couldn't find user %s. error: %s", - username, error); - return 0; -} - -static void -matrix_db_load_account (MatrixDb *self, - GTask *task) -{ - sqlite3_stmt *stmt; - char *username, *device_id; - int status; - - g_assert (MATRIX_IS_DB (self)); - g_assert (G_IS_TASK (task)); - g_assert (g_thread_self () == self->worker_thread); - g_assert (self->db); - - username = g_object_get_data (G_OBJECT (task), "username"); - device_id = g_object_get_data (G_OBJECT (task), "device"); - - status = sqlite3_prepare_v2 (self->db, - "SELECT enabled,pickle,next_batch,device " - "FROM accounts " - "INNER JOIN users " - "ON users.username=?1 AND users.id=user_id " - "LEFT JOIN devices " - "ON device_id=devices.id AND devices.device=?2 " - "WHERE (?2 is NULL AND device is NULL) OR (?2 is NOT NULL AND device=?2)", - -1, &stmt, NULL); - matrix_bind_text (stmt, 1, username, "binding when loading account"); - matrix_bind_text (stmt, 2, device_id, "binding when loading account"); - - status = sqlite3_step (stmt); - - if (status == SQLITE_ROW) { - GObject *object = G_OBJECT (task); - - g_object_set_data (object, "enabled", GINT_TO_POINTER (sqlite3_column_int (stmt, 0))); - g_object_set_data_full (object, "pickle", g_strdup ((char *)sqlite3_column_text (stmt, 1)), g_free); - g_object_set_data_full (object, "batch", g_strdup ((char *)sqlite3_column_text (stmt, 2)), g_free); - g_object_set_data_full (object, "device", g_strdup ((char *)sqlite3_column_text (stmt, 3)), g_free); - } - - sqlite3_finalize (stmt); - g_task_return_boolean (task, status == SQLITE_ROW); -} - -static void -matrix_db_save_room (MatrixDb *self, - GTask *task) -{ - sqlite3_stmt *stmt; - const char *username, *room_name, *batch, *account_device; - int status, account_id; - - g_assert (MATRIX_IS_DB (self)); - g_assert (G_IS_TASK (task)); - g_assert (g_thread_self () == self->worker_thread); - g_assert (self->db); - - batch = g_object_get_data (G_OBJECT (task), "prev-batch"); - username = g_object_get_data (G_OBJECT (task), "account-id"); - room_name = g_object_get_data (G_OBJECT (task), "room"); - account_device = g_object_get_data (G_OBJECT (task), "account-device"); - - account_id = matrix_db_get_account_id (self, task, username, account_device); - - if (!account_id) - return; - - sqlite3_prepare_v2 (self->db, - "INSERT INTO rooms(account_id,room_name,prev_batch) " - "VALUES(?1,?2,?3) " - "ON CONFLICT(account_id, room_name) " - "DO UPDATE SET prev_batch=?3", - -1, &stmt, NULL); - matrix_bind_int (stmt, 1, account_id, "binding when saving room"); - matrix_bind_text (stmt, 2, room_name, "binding when saving room"); - matrix_bind_text (stmt, 3, batch, "binding when saving room"); - - status = sqlite3_step (stmt); - sqlite3_finalize (stmt); - g_task_return_boolean (task, status == SQLITE_DONE); -} - -static void -matrix_db_load_room (MatrixDb *self, - GTask *task) -{ - const char *username, *room_name, *account_device; - sqlite3_stmt *stmt; - int account_id; - - g_assert (MATRIX_IS_DB (self)); - g_assert (G_IS_TASK (task)); - g_assert (g_thread_self () == self->worker_thread); - g_assert (self->db); - - username = g_object_get_data (G_OBJECT (task), "account-id"); - room_name = g_object_get_data (G_OBJECT (task), "room"); - account_device = g_object_get_data (G_OBJECT (task), "account-device"); - - account_id = matrix_db_get_account_id (self, task, username, account_device); - - if (!account_id) - return; - - sqlite3_prepare_v2 (self->db, - "SELECT prev_batch FROM rooms " - "WHERE account_id=? AND room_name=? ", - -1, &stmt, NULL); - - matrix_bind_int (stmt, 1, account_id, "binding when loading room"); - matrix_bind_text (stmt, 2, room_name, "binding when loading room"); - - if (sqlite3_step (stmt) == SQLITE_ROW) - g_task_return_pointer (task, g_strdup ((char *)sqlite3_column_text (stmt, 0)), g_free); - else - g_task_return_pointer (task, NULL, NULL); - - sqlite3_finalize (stmt); -} - -static void -matrix_db_delete_account (MatrixDb *self, - GTask *task) -{ - sqlite3_stmt *stmt; - char *username; - int status; - - g_assert (MATRIX_IS_DB (self)); - g_assert (G_IS_TASK (task)); - g_assert (g_thread_self () == self->worker_thread); - g_assert (self->db); - - username = g_object_get_data (G_OBJECT (task), "username"); - - status = sqlite3_prepare_v2 (self->db, - "DELETE FROM accounts " - "WHERE accounts.id IN (" - "SELECT users.id FROM users " - "WHERE users.username=?)", - -1, &stmt, NULL); - matrix_bind_text (stmt, 1, username, "binding when loading account"); - - status = sqlite3_step (stmt); - sqlite3_finalize (stmt); - - g_task_return_boolean (task, status == SQLITE_ROW); -} - -static void -matrix_db_add_session (MatrixDb *self, - GTask *task) -{ - sqlite3_stmt *stmt; - const char *username, *account_device, *session_id, *sender_key, *pickle; - MatrixSessionType type; - int status, account_id; - - g_assert (MATRIX_IS_DB (self)); - g_assert (G_IS_TASK (task)); - g_assert (g_thread_self () == self->worker_thread); - g_assert (self->db); - - type = GPOINTER_TO_INT (g_object_get_data (G_OBJECT (task), "type")); - - pickle = g_object_get_data (G_OBJECT (task), "pickle"); - username = g_object_get_data (G_OBJECT (task), "account-id"); - session_id = g_object_get_data (G_OBJECT (task), "session-id"); - sender_key = g_object_get_data (G_OBJECT (task), "sender-key"); - account_device = g_object_get_data (G_OBJECT (task), "account-device"); - - account_id = matrix_db_get_account_id (self, task, username, account_device); - - if (!account_id) - return; - - status = sqlite3_prepare_v2 (self->db, - "INSERT INTO session(account_id,sender_key,session_id,type,pickle) " - "VALUES(?1,?2,?3,?4,?5)", - -1, &stmt, NULL); - - matrix_bind_int (stmt, 1, account_id, "binding when adding session"); - matrix_bind_text (stmt, 2, sender_key, "binding when adding session"); - matrix_bind_text (stmt, 3, session_id, "binding when adding session"); - matrix_bind_int (stmt, 4, type, "binding when adding session"); - matrix_bind_text (stmt, 5, pickle, "binding when adding session"); - - status = sqlite3_step (stmt); - sqlite3_finalize (stmt); - g_task_return_boolean (task, status == SQLITE_DONE); -} - -static void -matrix_db_save_file_url (MatrixDb *self, - GTask *task) -{ - ChattyFileInfo *file; - MatrixFileEncInfo *enc; - sqlite3_stmt *stmt; - int status, version, type, extractable, algorithm; - - g_assert (MATRIX_IS_DB (self)); - g_assert (G_IS_TASK (task)); - g_assert (g_thread_self () == self->worker_thread); - g_assert (self->db); - - file = g_object_get_data (G_OBJECT (task), "file"); - g_return_if_fail (file && file->user_data); - enc = file->user_data; - - type = GPOINTER_TO_INT (g_object_get_data (G_OBJECT (task), "type")); - version = GPOINTER_TO_INT (g_object_get_data (G_OBJECT (task), "version")); - algorithm = GPOINTER_TO_INT (g_object_get_data (G_OBJECT (task), "algorithm")); - extractable = GPOINTER_TO_INT (g_object_get_data (G_OBJECT (task), "extractable")); - - status = sqlite3_prepare_v2 (self->db, - "INSERT INTO encryption_keys(file_url,file_sha256," - "iv,version,algorithm,key,type,extractable) " - "VALUES(?1,?2,?3,?4,?5,?6,?7,?8)", - -1, &stmt, NULL); - - matrix_bind_text (stmt, 1, file->url, "binding when adding file url"); - matrix_bind_text (stmt, 2, enc->sha256_base64, "binding when adding file url"); - matrix_bind_text (stmt, 3, enc->aes_iv_base64, "binding when adding file url"); - matrix_bind_int (stmt, 4, version, "binding when adding file url"); - matrix_bind_int (stmt, 5, algorithm, "binding when adding file url"); - matrix_bind_text (stmt, 6, enc->aes_key_base64, "binding when adding file url"); - matrix_bind_int (stmt, 7, type, "binding when adding file url"); - matrix_bind_int (stmt, 8, extractable, "binding when adding file url"); - - status = sqlite3_step (stmt); - sqlite3_finalize (stmt); - - g_task_return_boolean (task, status == SQLITE_DONE); -} - -static void -db_lookup_session (MatrixDb *self, - GTask *task) -{ - sqlite3_stmt *stmt; - const char *username, *account_device, *session_id, *sender_key; - char *pickle = NULL; - MatrixSessionType type; - int status, account_id; - - g_assert (MATRIX_IS_DB (self)); - g_assert (g_thread_self () == self->worker_thread); - g_assert (G_IS_TASK (task)); - - type = GPOINTER_TO_INT (g_object_get_data (G_OBJECT (task), "type")); - - pickle = g_object_get_data (G_OBJECT (task), "pickle"); - username = g_object_get_data (G_OBJECT (task), "account-id"); - session_id = g_object_get_data (G_OBJECT (task), "session-id"); - sender_key = g_object_get_data (G_OBJECT (task), "sender-key"); - account_device = g_object_get_data (G_OBJECT (task), "account-device"); - - account_id = matrix_db_get_account_id (self, task, username, account_device); - - if (!account_id) - return; - - sqlite3_prepare_v2 (self->db, - "SELECT pickle FROM session " - "WHERE account_id=? AND sender_key=? AND session_id=? AND type=?", - -1, &stmt, NULL); - - matrix_bind_int (stmt, 1, account_id, "binding when adding session"); - matrix_bind_text (stmt, 2, sender_key, "binding when looking up session"); - matrix_bind_text (stmt, 3, session_id, "binding when looking up session"); - matrix_bind_int (stmt, 4, type, "binding when looking up session"); - - status = sqlite3_step (stmt); - - if (status == SQLITE_ROW) - pickle = g_strdup ((char *)sqlite3_column_text (stmt, 0)); - - sqlite3_finalize (stmt); - g_task_return_pointer (task, pickle, g_free); -} - -static void -history_get_olm_sessions (MatrixDb *self, - GTask *task) -{ - GPtrArray *sessions = NULL; - sqlite3_stmt *stmt; - int status; - - g_assert (MATRIX_IS_DB (self)); - g_assert (g_thread_self () == self->worker_thread); - g_assert (G_IS_TASK (task)); - - status = sqlite3_prepare_v2 (self->db, - "SELECT sender,sender_key,session_pickle FROM olm_session " - "ORDER BY id DESC LIMIT 100", -1, &stmt, NULL); - - warn_if_sql_error (status, "getting olm sessions"); - - while (sqlite3_step (stmt) == SQLITE_ROW) { - MatrixDbData *data; - - if (!sessions) - sessions = g_ptr_array_new_full (100, NULL); - - data = g_new (MatrixDbData, 1); - data->sender = g_strdup ((char *)sqlite3_column_text (stmt, 0)); - data->sender_key = g_strdup ((char *)sqlite3_column_text (stmt, 1)); - data->session_pickle = g_strdup ((char *)sqlite3_column_text (stmt, 2)); - - g_ptr_array_insert (sessions, 0, data); - } - - status = sqlite3_finalize (stmt); - warn_if_sql_error (status, "finalizing when getting olm sessions"); - - g_task_return_pointer (task, sessions, (GDestroyNotify)g_ptr_array_unref); -} - -static gpointer -matrix_db_worker (gpointer user_data) -{ - MatrixDb *self = user_data; - GTask *task; - - g_assert (MATRIX_IS_DB (self)); - - while ((task = g_async_queue_pop (self->queue))) { - MatrixDbCallback callback; - - g_assert (task); - callback = g_task_get_task_data (task); - callback (self, task); - g_object_unref (task); - - if (callback == matrix_close_db) - break; - } - - return NULL; -} - -static void -ma_finish_cb (GObject *object, - GAsyncResult *result, - gpointer user_data) -{ - g_autoptr(GError) error = NULL; - - g_task_propagate_boolean (G_TASK (result), &error); - - if (error) - g_warning ("Error: %s", error->message); - - g_task_return_boolean (G_TASK (user_data), !error); -} - -static void -matrix_db_close (MatrixDb *self) -{ - g_autoptr(GTask) task = NULL; - - g_return_if_fail (MATRIX_IS_DB (self)); - - if (!self->db) - return; - - task = g_task_new (NULL, NULL, NULL, NULL); - matrix_db_close_async (self, ma_finish_cb, task); - - /* Wait until the task is completed */ - while (!g_task_get_completed (task)) - g_main_context_iteration (NULL, TRUE); -} - -static void -matrix_db_dispose (GObject *object) -{ - MatrixDb *self = (MatrixDb *)object; - - matrix_db_close (self); - - g_clear_pointer (&self->worker_thread, g_thread_unref); - - G_OBJECT_CLASS (matrix_db_parent_class)->dispose (object); -} - -static void -matrix_db_finalize (GObject *object) -{ - MatrixDb *self = (MatrixDb *)object; - - if (self->db) - g_warning ("Database not closed"); - - g_clear_pointer (&self->queue, g_async_queue_unref); - - G_OBJECT_CLASS (matrix_db_parent_class)->finalize (object); -} - -static void -matrix_db_class_init (MatrixDbClass *klass) -{ - GObjectClass *object_class = G_OBJECT_CLASS (klass); - - object_class->dispose = matrix_db_dispose; - object_class->finalize = matrix_db_finalize; -} - -static void -matrix_db_init (MatrixDb *self) -{ - self->queue = g_async_queue_new (); -} - -MatrixDb * -matrix_db_new (void) -{ - return g_object_new (MATRIX_TYPE_DB, NULL); -} - -/** - * matrix_db_open_async: - * @self: a #MatrixDb - * @dir: (transfer full): The database directory - * @file_name: The file name of database - * @callback: a #GAsyncReadyCallback, or %NULL - * @user_data: closure data for @callback - * - * Open the database file @file_name from path @dir. - * Complete with matrix_db_open_finish() to get - * the result. - */ -void -matrix_db_open_async (MatrixDb *self, - char *dir, - const char *file_name, - GAsyncReadyCallback callback, - gpointer user_data) -{ - GTask *task; - - g_return_if_fail (MATRIX_IS_DB (self)); - g_return_if_fail (dir || !*dir); - g_return_if_fail (file_name || !*file_name); - - if (self->db) { - g_warning ("A DataBase is already open"); - return; - } - - if (!self->worker_thread) - self->worker_thread = g_thread_new ("matrix-db-worker", - matrix_db_worker, - self); - - task = g_task_new (self, NULL, callback, user_data); - g_task_set_source_tag (task, matrix_db_open_async); - g_task_set_task_data (task, matrix_open_db, NULL); - g_object_set_data_full (G_OBJECT (task), "dir", dir, g_free); - g_object_set_data_full (G_OBJECT (task), "file-name", g_strdup (file_name), g_free); - - g_async_queue_push (self->queue, task); -} - -/** - * matrix_db_open_finish: - * @self: a #MatrixDb - * @result: a #GAsyncResult provided to callback - * @error: a location for a #GError or %NULL - * - * Completes opening a database started with - * matrix_db_open_async(). - * - * Returns: %TRUE if database was opened successfully. - * %FALSE otherwise with @error set. - */ -gboolean -matrix_db_open_finish (MatrixDb *self, - GAsyncResult *result, - GError **error) -{ - g_return_val_if_fail (MATRIX_IS_DB (self), FALSE); - g_return_val_if_fail (G_IS_TASK (result), FALSE); - g_return_val_if_fail (!error || !*error, FALSE); - - return g_task_propagate_boolean (G_TASK (result), error); -} - -/** - * matrix_db_is_open: - * @self: a #MatrixDb - * - * Get if the database is open or not - * - * Returns: %TRUE if a database is open. - * %FALSE otherwise. - */ -gboolean -matrix_db_is_open (MatrixDb *self) -{ - g_return_val_if_fail (MATRIX_IS_DB (self), FALSE); - - return !!self->db; -} - -/** - * matrix_db_close_async: - * @self: a #MatrixDb - * @callback: a #GAsyncReadyCallback, or %NULL - * @user_data: closure data for @callback - * - * Close the database opened. - * Complete with matrix_db_close_finish() to get - * the result. - */ -void -matrix_db_close_async (MatrixDb *self, - GAsyncReadyCallback callback, - gpointer user_data) -{ - GTask *task; - - g_return_if_fail (MATRIX_IS_DB (self)); - - task = g_task_new (self, NULL, callback, user_data); - g_task_set_source_tag (task, matrix_db_close_async); - g_task_set_task_data (task, matrix_close_db, NULL); - - g_async_queue_push (self->queue, task); -} - -/** - * matrix_db_open_finish: - * @self: a #MatrixDb - * @result: a #GAsyncResult provided to callback - * @error: a location for a #GError or %NULL - * - * Completes closing a database started with - * matrix_db_close_async(). @self is - * g_object_unref() if closing succeeded. - * So @self will be freed if you haven’t kept - * your own reference on @self. - * - * Returns: %TRUE if database was closed successfully. - * %FALSE otherwise with @error set. - */ -gboolean -matrix_db_close_finish (MatrixDb *self, - GAsyncResult *result, - GError **error) -{ - g_return_val_if_fail (MATRIX_IS_DB (self), FALSE); - g_return_val_if_fail (G_IS_TASK (result), FALSE); - g_return_val_if_fail (!error || !*error, FALSE); - - return g_task_propagate_boolean (G_TASK (result), error); -} - -void -matrix_db_save_account_async (MatrixDb *self, - ChattyAccount *account, - gboolean enabled, - char *pickle, - const char *device_id, - const char *next_batch, - GAsyncReadyCallback callback, - gpointer user_data) -{ - GObject *object; - GTask *task; - const char *username; - - g_return_if_fail (MATRIX_IS_DB (self)); - g_return_if_fail (CHATTY_IS_ACCOUNT (account)); - - task = g_task_new (self, NULL, callback, user_data); - g_task_set_source_tag (task, matrix_db_save_account_async); - g_task_set_task_data (task, matrix_db_save_account, NULL); - - object = G_OBJECT (task); - username = chatty_item_get_username (CHATTY_ITEM (account)); - - if (g_application_get_default ()) - g_application_hold (g_application_get_default ()); - - if (!username || !*username) { - g_task_return_boolean (task, FALSE); - return; - } - - g_object_set_data (object, "enabled", GINT_TO_POINTER (enabled)); - g_object_set_data_full (object, "pickle", pickle, g_free); - g_object_set_data_full (object, "device", g_strdup (device_id), g_free); - g_object_set_data_full (object, "batch", g_strdup (next_batch), g_free); - g_object_set_data_full (object, "username", g_strdup (username), g_free); - g_object_set_data_full (object, "account", g_object_ref (account), g_object_unref); - - g_async_queue_push (self->queue, task); -} - -gboolean -matrix_db_save_account_finish (MatrixDb *self, - GAsyncResult *result, - GError **error) -{ - g_return_val_if_fail (MATRIX_IS_DB (self), FALSE); - g_return_val_if_fail (G_IS_TASK (result), FALSE); - - if (g_application_get_default ()) - g_application_release (g_application_get_default ()); - - return g_task_propagate_boolean (G_TASK (result), error); -} - -void -matrix_db_load_account_async (MatrixDb *self, - ChattyAccount *account, - const char *device_id, - GAsyncReadyCallback callback, - gpointer user_data) -{ - GTask *task; - const char *username; - - g_return_if_fail (!device_id || *device_id); - g_return_if_fail (MATRIX_IS_DB (self)); - g_return_if_fail (CHATTY_IS_ACCOUNT (account)); - - task = g_task_new (self, NULL, callback, user_data); - g_task_set_source_tag (task, matrix_db_load_account_async); - g_task_set_task_data (task, matrix_db_load_account, NULL); - - username = chatty_item_get_username (CHATTY_ITEM (account)); - - if (!username || !*username) { - g_task_return_boolean (task, FALSE); - return; - } - - g_object_set_data_full (G_OBJECT (task), "device", g_strdup (device_id), g_free); - g_object_set_data_full (G_OBJECT (task), "username", g_strdup (username), g_free); - g_object_set_data_full (G_OBJECT (task), "account", g_object_ref (account), g_object_unref); - - g_async_queue_push (self->queue, task); -} - -gboolean -matrix_db_load_account_finish (MatrixDb *self, - GAsyncResult *result, - GError **error) -{ - g_return_val_if_fail (MATRIX_IS_DB (self), FALSE); - g_return_val_if_fail (G_IS_TASK (result), FALSE); - - return g_task_propagate_boolean (G_TASK (result), error); -} - -void -matrix_db_save_room_async (MatrixDb *self, - ChattyAccount *account, - const char *account_device, - const char *room_id, - const char *prev_batch, - GAsyncReadyCallback callback, - gpointer user_data) -{ - GTask *task; - const char *username; - - g_return_if_fail (MATRIX_IS_DB (self)); - g_return_if_fail (CHATTY_IS_ACCOUNT (account)); - g_return_if_fail (room_id && *room_id == '!'); - - task = g_task_new (self, NULL, callback, user_data); - g_task_set_source_tag (task, matrix_db_save_room_async); - g_task_set_task_data (task, matrix_db_save_room, NULL); - - username = chatty_item_get_username (CHATTY_ITEM (account)); - g_object_set_data_full (G_OBJECT (task), "room", g_strdup (room_id), g_free); - g_object_set_data_full (G_OBJECT (task), "username", g_strdup (username), g_free); - g_object_set_data_full (G_OBJECT (task), "account-id", g_strdup (username), g_free); - g_object_set_data_full (G_OBJECT (task), "prev-batch", g_strdup (prev_batch), g_free); - g_object_set_data_full (G_OBJECT (task), "account", g_object_ref (account), g_object_unref); - g_object_set_data_full (G_OBJECT (task), "account-device", g_strdup (account_device), g_free); - - g_async_queue_push (self->queue, task); -} - -gboolean -matrix_db_save_room_finish (MatrixDb *self, - GAsyncResult *result, - GError **error) -{ - g_return_val_if_fail (MATRIX_IS_DB (self), FALSE); - g_return_val_if_fail (G_IS_TASK (result), FALSE); - - return g_task_propagate_boolean (G_TASK (result), error); -} - -void -matrix_db_load_room_async (MatrixDb *self, - ChattyAccount *account, - const char *account_device, - const char *room_id, - GAsyncReadyCallback callback, - gpointer user_data) -{ - GTask *task; - const char *username; - - g_return_if_fail (MATRIX_IS_DB (self)); - g_return_if_fail (CHATTY_IS_ACCOUNT (account)); - g_return_if_fail (room_id && *room_id == '!'); - - task = g_task_new (self, NULL, callback, user_data); - g_task_set_source_tag (task, matrix_db_load_room_async); - g_task_set_task_data (task, matrix_db_load_room, NULL); - - username = chatty_item_get_username (CHATTY_ITEM (account)); - g_object_set_data_full (G_OBJECT (task), "room", g_strdup (room_id), g_free); - g_object_set_data_full (G_OBJECT (task), "username", g_strdup (username), g_free); - g_object_set_data_full (G_OBJECT (task), "account-id", g_strdup (username), g_free); - g_object_set_data_full (G_OBJECT (task), "account", g_object_ref (account), g_object_unref); - g_object_set_data_full (G_OBJECT (task), "account-device", g_strdup (account_device), g_free); - - g_async_queue_push (self->queue, task); -} - -char * -matrix_db_load_room_finish (MatrixDb *self, - GAsyncResult *result, - GError **error) -{ - g_return_val_if_fail (MATRIX_IS_DB (self), FALSE); - g_return_val_if_fail (G_IS_TASK (result), FALSE); - - return g_task_propagate_pointer (G_TASK (result), error); -} - -void -matrix_db_delete_account_async (MatrixDb *self, - ChattyAccount *account, - GAsyncReadyCallback callback, - gpointer user_data) -{ - GTask *task; - const char *username; - - g_return_if_fail (MATRIX_IS_DB (self)); - g_return_if_fail (CHATTY_IS_ACCOUNT (account)); - - task = g_task_new (self, NULL, callback, user_data); - g_task_set_source_tag (task, matrix_db_delete_account_async); - g_task_set_task_data (task, matrix_db_delete_account, NULL); - - username = chatty_item_get_username (CHATTY_ITEM (account)); - g_object_set_data_full (G_OBJECT (task), "username", g_strdup (username), g_free); - g_object_set_data_full (G_OBJECT (task), "account", g_object_ref (account), g_object_unref); - - g_async_queue_push (self->queue, task); -} - -gboolean -matrix_db_delete_account_finish (MatrixDb *self, - GAsyncResult *result, - GError **error) -{ - g_return_val_if_fail (MATRIX_IS_DB (self), FALSE); - g_return_val_if_fail (G_IS_TASK (result), FALSE); - - return g_task_propagate_boolean (G_TASK (result), error); -} - -void -matrix_db_add_session_async (MatrixDb *self, - const char *account_id, - const char *account_device, - const char *room_id, - const char *session_id, - const char *sender_key, - char *pickle, - MatrixSessionType type, - GAsyncReadyCallback callback, - gpointer user_data) -{ - GObject *object; - GTask *task; - - g_return_if_fail (MATRIX_IS_DB (self)); - g_return_if_fail (account_id && *account_id); - g_return_if_fail (account_device && *account_device); - g_return_if_fail (room_id && *room_id); - g_return_if_fail (session_id && *session_id); - g_return_if_fail (sender_key && *sender_key); - g_return_if_fail (pickle && *pickle); - - task = g_task_new (self, NULL, callback, user_data); - g_task_set_source_tag (task, matrix_db_add_session_async); - g_task_set_task_data (task, matrix_db_add_session, NULL); - object = G_OBJECT (task); - - g_object_set_data_full (object, "account-id", g_strdup (account_id), g_free); - g_object_set_data_full (object, "account-device", g_strdup (account_device), g_free); - g_object_set_data_full (object, "room-id", g_strdup (room_id), g_free); - g_object_set_data_full (object, "session-id", g_strdup (session_id), g_free); - g_object_set_data_full (object, "sender-key", g_strdup (sender_key), g_free); - g_object_set_data_full (object, "pickle", pickle, g_free); - g_object_set_data (object, "type", GINT_TO_POINTER (type)); - - g_async_queue_push (self->queue, task); -} - -void -matrix_db_save_file_url_async (MatrixDb *self, - ChattyMessage *message, - ChattyFileInfo *file, - int version, - int algorithm, - int type, - gboolean extractable, - GAsyncReadyCallback callback, - gpointer user_data) -{ - GObject *object; - GTask *task; - - g_return_if_fail (MATRIX_IS_DB (self)); - g_return_if_fail (version == 2); - g_return_if_fail (extractable); - - task = g_task_new (self, NULL, callback, user_data); - g_task_set_source_tag (task, matrix_db_save_file_url_async); - g_task_set_task_data (task, matrix_db_save_file_url, NULL); - object = G_OBJECT (task); - - g_object_set_data (object, "file", file); - g_object_set_data_full (object, "message", g_object_ref (message), g_object_unref); - - g_object_set_data (object, "type", GINT_TO_POINTER (type)); - g_object_set_data (object, "version", GINT_TO_POINTER (version)); - g_object_set_data (object, "algorithm", GINT_TO_POINTER (algorithm)); - g_object_set_data (object, "extractable", GINT_TO_POINTER (extractable)); - - g_async_queue_push (self->queue, task); -} - -char * -matrix_db_lookup_session (MatrixDb *self, - const char *account_id, - const char *account_device, - const char *session_id, - const char *sender_key, - MatrixSessionType type) -{ - g_autoptr(GTask) task = NULL; - g_autoptr(GError) error = NULL; - GObject *object; - char *pickle; - - g_return_val_if_fail (MATRIX_IS_DB (self), NULL); - g_return_val_if_fail (account_id && *account_id, NULL); - g_return_val_if_fail (account_device && *account_device, NULL); - g_return_val_if_fail (session_id && *session_id, NULL); - g_return_val_if_fail (sender_key && *sender_key, NULL); - - task = g_task_new (self, NULL, NULL, NULL); - g_object_ref (task); - - g_task_set_source_tag (task, matrix_db_lookup_session); - g_task_set_task_data (task, db_lookup_session, NULL); - object = G_OBJECT (task); - - g_object_set_data_full (object, "account-id", g_strdup (account_id), g_free); - g_object_set_data_full (object, "account-device", g_strdup (account_device), g_free); - g_object_set_data_full (object, "session-id", g_strdup (session_id), g_free); - g_object_set_data_full (object, "sender-key", g_strdup (sender_key), g_free); - g_object_set_data (object, "type", GINT_TO_POINTER (type)); - - g_async_queue_push (self->queue, task); - g_assert (task); - - /* Wait until the task is completed */ - while (!g_task_get_completed (task)) - g_main_context_iteration (NULL, TRUE); - - pickle = g_task_propagate_pointer (task, &error); - - if (error) - g_debug ("Error getting session: %s", error->message); - - return pickle; -} - -void -matrix_db_get_olm_sessions_async (MatrixDb *self, - GAsyncReadyCallback callback, - gpointer user_data) -{ - GTask *task; - - g_return_if_fail (MATRIX_IS_DB (self)); - - task = g_task_new (self, NULL, callback, user_data); - g_task_set_source_tag (task, matrix_db_get_olm_sessions_async); - g_task_set_task_data (task, history_get_olm_sessions, NULL); - - g_async_queue_push (self->queue, task); -} - -GPtrArray * -matrix_db_get_olm_sessions_finish (MatrixDb *self, - GAsyncResult *result, - GError **error) -{ - g_return_val_if_fail (MATRIX_IS_DB (self), NULL); - g_return_val_if_fail (G_IS_TASK (result), NULL); - g_return_val_if_fail (!error || !*error, NULL); - - return g_task_propagate_pointer (G_TASK (result), error); -} diff --git a/src/matrix/matrix-db.h b/src/matrix/matrix-db.h deleted file mode 100644 index 8a0ed253f5c5e966878baf30315dc2923c75db7b..0000000000000000000000000000000000000000 --- a/src/matrix/matrix-db.h +++ /dev/null @@ -1,165 +0,0 @@ -/* -*- mode: c; c-basic-offset: 2; indent-tabs-mode: nil; -*- */ -/* matrix-db.h - * - * Copyright 2020 Purism SPC - * - * Author(s): - * Mohammed Sadiq <sadiq@sadiqpk.org> - * - * SPDX-License-Identifier: GPL-3.0-or-later - */ - -#pragma once - -#include <glib.h> - -#include "chatty-account.h" -#include "chatty-message.h" - -G_BEGIN_DECLS - -typedef struct { - char *sender; - char *sender_key; - char *session_pickle; -} MatrixDbData; - -/* These values shouldn’t be changed. They are used in DB */ -typedef enum { - SESSION_OLM_V1_IN = 1, - SESSION_OLM_V1_OUT = 2, - SESSION_MEGOLM_V1_IN = 3, - SESSION_MEGOLM_V1_OUT = 4, -} MatrixSessionType; - -#define CHATTY_ALGORITHM_A256CTR 1 - -#define CHATTY_KEY_TYPE_OCT 1 - -#define MATRIX_TYPE_DB (matrix_db_get_type ()) - -G_DECLARE_FINAL_TYPE (MatrixDb, matrix_db, MATRIX, DB, GObject) - -MatrixDb *matrix_db_new (void); -void matrix_db_open_async (MatrixDb *self, - char *dir, - const char *file_name, - GAsyncReadyCallback callback, - gpointer user_data); -gboolean matrix_db_open_finish (MatrixDb *self, - GAsyncResult *result, - GError **error); -gboolean matrix_db_is_open (MatrixDb *self); -void matrix_db_close_async (MatrixDb *self, - GAsyncReadyCallback callback, - gpointer user_data); -gboolean matrix_db_close_finish (MatrixDb *self, - GAsyncResult *result, - GError **error); -void matrix_db_save_account_async (MatrixDb *db, - ChattyAccount *account, - gboolean enabled, - char *pickle, - const char *device_id, - const char *next_batch, - GAsyncReadyCallback callback, - gpointer user_data); -gboolean matrix_db_save_account_finish (MatrixDb *self, - GAsyncResult *result, - GError **error); -void matrix_db_load_account_async (MatrixDb *db, - ChattyAccount *account, - const char *device_id, - GAsyncReadyCallback callback, - gpointer user_data); -gboolean matrix_db_load_account_finish (MatrixDb *self, - GAsyncResult *result, - GError **error); -void matrix_db_save_room_async (MatrixDb *self, - ChattyAccount *account, - const char *account_device, - const char *room_id, - const char *prev_batch, - GAsyncReadyCallback callback, - gpointer user_data); -gboolean matrix_db_save_room_finish (MatrixDb *self, - GAsyncResult *result, - GError **error); -void matrix_db_load_room_async (MatrixDb *self, - ChattyAccount *account, - const char *account_device, - const char *room_id, - GAsyncReadyCallback callback, - gpointer user_data); -char *matrix_db_load_room_finish (MatrixDb *self, - GAsyncResult *result, - GError **error); -void matrix_db_delete_account_async (MatrixDb *self, - ChattyAccount *account, - GAsyncReadyCallback callback, - gpointer user_data); -gboolean matrix_db_delete_account_finish (MatrixDb *self, - GAsyncResult *result, - GError **error); -void matrix_db_save_file_url_async (MatrixDb *self, - ChattyMessage *message, - ChattyFileInfo *file, - int version, - int algorithm, - int type, - gboolean extractable, - GAsyncReadyCallback callback, - gpointer user_data); -gboolean matrix_db_save_file_url_finish (MatrixDb *self, - GAsyncResult *result, - GError **error); -void matrix_db_add_session_async (MatrixDb *self, - const char *account_id, - const char *account_device, - const char *room_id, - const char *session_id, - const char *sender_key, - char *pickle, - MatrixSessionType type, - GAsyncReadyCallback callback, - gpointer user_data); -gboolean matrix_db_add_session_finish (MatrixDb *self, - GAsyncResult *result, - GError **error); -char *matrix_db_lookup_session (MatrixDb *self, - const char *account_id, - const char *account_device, - const char *session_id, - const char *sender_key, - MatrixSessionType type); -void matrix_db_get_olm_sessions_async (MatrixDb *self, - GAsyncReadyCallback callback, - gpointer user_data); -GPtrArray *matrix_db_get_olm_sessions_finish (MatrixDb *self, - GAsyncResult *result, - GError **error); -void matrix_db_add_olm_session_async (MatrixDb *self, - const char *sender, - const char *sender_key, - const char *pickle, - GAsyncReadyCallback callback, - gpointer user_data); -GPtrArray *matrix_db_get_messages_finish (MatrixDb *self, - GAsyncResult *result, - GError **error); -void matrix_db_get_group_in_sessions_async (MatrixDb *self, - guint limit, - GAsyncReadyCallback callback, - gpointer user_data); -GPtrArray *matrix_db_group_in_sessions_finish (MatrixDb *self, - GAsyncResult *result, - GError **error); -void matrix_db_get_group_out_sessions_async (MatrixDb *self, - guint limit, - GAsyncReadyCallback callback, - gpointer user_data); -GPtrArray *matrix_db_get_group_out_sessions_finish (MatrixDb *self, - GAsyncResult *result, - GError **error); - -G_END_DECLS diff --git a/src/matrix/matrix-enc.c b/src/matrix/matrix-enc.c deleted file mode 100644 index 145c04c3fc6b9a3bcd6807ae0ac04dc4a16e77b6..0000000000000000000000000000000000000000 --- a/src/matrix/matrix-enc.c +++ /dev/null @@ -1,1330 +0,0 @@ -/* -*- mode: c; c-basic-offset: 2; indent-tabs-mode: nil; -*- */ -/* matrix-enc.c - * - * Copyright 2020 Purism SPC - * - * Author(s): - * Mohammed Sadiq <sadiq@sadiqpk.org> - * - * SPDX-License-Identifier: GPL-3.0-or-later - */ - -#define G_LOG_DOMAIN "chatty-matrix-enc" - -#ifdef HAVE_CONFIG_H -# include "config.h" -#endif - -#include <json-glib/json-glib.h> -#include <olm/olm.h> -#include <sys/random.h> - -#include "chatty-settings.h" -#include "chatty-ma-buddy.h" -#include "matrix-utils.h" -#include "matrix-db.h" -#include "matrix-enc.h" -#include "chatty-log.h" - -#define KEY_LABEL_SIZE 6 -#define STRING_ALLOCATION 512 - -/** - * SECTION: chatty-contact - * @title: MatrixEnc - * @short_description: An abstraction over #FolksIndividual - * @include: "chatty-contact.h" - */ - -/* - * Documentations: - * https://matrix.org/docs/guides/end-to-end-encryption-implementation-guide - * - * Other: - * * We use g_malloc(size) instead of g_malloc(size * sizeof(type)) for all ‘char’ - * and ‘[u]int8_t’, unless there is a possibility that the type can change. - */ -struct _MatrixEnc -{ - GObject parent_instance; - - OlmAccount *account; - OlmUtility *utility; - char *pickle_key; - - GHashTable *in_olm_sessions; - GHashTable *out_olm_sessions; - GHashTable *in_group_sessions; - GHashTable *out_group_sessions; - - /* Use something better, like sqlite */ - MatrixDb *matrix_db; - - char *user_id; - char *device_id; - - char *curve_key; /* Public part of Curve25519 identity key */ - char *ed_key; /* Public part of Ed25519 fingerprint key */ -}; - -G_DEFINE_TYPE (MatrixEnc, matrix_enc, G_TYPE_OBJECT) - -static void -free_olm_session (gpointer data) -{ - olm_clear_session (data); - g_free (data); -} - -static void -free_in_group_session (gpointer data) -{ - olm_clear_inbound_group_session (data); - g_free (data); -} - -static void -free_out_group_session (gpointer data) -{ - olm_clear_outbound_group_session (data); - g_free (data); -} - -static void -free_all_details (MatrixEnc *self) -{ - if (self->account) - olm_clear_account (self->account); - - g_clear_pointer (&self->account, g_free); - g_hash_table_remove_all (self->in_olm_sessions); - g_hash_table_remove_all (self->out_olm_sessions); - g_hash_table_remove_all (self->in_group_sessions); - g_hash_table_remove_all (self->out_group_sessions); -} - -static char * -ma_olm_encrypt (OlmSession *session, - const char *plain_text) -{ - g_autofree char *encrypted = NULL; - g_autofree void *random = NULL; - size_t length, rand_len; - - g_assert (session); - - if (!plain_text) - return NULL; - - rand_len = olm_encrypt_random_length (session); - random = g_malloc (rand_len); - getrandom (random, rand_len, GRND_NONBLOCK); - - length = olm_encrypt_message_length (session, strlen (plain_text)); - encrypted = g_malloc (length + 1); - length = olm_encrypt (session, plain_text, strlen (plain_text), - random, rand_len, - encrypted, length); - - if (length == olm_error ()) { - g_warning ("Error encrypting: %s", olm_session_last_error (session)); - - return NULL; - } - encrypted[length] = '\0'; - - return g_steal_pointer (&encrypted); -} - -static OlmSession * -ma_create_olm_out_session (MatrixEnc *self, - const char *curve_key, - const char *one_time_key) -{ - g_autofree OlmSession *session = NULL; - g_autofree void *buffer = NULL; - size_t length, error; - - g_assert (MATRIX_ENC (self)); - - if (!curve_key || !one_time_key) - return NULL; - - session = g_malloc (olm_session_size ()); - olm_session (session); - - length = olm_create_outbound_session_random_length (session); - buffer = g_malloc (length); - getrandom (buffer, length, GRND_NONBLOCK); - - error = olm_create_outbound_session (session, - self->account, - curve_key, strlen (curve_key), - one_time_key, strlen (one_time_key), - buffer, length); - olm_encrypt_message_type (session); - if (error == olm_error ()) { - g_warning ("Error creating outbound olm session: %s", - olm_session_last_error (session)); - return NULL; - } - - return g_steal_pointer (&session); -} - -/* - * matrix_enc_load_identity_keys: - * @self: A #MatrixEnc - * - * Load the public part of Ed25519 fingerprint - * key pair and Curve25519 identity key pair. - */ -static void -matrix_enc_load_identity_keys (MatrixEnc *self) -{ - g_autoptr(JsonParser) parser = NULL; - g_autoptr(GError) error = NULL; - g_autofree char *key = NULL; - JsonObject *object; - JsonNode *node; - size_t length, err; - - length = olm_account_identity_keys_length (self->account); - key = malloc (length + 1); - err = olm_account_identity_keys (self->account, key, length); - key[length] = '\0'; - - if (err == olm_error ()) { - g_warning ("error getting identity keys: %s", olm_account_last_error (self->account)); - return; - } - - parser = json_parser_new (); - json_parser_load_from_data (parser, key, length, &error); - - if (error) { - g_warning ("error parsing keys: %s", error->message); - return; - } - - node = json_parser_get_root (parser); - object = json_node_get_object (node); - - g_free (self->curve_key); - g_free (self->ed_key); - - self->curve_key = g_strdup (json_object_get_string_member (object, "curve25519")); - self->ed_key = g_strdup (json_object_get_string_member (object, "ed25519")); -} - -static void -create_new_details (MatrixEnc *self) -{ - g_autofree void *buffer = NULL; - size_t length, err; - - g_assert (MATRIX_ENC (self)); - - CHATTY_TRACE_MSG ("Creating new encryption keys"); - - free_all_details (self); - - self->account = g_malloc (olm_account_size ()); - olm_account (self->account); - - matrix_utils_free_buffer (self->pickle_key); - self->pickle_key = g_uuid_string_random (); - - length = olm_create_account_random_length (self->account); - buffer = g_malloc (length); - getrandom (buffer, length, GRND_NONBLOCK); - err = olm_create_account (self->account, buffer, length); - if (err == olm_error ()) - g_warning ("Error creating account: %s", olm_account_last_error (self->account)); -} - -static void -matrix_enc_sign_json_object (MatrixEnc *self, - JsonObject *object) -{ - g_autoptr(GString) str = NULL; - g_autofree char *signature = NULL; - g_autofree char *label = NULL; - JsonObject *sign, *child; - - g_assert (MATRIX_IS_ENC (self)); - g_assert (object); - - /* The JSON is in canonical form. Required for signing */ - /* https://matrix.org/docs/spec/appendices#signing-json */ - str = matrix_utils_json_get_canonical (object, NULL); - signature = matrix_enc_sign_string (self, str->str, str->len); - - sign = json_object_new (); - label = g_strconcat ("ed25519:", self->device_id, NULL); - json_object_set_string_member (sign, label, signature); - - child = json_object_new (); - json_object_set_object_member (child, self->user_id, sign); - json_object_set_object_member (object, "signatures", child); -} - -static void -matrix_enc_finalize (GObject *object) -{ - MatrixEnc *self = (MatrixEnc *)object; - - olm_clear_account (self->account); - g_free (self->account); - - olm_clear_utility (self->utility); - g_free (self->utility); - - g_hash_table_unref (self->in_olm_sessions); - g_hash_table_unref (self->out_olm_sessions); - g_hash_table_unref (self->in_group_sessions); - g_hash_table_unref (self->out_group_sessions); - g_free (self->user_id); - g_free (self->device_id); - matrix_utils_free_buffer (self->pickle_key); - matrix_utils_free_buffer (self->curve_key); - matrix_utils_free_buffer (self->ed_key); - g_clear_object (&self->matrix_db); - - G_OBJECT_CLASS (matrix_enc_parent_class)->finalize (object); -} - - -static void -matrix_enc_class_init (MatrixEncClass *klass) -{ - GObjectClass *object_class = G_OBJECT_CLASS (klass); - - object_class->finalize = matrix_enc_finalize; -} - - -static void -matrix_enc_init (MatrixEnc *self) -{ - self->utility = g_malloc (olm_utility_size ()); - olm_utility (self->utility); - - self->in_olm_sessions = g_hash_table_new_full (g_str_hash, g_str_equal, - g_free, free_olm_session); - self->out_olm_sessions = g_hash_table_new_full (g_str_hash, g_str_equal, - g_free, free_olm_session); - self->in_group_sessions = g_hash_table_new_full (g_str_hash, g_str_equal, - g_free, free_in_group_session); - self->out_group_sessions = g_hash_table_new_full (g_str_hash, g_str_equal, - g_free, free_out_group_session); -} - -/** - * matrix_enc_new: - * @pickle: (nullable): The account pickle - * @key: @pickle key, can be %NULL if @pickle is %NULL - * - * If @pickle is non-null, the olm account is created - * using the pickled data. Otherwise a new olm account - * is created. If @pickle is non-null and invalid - * %NULL is returned. - * - * For @self to be ready for use, the details of @self - * should be set with matrix_enc_set_details(). - * - * Also see matrix_enc_get_pickle(). - * - * Returns: (transfer full) (nullable): A new #MatrixEnc. - * Free with g_object_unref() - */ -MatrixEnc * -matrix_enc_new (gpointer matrix_db, - const char *pickle, - const char *key) -{ - g_autoptr(MatrixEnc) self = NULL; - - g_return_val_if_fail (!pickle || (*pickle && key && *key), NULL); - - self = g_object_new (MATRIX_TYPE_ENC, NULL); - g_set_object (&self->matrix_db, matrix_db); - - /* Deserialize the pickle to create the account */ - if (pickle && *pickle) { - g_autofree char *duped = NULL; - size_t err; - - self->pickle_key = g_strdup (key); - self->account = g_malloc (olm_account_size ()); - olm_account (self->account); - - duped = g_strdup (pickle); - err = olm_unpickle_account (self->account, key, strlen (key), - duped, strlen (duped)); - - if (err == olm_error ()) { - g_warning ("Error account unpickle: %s", olm_account_last_error (self->account)); - return NULL; - } - } else { - create_new_details (self); - } - - matrix_enc_load_identity_keys (self); - - return g_steal_pointer (&self); -} - -/** - * matrix_enc_set_details: - * @self: A #MatrixEnc - * @user_id: Fully qualified Matrix user ID - * @device_id: The device id string - * - * Set user id and device id of @self. @user_id - * should be fully qualified Matrix user ID - * (ie, @user:example.com) - */ -void -matrix_enc_set_details (MatrixEnc *self, - const char *user_id, - const char *device_id) -{ - g_autofree char *old_user = NULL; - g_autofree char *old_device = NULL; - - g_return_if_fail (MATRIX_IS_ENC (self)); - g_return_if_fail (!user_id || *user_id == '@'); - - old_user = self->user_id; - old_device = self->device_id; - - self->user_id = g_strdup (user_id); - self->device_id = g_strdup (device_id); - - if (self->user_id && old_device && - g_strcmp0 (device_id, old_device) == 0) { - create_new_details (self); - matrix_enc_load_identity_keys (self); - } -} - -char * -matrix_enc_get_account_pickle (MatrixEnc *self) -{ - g_autofree char *pickle = NULL; - size_t length, err; - - g_return_val_if_fail (MATRIX_IS_ENC (self), NULL); - - length = olm_pickle_account_length (self->account); - pickle = malloc (length + 1); - err = olm_pickle_account (self->account, self->pickle_key, - strlen (self->pickle_key), pickle, length); - pickle[length] = '\0'; - - if (err == olm_error ()) { - g_warning ("Error getting account pickle: %s", olm_account_last_error (self->account)); - - return NULL; - } - - return g_steal_pointer (&pickle); -} - -char * -matrix_enc_get_pickle_key (MatrixEnc *self) -{ - g_return_val_if_fail (MATRIX_ENC (self), NULL); - - return g_strdup (self->pickle_key); -} - -/** - * matrix_enc_sign_string: - * @self: A #MatrixEnc - * @str: A string to sign - * @len: The length of @str, or -1 - * - * Sign @str and return the signature. - * Returns %NULL on error. - * - * Returns: (transfer full): The signature string. - * Free with g_free() - */ -char * -matrix_enc_sign_string (MatrixEnc *self, - const char *str, - size_t len) -{ - char *signature; - size_t length, err; - - g_return_val_if_fail (MATRIX_IS_ENC (self), NULL); - g_return_val_if_fail (str, NULL); - g_return_val_if_fail (*str, NULL); - - if (len == (size_t) -1) - len = strlen (str); - - length = olm_account_signature_length (self->account); - signature = malloc (length + 1); - err = olm_account_sign (self->account, str, len, signature, length); - signature[length] = '\0'; - - if (err == olm_error ()) { - g_warning ("Error signing data: %s", olm_account_last_error (self->account)); - - return NULL; - } - - return signature; -} - -/** - * matrix_enc_verify: - * @self: A #MatrixEnc - * @object: A #JsonObject - * @matrix_id: A Fully qualified Matrix ID - * @device_id: The device id string. - * @ed_key: The ED25519 key of @matrix_id - * - * Verify if the content in @object is signed by - * the user @matrix_id with device @device_id. - * - * This function may modify @object by removing - * "signatures" and "unsigned" members. - * - * Returns; %TRUE if verification succeeded. Or - * %FALSE otherwise. - */ -gboolean -matrix_enc_verify (MatrixEnc *self, - JsonObject *object, - const char *matrix_id, - const char *device_id, - const char *ed_key) -{ - JsonNode *signatures, *non_signed; - g_autoptr(GString) json_str = NULL; - g_autofree char *signature = NULL; - g_autofree char *key_name = NULL; - JsonObject *child; - size_t error; - - if (!object) - return FALSE; - - g_return_val_if_fail (MATRIX_IS_ENC (self), FALSE); - g_return_val_if_fail (matrix_id && *matrix_id == '@', FALSE); - g_return_val_if_fail (device_id && *device_id, FALSE); - g_return_val_if_fail (ed_key && *ed_key, FALSE); - - /* https://matrix.org/docs/spec/appendices#checking-for-a-signature */ - key_name = g_strconcat ("ed25519:", device_id, NULL); - child = matrix_utils_json_object_get_object (object, "signatures"); - child = matrix_utils_json_object_get_object (child, matrix_id); - signature = g_strdup (matrix_utils_json_object_get_string (child, key_name)); - - if (!signature) - return FALSE; - - signatures = json_object_dup_member (object, "signatures"); - non_signed = json_object_dup_member (object, "signatures"); - json_object_remove_member (object, "signatures"); - json_object_remove_member (object, "unsigned"); - - json_str = matrix_utils_json_get_canonical (object, NULL); - - if (signatures) - json_object_set_member (object, "signatures", signatures); - if (non_signed) - json_object_set_member (object, "unsigned", non_signed); - - error = olm_ed25519_verify (self->utility, - ed_key, ED25519_SIZE, - json_str->str, json_str->len, - signature, strlen (signature)); - - /* XXX: the libolm documentation is not much clear on this */ - if (error == olm_error ()) { - g_debug ("Error verifying signature: %s", olm_utility_last_error (self->utility)); - return FALSE; - } - - return TRUE; -} - -/** - * matrix_enc_max_one_time_keys: - * @self: A #MatrixEnc - * - * Get the maximum number of one time keys Olm - * library can handle. - * - * Returns: The number of maximum one-time keys. - */ -size_t -matrix_enc_max_one_time_keys (MatrixEnc *self) -{ - g_return_val_if_fail (MATRIX_IS_ENC (self), 0); - - return olm_account_max_number_of_one_time_keys (self->account); -} - -/** - * matrix_enc_create_one_time_keys: - * @self: A #MatrixEnc - * @count: A non-zero number - * - * Generate @count number of curve25519 one time keys. - * @count is capped to the half of what Olm library - * can handle. - * - * Returns: The number of one-time keys generated. - * It will be <= @count. - */ -size_t -matrix_enc_create_one_time_keys (MatrixEnc *self, - size_t count) -{ - g_autofree void *buffer = NULL; - size_t length, err; - - g_return_val_if_fail (MATRIX_IS_ENC (self), 0); - g_return_val_if_fail (count, 0); - - /* doc: The maximum number of active keys supported by libolm - is returned by olm_account_max_number_of_one_time_keys. - The client should try to maintain about half this number on the homeserver. */ - count = MIN (count, olm_account_max_number_of_one_time_keys (self->account) / 2); - - length = olm_account_generate_one_time_keys_random_length (self->account, count); - buffer = g_malloc (length); - getrandom (buffer, length, GRND_NONBLOCK); - err = olm_account_generate_one_time_keys (self->account, count, buffer, length); - - if (err == olm_error ()) { - g_warning ("Error creating one time keys: %s", olm_account_last_error (self->account)); - - return 0; - } - - return count; -} - -/** - * matrix_enc_publish_one_time_keys: - * @self: A #MatrixEnc - * - * Mark current set of one-time keys as published - */ -void -matrix_enc_publish_one_time_keys (MatrixEnc *self) -{ - - g_return_if_fail (MATRIX_IS_ENC (self)); - - olm_account_mark_keys_as_published (self->account); -} - -/** - * matrix_enc_get_one_time_keys: - * @self: A #MatrixEnc - * - * Get public part of unpublished Curve25519 one-time keys in @self. - * - * The returned data is a JSON-formatted object with the single - * property curve25519, which is itself an object mapping key id - * to base64-encoded Curve25519 key. For example: - * - * { - * "curve25519": { - * "AAAAAA": "wo76WcYtb0Vk/pBOdmduiGJ0wIEjW4IBMbbQn7aSnTo", - * "AAAAAB": "LRvjo46L1X2vx69sS9QNFD29HWulxrmW11Up5AfAjgU" - * } - * } - * - * Returns: (nullable) (transfer full): The unpublished one time keys. - * Free with g_free() - */ -JsonObject * -matrix_enc_get_one_time_keys (MatrixEnc *self) -{ - g_autoptr(JsonParser) parser = NULL; - g_autoptr(GError) error = NULL; - g_autofree char *buffer = NULL; - size_t length, err; - - g_return_val_if_fail (MATRIX_IS_ENC (self), NULL); - - length = olm_account_one_time_keys_length (self->account); - buffer = g_malloc (length + 1); - err = olm_account_one_time_keys (self->account, buffer, length); - buffer[length] = '\0'; - - if (err == olm_error ()) { - g_warning ("Error getting one time keys: %s", olm_account_last_error (self->account)); - - return NULL; - } - - /* Return NULL if there are no keys */ - if (g_str_equal (buffer, "{\"curve25519\":{}}")) - return NULL; - - parser = json_parser_new (); - json_parser_load_from_data (parser, buffer, length, &error); - - if (error) { - g_warning ("error parsing keys: %s", error->message); - return NULL; - } - - return json_node_dup_object (json_parser_get_root (parser)); -} - -/** - * matrix_enc_get_one_time_keys_json: - * @self: A #MatrixEnc - * - * Get the signed Curve25519 one-time keys JSON. The JSON shall - * be in the following format: - * - * { - * "signed_curve25519:AAAAHg": { - * "key": "zKbLg+NrIjpnagy+pIY6uPL4ZwEG2v+8F9lmgsnlZzs", - * "signatures": { - * "@alice:example.com": { - * "ed25519:JLAFKJWSCS": "FLWxXqGbwrb8SM3Y795eB6OA8bwBcoMZFXBqnTn58AYWZSqiD45tlBVcDa2L7RwdKXebW/VzDlnfVJ+9jok1Bw" - * } - * } - * }, - * "signed_curve25519:AAAAHQ": { - * "key": "j3fR3HemM16M7CWhoI4Sk5ZsdmdfQHsKL1xuSft6MSw", - * "signatures": { - * "@alice:example.com": { - * "ed25519:JLAFKJWSCS": "IQeCEPb9HFk217cU9kw9EOiusC6kMIkoIRnbnfOh5Oc63S1ghgyjShBGpu34blQomoalCyXWyhaaT3MrLZYQAA" - * } - * } - * } - * } - * - * Returns: (transfer full): A JSON encoded string. - * Free with g_free() - */ -char * -matrix_enc_get_one_time_keys_json (MatrixEnc *self) -{ - g_autoptr(JsonObject) object = NULL; - g_autoptr(JsonObject) root = NULL; - g_autoptr(GList) members = NULL; - JsonObject *keys, *child; - - g_return_val_if_fail (MATRIX_IS_ENC (self), NULL); - - object = matrix_enc_get_one_time_keys (self); - - if (!object) - return NULL; - - keys = json_object_new (); - object = json_object_get_object_member (object, "curve25519"); - members = json_object_get_members (object); - - for (GList *item = members; item; item = item->next) { - g_autofree char *label = NULL; - const char *value; - - child = json_object_new (); - value = json_object_get_string_member (object, item->data); - json_object_set_string_member (child, "key", value); - matrix_enc_sign_json_object (self, child); - - label = g_strconcat ("signed_curve25519:", item->data, NULL); - json_object_set_object_member (keys, label, child); - } - - root = json_object_new (); - json_object_set_object_member (root, "one_time_keys", keys); - - return matrix_utils_json_object_to_string (root, FALSE); -} - -/** - * matrix_enc_get_one_time_keys: - * @self: A #MatrixEnc - * - * Get the signed device key JSON. The JSON shall - * be in the following format: - * - * { - * "user_id": "@alice:example.com", - * "device_id": "JLAFKJWSCS", - * "algorithms": [ - * "m.olm.curve25519-aes-sha256", - * "m.megolm.v1.aes-sha2" - * ], - * "keys": { - * "curve25519:JLAFKJWSCS": "3C5BFWi2Y8MaVvjM8M22DBmh24PmgR0nPvJOIArzgyI", - * "ed25519:JLAFKJWSCS": "lEuiRJBit0IG6nUf5pUzWTUEsRVVe/HJkoKuEww9ULI" - * }, - * "signatures": { - * "@alice:example.com": { - * "ed25519:JLAFKJWSCS": "dSO80A01XiigH3uBiDVx/EjzaoycHcjq9lfQX0uWsqxl2giMIiSPR8a4d291W1ihKJL/a+myXS367WT6NAIcBA" - * } - * } - * } - * - * Returns: (nullable): A JSON encoded string. - * Free with g_free() - */ -char * -matrix_enc_get_device_keys_json (MatrixEnc *self) -{ - g_autoptr(JsonObject) root = NULL; - JsonObject *keys, *device_keys; - JsonArray *array; - char *label; - - g_return_val_if_fail (MATRIX_IS_ENC (self), NULL); - g_return_val_if_fail (self->user_id, NULL); - g_return_val_if_fail (self->device_id, NULL); - - device_keys = json_object_new (); - json_object_set_string_member (device_keys, "user_id", self->user_id); - json_object_set_string_member (device_keys, "device_id", self->device_id); - - array = json_array_new (); - json_array_add_string_element (array, ALGORITHM_OLM); - json_array_add_string_element (array, ALGORITHM_MEGOLM); - json_object_set_array_member (device_keys, "algorithms", array); - - keys = json_object_new (); - - label = g_strconcat ("curve25519:", self->device_id, NULL); - json_object_set_string_member (keys, label, self->curve_key); - g_free (label); - - label = g_strconcat ("ed25519:", self->device_id, NULL); - json_object_set_string_member (keys, label, self->ed_key); - g_free (label); - - json_object_set_object_member (device_keys, "keys", keys); - matrix_enc_sign_json_object (self, device_keys); - - root = json_object_new (); - json_object_set_object_member (root, "device_keys", device_keys); - - return matrix_utils_json_object_to_string (root, FALSE); -} - -static gboolean -in_olm_matches (gpointer key, - gpointer value, - gpointer user_data) -{ - g_autofree char *body = NULL; - size_t match; - - body = g_strdup (user_data); - match = olm_matches_inbound_session (value, body, strlen (body)); - - if (match == olm_error ()) { - g_warning ("Error matching inbound session: %s", olm_session_last_error (key)); - return FALSE; - } - - return match; -} - -static void -handle_m_room_key (MatrixEnc *self, - JsonObject *root, - const char *sender_key) -{ - g_autofree OlmInboundGroupSession *session = NULL; - JsonObject *object; - const char *session_key, *session_id, *room_id; - - g_assert (MATRIX_IS_ENC (self)); - g_assert (root); - - session = g_malloc (olm_inbound_group_session_size ()); - olm_inbound_group_session (session); - - object = matrix_utils_json_object_get_object (root, "content"); - session_key = matrix_utils_json_object_get_string (object, "session_key"); - session_id = matrix_utils_json_object_get_string (object, "session_id"); - room_id = matrix_utils_json_object_get_string (object, "room_id"); - - if (session_key) { - size_t error; - - error = olm_init_inbound_group_session (session, (gpointer)session_key, - strlen (session_key)); - if (error == olm_error ()) - g_warning ("Error creating group session from key: %s", olm_inbound_group_session_last_error (session)); - - if (!error) { - g_autofree char *pickle = NULL; - int length; - - length = olm_pickle_inbound_group_session_length (session); - pickle = g_malloc (length + 1); - olm_pickle_inbound_group_session (session, self->pickle_key, - strlen (self->pickle_key), - pickle, length); - pickle[length] = '\0'; - CHATTY_TRACE (room_id, "saving session, room id: "); - if (self->matrix_db) - matrix_db_add_session_async (self->matrix_db, self->user_id, self->device_id, - room_id, session_id, sender_key, - g_steal_pointer (&pickle), - SESSION_MEGOLM_V1_IN, NULL, NULL); - g_hash_table_insert (self->in_group_sessions, g_strdup (session_id), - g_steal_pointer (&session)); - - } - } -} - -void -matrix_enc_handle_room_encrypted (MatrixEnc *self, - JsonObject *object) -{ - const char *algorithm, *sender, *sender_key; - g_autofree char *plaintext = NULL; - g_autofree char *body = NULL; - g_autofree char *copy = NULL; - OlmSession *session; - size_t error, length; - int type; - - g_return_if_fail (MATRIX_IS_ENC (self)); - g_return_if_fail (object); - - sender = matrix_utils_json_object_get_string (object, "sender"); - object = matrix_utils_json_object_get_object (object, "content"); - algorithm = matrix_utils_json_object_get_string (object, "algorithm"); - sender_key = matrix_utils_json_object_get_string (object, "sender_key"); - - if (!algorithm || !sender_key || !sender) - g_return_if_reached (); - - if (!g_str_equal (algorithm, ALGORITHM_MEGOLM) && - !g_str_equal (algorithm, ALGORITHM_OLM)) - g_return_if_reached (); - - object = matrix_utils_json_object_get_object (object, "ciphertext"); - object = matrix_utils_json_object_get_object (object, self->curve_key); - - body = g_strdup (matrix_utils_json_object_get_string (object, "body")); - type = matrix_utils_json_object_get_int (object, "type"); - - if (!body) - return; - - if (type == OLM_MESSAGE_TYPE_PRE_KEY) { - session = g_hash_table_find (self->in_olm_sessions, in_olm_matches, body); - CHATTY_TRACE_MSG ("message with pre key received, session exits: %d", !!session); - - if (!session) { - g_autofree char *body_copy = NULL; - session = g_malloc (olm_session_size ()); - olm_session (session); - - body_copy = g_strdup (body); - error = olm_create_inbound_session_from (session, self->account, - sender_key, strlen (sender_key), - body_copy, strlen (body_copy)); - if (error == olm_error ()) { - g_warning ("Error creating session: %s", olm_session_last_error (session)); - free_olm_session (session); - - return; - } - - /* Remove old used keys */ - error = olm_remove_one_time_keys (self->account, session); - if (error == olm_error ()) - g_warning ("Error removing key: %s", olm_account_last_error (self->account)); - - g_hash_table_insert (self->in_olm_sessions, g_strdup (sender_key), session); - } - } else { - CHATTY_TRACE_MSG ("normal message received "); - - session = g_hash_table_lookup (self->in_olm_sessions, sender_key); - - if (!session) - session = g_hash_table_lookup (self->out_olm_sessions, sender_key); - - if (!session) { - g_warning ("Couldn't find session for normal message"); - return; - } - } - - copy = g_strdup (body); - length = olm_decrypt_max_plaintext_length (session, type, copy, strlen (copy)); - - if (length == olm_error ()) { - g_warning ("Error getting max length: %s", olm_session_last_error (session)); - - return; - } - - plaintext = g_malloc (length + 1); - length = olm_decrypt (session, type, body, strlen (body), plaintext, length); - if (length == olm_error ()) { - g_warning ("Error decrypt session: %s", olm_session_last_error (session)); - - return; - } - - plaintext[length] = '\0'; - if (plaintext) { - g_autoptr(JsonObject) content = NULL; - const char *message_type; - - content = matrix_utils_string_to_json_object (plaintext); - message_type = matrix_utils_json_object_get_string (content, "type"); - - CHATTY_TRACE_MSG ("message decrypted. type: %s", message_type); - - if (g_strcmp0 (sender, matrix_utils_json_object_get_string (content, "sender")) != 0) { - g_warning ("Sender mismatch in encrypted content"); - return; - } - - if (g_strcmp0 (message_type, "m.room_key") == 0) - handle_m_room_key (self, content, sender_key); - } -} - -char * -matrix_enc_handle_join_room_encrypted (MatrixEnc *self, - const char *room_id, - JsonObject *object) -{ - OlmInboundGroupSession *session = NULL; - const char *sender_key; - const char *ciphertext, *session_id; - g_autofree char *plaintext = NULL; - char *body; - size_t length; - - g_return_val_if_fail (MATRIX_IS_ENC (self), NULL); - g_return_val_if_fail (object, NULL); - - sender_key = matrix_utils_json_object_get_string (object, "sender_key"); - - ciphertext = matrix_utils_json_object_get_string (object, "ciphertext"); - session_id = matrix_utils_json_object_get_string (object, "session_id"); - g_return_val_if_fail (ciphertext, NULL); - - if (session_id) - session = g_hash_table_lookup (self->in_group_sessions, session_id); - - CHATTY_TRACE_MSG ("Got room encrypted. session exits: %d", !!session); - - if (!session) { - g_autofree char *pickle = NULL; - - if (self->matrix_db) - pickle = matrix_db_lookup_session (self->matrix_db, self->user_id, - self->device_id, session_id, - sender_key, SESSION_MEGOLM_V1_IN); - if (pickle) { - int err; - session = g_malloc (olm_inbound_group_session_size ()); - err = olm_unpickle_inbound_group_session (session, self->pickle_key, - strlen (self->pickle_key), - pickle, strlen (pickle)); - if (err == olm_error ()) { - g_debug ("Error in group unpickle: %s", olm_inbound_group_session_last_error (session)); - g_free (session); - session = NULL; - } else { - g_hash_table_insert (self->in_group_sessions, g_strdup (session_id), session); - CHATTY_TRACE_MSG ("Got session from matrix db"); - } - } - } - - if (!session) - return NULL; - - g_return_val_if_fail (session, NULL); - - body = g_strdup (ciphertext); - length = olm_group_decrypt_max_plaintext_length (session, (gpointer)body, strlen (body)); - g_free (body); - - plaintext = g_malloc (length + 1); - body = g_strdup (ciphertext); - length = olm_group_decrypt (session, (gpointer)body, strlen (body), - (gpointer)plaintext, length, NULL); - g_free (body); - - if (length == olm_error ()) { - g_warning ("Error decrypting: %s", olm_inbound_group_session_last_error (session)); - return NULL; - } - - plaintext[length] = '\0'; - - return g_steal_pointer (&plaintext); -} - -JsonObject * -matrix_enc_encrypt_for_chat (MatrixEnc *self, - const char *room_id, - const char *message) -{ - OlmOutboundGroupSession *session; - g_autofree char *encrypted = NULL; - g_autofree char *session_id = NULL; - JsonObject *root; - size_t message_len, length; - - g_return_val_if_fail (MATRIX_IS_ENC (self), NULL); - g_return_val_if_fail (message && *message, NULL); - - session = g_hash_table_lookup (self->out_group_sessions, room_id); - g_return_val_if_fail (session, NULL); - - message_len = strlen (message); - length = olm_group_encrypt_message_length (session, message_len); - encrypted = g_malloc (length + 1); - length = olm_group_encrypt (session, (gpointer)message, message_len, - (gpointer)encrypted, length); - - if (length == olm_error ()) { - g_warning ("Error encryption: %s", olm_outbound_group_session_last_error (session)); - return NULL; - } - - encrypted[length] = '\0'; - - length = olm_outbound_group_session_id_length (session); - session_id = g_malloc (length + 1); - length = olm_outbound_group_session_id (session, (gpointer)session_id, length); - session_id[length] = '\0'; - - root = json_object_new (); - json_object_set_string_member (root, "algorithm", ALGORITHM_MEGOLM); - json_object_set_string_member (root, "sender_key", self->curve_key); - json_object_set_string_member (root, "ciphertext", encrypted); - json_object_set_string_member (root, "session_id", session_id); - json_object_set_string_member (root, "device_id", self->device_id); - - return root; -} - -JsonObject * -matrix_enc_create_out_group_keys (MatrixEnc *self, - const char *room_id, - GListModel *members_list) -{ - g_autofree OlmOutboundGroupSession *session = NULL; - g_autofree uint8_t *session_key = NULL; - g_autofree uint8_t *session_id = NULL; - g_autofree uint8_t *random = NULL; - JsonObject *root, *child; - BuddyDevice *device; - size_t length; - size_t error; - - g_return_val_if_fail (MATRIX_IS_ENC (self), FALSE); - - /* Return early if the chat has an existing outbound group session */ - if (g_hash_table_contains (self->out_group_sessions, room_id)) - return NULL; - - /* Initialize session */ - session = g_malloc (olm_outbound_group_session_size ()); - olm_outbound_group_session (session); - - /* Feed in random bits */ - length = olm_init_outbound_group_session_random_length (session); - random = g_malloc (length); - getrandom (random, length, GRND_NONBLOCK); - error = olm_init_outbound_group_session (session, random, length); - if (error == olm_error ()) { - g_warning ("Error init out group session: %s", olm_outbound_group_session_last_error (session)); - - return NULL; - } - - /* Get session id */ - length = olm_outbound_group_session_id_length (session); - session_id = g_malloc (length + 1); - length = olm_outbound_group_session_id (session, session_id, length); - if (length == olm_error ()) { - g_warning ("Error decrypt session: %s", olm_outbound_group_session_last_error (session)); - - return NULL; - } - session_id[length] = '\0'; - - /* Get session key */ - length = olm_outbound_group_session_key_length (session); - session_key = g_malloc (length + 1); - length = olm_outbound_group_session_key (session, session_key, length); - if (length == olm_error ()) { - g_warning ("Error getting session key: %s", olm_outbound_group_session_last_error (session)); - - return NULL; - } - session_key[length] = '\0'; - - root = json_object_new (); - - /* https://matrix.org/docs/spec/client_server/r0.6.1#m-room-key */ - for (guint i = 0; i < g_list_model_get_n_items (members_list); i++) { - g_autoptr(ChattyMaBuddy) buddy = NULL; - g_autoptr(GList) devices = NULL; - const char *curve_key/* , *ed_key */; - JsonObject *user; - - buddy = g_list_model_get_item (members_list, i); - devices = chatty_ma_buddy_get_devices (buddy); - - user = json_object_new (); - json_object_set_object_member (root, chatty_item_get_username (CHATTY_ITEM (buddy)), user); - - for (GList *node = devices; node; node = node->next) { - OlmSession *olm_session = NULL; - g_autofree char *one_time_key = NULL; - JsonObject *content; - - device = node->data; - curve_key = chatty_ma_device_get_curve_key (device); - - one_time_key = chatty_ma_device_get_one_time_key (device); - olm_session = ma_create_olm_out_session (self, curve_key, one_time_key); - - if (!one_time_key || !curve_key || !olm_session) - continue; - - g_hash_table_insert (self->out_olm_sessions, g_strdup (curve_key), olm_session); - - /* Create per device object */ - child = json_object_new (); - json_object_set_object_member (user, chatty_ma_device_get_id (device), child); - - json_object_set_string_member (child, "algorithm", ALGORITHM_OLM); - json_object_set_string_member (child, "sender_key", self->curve_key); - json_object_set_object_member (child, "ciphertext", json_object_new ()); - - content = json_object_new (); - child = json_object_get_object_member (child, "ciphertext"); - g_assert (child); - json_object_set_object_member (child, curve_key, content); - - /* Body to be encrypted */ - { - g_autoptr(JsonObject) object = NULL; - g_autofree char *encrypted = NULL; - g_autofree char *data = NULL; - - /* Create a json object with common data */ - object = json_object_new (); - json_object_set_string_member (object, "type", "m.room_key"); - json_object_set_string_member (object, "sender", self->user_id); - json_object_set_string_member (object, "sender_device", self->device_id); - - child = json_object_new (); - json_object_set_string_member (child, "ed25519", self->ed_key); - json_object_set_object_member (object, "keys", child); - - child = json_object_new (); - json_object_set_string_member (child, "algorithm", "m.megolm.v1.aes-sha2"); - json_object_set_string_member (child, "room_id", room_id); - json_object_set_string_member (child, "session_id", (char *)session_id); - json_object_set_string_member (child, "session_key", (char *)session_key); - json_object_set_int_member (child, "chain_index", olm_outbound_group_session_message_index (session)); - json_object_set_object_member (object, "content", child); - - /* User specific data */ - json_object_set_string_member (object, "recipient", chatty_item_get_username (CHATTY_ITEM (buddy))); - - /* Device specific data */ - child = json_object_new (); - json_object_set_string_member (child, "ed25519", chatty_ma_device_get_ed_key (device)); - json_object_set_object_member (object, "recipient_keys", child); - - /* Now encrypt the above JSON */ - data = matrix_utils_json_object_to_string (object, FALSE); - encrypted = ma_olm_encrypt (olm_session, data); - - /* Add the encrypted data as the content */ - json_object_set_int_member (content, "type", olm_encrypt_message_type (olm_session)); - json_object_set_string_member (content, "body", encrypted); - } - } - } - - /* - * We should also create an inbound session with the same key so - * that we we'll be able to decrypt the messages we sent (when - * we receive them via sync requests) - */ - { - OlmInboundGroupSession *in_session; - - in_session = g_malloc (olm_inbound_group_session_size ()); - olm_inbound_group_session (in_session); - olm_init_inbound_group_session (in_session, (gpointer)session_key, - strlen ((char *)session_key)); - g_hash_table_insert (self->in_group_sessions, - g_strdup ((char *)session_id), in_session); - } - - g_hash_table_insert (self->out_group_sessions, - g_strdup (room_id), g_steal_pointer (&session)); - - matrix_utils_clear ((char *)session_key, strlen ((char *)session_key)); - matrix_utils_clear ((char *)session_id, strlen ((char *)session_id)); - - return root; -} - -void -matrix_file_enc_info_free (MatrixFileEncInfo *enc_info) -{ - if (!enc_info) - return; - - matrix_utils_free_buffer (enc_info->aes_iv_base64); - matrix_utils_free_buffer (enc_info->aes_key_base64); - matrix_utils_free_buffer (enc_info->sha256_base64); - - matrix_utils_clear ((char *)enc_info->aes_iv, enc_info->aes_iv_len); - matrix_utils_clear ((char *)enc_info->aes_key, enc_info->aes_key_len); - matrix_utils_clear ((char *)enc_info->sha256, enc_info->sha256_len); - g_free (enc_info->aes_iv); - g_free (enc_info->aes_key); - g_free (enc_info->sha256); - - g_free (enc_info); -} - -const char * -matrix_enc_get_curve25519_key (MatrixEnc *self) -{ - g_return_val_if_fail (MATRIX_IS_ENC (self), NULL); - - return self->curve_key; -} - -const char * -matrix_enc_get_ed25519_key (MatrixEnc *self) -{ - g_return_val_if_fail (MATRIX_IS_ENC (self), NULL); - - return self->ed_key; -} diff --git a/src/matrix/matrix-enc.h b/src/matrix/matrix-enc.h deleted file mode 100644 index 4acf1c514a252180e42ac1eed2438c79609b79f9..0000000000000000000000000000000000000000 --- a/src/matrix/matrix-enc.h +++ /dev/null @@ -1,88 +0,0 @@ -/* -*- mode: c; c-basic-offset: 2; indent-tabs-mode: nil; -*- */ -/* matrix-enc.h - * - * Copyright 2020 Purism SPC - * - * Author(s): - * Mohammed Sadiq <sadiq@sadiqpk.org> - * - * SPDX-License-Identifier: GPL-3.0-or-later - */ - -#pragma once - -#include <json-glib/json-glib.h> -#include <glib-object.h> - -#include "chatty-chat.h" - -G_BEGIN_DECLS - -typedef struct _MatrixFileEncInfo MatrixFileEncInfo; - -struct _MatrixFileEncInfo { - guchar *aes_iv; - guchar *aes_key; - guchar *sha256; - - char *aes_iv_base64; - char *aes_key_base64; - char *sha256_base64; - - gsize aes_iv_len; - gsize aes_key_len; - gsize sha256_len; -}; - -#define ALGORITHM_MEGOLM "m.megolm.v1.aes-sha2" -#define ALGORITHM_OLM "m.olm.v1.curve25519-aes-sha2" -#define CURVE25519_SIZE 43 /* when base64 encoded */ -#define ED25519_SIZE 43 /* when base64 encoded */ - -#define MATRIX_TYPE_ENC (matrix_enc_get_type ()) - -G_DECLARE_FINAL_TYPE (MatrixEnc, matrix_enc, MATRIX, ENC, GObject) - -MatrixEnc *matrix_enc_new (gpointer matrix_db, - const char *pickle, - const char *key); -void matrix_enc_set_details (MatrixEnc *self, - const char *user_id, - const char *device_id); -char *matrix_enc_get_account_pickle (MatrixEnc *self); -char *matrix_enc_get_pickle_key (MatrixEnc *self); -char *matrix_enc_sign_string (MatrixEnc *self, - const char *str, - size_t len); -gboolean matrix_enc_verify (MatrixEnc *self, - JsonObject *object, - const char *matrix_id, - const char *device_id, - const char *ed_key); -size_t matrix_enc_max_one_time_keys (MatrixEnc *self); -size_t matrix_enc_create_one_time_keys (MatrixEnc *self, - size_t count); -void matrix_enc_publish_one_time_keys (MatrixEnc *self); -JsonObject *matrix_enc_get_one_time_keys (MatrixEnc *self); -char *matrix_enc_get_one_time_keys_json (MatrixEnc *self); -char *matrix_enc_get_device_keys_json (MatrixEnc *self); -void matrix_enc_handle_room_encrypted (MatrixEnc *self, - JsonObject *object); -char *matrix_enc_handle_join_room_encrypted (MatrixEnc *self, - const char *room_id, - JsonObject *object); -JsonObject *matrix_enc_encrypt_for_chat (MatrixEnc *self, - const char *room_id, - const char *message); -JsonObject *matrix_enc_create_out_group_keys (MatrixEnc *self, - const char *room_id, - GListModel *members_list); -void matrix_file_enc_info_free (MatrixFileEncInfo *enc_info); - -G_DEFINE_AUTOPTR_CLEANUP_FUNC (MatrixFileEncInfo, matrix_file_enc_info_free) - -/* For tests */ -const char *matrix_enc_get_curve25519_key (MatrixEnc *self); -const char *matrix_enc_get_ed25519_key (MatrixEnc *self); - -G_END_DECLS diff --git a/src/matrix/matrix-enums.h b/src/matrix/matrix-enums.h deleted file mode 100644 index ec11579641be5d2a59ac457cf6c0c0fe60f45cc4..0000000000000000000000000000000000000000 --- a/src/matrix/matrix-enums.h +++ /dev/null @@ -1,83 +0,0 @@ -/* -*- mode: c; c-basic-offset: 2; indent-tabs-mode: nil; -*- */ -/* matrix-enums.h - * - * Copyright 2020 Purism SPC - * - * Author(s): - * Mohammed Sadiq <sadiq@sadiqpk.org> - * - * SPDX-License-Identifier: GPL-3.0-or-later - */ - -#pragma once - - -/** - * MatrixError: - * - * The Error returned by the Matrix Server - * See https://matrix.org/docs/spec/client_server/r0.6.1#api-standards - * for details. - */ -typedef enum { - M_FORBIDDEN = 1, - M_UNKNOWN_TOKEN, - M_MISSING_TOKEN, - M_BAD_JSON, - M_NOT_JSON, - M_NOT_FOUND, - M_LIMIT_EXCEEDED, - M_UNKNOWN, - M_UNRECOGNIZED, - M_UNAUTHORIZED, - M_USER_DEACTIVATED, - M_USER_IN_USE, - M_INVALID_USERNAME, - M_ROOM_IN_USE, - M_INVALID_ROOM_STATE, - M_THREEPID_IN_USE, - M_THREEPID_NOT_FOUND, - M_THREEPID_AUTH_FAILED, - M_THREEPID_DENIED, - M_SERVER_NOT_TRUSTED, - M_UNSUPPORTED_ROOM_VERSION, - M_INCOMPATIBLE_ROOM_VERSION, - M_BAD_STATE, - M_GUEST_ACCESS_FORBIDDEN, - M_CAPTCHA_NEEDED, - M_CAPTCHA_INVALID, - M_MISSING_PARAM, - M_INVALID_PARAM, - M_TOO_LARGE, - M_EXCLUSIVE, - M_RESOURCE_LIMIT_EXCEEDED, - M_CANNOT_LEAVE_SERVER_NOTICE_ROOM, - - /* Local options */ - M_BAD_PASSWORD, - M_NO_HOME_SERVER, - M_BAD_HOME_SERVER, -} MatrixError; - - -/* - * MATRIX_BLUE_PILL and MATRIX_RED_PILL - * are objects than actions - */ -typedef enum { - /* When nothing real is happening */ - MATRIX_BLUE_PILL, /* For no/unknown command */ - MATRIX_GET_HOMESERVER, - MATRIX_VERIFY_HOMESERVER, - MATRIX_PASSWORD_LOGIN, - MATRIX_ACCESS_TOKEN_LOGIN, - MATRIX_UPLOAD_KEY, - MATRIX_GET_JOINED_ROOMS, - MATRIX_SET_TYPING, - MATRIX_SEND_MESSAGE, - MATRIX_SEND_IMAGE, - MATRIX_SEND_VIDEO, - MATRIX_SEND_FILE, - /* sync: plugged into the Matrix from real world */ - MATRIX_RED_PILL, -} MatrixAction; diff --git a/src/matrix/matrix-filter.json b/src/matrix/matrix-filter.json deleted file mode 100644 index 1fa90c65bd4051eb3a34481e4d595840f6f151cd..0000000000000000000000000000000000000000 --- a/src/matrix/matrix-filter.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "room": { - "timeline": { - "limit": 20 - }, - "state": { - "lazy_load_members": true - } - } -} diff --git a/src/matrix/matrix-net.c b/src/matrix/matrix-net.c deleted file mode 100644 index d5305cf5d867069051015a1a718b8316d46fc854..0000000000000000000000000000000000000000 --- a/src/matrix/matrix-net.c +++ /dev/null @@ -1,610 +0,0 @@ -/* -*- mode: c; c-basic-offset: 2; indent-tabs-mode: nil; -*- */ -/* matrix-api.c - * - * Copyright 2021 Purism SPC - * - * Author(s): - * Mohammed Sadiq <sadiq@sadiqpk.org> - * - * SPDX-License-Identifier: GPL-3.0-or-later - */ - -#define G_LOG_DOMAIN "chatty-matrix-net" - -#ifdef HAVE_CONFIG_H -# include "config.h" -#endif - -#define GCRYPT_NO_DEPRECATED -#include <gcrypt.h> -#include <libsoup/soup.h> -#include <json-glib/json-glib.h> - -#include "chatty-utils.h" -#include "matrix-utils.h" -#include "matrix-enums.h" -#include "matrix-enc.h" -#include "matrix-net.h" -#include "chatty-log.h" - -/** - * SECTION: matrix-net - * @title: MatrixNet - * @short_description: Matrix Network related methods - * @include: "matrix-net.h" - */ - -#define MAX_CONNECTIONS 4 - -struct _MatrixNet -{ - GObject parent_instance; - - SoupSession *soup_session; - SoupSession *file_session; - GCancellable *cancellable; - char *homeserver; - char *access_token; -}; - - -G_DEFINE_TYPE (MatrixNet, matrix_net, G_TYPE_OBJECT) - -static void -net_download_stream_cb (GObject *obj, - GAsyncResult *result, - gpointer user_data) -{ - g_autoptr(GTask) task = user_data; - GCancellable *cancellable; - GOutputStream *out_stream = NULL; - GError *error = NULL; - char *buffer, *secret; - gsize n_written; - gssize n_read; - - g_assert (G_IS_TASK (task)); - - n_read = g_input_stream_read_finish (G_INPUT_STREAM (obj), result, &error); - - if (error) { - g_task_return_error (task, error); - - return; - } - - cancellable = g_task_get_cancellable (task); - buffer = g_task_get_task_data (task); - secret = g_object_get_data (user_data, "secret"); - out_stream = g_object_get_data (user_data, "out-stream"); - g_assert (out_stream); - - if (secret) { - gcry_cipher_hd_t cipher_hd; - gcry_error_t err; - - cipher_hd = g_object_get_data (user_data, "cipher"); - g_assert (cipher_hd); - - err = gcry_cipher_decrypt (cipher_hd, secret, n_read, buffer, n_read); - if (!err) - buffer = secret; - } - - g_output_stream_write_all (out_stream, buffer, n_read, &n_written, NULL, NULL); - if (n_read == 0 || n_read == -1) { - g_output_stream_close (out_stream, cancellable, NULL); - - if (n_read == 0) { - g_autoptr(GFile) parent = NULL; - GFile *out_file; - ChattyFileInfo *file; - - file = g_object_get_data (user_data, "file"); - out_file = g_object_get_data (user_data, "out-file"); - - /* We don't use absolute directory so that the path is user agnostic */ - parent = g_file_new_build_filename (g_get_user_cache_dir (), "chatty", NULL); - file->path = g_file_get_relative_path (parent, out_file); - } - - g_task_return_boolean (task, n_read == 0); - - return; - } - - buffer = g_task_get_task_data (task); - g_input_stream_read_async (G_INPUT_STREAM (obj), buffer, 1024 * 8, G_PRIORITY_DEFAULT, cancellable, - net_download_stream_cb, g_steal_pointer (&task)); -} - -static void -net_get_file_stream_cb (GObject *obj, - GAsyncResult *result, - gpointer user_data) -{ - MatrixNet *self; - g_autoptr(GTask) task = user_data; - GCancellable *cancellable; - ChattyMessage *message; - ChattyFileInfo *file; - GInputStream *stream; - GError *error = NULL; - char *buffer = NULL; - - g_assert (G_IS_TASK (task)); - - self = g_task_get_source_object (task); - g_assert (MATRIX_IS_NET (self)); - - stream = soup_session_send_finish (SOUP_SESSION (obj), result, &error); - file = g_object_get_data (user_data, "file"); - message = g_object_get_data (user_data, "message"); - cancellable = g_task_get_cancellable (task); - - if (!error) { - GFileOutputStream *out_stream; - GFile *out_file; - gboolean is_thumbnail = FALSE; - g_autofree char *file_name = NULL; - - if (message && - chatty_message_get_preview (message) == file) - is_thumbnail = TRUE; - - file_name = g_path_get_basename (file->url); - - /* If @message is NULL, @file is an avatar image */ - out_file = g_file_new_build_filename (g_get_user_cache_dir (), "chatty", "matrix", - message ? "files" : "avatars", - is_thumbnail ? "thumbnail" : "", file_name, - NULL); - out_stream = g_file_append_to (out_file, 0, cancellable, &error); - g_object_set_data_full (user_data, "out-file", out_file, g_object_unref); - g_object_set_data_full (user_data, "out-stream", out_stream, g_object_unref); - } - - if (error) { - g_task_return_error (task, error); - return; - } - - buffer = g_malloc (1024 * 8); - g_task_set_task_data (task, buffer, g_free); - - if (message && - chatty_message_get_encrypted (message) && file->user_data) { - MatrixFileEncInfo *key; - gcry_cipher_hd_t cipher_hd; - gcry_error_t err; - - key = file->user_data; - err = gcry_cipher_open (&cipher_hd, GCRY_CIPHER_AES256, GCRY_CIPHER_MODE_CTR, 0); - - if (!err) - err = gcry_cipher_setkey (cipher_hd, key->aes_key, key->aes_key_len); - - if (!err) - err = gcry_cipher_setctr (cipher_hd, key->aes_iv, key->aes_iv_len); - - if (!err) { - char *secret = g_malloc (1024 * 8); - g_object_set_data_full (user_data, "secret", secret, g_free); - g_object_set_data_full (user_data, "cipher", cipher_hd, - (GDestroyNotify)gcry_cipher_close); - } - } - - g_input_stream_read_async (stream, buffer, 1024 * 8, G_PRIORITY_DEFAULT, cancellable, - net_download_stream_cb, g_steal_pointer (&task)); -} - -static void -net_load_from_stream_cb (GObject *object, - GAsyncResult *result, - gpointer user_data) -{ - MatrixNet *self; - JsonParser *parser = JSON_PARSER (object); - g_autoptr(GTask) task = user_data; - JsonNode *root = NULL; - GError *error = NULL; - - g_assert (JSON_IS_PARSER (parser)); - g_assert (G_IS_TASK (task)); - - self = g_task_get_source_object (task); - g_assert (MATRIX_IS_NET (self)); - - json_parser_load_from_stream_finish (parser, result, &error); - - if (!error) { - root = json_parser_get_root (parser); - error = matrix_utils_json_node_get_error (root); - } - - if (error) { - if (g_error_matches (error, MATRIX_ERROR, M_LIMIT_EXCEEDED) && - root && - JSON_NODE_HOLDS_OBJECT (root)) { - JsonObject *obj; - guint retry; - - obj = json_node_get_object (root); - retry = matrix_utils_json_object_get_int (obj, "retry_after_ms"); - g_object_set_data (G_OBJECT (task), "retry-after", GINT_TO_POINTER (retry)); - } else { - CHATTY_DEBUG_MSG ("Error loading from stream: %s", error->message); - } - - g_task_return_error (task, error); - return; - } - - if (JSON_NODE_HOLDS_OBJECT (root)) - g_task_return_pointer (task, json_node_dup_object (root), - (GDestroyNotify)json_object_unref); - else if (JSON_NODE_HOLDS_ARRAY (root)) - g_task_return_pointer (task, json_node_dup_array (root), - (GDestroyNotify)json_array_unref); - else - g_task_return_new_error (task, G_IO_ERROR, G_IO_ERROR_INVALID_DATA, - "Received invalid data"); -} - -static void -session_send_cb (GObject *object, - GAsyncResult *result, - gpointer user_data) -{ - MatrixNet *self; - g_autoptr(GTask) task = user_data; - g_autoptr(GInputStream) stream = NULL; - g_autoptr(JsonParser) parser = NULL; - GCancellable *cancellable; - GError *error = NULL; - - g_assert (G_IS_TASK (task)); - - self = g_task_get_source_object (task); - g_assert (MATRIX_IS_NET (self)); - - stream = soup_session_send_finish (self->soup_session, result, &error); - - if (error) { - if (!g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CANCELLED)) - CHATTY_TRACE_MSG ("Error session send: %s", error->message); - g_task_return_error (task, error); - return; - } - - cancellable = g_task_get_cancellable (task); - parser = json_parser_new (); - json_parser_load_from_stream_async (parser, stream, cancellable, - net_load_from_stream_cb, - g_steal_pointer (&task)); -} - -/* - * queue_data: - * @data: (transfer full) - * @size: non-zero if @data is not %NULL - * @task: (transfer full) - */ -static void -queue_data (MatrixNet *self, - char *data, - gsize size, - const char *uri_path, - const char *method, /* interned */ - GHashTable *query, - GTask *task) -{ - g_autoptr(SoupMessage) message = NULL; - g_autoptr(SoupURI) uri = NULL; - GCancellable *cancellable; - SoupMessagePriority msg_priority; - int priority = 0; - - g_assert (MATRIX_IS_NET (self)); - g_assert (uri_path && *uri_path); - g_assert (method && *method); - g_return_if_fail (self->homeserver && *self->homeserver); - - g_assert (method == SOUP_METHOD_GET || - method == SOUP_METHOD_POST || - method == SOUP_METHOD_PUT); - - uri = soup_uri_new (self->homeserver); - soup_uri_set_path (uri, uri_path); - - if (self->access_token) { - if (!query) - query = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, - (GDestroyNotify)matrix_utils_free_buffer); - - g_hash_table_replace (query, g_strdup ("access_token"), g_strdup (self->access_token)); - soup_uri_set_query_from_form (uri, query); - g_hash_table_unref (query); - } - - message = soup_message_new_from_uri (method, uri); - soup_message_headers_append (message->request_headers, "Accept-Encoding", "gzip"); - - priority = GPOINTER_TO_INT (g_object_get_data (G_OBJECT (task), "priority")); - - if (priority <= -2) - msg_priority = SOUP_MESSAGE_PRIORITY_VERY_LOW; - else if (priority == -1) - msg_priority = SOUP_MESSAGE_PRIORITY_LOW; - else if (priority == 1) - msg_priority = SOUP_MESSAGE_PRIORITY_HIGH; - else if (priority >= 2) - msg_priority = SOUP_MESSAGE_PRIORITY_VERY_HIGH; - else - msg_priority = SOUP_MESSAGE_PRIORITY_NORMAL; - - soup_message_set_priority (message, msg_priority); - - if (data) - soup_message_set_request (message, "application/json", SOUP_MEMORY_TAKE, data, size); - - cancellable = g_task_get_cancellable (task); - g_task_set_task_data (task, g_object_ref (message), g_object_unref); - soup_session_send_async (self->soup_session, message, cancellable, - session_send_cb, task); -} - -static void -matrix_net_finalize (GObject *object) -{ - MatrixNet *self = (MatrixNet *)object; - - if (self->cancellable) - g_cancellable_cancel (self->cancellable); - - soup_session_abort (self->soup_session); - soup_session_abort (self->file_session); - - g_clear_object (&self->cancellable); - g_clear_object (&self->soup_session); - g_clear_object (&self->file_session); - - g_free (self->homeserver); - - matrix_utils_free_buffer (self->access_token); - G_OBJECT_CLASS (matrix_net_parent_class)->finalize (object); -} - -static void -matrix_net_class_init (MatrixNetClass *klass) -{ - GObjectClass *object_class = G_OBJECT_CLASS (klass); - - object_class->finalize = matrix_net_finalize; -} - -static void -matrix_net_init (MatrixNet *self) -{ - self->soup_session = g_object_new (SOUP_TYPE_SESSION, - "max-conns-per-host", MAX_CONNECTIONS, - NULL); - self->file_session = g_object_new (SOUP_TYPE_SESSION, - "max-conns-per-host", MAX_CONNECTIONS, - NULL); - self->cancellable = g_cancellable_new (); -} - -MatrixNet * -matrix_net_new (void) -{ - return g_object_new (MATRIX_TYPE_NET, NULL); -} - -void -matrix_net_set_homeserver (MatrixNet *self, - const char *homeserver) -{ - g_return_if_fail (MATRIX_IS_NET (self)); - g_return_if_fail (homeserver && *homeserver); - - g_free (self->homeserver); - self->homeserver = g_strdup (homeserver); -} - -void -matrix_net_set_access_token (MatrixNet *self, - const char *access_token) -{ - g_return_if_fail (MATRIX_IS_NET (self)); - - matrix_utils_free_buffer (self->access_token); - self->access_token = g_strdup (access_token); -} - -/** - * matrix_net_send_data_async: - * @self: A #MatrixNet - * @priority: The priority of request, 0 for default - * @data: (nullable) (transfer full): The data to send - * @size: The @data size in bytes - * @uri_path: A string of the matrix uri path - * @method: An interned string for GET, PUT, POST, etc. - * @query: (nullable): A query to pass to internal #SoupURI - * @cancellable: (nullable): A #GCancellable - * @callback: The callback to run when completed - * @user_data: user data for @callback - * - * Send a JSON data @object to the @uri_path endpoint. - * @method should be one of %SOUP_METHOD_GET, %SOUP_METHOD_PUT - * or %SOUP_METHOD_POST. - * If @cancellable is %NULL, the internal cancellable - * shall be used - */ -void -matrix_net_send_data_async (MatrixNet *self, - int priority, - char *data, - gsize size, - const char *uri_path, - const char *method, /* interned */ - GHashTable *query, - GCancellable *cancellable, - GAsyncReadyCallback callback, - gpointer user_data) -{ - GTask *task; - - g_return_if_fail (MATRIX_IS_NET (self)); - g_return_if_fail (uri_path && *uri_path); - g_return_if_fail (method && *method); - g_return_if_fail (!cancellable || G_IS_CANCELLABLE (cancellable)); - g_return_if_fail (callback); - g_return_if_fail (self->homeserver && *self->homeserver); - - if (data && *data) - g_return_if_fail (size); - - if (!cancellable) - cancellable = self->cancellable; - - task = g_task_new (self, cancellable, callback, user_data); - g_object_set_data (G_OBJECT (task), "priority", GINT_TO_POINTER (priority)); - - queue_data (self, data, size, uri_path, method, query, task); -} - -/** - * matrix_net_send_json_async: - * @self: A #MatrixNet - * @priority: The priority of request, 0 for default - * @object: (nullable) (transfer full): The data to send - * @uri_path: A string of the matrix uri path - * @method: An interned string for GET, PUT, POST, etc. - * @query: (nullable): A query to pass to internal #SoupURI - * @cancellable: (nullable): A #GCancellable - * @callback: The callback to run when completed - * @user_data: user data for @callback - * - * Send a JSON data @object to the @uri_path endpoint. - * @method should be one of %SOUP_METHOD_GET, %SOUP_METHOD_PUT - * or %SOUP_METHOD_POST. - * If @cancellable is %NULL, the internal cancellable - * shall be used - */ -void -matrix_net_send_json_async (MatrixNet *self, - int priority, - JsonObject *object, - const char *uri_path, - const char *method, /* interned */ - GHashTable *query, - GCancellable *cancellable, - GAsyncReadyCallback callback, - gpointer user_data) -{ - GTask *task; - char *data = NULL; - gsize size = 0; - - g_return_if_fail (MATRIX_IS_NET (self)); - g_return_if_fail (uri_path && *uri_path); - g_return_if_fail (method && *method); - g_return_if_fail (!cancellable || G_IS_CANCELLABLE (cancellable)); - g_return_if_fail (callback); - g_return_if_fail (self->homeserver && *self->homeserver); - - if (object) { - data = matrix_utils_json_object_to_string (object, FALSE); - json_object_unref (object); - } - - if (data && *data) - size = strlen (data); - - if (!cancellable) - cancellable = self->cancellable; - - task = g_task_new (self, cancellable, callback, user_data); - g_object_set_data (G_OBJECT (task), "priority", GINT_TO_POINTER (priority)); - - queue_data (self, data, size, uri_path, method, query, task); -} - -/** - * matrix_net_get_file_async: - * @self: A #MatrixNet - * @message: (nullable) (transfer full): A #ChattyMessage - * @file: A #ChattyFileInfo - * @cancellable: (nullable): A #GCancellable - * @progress_callback: (nullable): A #GFileProgressCallback - * @callback: The callback to run when completed - * @user_data: user data for @callback - * - * Download the file @file. @file path shall be updated - * after download is completed, and if @file is encrypted - * and has keys to decrypt the file, the file shall be - * stored decrypted. - */ -void -matrix_net_get_file_async (MatrixNet *self, - ChattyMessage *message, - ChattyFileInfo *file, - GCancellable *cancellable, - GFileProgressCallback progress_callback, - GAsyncReadyCallback callback, - gpointer user_data) -{ - g_autofree char *url = NULL; - SoupMessage *msg; - GTask *task; - - g_return_if_fail (MATRIX_IS_NET (self)); - g_return_if_fail (!message || CHATTY_IS_MESSAGE (message)); - g_return_if_fail (file && file->url); - - if (message) - g_object_ref (message); - - if (!cancellable) - cancellable = self->cancellable; - - if (g_str_has_prefix (file->url, "mxc://")) { - const char *file_url; - - file_url = file->url + strlen ("mxc://"); - url = g_strconcat (self->homeserver, - "/_matrix/media/r0/download/", file_url, NULL); - } - - if (!url) - url = g_strdup (file->url); - - msg = soup_message_new (SOUP_METHOD_GET, url); - - task = g_task_new (self, cancellable, callback, user_data); - g_object_set_data (G_OBJECT (task), "progress", progress_callback); - g_object_set_data (G_OBJECT (task), "file", file); - g_object_set_data_full (G_OBJECT (task), "msg", msg, g_object_unref); - g_object_set_data_full (G_OBJECT (task), "message", message, g_object_unref); - - file->status = CHATTY_FILE_DOWNLOADING; - if (message) - chatty_message_emit_updated (message); - - soup_session_send_async (self->file_session, msg, cancellable, - net_get_file_stream_cb, task); -} - -gboolean -matrix_net_get_file_finish (MatrixNet *self, - GAsyncResult *result, - GError **error) -{ - g_return_val_if_fail (MATRIX_IS_NET (self), FALSE); - g_return_val_if_fail (G_IS_TASK (result), FALSE); - g_return_val_if_fail (!error || !*error, FALSE); - - return g_task_propagate_boolean (G_TASK (result), error); -} diff --git a/src/matrix/matrix-net.h b/src/matrix/matrix-net.h deleted file mode 100644 index a243f27b998fe5c26999d230055d255eb15245e7..0000000000000000000000000000000000000000 --- a/src/matrix/matrix-net.h +++ /dev/null @@ -1,59 +0,0 @@ -/* -*- mode: c; c-basic-offset: 2; indent-tabs-mode: nil; -*- */ -/* matrix-net.h - * - * Copyright 2021 Purism SPC - * - * Author(s): - * Mohammed Sadiq <sadiq@sadiqpk.org> - * - * SPDX-License-Identifier: GPL-3.0-or-later - */ - -#pragma once - -#include <glib-object.h> - -#include "chatty-message.h" - -G_BEGIN_DECLS - -#define MATRIX_TYPE_NET (matrix_net_get_type ()) - -G_DECLARE_FINAL_TYPE (MatrixNet, matrix_net, MATRIX, NET, GObject) - -MatrixNet *matrix_net_new (void); -void matrix_net_set_homeserver (MatrixNet *self, - const char *homeserver); -void matrix_net_set_access_token (MatrixNet *self, - const char *access_token); -void matrix_net_send_data_async (MatrixNet *self, - int priority, - char *data, - gsize size, - const char *uri_path, - const char *method, /* interned */ - GHashTable *query, - GCancellable *cancellable, - GAsyncReadyCallback callback, - gpointer user_data); -void matrix_net_send_json_async (MatrixNet *self, - int priority, - JsonObject *object, - const char *uri_path, - const char *method, /* interned */ - GHashTable *query, - GCancellable *cancellable, - GAsyncReadyCallback callback, - gpointer user_data); -void matrix_net_get_file_async (MatrixNet *self, - ChattyMessage *message, - ChattyFileInfo *file, - GCancellable *cancellable, - GFileProgressCallback progress_callback, - GAsyncReadyCallback callback, - gpointer user_data); -gboolean matrix_net_get_file_finish (MatrixNet *self, - GAsyncResult *result, - GError **error); - -G_END_DECLS diff --git a/src/matrix/matrix-utils.c b/src/matrix/matrix-utils.c index 8c7b62b038afde23f03e52c1431942efe5a27097..49331b97085c8cc83d5edb456191343b9e2de4f8 100644 --- a/src/matrix/matrix-utils.c +++ b/src/matrix/matrix-utils.c @@ -1,7 +1,7 @@ /* -*- mode: c; c-basic-offset: 2; indent-tabs-mode: nil; -*- */ /* matrix-utils.c * - * Copyright 2020 Purism SPC + * Copyright 2020, 2022 Purism SPC * * Author(s): * Mohammed Sadiq <sadiq@sadiqpk.org> @@ -16,868 +16,12 @@ # include "config.h" #endif -#define __STDC_WANT_LIB_EXT1__ 1 #include <string.h> -#include <glib/gi18n.h> -#include "matrix-enums.h" #include "chatty-log.h" #include "chatty-utils.h" #include "matrix-utils.h" -static const char *error_codes[] = { - "", /* Index 0 is reserved for no error */ - "M_FORBIDDEN", - "M_UNKNOWN_TOKEN", - "M_MISSING_TOKEN", - "M_BAD_JSON", - "M_NOT_JSON", - "M_NOT_FOUND", - "M_LIMIT_EXCEEDED", - "M_UNKNOWN", - "M_UNRECOGNIZED", - "M_UNAUTHORIZED", - "M_USER_DEACTIVATED", - "M_USER_IN_USE", - "M_INVALID_USERNAME", - "M_ROOM_IN_USE", - "M_INVALID_ROOM_STATE", - "M_THREEPID_IN_USE", - "M_THREEPID_NOT_FOUND", - "M_THREEPID_AUTH_FAILED", - "M_THREEPID_DENIED", - "M_SERVER_NOT_TRUSTED", - "M_UNSUPPORTED_ROOM_VERSION", - "M_INCOMPATIBLE_ROOM_VERSION", - "M_BAD_STATE", - "M_GUEST_ACCESS_FORBIDDEN", - "M_CAPTCHA_NEEDED", - "M_CAPTCHA_INVALID", - "M_MISSING_PARAM", - "M_INVALID_PARAM", - "M_TOO_LARGE", - "M_EXCLUSIVE", - "M_RESOURCE_LIMIT_EXCEEDED", - "M_CANNOT_LEAVE_SERVER_NOTICE_ROOM", -}; - -/** - * matrix_error_quark: - * - * Get the Matrix Error Quark. - * - * Returns: a #GQuark. - **/ -G_DEFINE_QUARK (matrix-error-quark, matrix_error) - -GError * -matrix_utils_json_node_get_error (JsonNode *node) -{ - JsonObject *object = NULL; - const char *error, *err_code; - - if (!node || (!JSON_NODE_HOLDS_OBJECT (node) && !JSON_NODE_HOLDS_ARRAY (node))) - return g_error_new (MATRIX_ERROR, M_NOT_JSON, - "Not JSON Object"); - - /* Returned by /_matrix/client/r0/rooms/{roomId}/state */ - if (JSON_NODE_HOLDS_ARRAY (node)) - return NULL; - - object = json_node_get_object (node); - err_code = matrix_utils_json_object_get_string (object, "errcode"); - - if (!err_code) - return NULL; - - error = matrix_utils_json_object_get_string (object, "error"); - - if (!error) - error = "Unknown Error"; - - if (!g_str_has_prefix (err_code, "M_")) - return g_error_new (MATRIX_ERROR, M_UNKNOWN, - "Invalid Error code"); - - for (guint i = 0; i < G_N_ELEMENTS (error_codes); i++) - if (g_str_equal (error_codes[i], err_code)) - return g_error_new (MATRIX_ERROR, i, "%s", error); - - return g_error_new (MATRIX_ERROR, M_UNKNOWN, - "Unknown Error"); -} - -void -matrix_utils_clear (char *buffer, - size_t length) -{ - if (!buffer || length == 0) - return; - - /* Brushing up your C: Note: we are not comparing with -1 */ - if (length == -1) - length = strlen (buffer); - -#ifdef __STDC_LIB_EXT1__ - memset_s (buffer, length, 0, length); -#elif HAVE_EXPLICIT_BZERO - explicit_bzero (buffer, length); -#else - volatile char *end = buffer + length; - - while (buffer != end) - *(buffer++) = 0; -#endif -} - -void -matrix_utils_free_buffer (char *buffer) -{ - matrix_utils_clear (buffer, -1); - g_free (buffer); -} - -gboolean -matrix_utils_username_is_complete (const char *username) -{ - if (!username || *username != '@') - return FALSE; - - if (strchr (username, ':')) - return TRUE; - - return FALSE; -} - -const char * -matrix_utils_get_url_from_username (const char *username) -{ - if (!chatty_utils_username_is_valid (username, CHATTY_PROTOCOL_MATRIX)) - return NULL; - - /* Return the string after ‘:’ */ - return strchr (username, ':') + 1; -} - -char * -matrix_utils_json_object_to_string (JsonObject *object, - gboolean prettify) -{ - g_autoptr(JsonNode) node = NULL; - - g_return_val_if_fail (object, NULL); - - node = json_node_new (JSON_NODE_OBJECT); - json_node_init_object (node, object); - - return json_to_string (node, !!prettify); -} - -static void utils_json_canonical_array (JsonArray *array, - GString *out); -static void -utils_handle_node (JsonNode *node, - GString *out) -{ - GType type; - - g_assert (node); - g_assert (out); - - type = json_node_get_value_type (node); - - if (type == JSON_TYPE_OBJECT) - matrix_utils_json_get_canonical (json_node_get_object (node), out); - else if (type == JSON_TYPE_ARRAY) - utils_json_canonical_array (json_node_get_array (node), out); - else if (type == G_TYPE_INVALID) - g_string_append (out, "null"); - else if (type == G_TYPE_STRING) - g_string_append_printf (out, "\"%s\"", json_node_get_string (node)); - else if (type == G_TYPE_INT64) - g_string_append_printf (out, "%" G_GINT64_FORMAT, json_node_get_int (node)); - else if (type == G_TYPE_DOUBLE) - g_string_append_printf (out, "%f", json_node_get_double (node)); - else if (type == G_TYPE_BOOLEAN) - g_string_append (out, json_node_get_boolean (node) ? "true" : "false"); - else - g_return_if_reached (); -} - -static void -utils_json_canonical_array (JsonArray *array, - GString *out) -{ - g_autoptr(GList) elements = NULL; - - g_assert (array); - g_assert (out); - - g_string_append_c (out, '['); - elements = json_array_get_elements (array); - - /* The order of array members shouldn’t be changed */ - for (GList *item = elements; item; item = item->next) { - utils_handle_node (item->data, out); - - if (item->next) - g_string_append_c (out, ','); - } - - g_string_append_c (out, ']'); -} - -GString * -matrix_utils_json_get_canonical (JsonObject *object, - GString *out) -{ - g_autoptr(GList) members = NULL; - - g_return_val_if_fail (object, NULL); - - if (!out) - out = g_string_sized_new (BUFFER_SIZE); - - g_string_append_c (out, '{'); - - members = json_object_get_members (object); - members = g_list_sort (members, (GCompareFunc)g_strcmp0); - - for (GList *item = members; item; item = item->next) { - JsonNode *node; - - g_string_append_printf (out, "\"%s\":", (char *)item->data); - - node = json_object_get_member (object, item->data); - utils_handle_node (node, out); - - if (item->next) - g_string_append_c (out, ','); - } - - g_string_append_c (out, '}'); - - return out; -} - -JsonObject * -matrix_utils_string_to_json_object (const char *json_str) -{ - g_autoptr(JsonParser) parser = NULL; - JsonNode *node; - - parser = json_parser_new (); - if (!json_parser_load_from_data (parser, json_str, -1, NULL)) - return NULL; - - node = json_parser_get_root (parser); - - if (!JSON_NODE_HOLDS_OBJECT (node)) - return NULL; - - return json_node_dup_object (node); -} - -gint64 -matrix_utils_json_object_get_int (JsonObject *object, - const char *member) -{ - JsonNode *node; - - if (!object || !member || !*member) - return 0; - - node = json_object_get_member (object, member); - - if (node && JSON_NODE_HOLDS_VALUE (node)) - return json_node_get_int (node); - - return 0; -} - -gboolean -matrix_utils_json_object_get_bool (JsonObject *object, - const char *member) -{ - JsonNode *node; - - if (!object || !member || !*member) - return FALSE; - - node = json_object_get_member (object, member); - - if (node && JSON_NODE_HOLDS_VALUE (node)) - return json_node_get_boolean (node); - - return FALSE; -} - -const char * -matrix_utils_json_object_get_string (JsonObject *object, - const char *member) -{ - JsonNode *node; - - if (!object || !member || !*member) - return NULL; - - node = json_object_get_member (object, member); - - if (node && JSON_NODE_HOLDS_VALUE (node)) - return json_node_get_string (node); - - return NULL; -} - -JsonObject * -matrix_utils_json_object_get_object (JsonObject *object, - const char *member) -{ - JsonNode *node; - - if (!object || !member || !*member) - return NULL; - - node = json_object_get_member (object, member); - - if (node && JSON_NODE_HOLDS_OBJECT (node)) - return json_node_get_object (node); - - return NULL; -} - -JsonArray * -matrix_utils_json_object_get_array (JsonObject *object, - const char *member) -{ - JsonNode *node; - - if (!object || !member || !*member) - return NULL; - - node = json_object_get_member (object, member); - - if (node && JSON_NODE_HOLDS_ARRAY (node)) - return json_node_get_array (node); - - return NULL; -} - -JsonObject * -matrix_utils_get_message_json_object (SoupMessage *message, - const char *member) -{ - g_autoptr(JsonParser) parser = NULL; - g_autoptr(SoupBuffer) buffer = NULL; - JsonObject *object = NULL; - gboolean is_json; - - if (!message || !message->response_body) - return NULL; - - buffer = soup_message_body_flatten (message->response_body); - parser = json_parser_new (); - is_json = json_parser_load_from_data (parser, buffer->data, buffer->length, NULL); - - if (is_json) { - JsonNode *root; - - root = json_parser_get_root (parser); - - if (root && JSON_NODE_HOLDS_OBJECT (root)) - object = json_node_get_object (root); - - if (member && object) - object = json_object_get_object_member (object, member); - } - - return object ? json_object_ref (object) : NULL; -} - -static gboolean -cancel_read_uri (gpointer user_data) -{ - g_autoptr(GTask) task = user_data; - - g_assert (G_IS_TASK (task)); - - g_object_set_data (G_OBJECT (task), "timeout-id", 0); - - /* XXX: Not thread safe? */ - if (g_task_get_completed (task) || g_task_had_error (task)) - return G_SOURCE_REMOVE; - - g_task_set_task_data (task, GINT_TO_POINTER (TRUE), NULL); - g_task_return_new_error (task, G_IO_ERROR, G_IO_ERROR_TIMED_OUT, - "Request timeout"); - g_cancellable_cancel (g_task_get_cancellable (task)); - - return G_SOURCE_REMOVE; -} - -static void -load_from_stream_cb (JsonParser *parser, - GAsyncResult *result, - gpointer user_data) -{ - g_autoptr(GTask) task = user_data; - GError *error = NULL; - gboolean timeout; - - g_assert (JSON_IS_PARSER (parser)); - g_assert (G_IS_TASK (task)); - - timeout = GPOINTER_TO_INT (g_task_get_task_data (task)); - - /* Task return is handled somewhere else */ - if (timeout) - return; - - if (json_parser_load_from_stream_finish (parser, result, &error)) - g_task_return_pointer (task, json_parser_steal_root (parser), - (GDestroyNotify)json_node_unref); - else - g_task_return_error (task, error); -} - -static gboolean -matrix_utils_handle_ssl_error (SoupMessage *message) -{ - GTlsCertificate *cert = NULL; - GApplication *app; - GtkWidget *dialog; - GtkWindow *window = NULL; - SoupURI *uri; - g_autofree char *msg = NULL; - const char *host; - GTlsCertificateFlags err_flags; - gboolean cancelled = FALSE; - - if (!SOUP_IS_MESSAGE (message) || - !soup_message_get_https_status (message, &cert, &err_flags) || - !err_flags) - return cancelled; - - app = g_application_get_default (); - if (app) - window = gtk_application_get_active_window (GTK_APPLICATION (app)); - - if (!window) - return cancelled; - - uri = soup_message_get_uri (message); - host = soup_uri_get_host (uri); - - switch (err_flags) { - case G_TLS_CERTIFICATE_UNKNOWN_CA: - if (g_tls_certificate_get_issuer (cert)) - msg = g_strdup_printf (_("The certificate for ‘%s’ has unknown CA"), host); - else - msg = g_strdup_printf (_("The certificate for ‘%s’ is self-signed"), host); - break; - - case G_TLS_CERTIFICATE_EXPIRED: - msg = g_strdup_printf (_("The certificate for ‘%s’ has expired"), host); - break; - - case G_TLS_CERTIFICATE_REVOKED: - msg = g_strdup_printf (_("The certificate for ‘%s’ has been revoked"), host); - break; - - case G_TLS_CERTIFICATE_BAD_IDENTITY: - case G_TLS_CERTIFICATE_NOT_ACTIVATED: - case G_TLS_CERTIFICATE_INSECURE: - case G_TLS_CERTIFICATE_GENERIC_ERROR: - case G_TLS_CERTIFICATE_VALIDATE_ALL: - default: - msg = g_strdup_printf (_("Error validating certificate for ‘%s’"), host); - } - - dialog = gtk_message_dialog_new (window, - GTK_DIALOG_MODAL | GTK_DIALOG_DESTROY_WITH_PARENT, - GTK_MESSAGE_QUESTION, - GTK_BUTTONS_NONE, - "%s", msg); - - /* XXX: This may not work, see https://gitlab.gnome.org/GNOME/glib-networking/-/issues/32 */ - if (err_flags == G_TLS_CERTIFICATE_REVOKED) - gtk_dialog_add_buttons (GTK_DIALOG (dialog), - _("Close"), GTK_RESPONSE_CLOSE, - NULL); - else - gtk_dialog_add_buttons (GTK_DIALOG (dialog), - _("Reject"), GTK_RESPONSE_REJECT, - _("Accept"), GTK_RESPONSE_ACCEPT, - NULL); - - if (gtk_dialog_run (GTK_DIALOG (dialog)) != GTK_RESPONSE_ACCEPT) - cancelled = TRUE; - - gtk_widget_destroy (dialog); - - return cancelled; -} - -static void -uri_file_read_cb (GObject *object, - GAsyncResult *result, - gpointer user_data) -{ - SoupSession *session = (SoupSession *)object; - g_autoptr(GTask) task = user_data; - g_autoptr(GInputStream) stream = NULL; - g_autoptr(JsonParser) parser = NULL; - GCancellable *cancellable; - SoupMessage *message; - GError *error = NULL; - gboolean has_timeout; - GTlsCertificateFlags err_flags; - - g_assert (G_IS_TASK (task)); - g_assert (SOUP_IS_SESSION (session)); - - stream = soup_session_send_finish (session, result, &error); - message = g_object_get_data (G_OBJECT (task), "message"); - has_timeout = GPOINTER_TO_INT (g_task_get_task_data (task)); - - /* Task return is handled somewhere else */ - if (has_timeout) - return; - - if (error) { - g_task_return_error (task, error); - return; - } - - soup_message_get_https_status (message, NULL, &err_flags); - - if (message && - soup_message_get_https_status (message, NULL, &err_flags) && - err_flags) { - guint timeout_id, timeout; - - timeout = GPOINTER_TO_UINT (g_object_get_data (G_OBJECT (task), "timeout")); - timeout_id = GPOINTER_TO_UINT (g_object_get_data (G_OBJECT (task), "timeout-id")); - g_clear_handle_id (&timeout_id, g_source_remove); - g_object_unref (task); - - if (matrix_utils_handle_ssl_error (message)) { - g_task_return_new_error (task, G_IO_ERROR, G_IO_ERROR_CANCELLED, - "Cancelled"); - return; - } - - timeout_id = g_timeout_add_seconds (timeout, cancel_read_uri, g_object_ref (task)); - g_object_set_data (G_OBJECT (task), "timeout-id", GUINT_TO_POINTER (timeout_id)); - } - - cancellable = g_task_get_cancellable (task); - parser = json_parser_new (); - json_parser_load_from_stream_async (parser, stream, cancellable, - (GAsyncReadyCallback)load_from_stream_cb, - g_steal_pointer (&task)); -} - -static void -message_network_event_cb (SoupMessage *msg, - GSocketClientEvent event, - GIOStream *connection, - gpointer user_data) -{ - GSocketAddress *address; - - /* We shall have a non %NULL @connection by %G_SOCKET_CLIENT_CONNECTING event */ - if (event != G_SOCKET_CLIENT_CONNECTING) - return; - - /* @connection is a #GSocketConnection */ - address = g_socket_connection_get_remote_address (G_SOCKET_CONNECTION (connection), NULL); - g_object_set_data_full (user_data, "address", address, g_object_unref); -} - -void -matrix_utils_read_uri_async (const char *uri, - guint timeout, - GCancellable *cancellable, - GAsyncReadyCallback callback, - gpointer user_data) -{ - g_autoptr(SoupSession) session = NULL; - g_autoptr(SoupMessage) message = NULL; - g_autoptr(GCancellable) cancel = NULL; - g_autoptr(GTask) task = NULL; - guint timeout_id; - - g_return_if_fail (uri && *uri); - g_return_if_fail (!cancellable || G_IS_CANCELLABLE (cancellable)); - - if (cancellable) - cancel = g_object_ref (cancellable); - else - cancel = g_cancellable_new (); - - task = g_task_new (NULL, cancel, callback, user_data); - /* if this changes to TRUE, we consider it has been timeout */ - g_task_set_task_data (task, GINT_TO_POINTER (FALSE), NULL); - g_task_set_source_tag (task, matrix_utils_read_uri_async); - - timeout = CLAMP (timeout, 5, 60); - timeout_id = g_timeout_add_seconds (timeout, cancel_read_uri, g_object_ref (task)); - g_object_set_data (G_OBJECT (task), "timeout", GUINT_TO_POINTER (timeout)); - g_object_set_data (G_OBJECT (task), "timeout-id", GUINT_TO_POINTER (timeout_id)); - - message = soup_message_new (SOUP_METHOD_GET, uri); - if (!message) { - g_task_return_new_error (task, G_IO_ERROR, G_IO_ERROR_INVALID_FILENAME, - "%s is not a valid uri", uri); - return; - } - - soup_message_set_flags (message, SOUP_MESSAGE_NO_REDIRECT); - g_object_set_data_full (G_OBJECT (task), "message", g_object_ref (message), g_object_unref); - - /* XXX: Switch to */ - g_signal_connect_object (message, "network-event", - G_CALLBACK (message_network_event_cb), task, - G_CONNECT_AFTER); - session = soup_session_new (); - g_object_set (G_OBJECT (session), SOUP_SESSION_SSL_STRICT, FALSE, NULL); - - soup_session_send_async (session, message, cancel, - uri_file_read_cb, - g_steal_pointer (&task)); -} - -JsonNode * -matrix_utils_read_uri_finish (GAsyncResult *result, - GError **error) -{ - GTask *task = (GTask *)result; - - g_return_val_if_fail (G_IS_TASK (result), NULL); - g_return_val_if_fail (g_task_get_source_tag (task) == matrix_utils_read_uri_async, NULL); - - return g_task_propagate_pointer (G_TASK (result), error); -} - -static void -get_homeserver_cb (GObject *obj, - GAsyncResult *result, - gpointer user_data) -{ - g_autoptr(GTask) task = user_data; - g_autoptr(JsonNode) root = NULL; - JsonObject *object = NULL; - const char *homeserver = NULL; - GError *error = NULL; - - g_assert (G_IS_TASK (task)); - - root = matrix_utils_read_uri_finish (result, &error); - - if (!root) { - g_task_return_error (task, error); - return; - } - - g_object_set_data_full (G_OBJECT (task), "address", - g_object_steal_data (G_OBJECT (result), "address"), - g_object_unref); - - if (JSON_NODE_HOLDS_OBJECT (root)) - object = json_node_get_object (root); - - if (object) - object = matrix_utils_json_object_get_object (object, "m.homeserver"); - - if (object) - homeserver = matrix_utils_json_object_get_string (object, "base_url"); - - if (!homeserver) - g_task_return_new_error (task, G_IO_ERROR, G_IO_ERROR_INVALID_DATA, - "Got invalid response from server"); - else - g_task_return_pointer (task, g_strdup (homeserver), g_free); -} - -/** - * matrix_utils_get_homeserver_async: - * @username: A complete matrix username - * @timeout: timeout in seconds - * @cancellable: (nullable): A #GCancellable - * @callback: The callback to run - * @user_data: (nullable): The data passed to @callback - * - * Get homeserver from the given @username. @userename - * should be in complete form (eg: @user:example.org) - * - * @timeout is clamped between 5 and 60 seconds. - * - * This is a network operation and shall connect to the - * network to fetch homeserver details. - * - * See https://matrix.org/docs/spec/client_server/r0.6.1#server-discovery - */ -void -matrix_utils_get_homeserver_async (const char *username, - guint timeout, - GCancellable *cancellable, - GAsyncReadyCallback callback, - gpointer user_data) -{ - g_autoptr(GTask) task = NULL; - g_autofree char *uri = NULL; - const char *url; - - g_return_if_fail (username); - g_return_if_fail (!cancellable || G_IS_CANCELLABLE (cancellable)); - g_return_if_fail (callback); - - task = g_task_new (NULL, cancellable, callback, user_data); - g_task_set_source_tag (task, matrix_utils_get_homeserver_async); - - if (!chatty_utils_username_is_valid (username, CHATTY_PROTOCOL_MATRIX)) { - g_task_return_new_error (task, G_IO_ERROR, G_IO_ERROR_INVALID_FILENAME, - "Username '%s' is not a complete matrix id", username); - return; - } - - url = matrix_utils_get_url_from_username (username); - uri = g_strconcat ("https://", url, "/.well-known/matrix/client", NULL); - - matrix_utils_read_uri_async (uri, timeout, cancellable, - get_homeserver_cb, g_steal_pointer (&task)); -} - -/** - * matrix_utils_get_homeserver_finish: - * @result: A #GAsyncResult - * @error: (optional): A #GError - * - * Finish call to matrix_utils_get_homeserver_async(). - * - * Returns: (nullable) : The homeserver string or %NULL - * on error. Free with g_free(). - */ -char * -matrix_utils_get_homeserver_finish (GAsyncResult *result, - GError **error) -{ - GTask *task = (GTask *)result; - - g_return_val_if_fail (G_IS_TASK (result), NULL); - g_return_val_if_fail (g_task_get_source_tag (task) == matrix_utils_get_homeserver_async, NULL); - - return g_task_propagate_pointer (G_TASK (result), error); -} - -static void -api_get_version_cb (GObject *obj, - GAsyncResult *result, - gpointer user_data) -{ - g_autoptr(GTask) task = user_data; - g_autoptr(JsonNode) root = NULL; - JsonObject *object = NULL; - JsonArray *array = NULL; - GError *error = NULL; - const char *server; - gboolean valid; - - g_assert (G_IS_TASK (task)); - - server = g_task_get_task_data (task); - root = matrix_utils_read_uri_finish (result, &error); - - if (!error && root) - error = matrix_utils_json_node_get_error (root); - - if (!root || - g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CANCELLED) || - g_error_matches (error, G_IO_ERROR, G_IO_ERROR_TIMED_OUT)) { - if (error) - g_task_return_error (task, error); - else - g_task_return_new_error (task, G_IO_ERROR, G_IO_ERROR_FAILED, - "Failed to get version for server '%s'", server); - return; - } - - g_object_set_data_full (G_OBJECT (task), "address", - g_object_steal_data (G_OBJECT (result), "address"), - g_object_unref); - - object = json_node_get_object (root); - array = matrix_utils_json_object_get_array (object, "versions"); - valid = FALSE; - - if (array) { - g_autoptr(GString) versions = NULL; - guint length; - - versions = g_string_new (""); - length = json_array_get_length (array); - - for (guint i = 0; i < length; i++) { - const char *version; - - version = json_array_get_string_element (array, i); - g_string_append_printf (versions, " %s", version); - - /* We have tested only with r0.6.x and r0.5.0 */ - if (g_str_has_prefix (version, "r0.5.") || - g_str_has_prefix (version, "r0.6.")) - valid = TRUE; - } - - g_log (G_LOG_DOMAIN, CHATTY_LOG_LEVEL_TRACE, - "'%s' has versions:%s, valid: %s", - server, versions->str, CHATTY_LOG_BOOL (valid)); - } - - g_task_return_boolean (task, valid); -} - -void -matrix_utils_verify_homeserver_async (const char *server, - guint timeout, - GCancellable *cancellable, - GAsyncReadyCallback callback, - gpointer user_data) -{ - g_autoptr(GTask) task = NULL; - g_autofree char *uri = NULL; - - g_return_if_fail (!cancellable || G_IS_CANCELLABLE (cancellable)); - g_return_if_fail (callback); - - task = g_task_new (NULL, cancellable, callback, user_data); - g_task_set_task_data (task, g_strdup (server), g_free); - g_task_set_source_tag (task, matrix_utils_verify_homeserver_async); - - if (!server || !*server || - !g_str_has_prefix (server, "http")) { - g_task_return_new_error (task, G_IO_ERROR, - G_IO_ERROR_INVALID_DATA, - "URI '%s' is invalid", server); - return; - } - - uri = g_strconcat (server, "/_matrix/client/versions", NULL); - matrix_utils_read_uri_async (uri, timeout, cancellable, - api_get_version_cb, - g_steal_pointer (&task)); -} - -gboolean -matrix_utils_verify_homeserver_finish (GAsyncResult *result, - GError **error) -{ - g_return_val_if_fail (G_IS_TASK (result), FALSE); - - return g_task_propagate_boolean (G_TASK (result), error); -} - static void pixbuf_load_stream_cb (GObject *object, GAsyncResult *result, diff --git a/src/matrix/matrix-utils.h b/src/matrix/matrix-utils.h index 4ad1e1acafe8254cb1e5a6276a1284a008f64637..77a05943c2efd40e2a90b43cc10574b88be3977a 100644 --- a/src/matrix/matrix-utils.h +++ b/src/matrix/matrix-utils.h @@ -13,58 +13,9 @@ #include <gdk-pixbuf/gdk-pixbuf.h> #include <glib-object.h> -#include <json-glib/json-glib.h> -#include <libsoup/soup.h> -#define MATRIX_ERROR (matrix_error_quark ()) +#include "cm-enums.h" -GQuark matrix_error_quark (void); -GError *matrix_utils_json_node_get_error (JsonNode *node); -void matrix_utils_clear (char *buffer, - size_t length); -void matrix_utils_free_buffer (char *buffer); -gboolean matrix_utils_username_is_complete (const char *username); -const char *matrix_utils_get_url_from_username (const char *username); -char *matrix_utils_json_object_to_string (JsonObject *object, - gboolean prettify); -GString *matrix_utils_json_get_canonical (JsonObject *object, - GString *out); -JsonObject *matrix_utils_string_to_json_object (const char *json_str); -gint64 matrix_utils_json_object_get_int (JsonObject *object, - const char *member); -gboolean matrix_utils_json_object_get_bool (JsonObject *object, - const char *member); -const char *matrix_utils_json_object_get_string (JsonObject *object, - const char *member); -JsonObject *matrix_utils_json_object_get_object (JsonObject *object, - const char *member); -JsonArray *matrix_utils_json_object_get_array (JsonObject *object, - const char *member); - -JsonObject *matrix_utils_get_message_json_object (SoupMessage *message, - const char *member); - -void matrix_utils_read_uri_async (const char *uri, - guint timeout, - GCancellable *cancellable, - GAsyncReadyCallback callback, - gpointer user_data); -JsonNode *matrix_utils_read_uri_finish (GAsyncResult *result, - GError **error); -void matrix_utils_get_homeserver_async (const char *username, - guint timeout, - GCancellable *cancellable, - GAsyncReadyCallback callback, - gpointer user_data); -void matrix_utils_verify_homeserver_async (const char *server, - guint timeout, - GCancellable *cancellable, - GAsyncReadyCallback callback, - gpointer user_data); -gboolean matrix_utils_verify_homeserver_finish (GAsyncResult *result, - GError **error); -char *matrix_utils_get_homeserver_finish (GAsyncResult *result, - GError **error); void matrix_utils_get_pixbuf_async (const char *file, GCancellable *cancellable, GAsyncReadyCallback callback, diff --git a/src/matrix/meson.build b/src/matrix/meson.build index 18d77c3e7112f5ccf187fed469107c636a08c685..e14688cb42d6b4b50e770d7fa04ebc7885c3390f 100644 --- a/src/matrix/meson.build +++ b/src/matrix/meson.build @@ -2,19 +2,9 @@ src_inc += include_directories('.') libsrc += files([ 'chatty-matrix.c', - 'matrix-api.c', - 'matrix-enc.c', - 'matrix-db.c', - 'matrix-net.c', 'matrix-utils.c', 'chatty-ma-account.c', 'chatty-ma-buddy.c', 'chatty-ma-chat.c', + 'chatty-ma-key-chat.c', ]) - -chatty_deps += [ - dependency('libgcrypt'), - dependency('libsoup-2.4'), - dependency('json-glib-1.0'), - libolm_dep, -] diff --git a/src/meson.build b/src/meson.build index a133d679783284254b118fcfe4674cc2bda55096..ad89a67cb04fc64a9df3d9a13193f34f40c584e9 100644 --- a/src/meson.build +++ b/src/meson.build @@ -68,19 +68,21 @@ ui_files = files ( 'ui/chatty-contact-row.ui', # FIXME: Testing this fails in CI, # but works fine locally - # 'ui/chatty-info-dialog.ui', - # 'ui/chatty-dialog-join-muc.ui', + 'ui/chatty-info-dialog.ui', + 'ui/chatty-dialog-join-muc.ui', + # Can't test as this requires chatty-contact-list, # 'ui/chatty-dialog-new-chat.ui', 'ui/chatty-fp-row.ui', 'ui/chatty-list-row.ui', 'ui/chatty-message-row.ui', # FIXME - # 'ui/chatty-pp-account-details.ui', - # 'ui/chatty-ma-chat-info.ui', - # 'ui/chatty-pp-user-info.ui', - # 'ui/chatty-mm-chat-info.ui', - # 'ui/chatty-settings-dialog.ui', - # We can't test with GdTaggedEntry + 'ui/chatty-pp-account-details.ui', + 'ui/chatty-ma-chat-info.ui', + 'ui/chatty-pp-chat-info.ui', + 'ui/chatty-mm-chat-info.ui', + 'ui/chatty-settings-dialog.ui', + # Can't test as this requires chatty-chat-list, + # which is not in libsrc # 'ui/chatty-window.ui', ) @@ -90,6 +92,10 @@ chatty_sources += [ 'chatty-application.c', 'chatty-chat-list.c', 'chatty-contact-list.c', + 'chatty-header-bar.c', + 'chatty-main-view.c', + 'chatty-invite-view.c', + 'chatty-verification-view.c', 'chatty-window.c', 'dialogs/chatty-info-dialog.c', 'dialogs/chatty-settings-dialog.c', @@ -101,7 +107,7 @@ chatty_sources += [ libphonenumber_dep = cc.find_library('phonenumber', required: true) chatty_deps += [ - dependency('gio-2.0', version: '>= 2.50'), + dependency('gio-2.0', version: '>= 2.66'), dependency('gtk+-3.0', version: '>= 3.22'), libgd_dep, dependency('libsecret-1'), @@ -116,6 +122,7 @@ chatty_deps += [ libfeedback_dep, libm_dep, libphonenumber_dep, + libcmatrix_dep, ] gnome = import('gnome') @@ -125,16 +132,24 @@ resources = gnome.compile_resources('chatty-resources', c_name: 'chatty' ) -libchatty = both_libraries( - 'chatty', libsrc, resources, +libchatty_shared = shared_library( + 'chatty', [libsrc, 'library.c'], + resources, include_directories: src_inc, install: false, dependencies: chatty_deps, ) +libchatty_static = static_library( + 'chatty', libsrc, + resources, + include_directories: src_inc, + dependencies: chatty_deps, +) + gtk_builder_tool = find_program('gtk-builder-tool', required: false) if gtk_builder_tool.found() - preload_env = 'LD_PRELOAD=@0@:libhandy-1.so'.format(libchatty.get_shared_lib().full_path()) + preload_env = 'LD_PRELOAD=libhandy-1.so:@0@'.format(libchatty_shared.full_path()) foreach file: ui_files test('Validate @0@'.format(file), gtk_builder_tool, env: [preload_env], @@ -145,7 +160,7 @@ endif executable('chatty', chatty_sources, resources, include_directories: src_inc, dependencies: chatty_deps, - link_with: libchatty.get_static_lib(), + link_with: libchatty_static, install: true, install_rpath: purple_plugdir, ) diff --git a/src/mm/chatty-mm-account.c b/src/mm/chatty-mm-account.c index 978265986a02aeb19c96c2445b8b194191bb6ce1..6e9ee3d628469a21e92fab4c53721a93459269e3 100644 --- a/src/mm/chatty-mm-account.c +++ b/src/mm/chatty-mm-account.c @@ -178,9 +178,11 @@ create_sorted_numbers (const char *numbers, /* Make the array bigger so that we can assure it's NULL terminated */ g_ptr_array_set_size (sorted, sorted->len + 1); - for (guint i = 0; i < sorted->len - 1; i++) { + for (guint i = 0; i < sorted->len - 1;) { if (g_strcmp0 (sorted->pdata[i], sorted->pdata[i + 1]) == 0) g_ptr_array_remove_index (sorted, i); + else + i++; } return g_strjoinv (",", (char **)sorted->pdata); diff --git a/src/mm/chatty-mmsd.c b/src/mm/chatty-mmsd.c index b5f0347a390593dcdff86249f096511d96fc8808..195aa0e93b874e9739a7c8303a7b2ab4284d514e 100644 --- a/src/mm/chatty-mmsd.c +++ b/src/mm/chatty-mmsd.c @@ -1508,6 +1508,93 @@ chatty_mmsd_service_added_cb (ChattyMmsd *self, chatty_mmsd_connect_to_service (self, parameters); } +enum mms_tx_rx_error { + MMS_TX_RX_ERROR_UNKNOWN, + MMS_TX_RX_ERROR_DNS, + MMS_TX_RX_ERROR_HTTP +}; + +static void +chatty_mmsd_service_send_error_cb (ChattyMmsd *self, + GVariant *parameters) +{ + g_autoptr(GVariant) error_props = NULL; + g_autofree char *recipientlist = NULL; + g_autofree char *mms_path = NULL; + g_autofree char *sender = NULL; + g_autofree char *host = NULL; + unsigned int error_type; + mms_payload *payload; + ChattyMessage *message; + GVariantDict dict; + + g_variant_get (parameters, "(@a{?*})", &error_props); + g_variant_dict_init (&dict, error_props); + + if (!g_variant_dict_lookup (&dict, "ErrorType", "u", &error_type)) + error_type = MMS_TX_RX_ERROR_UNKNOWN; + + g_variant_dict_lookup (&dict, "Host", "s", &host); + g_variant_dict_lookup (&dict, "MessagePath", "s", &mms_path); + + g_warning ("Error sending MMS of type %u, host %s, Chatty message uid %s", + error_type, + host, + mms_path); + + payload = g_hash_table_lookup (self->mms_hash_table, mms_path); + + if (payload == NULL) { + g_debug ("MMS not found. Was it already deleted?"); + return; + } + + message = payload->message; + sender = g_strdup (payload->sender); + recipientlist = g_strdup (payload->chat); + g_return_if_fail (recipientlist && *recipientlist); + + chatty_message_set_status (payload->message, + CHATTY_STATUS_SENDING_FAILED, + 0); + + if (!chatty_mm_account_recieve_mms_cb (self->mm_account, + message, + sender, + recipientlist)) { + g_debug ("Message was deleted!"); + return; + } + /* TODO: Have a user notification that the send failed */ + +} + +static void +chatty_mmsd_service_receive_error_cb (ChattyMmsd *self, + GVariant *parameters) +{ + g_autoptr(GVariant) error_props = NULL; + g_autofree char *mms_from = NULL; + g_autofree char *host = NULL; + unsigned int error_type; + GVariantDict dict; + + g_variant_get (parameters, "(@a{?*})", &error_props); + g_variant_dict_init (&dict, error_props); + + if (!g_variant_dict_lookup (&dict, "ErrorType", "u", &error_type)) + error_type = MMS_TX_RX_ERROR_UNKNOWN; + + g_variant_dict_lookup (&dict, "Host", "s", &host); + g_variant_dict_lookup (&dict, "From", "s", &mms_from); + + /* TODO: Notify user of issue */ + g_warning ("Error receiving MMS of type %u, host %s, from %s", + error_type, + host, + mms_from); +} + static void chatty_mmsd_service_removed_cb (ChattyMmsd *self, GVariant *parameters) @@ -1717,6 +1804,14 @@ chatty_mmsd_signal_emitted_cb (GDBusConnection *connection, g_strcmp0 (interface_name, MMSD_MANAGER_INTERFACE) == 0 && g_strcmp0 (object_path, MMSD_PATH) == 0) chatty_mmsd_service_removed_cb (self, parameters); + else if (g_strcmp0 (signal_name, "MessageSendError") == 0 && + g_strcmp0 (interface_name, MMSD_SERVICE_INTERFACE) == 0 && + g_strcmp0 (object_path, MMSD_MODEMMANAGER_PATH) == 0) + chatty_mmsd_service_send_error_cb (self, parameters); + else if (g_strcmp0 (signal_name, "MessageReceiveError") == 0 && + g_strcmp0 (interface_name, MMSD_SERVICE_INTERFACE) == 0 && + g_strcmp0 (object_path, MMSD_MODEMMANAGER_PATH) == 0) + chatty_mmsd_service_receive_error_cb (self, parameters); else if (g_strcmp0 (signal_name, "MessageAdded") == 0 && g_strcmp0 (interface_name, MMSD_SERVICE_INTERFACE) == 0 && g_strcmp0 (object_path, MMSD_MODEMMANAGER_PATH) == 0) diff --git a/src/mm/chatty-sms-uri.c b/src/mm/chatty-sms-uri.c index 6259702d11aa22274de643106e0ca146d7c14668..f8330dc2dc6201b7651a54deb64f6f5a3f8e0ac6 100644 --- a/src/mm/chatty-sms-uri.c +++ b/src/mm/chatty-sms-uri.c @@ -86,9 +86,11 @@ chatty_sms_parse_numbers (ChattySmsUri *self) /* Make the array bigger so that we can assure it's NULL terminated */ g_ptr_array_set_size (array, array->len + 1); - for (guint i = 0; i < array->len - 1; i++) { + for (guint i = 0; i < array->len - 1;) { if (g_strcmp0 (array->pdata[i], array->pdata[i + 1]) == 0) g_ptr_array_remove_index (array, i); + else + i++; } self->numbers_str = g_strjoinv (",", (char **)array->pdata); diff --git a/src/purple/chatty-purple.c b/src/purple/chatty-purple.c index 43657e9a156883045d32f019ca200f73f11818e7..46e240ce02499ae68434faa2b34d9af0138529a4 100644 --- a/src/purple/chatty-purple.c +++ b/src/purple/chatty-purple.c @@ -503,8 +503,7 @@ purple_account_added_cb (PurpleAccount *pp_account, protocol_id = purple_account_get_protocol_id (pp_account); /* We handles matrix accounts native. */ - if (chatty_settings_get_experimental_features (chatty_settings_get_default ()) && - g_strcmp0 (protocol_id, "prpl-matrix") == 0) { + if (g_strcmp0 (protocol_id, "prpl-matrix") == 0) { return; } @@ -1725,10 +1724,8 @@ chatty_purple_load_plugins (ChattyPurple *self) chatty_xeps_init (); settings = chatty_settings_get_default (); - if (chatty_settings_get_experimental_features (settings)) - chatty_purple_unload_plugin (purple_plugins_find_with_id ("prpl-matrix")); - - /* We now have native SMS */ + /* We now have native Matrix and SMS */ + chatty_purple_unload_plugin (purple_plugins_find_with_id ("prpl-matrix")); chatty_purple_unload_plugin (purple_plugins_find_with_id ("prpl-mm-sms")); g_signal_connect_object (settings, "notify::message-carbons", diff --git a/src/purple/meson.build b/src/purple/meson.build index adab23478a4dec96f9ecbb11f1d2e6accb4c2b69..68fc29db378f14b73eb8b3fd197e7aabd2461fc4 100644 --- a/src/purple/meson.build +++ b/src/purple/meson.build @@ -4,7 +4,7 @@ if (not purple_dep.found()) subdir_done() endif -purple_plugdir = purple_dep.get_pkgconfig_variable('plugindir') +purple_plugdir = purple_dep.get_variable(pkgconfig: 'plugindir') jabber = meson.get_compiler('c').find_library('jabber', dirs: purple_plugdir) jabber_incdir = include_directories('xeps/prpl/jabber') src_inc += jabber_incdir diff --git a/src/ui/chatty-chat-view.ui b/src/ui/chatty-chat-view.ui index 61dd53cf4114d00edf30fc86705b28a71d6201de..d4ce7b755eecb232863cbaa3a3c3435416ca24cf 100644 --- a/src/ui/chatty-chat-view.ui +++ b/src/ui/chatty-chat-view.ui @@ -227,12 +227,6 @@ </object> </child> - <child> - <object class="GtkBox" id="empty_view"> - <property name="visible">True</property> - </object> - </child> - </template> <object class="GtkTextBuffer" id="message_input_buffer"> diff --git a/src/ui/chatty-header-bar.ui b/src/ui/chatty-header-bar.ui new file mode 100644 index 0000000000000000000000000000000000000000..b8fb9da3087e013e474cfbc793f829ed7f07ab20 --- /dev/null +++ b/src/ui/chatty-header-bar.ui @@ -0,0 +1,407 @@ +<?xml version="1.0" encoding="UTF-8"?> +<interface> + <template class="ChattyHeaderBar" parent="GtkBox"> + <property name="orientation">vertical</property> + + <child> + <object class="HdyLeaflet" id="leaflet"> + <property name="visible">1</property> + <property name="vexpand">0</property> + + <child> + <object class="HdyHeaderBar" id="main_header_bar"> + <property name="visible">1</property> + <property name="show-close-button">1</property> + <property name="title" translatable="yes">Chats</property> + + <child> + <object class="GtkButton" id="back_button"> + <property name="can-focus">0</property> + <property name="receives-default">0</property> + <signal name="clicked" handler="header_back_clicked_cb" swapped="yes"/> + <child internal-child="accessible"> + <object class="AtkObject"> + <property name="accessible-name" translatable="yes">Back</property> + </object> + </child> + <child> + <object class="GtkImage"> + <property name="visible">1</property> + <property name="icon-name">go-previous-symbolic</property> + </object> + </child> + </object> + </child> + + <child> + <object class="GtkMenuButton" id="add_chat_button"> + <property name="visible">1</property> + <property name="sensitive">0</property> + <property name="can-focus">0</property> + <property name="receives-default">0</property> + <property name="popover">new_message_popover</property> + <child internal-child="accessible"> + <object class="AtkObject"> + <property name="accessible-name" translatable="yes">Add Chat</property> + </object> + </child> + <child> + <object class="GtkImage"> + <property name="visible">1</property> + <property name="icon-name">list-add-symbolic</property> + </object> + </child> + </object> + </child> + + <child> + <object class="GtkMenuButton"> + <property name="visible" bind-source="add_chat_button" bind-property="visible" bind-flags="sync-create"/> + <property name="can-focus">0</property> + <property name="receives-default">0</property> + <property name="popover">main_menu_popover</property> + <child internal-child="accessible"> + <object class="AtkObject"> + <property name="accessible-name" translatable="yes">Menu</property> + </object> + </child> + <child> + <object class="GtkImage"> + <property name="visible">1</property> + <property name="icon-name">open-menu-symbolic</property> + </object> + </child> + </object> + <packing> + <property name="pack-type">end</property> + </packing> + </child> + + <child> + <object class="GtkToggleButton" id="search_button"> + <property name="visible">1</property> + <property name="can-focus">0</property> + <property name="receives-default">0</property> + <child internal-child="accessible"> + <object class="AtkObject"> + <property name="accessible-name" translatable="yes">Search</property> + </object> + </child> + <child> + <object class="GtkImage"> + <property name="visible">1</property> + <property name="icon-name">system-search-symbolic</property> + </object> + </child> + </object> + <packing> + <property name="pack-type">end</property> + </packing> + </child> + + </object> + <packing> + <!-- The name should match the name for the sidebar in main window --> + <!-- so that we can bind them by name --> + <property name="name">sidebar</property> + </packing> + </child> + + <child> + <object class="GtkSeparator"> + <property name="visible">1</property> + <style> + <class name="sidebar"/> + </style> + </object> + </child> + + <child> + <object class="HdyHeaderBar" id="content_header_bar"> + <property name="visible">1</property> + <property name="expand">1</property> + <property name="show-close-button">1</property> + <child> + <object class="GtkButton"> + <property name="visible">1</property> + <property name="can-focus">0</property> + <property name="receives-default">0</property> + <property name="visible" bind-source="leaflet" bind-property="folded" bind-flags="sync-create"/> + <signal name="clicked" handler="header_back_clicked_cb" swapped="yes"/> + <child internal-child="accessible"> + <object class="AtkObject"> + <property name="accessible-name" translatable="yes">Back</property> + </object> + </child> + <child> + <object class="GtkImage"> + <property name="visible">1</property> + <property name="icon-name">go-previous-symbolic</property> + <property name="icon-size">1</property> + </object> + </child> + </object> + <packing> + <property name="pack-type">start</property> + </packing> + </child> + <child type="title"> + <object class="GtkBox"> + <property name="visible">1</property> + <property name="orientation">horizontal</property> + <property name="halign">center</property> + <property name="valign">center</property> + <child> + <object class="ChattyAvatar" id="content_avatar"> + <property name="halign">center</property> + <property name="size">28</property> + </object> + </child> + <child> + <object class="GtkLabel" id="content_title"> + <property name="visible">1</property> + <property name="margin-start">6</property> + <property name="halign">center</property> + <property name="ellipsize">end</property> + <attributes> + <attribute name="weight" value="bold"/> + </attributes> + </object> + </child> + </object> + </child> + <child> + <object class="GtkMenuButton" id="content_menu_button"> + <property name="can-focus">0</property> + <property name="receives-default">0</property> + <property name="popover">content_menu_popover</property> + <child> + <object class="GtkImage"> + <property name="visible">1</property> + <property name="icon-name">view-more-symbolic</property> + </object> + </child> + </object> + <packing> + <property name="position">0</property> + <property name="pack-type">end</property> + </packing> + </child> + <child> + <object class="GtkButton" id="call_button"> + <property name="visible">0</property> + <property name="can-focus">0</property> + <property name="receives-default">0</property> + <property name="action-name">win.call-user</property> + <child internal-child="accessible"> + <object class="AtkObject"> + <property name="accessible-name" translatable="yes">Call</property> + </object> + </child> + <child> + <object class="GtkImage"> + <property name="visible">1</property> + <property name="icon-name">call-start-symbolic</property> + </object> + </child> + </object> + <packing> + <property name="position">1</property> + <property name="pack-type">end</property> + </packing> + </child> + </object> + <packing> + <!-- The name should match the name for the content widget in main window --> + <!-- so that we can bind them by name --> + <property name="name">content</property> + </packing> + </child> + + </object> + </child> + + </template> + + <object class="GtkPopoverMenu" id="main_menu_popover"> + <child> + <object class="GtkBox"> + <property name="visible">1</property> + <property name="margin">12</property> + <property name="orientation">vertical</property> + <child> + <object class="GtkModelButton"> + <property name="visible">1</property> + <property name="receives-default">0</property> + <property name="text" translatable="yes" context="show archived chat list when clicked">Archived</property> + <property name="action-name">win.show-archived</property> + </object> + </child> + <child> + <object class="GtkModelButton"> + <property name="visible">1</property> + <property name="receives-default">0</property> + <property name="text" translatable="yes">Preferences</property> + <property name="action-name">win.show-settings</property> + </object> + </child> + <child> + <object class="GtkModelButton"> + <property name="visible">1</property> + <property name="text" translatable="yes">Keyboard _Shortcuts</property> + <property name="action-name">win.show-help-overlay</property> + </object> + </child> + <child> + <object class="GtkModelButton"> + <property name="visible">1</property> + <property name="text" translatable="yes">Help</property> + <property name="action-name">app.help</property> + </object> + </child> + <child> + <object class="GtkModelButton"> + <property name="visible">1</property> + <property name="receives-default">0</property> + <property name="text" translatable="yes">About Chats</property> + <property name="action-name">app.about</property> + </object> + </child> + </object> + </child> + </object> + + <object class="GtkPopoverMenu" id="new_message_popover"> + <property name="can-focus">0</property> + <property name="relative-to">add_chat_button</property> + <child> + <object class="GtkBox"> + <property name="visible">1</property> + <property name="margin">12</property> + <property name="orientation">vertical</property> + <child> + <object class="GtkModelButton" id="new_chat_button"> + <property name="visible">1</property> + <property name="receives-default">0</property> + <property name="text" translatable="yes">New Message…</property> + <property name="action-name">win.new-chat</property> + </object> + </child> + <child> + <object class="GtkModelButton" id="new_sms_mms_button"> + <property name="visible">1</property> + <property name="receives-default">0</property> + <property name="text" translatable="yes">New SMS/MMS Message…</property> + <property name="action-name">win.new-sms-mms</property> + </object> + </child> + <child> + <object class="GtkModelButton" id="new_group_chat_button"> + <property name="visible">1</property> + <property name="receives-default">0</property> + <property name="text" translatable="yes">New Group Message…</property> + <property name="action-name">win.new-group-chat</property> + </object> + </child> + </object> + </child> + </object> + + <object class="GtkPopoverMenu" id="content_menu_popover"> + <child> + <object class="GtkBox"> + <property name="visible">1</property> + <property name="margin">12</property> + <property name="orientation">vertical</property> + <child> + <object class="GtkModelButton"> + <property name="visible">1</property> + <property name="active">0</property> + <property name="can-focus">0</property> + <property name="receives-default">0</property> + <property name="text" translatable="yes">Chat Details</property> + <property name="action-name">win.show-chat-details</property> + </object> + </child> + <child> + <object class="GtkSeparator"> + <property name="visible">1</property> + <property name="margin-top">6</property> + <property name="margin-bottom">6</property> + </object> + </child> + <child> + <object class="GtkModelButton" id="leave_button"> + <property name="visible">1</property> + <property name="can-focus">0</property> + <property name="receives-default">0</property> + <property name="text" translatable="yes">Leave Chat</property> + <property name="action-name">win.leave-chat</property> + </object> + </child> + <child> + <object class="GtkModelButton" id="block_button"> + <property name="can-focus">0</property> + <property name="receives-default">0</property> + <property name="text" translatable="yes">Block Contact</property> + <property name="action-name">win.block-chat</property> + </object> + </child> + <child> + <object class="GtkModelButton" id="unblock_button"> + <property name="can-focus">0</property> + <property name="receives-default">0</property> + <property name="text" translatable="yes">Unblock Contact</property> + <property name="action-name">win.unblock-chat</property> + </object> + </child> + <child> + <object class="GtkModelButton" id="archive_button"> + <property name="visible">1</property> + <property name="can-focus">0</property> + <property name="receives-default">0</property> + <property name="text" translatable="yes">Archive chat</property> + <property name="action-name">win.archive-chat</property> + </object> + </child> + <child> + <object class="GtkModelButton" id="unarchive_button"> + <property name="can-focus">0</property> + <property name="receives-default">0</property> + <property name="text" translatable="yes">Unarchive chat</property> + <property name="action-name">win.unarchive-chat</property> + </object> + </child> + <child> + <object class="GtkModelButton" id="delete_button"> + <property name="visible">1</property> + <property name="can-focus">0</property> + <property name="receives-default">0</property> + <property name="text" translatable="yes">Delete Chat</property> + <property name="action-name">win.delete-chat</property> + </object> + </child> + </object> + </child> + </object> + + <object class="GtkSizeGroup" id="sidebar_group"> + <widgets> + <widget name="main_header_bar"/> + </widgets> + </object> + + <object class="GtkSizeGroup" id="content_group"> + <widgets> + <widget name="content_header_bar"/> + </widgets> + </object> + + <object class="HdyHeaderGroup" id="header_group"> + <headerbars> + <headerbar name="main_header_bar"/> + <headerbar name="content_header_bar"/> + </headerbars> + </object> + +</interface> diff --git a/src/ui/chatty-invite-view.ui b/src/ui/chatty-invite-view.ui new file mode 100644 index 0000000000000000000000000000000000000000..634409491a4e0439c76bca629da35385e3723852 --- /dev/null +++ b/src/ui/chatty-invite-view.ui @@ -0,0 +1,121 @@ +<?xml version="1.0" encoding="UTF-8"?> +<interface> + <template class="ChattyInviteView" parent="GtkBox"> + + <child> + <object class="HdyClamp"> + <property name="visible">1</property> + <property name="hexpand">1</property> + <property name="margin-top">18</property> + <property name="margin-bottom">18</property> + <property name="margin-start">18</property> + <property name="margin-end">18</property> + <property name="halign">center</property> + <property name="valign">center</property> + + <child> + <object class="GtkBox"> + <property name="visible">1</property> + <property name="orientation">vertical</property> + <property name="spacing">18</property> + + <child> + <object class="GtkLabel" id="invite_title"> + <property name="visible">1</property> + <property name="wrap">1</property> + <property name="justify">center</property> + <style> + <class name="large-title"/> + </style> + </object> + </child> + + <child> + <object class="ChattyAvatar" id="chat_avatar"> + <property name="visible">1</property> + <property name="width-request">96</property> + <property name="height-request">96</property> + </object> + </child> + + <!-- todo: when we have inviter details --> + <child> + <object class="GtkLabel" id="invite_subtitle"> + <property name="visible">0</property> + <property name="wrap">1</property> + <property name="justify">center</property> + <style> + <class name="heading"/> + </style> + </object> + </child> + + <child> + <object class="GtkButton" id="accept_button"> + <property name="visible">1</property> + <signal name="clicked" handler="invite_accept_clicked_cb" swapped="yes"/> + <style> + <class name="suggested-action"/> + </style> + + <!-- todo: Use GtkCenterBox when porting to GTK4 --> + <child> + <object class="GtkBox"> + <property name="visible">1</property> + <property name="spacing">6</property> + <property name="halign">center</property> + <child> + <object class="GtkLabel"> + <property name="visible">1</property> + <property name="use-underline">1</property> + <property name="label" translatable="yes">_Accept</property> + </object> + </child> + <child> + <object class="GtkSpinner" id="accept_spinner"> + <property name="visible">1</property> + </object> + </child> + </object> + </child> + + </object> + </child> + + <child> + <object class="GtkButton" id="reject_button"> + <property name="visible">1</property> + <property name="sensitive" bind-source="accept_button" bind-property="sensitive" bind-flags="sync-create"/> + <signal name="clicked" handler="invite_reject_clicked_cb" swapped="yes"/> + + <child> + <object class="GtkBox"> + <property name="visible">1</property> + <property name="spacing">6</property> + <property name="halign">center</property> + <child> + <object class="GtkLabel"> + <property name="visible">1</property> + <property name="use-underline">1</property> + <property name="label" translatable="yes">_Reject</property> + </object> + </child> + <child> + <object class="GtkSpinner" id="reject_spinner"> + <property name="visible">1</property> + </object> + </child> + </object> + </child> + + </object> + </child> + + </object> + </child> + + </object> <!-- ./HdyClamp --> + </child> + + </template> +</interface> diff --git a/src/ui/chatty-main-view.ui b/src/ui/chatty-main-view.ui new file mode 100644 index 0000000000000000000000000000000000000000..3f961c04be70757c27cff20068fdeb7da5950bfb --- /dev/null +++ b/src/ui/chatty-main-view.ui @@ -0,0 +1,37 @@ +<?xml version="1.0" encoding="UTF-8"?> +<interface> + <template class="ChattyMainView" parent="GtkBox"> + + <child> + <object class="GtkStack" id="main_stack"> + <property name="visible">1</property> + + <child> + <object class="ChattyChatView" id="content_view"> + <property name="visible">1</property> + </object> + </child> + + <child> + <object class="ChattyInviteView" id="invite_view"> + <property name="visible">1</property> + </object> + </child> + + <child> + <object class="ChattyVerificationView" id="verification_view"> + <property name="visible">1</property> + </object> + </child> + + <child> + <object class="GtkBox" id="empty_view"> + <property name="visible">1</property> + </object> + </child> + + </object> + </child> + + </template> +</interface> diff --git a/src/ui/chatty-settings-dialog.ui b/src/ui/chatty-settings-dialog.ui index 953f2164207fa5aab609658d35fdeece0b108e22..d88fea6e58e421bc55942094e43e05155be79042 100644 --- a/src/ui/chatty-settings-dialog.ui +++ b/src/ui/chatty-settings-dialog.ui @@ -60,7 +60,7 @@ <property name="label" translatable="yes">_Add</property> <signal name="clicked" handler="chatty_settings_add_clicked_cb" swapped="yes" /> <style> - <class name="default" /> + <class name="suggested-action" /> </style> </object> <packing> diff --git a/src/ui/chatty-verification-view.ui b/src/ui/chatty-verification-view.ui new file mode 100644 index 0000000000000000000000000000000000000000..8ba214adabf7a286b6d4ea017a4980cb0236df54 --- /dev/null +++ b/src/ui/chatty-verification-view.ui @@ -0,0 +1,550 @@ +<?xml version="1.0" encoding="UTF-8"?> +<interface> + <template class="ChattyVerificationView" parent="GtkBox"> + + <child> + <object class="HdyClamp"> + <property name="visible">1</property> + <property name="hexpand">1</property> + <property name="margin-top">18</property> + <property name="margin-bottom">18</property> + <property name="margin-start">18</property> + <property name="margin-end">18</property> + <property name="halign">center</property> + <property name="valign">center</property> + + <child> + <object class="GtkBox"> + <property name="visible">1</property> + <property name="orientation">vertical</property> + <property name="spacing">18</property> + + <child> + <object class="GtkLabel"> + <property name="visible">1</property> + <property name="wrap">1</property> + <property name="justify">center</property> + <property name="label" translatable="yes">Incoming Verification Request</property> + <style> + <class name="large-title"/> + </style> + </object> + </child> + + <child> + <object class="GtkImage"> + <property name="visible">1</property> + <property name="icon-name">system-lock-screen-symbolic</property> + <property name="pixel-size">96</property> + </object> + </child> + + <child> + <object class="GtkLabel"> + <property name="visible">1</property> + <property name="wrap">1</property> + <property name="xalign">0.0</property> + <property name="label" translatable="yes">Verify this user to mark them as trusted.</property> + </object> + </child> + + <child> + <object class="GtkBox"> + <property name="visible">1</property> + <property name="spacing">12</property> + <child> + <object class="ChattyAvatar" id="user_avatar"> + <property name="visible">1</property> + </object> + </child> + <child> + <object class="GtkBox"> + <property name="visible">1</property> + <property name="orientation">vertical</property> + <property name="spacing">3</property> + <child> + <object class="GtkLabel" id="name_label"> + <property name="visible">1</property> + <property name="xalign">0.0</property> + </object> + </child> + <child> + <object class="GtkLabel" id="username_label"> + <property name="visible">1</property> + <property name="xalign">0.0</property> + <style> + <class name="dim-label"/> + </style> + </object> + </child> + </object> + </child> + + </object> + </child> + + <child> + <object class="GtkLabel"> + <property name="visible">1</property> + <property name="wrap">1</property> + <property name="xalign">0.0</property> + <property name="label" translatable="yes">Verifying this user will mark their session as trusted, and also mark your session as trusted to them.</property> + </object> + </child> + + <child> + <object class="GtkButton" id="continue_button"> + <property name="visible">1</property> + <signal name="clicked" handler="verification_continue_clicked_cb" swapped="yes"/> + <style> + <class name="suggested-action"/> + </style> + + <!-- todo: Use GtkCenterBox when porting to GTK4 --> + <child> + <object class="GtkBox"> + <property name="visible">1</property> + <property name="spacing">6</property> + <property name="hexpand">1</property> + <child type="center"> + <object class="GtkLabel"> + <property name="visible">1</property> + <property name="use-underline">1</property> + <property name="label" translatable="yes">C_ontinue</property> + </object> + </child> + <child> + <object class="GtkImage"> + <property name="visible">1</property> + <property name="icon-name">go-next-symbolic</property> + <style> + <class name="dim-label"/> + </style> + </object> + <packing> + <property name="pack-type">end</property> + </packing> + </child> + <child> + <object class="GtkSpinner" id="continue_spinner"> + <property name="visible">1</property> + </object> + <packing> + <property name="pack-type">end</property> + </packing> + </child> + </object> + </child> + + </object> + </child> + + <child> + <object class="GtkButton" id="cancel_button"> + <property name="visible">1</property> + <property name="sensitive" bind-source="continue_button" bind-property="sensitive" bind-flags="sync-create"/> + <signal name="clicked" handler="verification_cancel_clicked_cb" swapped="yes"/> + + <child> + <object class="GtkBox"> + <property name="visible">1</property> + <property name="spacing">6</property> + <property name="halign">center</property> + <child type="center"> + <object class="GtkLabel"> + <property name="visible">1</property> + <property name="use-underline">1</property> + <property name="label" translatable="yes">_Cancel</property> + </object> + </child> + <child> + <object class="GtkSpinner" id="cancel_spinner"> + <property name="visible">1</property> + </object> + <packing> + <property name="pack-type">end</property> + </packing> + </child> + </object> + </child> + + </object> + </child> + + </object> + </child> + + </object> <!-- ./HdyClamp --> + </child> + + </template> + + <object class="GtkDialog" id="verification_dialog"> + <property name="visible">0</property> + <property name="modal">1</property> + <property name="use-header-bar">1</property> + + <child internal-child="headerbar"> + <object class="GtkHeaderBar"> + <property name="visible">1</property> + <property name="show-close-button">0</property> + <child> + <object class="GtkButton"> + <property name="label" translatable="yes">_Cancel</property> + <property name="visible">1</property> + <property name="use-underline">1</property> + <property name="valign">center</property> + <signal name="clicked" handler="verification_cancel_clicked_cb" swapped="yes"/> + </object> + <packing> + <property name="pack-type">start</property> + </packing> + </child> + <child> + <object class="GtkButton" id="verification_type_button"> + <property name="visible">1</property> + <property name="valign">center</property> + <signal name="clicked" handler="verification_type_clicked_cb" swapped="yes"/> + </object> + <packing> + <property name="pack-type">end</property> + </packing> + </child> + </object> + </child> + + <child internal-child="vbox"> + <object class="GtkBox"> + <property name="visible">1</property> + <property name="margin-start">12</property> + <property name="margin-end">12</property> + <property name="margin-top">12</property> + <property name="margin-bottom">12</property> + <property name="spacing">32</property> + <property name="halign">center</property> + + <child> + <object class="GtkStack" id="content_stack"> + <property name="visible">1</property> + <signal name="notify::visible-child" handler="verification_content_child_changed_cb" swapped="yes"/> + + <child> + <object class="GtkGrid" id="emoji_content"> + <property name="visible">1</property> + <property name="row-spacing">9</property> + <property name="column-spacing">3</property> + <property name="column-homogeneous">1</property> + <property name="row-homogeneous">1</property> + + <child> + <object class="GtkLabel"> + <property name="visible">1</property> + <property name="xalign">0.0</property> + <property name="yalign">0.0</property> + <property name="wrap">1</property> + <property name="max-width-chars">40</property> + <property name="label" translatable="yes">Verify this user by confirming the following emoji appears on their screen</property> + </object> + <packing> + <property name="top-attach">0</property> + <property name="left-attach">0</property> + <property name="width">8</property> + </packing> + </child> + + <child> + <object class="GtkBox"> + <property name="visible">1</property> + <property name="spacing">3</property> + <property name="orientation">vertical</property> + <child> + <object class="GtkLabel" id="emoji1_label"> + <property name="visible">1</property> + <style> + <class name="large-title"/> + </style> + </object> + </child> + <child> + <object class="GtkLabel" id="emoji1_title"> + <property name="visible">1</property> + </object> + </child> + </object> + <packing> + <property name="top-attach">1</property> + <property name="left-attach">0</property> + <property name="width">2</property> + </packing> + </child> + + <child> + <object class="GtkBox"> + <property name="visible">1</property> + <property name="spacing">3</property> + <property name="orientation">vertical</property> + <child> + <object class="GtkLabel" id="emoji2_label"> + <property name="visible">1</property> + <style> + <class name="large-title"/> + </style> + </object> + </child> + <child> + <object class="GtkLabel" id="emoji2_title"> + <property name="visible">1</property> + </object> + </child> + </object> + <packing> + <property name="top-attach">1</property> + <property name="left-attach">2</property> + <property name="width">2</property> + </packing> + </child> + + <child> + <object class="GtkBox"> + <property name="visible">1</property> + <property name="spacing">3</property> + <property name="orientation">vertical</property> + <child> + <object class="GtkLabel" id="emoji3_label"> + <property name="visible">1</property> + <style> + <class name="large-title"/> + </style> + </object> + </child> + <child> + <object class="GtkLabel" id="emoji3_title"> + <property name="visible">1</property> + </object> + </child> + </object> + <packing> + <property name="top-attach">1</property> + <property name="left-attach">4</property> + <property name="width">2</property> + </packing> + </child> + + <child> + <object class="GtkBox"> + <property name="visible">1</property> + <property name="spacing">3</property> + <property name="orientation">vertical</property> + <child> + <object class="GtkLabel" id="emoji4_label"> + <property name="visible">1</property> + <style> + <class name="large-title"/> + </style> + </object> + </child> + <child> + <object class="GtkLabel" id="emoji4_title"> + <property name="visible">1</property> + </object> + </child> + </object> + <packing> + <property name="top-attach">1</property> + <property name="left-attach">6</property> + <property name="width">2</property> + </packing> + </child> + + <child> + <object class="GtkBox"> + <property name="visible">1</property> + <property name="spacing">3</property> + <property name="orientation">vertical</property> + <child> + <object class="GtkLabel" id="emoji5_label"> + <property name="visible">1</property> + <style> + <class name="large-title"/> + </style> + </object> + </child> + <child> + <object class="GtkLabel" id="emoji5_title"> + <property name="visible">1</property> + </object> + </child> + </object> + <packing> + <property name="top-attach">2</property> + <property name="left-attach">1</property> + <property name="width">2</property> + </packing> + </child> + + <child> + <object class="GtkBox"> + <property name="visible">1</property> + <property name="spacing">3</property> + <property name="orientation">vertical</property> + <child> + <object class="GtkLabel" id="emoji6_label"> + <property name="visible">1</property> + <style> + <class name="large-title"/> + </style> + </object> + </child> + <child> + <object class="GtkLabel" id="emoji6_title"> + <property name="visible">1</property> + </object> + </child> + </object> + <packing> + <property name="top-attach">2</property> + <property name="left-attach">3</property> + <property name="width">2</property> + </packing> + </child> + + <child> + <object class="GtkBox"> + <property name="visible">1</property> + <property name="spacing">3</property> + <property name="orientation">vertical</property> + <child> + <object class="GtkLabel" id="emoji7_label"> + <property name="visible">1</property> + <style> + <class name="large-title"/> + </style> + </object> + </child> + <child> + <object class="GtkLabel" id="emoji7_title"> + <property name="visible">1</property> + </object> + </child> + </object> + <packing> + <property name="top-attach">2</property> + <property name="left-attach">5</property> + <property name="width">2</property> + </packing> + </child> + + </object> + </child> + + <child> + <object class="GtkBox" id="decimal_content"> + <property name="visible">1</property> + <property name="orientation">vertical</property> + <property name="spacing">32</property> + + <child> + <object class="GtkLabel"> + <property name="visible">1</property> + <property name="xalign">0.0</property> + <property name="yalign">0.0</property> + <property name="wrap">1</property> + <property name="max-width-chars">40</property> + <property name="label" translatable="yes">Verify this user by confirming the following number appears on their screen.</property> + </object> + </child> + + <child> + <object class="GtkBox"> + <property name="visible">1</property> + <property name="vexpand">1</property> + <property name="halign">center</property> + <property name="spacing">24</property> + <style> + <class name="large-title"/> + </style> + + <child> + <object class="GtkLabel" id="decimal1_label"> + <property name="visible">1</property> + </object> + </child> + <child> + <object class="GtkLabel" id="decimal2_label"> + <property name="visible">1</property> + </object> + </child> + <child> + <object class="GtkLabel" id="decimal3_label"> + <property name="visible">1</property> + </object> + </child> + + </object> + </child> + + </object> + </child> + + </object> + </child> + + <child> + <object class="GtkLabel"> + <property name="visible">1</property> + <property name="xalign">0.0</property> + <property name="wrap">1</property> + <property name="max-width-chars">40</property> + <property name="label" translatable="yes">To be secure, do this in person or use a trusted way to communicate.</property> + </object> + </child> + + <child> + <object class="GtkBox"> + <property name="visible">1</property> + <property name="hexpand">1</property> + <property name="spacing">18</property> + <child> + <object class="GtkButton" id="no_match_button"> + <property name="visible">1</property> + <property name="use-underline">1</property> + <property name="label" translatable="yes" context="verb, which means 'both items are not the same'">_Don’t Match</property> + <signal name="clicked" handler="verification_cancel_clicked_cb" swapped="yes"/> + <style> + <class name="destructive-action"/> + </style> + </object> + <packing> + <property name="pack-type">start</property> + </packing> + </child> + <child> + <object class="GtkButton" id="match_button"> + <property name="visible">1</property> + <property name="use-underline">1</property> + <property name="label" translatable="yes" context="verb, which means 'both items are the same'">_Match</property> + <signal name="clicked" handler="verification_match_clicked_cb" swapped="yes"/> + <style> + <class name="suggested-action"/> + </style> + </object> + <packing> + <property name="pack-type">end</property> + </packing> + </child> + </object> + </child> + + </object> + </child> + + </object> + + <object class="GtkSizeGroup"> + <widgets> + <widget name="no_match_button"/> + <widget name="match_button"/> + </widgets> + </object> + +</interface> diff --git a/src/ui/chatty-window.ui b/src/ui/chatty-window.ui index 1b757fd3cf5b49b33165e53f1f9bdc95d0b6f950..6a3076cf4e5f29d3d357f170629f54dbf1b01479 100644 --- a/src/ui/chatty-window.ui +++ b/src/ui/chatty-window.ui @@ -1,245 +1,6 @@ <?xml version="1.0" encoding="UTF-8"?> <interface> <requires lib="gtk+" version="3.16"/> - <object class="GtkPopoverMenu" id="header_view_chat_list_popover"> - <property name="can_focus">False</property> - <child> - <object class="GtkBox"> - <property name="visible">True</property> - <property name="can_focus">False</property> - <property name="margin">12</property> - <property name="orientation">vertical</property> - <child> - <object class="GtkModelButton"> - <property name="visible">True</property> - <property name="can_focus">True</property> - <property name="receives_default">False</property> - <property name="text" translatable="yes" context="show archived chat list when clicked">Archived</property> - <signal name="clicked" handler="chatty_window_show_archived" swapped="yes"/> - </object> - <packing> - <property name="expand">False</property> - <property name="fill">True</property> - </packing> - </child> - <child> - <object class="GtkModelButton"> - <property name="visible">True</property> - <property name="can_focus">True</property> - <property name="receives_default">False</property> - <property name="text" translatable="yes">Preferences</property> - <signal name="clicked" handler="chatty_window_show_settings_dialog" swapped="yes"/> - </object> - <packing> - <property name="expand">False</property> - <property name="fill">True</property> - </packing> - </child> - <child> - <object class="GtkModelButton"> - <property name="visible">1</property> - <property name="text" translatable="yes">Keyboard _Shortcuts</property> - <property name="action-name">win.show-help-overlay</property> - </object> - </child> - <child> - <object class="GtkModelButton"> - <property name="visible">True</property> - <property name="text" translatable="yes">Help</property> - <property name="action-name">app.help</property> - </object> - </child> - <child> - <object class="GtkModelButton"> - <property name="visible">True</property> - <property name="can_focus">True</property> - <property name="receives_default">False</property> - <property name="text" translatable="yes">About Chats</property> - <signal name="clicked" handler="chatty_window_show_about_dialog" swapped="yes"/> - </object> - <packing> - <property name="expand">False</property> - <property name="fill">True</property> - </packing> - </child> - </object> - <packing> - <property name="submenu">main</property> - </packing> - </child> - </object> - <object class="GtkPopoverMenu" id="header_chat_list_new_msg_popover"> - <property name="can_focus">False</property> - <property name="relative-to">header_add_chat_button</property> - <child> - <object class="GtkBox"> - <property name="visible">True</property> - <property name="can_focus">False</property> - <property name="margin">12</property> - <property name="orientation">vertical</property> - <child> - <object class="GtkModelButton" id="menu_new_message_button"> - <property name="visible">True</property> - <property name="can_focus">True</property> - <property name="receives_default">False</property> - <property name="text" translatable="yes">New Message…</property> - <signal name="clicked" handler="window_new_message_clicked_cb" swapped="yes"/> - </object> - <packing> - <property name="expand">False</property> - <property name="fill">True</property> - </packing> - </child> - <child> - <object class="GtkModelButton" id="menu_new_sms_mms_message_button"> - <property name="visible">True</property> - <property name="can_focus">True</property> - <property name="receives_default">False</property> - <property name="text" translatable="yes">New SMS/MMS Message…</property> - <signal name="clicked" handler="window_new_sms_mms_message_clicked_cb" swapped="yes"/> - </object> - <packing> - <property name="expand">False</property> - <property name="fill">True</property> - </packing> - </child> - <child> - <object class="GtkModelButton" id="menu_new_group_message_button"> - <property name="visible">True</property> - <property name="can_focus">True</property> - <property name="receives_default">False</property> - <property name="text" translatable="yes">New Group Message…</property> - <signal name="clicked" handler="window_new_muc_clicked_cb" swapped="yes"/> - </object> - <packing> - <property name="expand">False</property> - <property name="fill">True</property> - </packing> - </child> - </object> - <packing> - <property name="submenu">main</property> - </packing> - </child> - </object> - <object class="GtkPopoverMenu" id="header_view_message_list_popover"> - <property name="can_focus">False</property> - <child> - <object class="GtkBox"> - <property name="visible">True</property> - <property name="can_focus">False</property> - <property name="margin">12</property> - <property name="orientation">vertical</property> - <child> - <object class="GtkModelButton"> - <property name="visible">True</property> - <property name="active">False</property> - <property name="can_focus">False</property> - <property name="receives_default">False</property> - <property name="text" translatable="yes">Chat Details</property> - <signal name="clicked" handler="window_show_chat_info_clicked_cb" swapped="yes"/> - </object> - <packing> - <property name="expand">False</property> - <property name="fill">True</property> - </packing> - </child> - <child> - <object class="GtkSeparator" id="separator_menu_msg_view2"> - <property name="visible">True</property> - <property name="can_focus">False</property> - <property name="margin_top">6</property> - <property name="margin_bottom">6</property> - </object> - <packing> - <property name="expand">False</property> - <property name="fill">True</property> - </packing> - </child> - <child> - <object class="GtkModelButton" id="leave_button"> - <property name="visible">True</property> - <property name="can_focus">False</property> - <property name="receives_default">False</property> - <property name="text" translatable="yes">Leave Chat</property> - <signal name="clicked" handler="window_leave_chat_clicked_cb" swapped="yes"/> - </object> - <packing> - <property name="expand">False</property> - <property name="fill">True</property> - </packing> - </child> - <child> - <object class="GtkModelButton" id="block_button"> - <property name="visible">False</property> - <property name="can_focus">False</property> - <property name="receives_default">False</property> - <property name="text" translatable="yes">Block Contact</property> - <signal name="clicked" handler="window_block_contact_clicked_cb" swapped="yes"/> - </object> - <packing> - <property name="expand">False</property> - <property name="fill">True</property> - </packing> - </child> - <child> - <object class="GtkModelButton" id="unblock_button"> - <property name="visible">False</property> - <property name="can_focus">False</property> - <property name="receives_default">False</property> - <property name="text" translatable="yes">Unblock Contact</property> - <signal name="clicked" handler="window_unblock_contact_clicked_cb" swapped="yes"/> - </object> - <packing> - <property name="expand">False</property> - <property name="fill">True</property> - </packing> - </child> - <child> - <object class="GtkModelButton" id="archive_button"> - <property name="visible">True</property> - <property name="can_focus">False</property> - <property name="receives_default">False</property> - <property name="text" translatable="yes">Archive chat</property> - <signal name="clicked" handler="window_archive_chat_clicked_cb" swapped="yes"/> - </object> - <packing> - <property name="expand">False</property> - <property name="fill">True</property> - </packing> - </child> - <child> - <object class="GtkModelButton" id="unarchive_button"> - <property name="visible">False</property> - <property name="can_focus">False</property> - <property name="receives_default">False</property> - <property name="text" translatable="yes">Unarchive chat</property> - <signal name="clicked" handler="window_unarchive_chat_clicked_cb" swapped="yes"/> - </object> - <packing> - <property name="expand">False</property> - <property name="fill">True</property> - </packing> - </child> - <child> - <object class="GtkModelButton" id="delete_button"> - <property name="visible">True</property> - <property name="can_focus">False</property> - <property name="receives_default">False</property> - <property name="text" translatable="yes">Delete Chat</property> - <signal name="clicked" handler="window_delete_buddy_clicked_cb" swapped="yes"/> - </object> - <packing> - <property name="expand">False</property> - <property name="fill">True</property> - </packing> - </child> - </object> - <packing> - <property name="submenu">main</property> - </packing> - </child> - </object> <template class="ChattyWindow" parent="HdyApplicationWindow"> <property name="can_focus">False</property> <property name="default_width">360</property> @@ -251,205 +12,11 @@ <object class="GtkBox"> <property name="visible">1</property> <property name="orientation">vertical</property> + <child> - <object class="HdyLeaflet" id="header_box"> - <property name="visible">True</property> - <property name="visible-child-name" bind-source="content_box" bind-property="visible-child-name" bind-flags="sync-create"/> - <property name="vexpand">0</property> - <signal name="notify::folded" handler="notify_fold_cb" after="yes" swapped="yes"/> - <child> - <object class="HdyHeaderBar" id="header_bar"> - <property name="visible">True</property> - <property name="can_focus">False</property> - <property name="show_close_button">True</property> - <property name="title" translatable="yes">Chats</property> - <child> - <object class="GtkButton" id="header_back_button"> - <property name="visible">False</property> - <property name="can_focus">False</property> - <property name="receives_default">False</property> - <signal name="clicked" handler="window_show_unarchived_clicked_cb" swapped="yes"/> - <child> - <object class="GtkImage"> - <property name="visible">True</property> - <property name="can_focus">False</property> - <property name="icon_name">go-previous-symbolic</property> - </object> - </child> - </object> - </child> - <child> - <object class="GtkMenuButton" id="header_add_chat_button"> - <property name="visible">True</property> - <property name="sensitive">False</property> - <property name="can_focus">False</property> - <property name="receives_default">False</property> - <property name="popover">header_chat_list_new_msg_popover</property> - <child> - <object class="GtkImage"> - <property name="visible">True</property> - <property name="can_focus">False</property> - <property name="icon_name">list-add-symbolic</property> - </object> - </child> - </object> - </child> - <child> - <object class="GtkMenuButton"> - <property name="visible" bind-source="header_add_chat_button" bind-property="visible" bind-flags="sync-create"/> - <property name="can_focus">False</property> - <property name="receives_default">False</property> - <property name="popover">header_view_chat_list_popover</property> - <accelerator key="F10" signal="clicked" /> - <child> - <object class="GtkImage"> - <property name="visible">True</property> - <property name="can_focus">False</property> - <property name="icon_name">open-menu-symbolic</property> - </object> - </child> - </object> - <packing> - <property name="pack_type">end</property> - </packing> - </child> - <child> - <object class="GtkToggleButton" id="search_button"> - <property name="visible">True</property> - <property name="can_focus">False</property> - <property name="receives_default">False</property> - <property name="active" bind-source="chats_search_bar" bind-property="search-mode-enabled" bind-flags="sync-create|bidirectional"/> - <accelerator key="f" modifiers="primary" signal="clicked" /> - <signal name="toggled" handler="window_search_toggled_cb" swapped="yes"/> - <child> - <object class="GtkImage"> - <property name="visible">True</property> - <property name="can_focus">False</property> - <property name="icon_name">system-search-symbolic</property> - </object> - </child> - </object> - <packing> - <property name="pack_type">end</property> - </packing> - </child> - </object> - <packing> - <property name="name">sidebar</property> - </packing> - </child> - <child> - <object class="GtkSeparator"> - <property name="visible">True</property> - <style> - <class name="sidebar"/> - </style> - </object> - </child> - <child> - <object class="HdyHeaderBar" id="sub_header_bar"> - <property name="visible">True</property> - <property name="expand">True</property> - <property name="show_close_button">True</property> - <child> - <object class="GtkButton"> - <property name="visible">True</property> - <property name="can_focus">False</property> - <property name="receives_default">False</property> - <property name="visible" bind-source="header_box" bind-property="folded" bind-flags="sync-create"/> - <signal name="clicked" handler="window_back_clicked_cb" swapped="yes"/> - <child> - <object class="GtkImage"> - <property name="visible">True</property> - <property name="can_focus">False</property> - <property name="icon_name">go-previous-symbolic</property> - <property name="icon_size">1</property> - </object> - </child> - </object> - <packing> - <property name="pack_type">start</property> - </packing> - </child> - <child type="title"> - <object class="GtkBox"> - <property name="visible">True</property> - <property name="orientation">horizontal</property> - <property name="can_focus">False</property> - <property name="halign">center</property> - <property name="valign">center</property> - <child> - <object class="ChattyAvatar" id="sub_header_icon"> - <property name="can_focus">False</property> - <property name="halign">center</property> - <property name="size">28</property> - </object> - </child> - <child> - <object class="GtkLabel" id="sub_header_label"> - <property name="visible">True</property> - <property name="can_focus">False</property> - <property name="margin-start">6</property> - <property name="halign">center</property> - <property name="ellipsize">end</property> - <attributes> - <attribute name="weight" value="bold"/> - </attributes> - </object> - <packing> - <property name="expand">False</property> - <property name="fill">True</property> - </packing> - </child> - </object> - </child> - <child> - <object class="GtkMenuButton" id="header_sub_menu_button"> - <property name="visible">False</property> - <property name="can_focus">False</property> - <property name="receives_default">False</property> - <property name="popover">header_view_message_list_popover</property> - <child> - <object class="GtkImage"> - <property name="visible">True</property> - <property name="can_focus">False</property> - <property name="icon_name">view-more-symbolic</property> - </object> - </child> - </object> - <packing> - <property name="position">0</property> - <property name="pack_type">end</property> - </packing> - </child> - <child> - <object class="GtkButton" id="call_button"> - <property name="visible">False</property> - <property name="can-focus">False</property> - <property name="receives-default">False</property> - <signal name="clicked" handler="window_call_button_clicked_cb" swapped="yes"/> - <child internal-child="accessible"> - <object class="AtkObject"> - <property name="accessible-name" translatable="yes">Call</property> - </object> - </child> - <child> - <object class="GtkImage"> - <property name="visible">True</property> - <property name="icon-name">call-start-symbolic</property> - </object> - </child> - </object> - <packing> - <property name="position">1</property> - <property name="pack_type">end</property> - </packing> - </child> - </object> - <packing> - <property name="name">content</property> - </packing> - </child> + <object class="ChattyHeaderBar" id="header_bar"> + <property name="visible">1</property> + <signal name="back-clicked" handler="window_back_clicked_cb" swapped="yes"/> </object> </child> @@ -460,6 +27,7 @@ <property name="transition-type">slide</property> <property name="can-swipe-back">True</property> <signal name="notify::visible-child-name" handler="window_content_box_changed" swapped="yes"/> + <signal name="notify::folded" handler="notify_fold_cb" after="yes" swapped="yes"/> <child> <object class="GtkBox" id="sidebar"> <property name="visible">True</property> @@ -470,6 +38,7 @@ <object class="HdySearchBar" id="chats_search_bar"> <property name="visible">True</property> <property name="hexpand">False</property> + <signal name="notify::search-mode-enabled" handler="window_search_enable_changed_cb" swapped="yes"/> <child> <object class="GtkBox"> <property name="visible">True</property> @@ -527,7 +96,7 @@ </child> <child> - <object class="ChattyChatView" id="chat_view"> + <object class="ChattyMainView" id="content_view"> <property name="visible">True</property> <property name="width_request">300</property> </object> @@ -557,25 +126,4 @@ </child> </object> - <object class="GtkSizeGroup" id="start_pane_size_group"> - <property name="mode">horizontal</property> - <widgets> - <widget name="header_bar"/> - <widget name="sidebar"/> - </widgets> - </object> - <object class="GtkSizeGroup" id="end_pane_size_group"> - <property name="mode">horizontal</property> - <widgets> - <widget name="sub_header_bar"/> - <widget name="chat_view"/> - </widgets> - </object> - <object class="HdyHeaderGroup" id="header_group"> - <property name="decorate-all" bind-source="content_box" bind-property="folded" bind-flags="sync-create"/> - <headerbars> - <headerbar name="header_bar"/> - <headerbar name="sub_header_bar"/> - </headerbars> - </object> </interface> diff --git a/subprojects/libcmatrix.wrap b/subprojects/libcmatrix.wrap new file mode 100644 index 0000000000000000000000000000000000000000..8e785abc2af6bb9e55d925eb7d35b49f5528d299 --- /dev/null +++ b/subprojects/libcmatrix.wrap @@ -0,0 +1,4 @@ +[wrap-git] +directory=libcmatrix +url=https://source.puri.sm/Librem5/libcmatrix +revision=main diff --git a/subprojects/libcmatrix/.gitignore b/subprojects/libcmatrix/.gitignore new file mode 100644 index 0000000000000000000000000000000000000000..fb74cb679ece330d5d19aa84fdb7b7ab0261da4e --- /dev/null +++ b/subprojects/libcmatrix/.gitignore @@ -0,0 +1,12 @@ +_build +*.swp +*~ +\#*# +.\#* +build +.buildconfig +.flatpak-builder + +GPATH +GTAGS +GRTAGS \ No newline at end of file diff --git a/subprojects/libcmatrix/.gitlab-ci.yml b/subprojects/libcmatrix/.gitlab-ci.yml new file mode 100644 index 0000000000000000000000000000000000000000..b4464b3b6d48cdfbe99d5135107b8023f7516167 --- /dev/null +++ b/subprojects/libcmatrix/.gitlab-ci.yml @@ -0,0 +1,96 @@ +stages: + - build + - deploy + +build-soup2: + tags: + - librem5 + image: debian:bullseye + stage: build + variables: + CCACHE_DIR: "${CI_PROJECT_DIR}/_ccache" + MESON_ARGS: "--buildtype=debugoptimized" + before_script: + - apt-get update -qq && apt-get install -qq ccache meson gcovr gcc + libsoup2.4-dev libsqlite3-dev libjson-glib-dev libgcrypt20-dev + libolm-dev libsecret-1-dev + script: + - meson ${MESON_ARGS} -Db_coverage=true _build + - ccache --zero-stats + - meson test -C _build + - ccache --show-stats + - mkdir -p _build/meson-logs/coveragereport + - ninja -C _build coverage-html + coverage: '/^\s+lines\.+:\s+([\d.]+\%)\s+/' + cache: + key: build-cmatrix-soup2 + paths: + - _ccache/ + artifacts: + when: always + paths: + - _build + +build-soup3: + tags: + - librem5:arm64 + image: debian:bookworm + stage: build + variables: + CCACHE_DIR: "${CI_PROJECT_DIR}/_ccache" + MESON_ARGS: "--buildtype=debugoptimized" + before_script: + - apt-get update -qq && apt-get install -qq ccache meson gcovr gcc + libsoup2.4-dev libsqlite3-dev libjson-glib-dev libgcrypt20-dev + libsoup-3.0-dev libolm-dev libsecret-1-dev + script: + - meson ${MESON_ARGS} -Db_coverage=true _build + - ccache --zero-stats + - meson test -C _build + - ccache --show-stats + - mkdir -p _build/meson-logs/coveragereport + - ninja -C _build coverage-html + coverage: '/^\s+lines\.+:\s+([\d.]+\%)\s+/' + cache: + key: build-cmatrix-soup3 + paths: + - _ccache/ + artifacts: + when: always + paths: + - _build + +doc: + tags: + - librem5:arm64 + image: debian:bookworm + stage: build + before_script: + - apt-get update -qq && apt-get install -qq ccache meson gcovr gcc + libsoup2.4-dev libsqlite3-dev libjson-glib-dev libgcrypt20-dev + libolm-dev gi-docgen libgirepository1.0-dev libsecret-1-dev + libsoup-3.0-dev + variables: + BUILD_OPTS: >- + -Dgtk_doc=true + script: + - meson ${BUILD_OPTS} _build + - ninja -C _build + - mv _build/doc/libcmatrix-0 _doc/ + artifacts: + paths: + - _doc/ + +pages: + tags: + - librem5 + image: busybox:1 + stage: deploy + script: + - mkdir public + - mv _build/meson-logs/coveragereport ${CI_PROJECT_DIR}/public/coverage + artifacts: + paths: + - public + only: + - main diff --git a/subprojects/libcmatrix/COPYING b/subprojects/libcmatrix/COPYING new file mode 100644 index 0000000000000000000000000000000000000000..4362b49151d7b34ef83b3067a8f9c9f877d72a0e --- /dev/null +++ b/subprojects/libcmatrix/COPYING @@ -0,0 +1,502 @@ + GNU LESSER GENERAL PUBLIC LICENSE + Version 2.1, February 1999 + + Copyright (C) 1991, 1999 Free Software Foundation, Inc. + 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + +[This is the first released version of the Lesser GPL. It also counts + as the successor of the GNU Library Public License, version 2, hence + the version number 2.1.] + + Preamble + + The licenses for most software are designed to take away your +freedom to share and change it. By contrast, the GNU General Public +Licenses are intended to guarantee your freedom to share and change +free software--to make sure the software is free for all its users. + + This license, the Lesser General Public License, applies to some +specially designated software packages--typically libraries--of the +Free Software Foundation and other authors who decide to use it. You +can use it too, but we suggest you first think carefully about whether +this license or the ordinary General Public License is the better +strategy to use in any particular case, based on the explanations below. + + When we speak of free software, we are referring to freedom of use, +not price. Our General Public Licenses are designed to make sure that +you have the freedom to distribute copies of free software (and charge +for this service if you wish); that you receive source code or can get +it if you want it; that you can change the software and use pieces of +it in new free programs; and that you are informed that you can do +these things. + + To protect your rights, we need to make restrictions that forbid +distributors to deny you these rights or to ask you to surrender these +rights. These restrictions translate to certain responsibilities for +you if you distribute copies of the library or if you modify it. + + For example, if you distribute copies of the library, whether gratis +or for a fee, you must give the recipients all the rights that we gave +you. You must make sure that they, too, receive or can get the source +code. If you link other code with the library, you must provide +complete object files to the recipients, so that they can relink them +with the library after making changes to the library and recompiling +it. And you must show them these terms so they know their rights. + + We protect your rights with a two-step method: (1) we copyright the +library, and (2) we offer you this license, which gives you legal +permission to copy, distribute and/or modify the library. + + To protect each distributor, we want to make it very clear that +there is no warranty for the free library. Also, if the library is +modified by someone else and passed on, the recipients should know +that what they have is not the original version, so that the original +author's reputation will not be affected by problems that might be +introduced by others. + + Finally, software patents pose a constant threat to the existence of +any free program. We wish to make sure that a company cannot +effectively restrict the users of a free program by obtaining a +restrictive license from a patent holder. Therefore, we insist that +any patent license obtained for a version of the library must be +consistent with the full freedom of use specified in this license. + + Most GNU software, including some libraries, is covered by the +ordinary GNU General Public License. This license, the GNU Lesser +General Public License, applies to certain designated libraries, and +is quite different from the ordinary General Public License. We use +this license for certain libraries in order to permit linking those +libraries into non-free programs. + + When a program is linked with a library, whether statically or using +a shared library, the combination of the two is legally speaking a +combined work, a derivative of the original library. The ordinary +General Public License therefore permits such linking only if the +entire combination fits its criteria of freedom. The Lesser General +Public License permits more lax criteria for linking other code with +the library. + + We call this license the "Lesser" General Public License because it +does Less to protect the user's freedom than the ordinary General +Public License. It also provides other free software developers Less +of an advantage over competing non-free programs. These disadvantages +are the reason we use the ordinary General Public License for many +libraries. However, the Lesser license provides advantages in certain +special circumstances. + + For example, on rare occasions, there may be a special need to +encourage the widest possible use of a certain library, so that it becomes +a de-facto standard. To achieve this, non-free programs must be +allowed to use the library. A more frequent case is that a free +library does the same job as widely used non-free libraries. In this +case, there is little to gain by limiting the free library to free +software only, so we use the Lesser General Public License. + + In other cases, permission to use a particular library in non-free +programs enables a greater number of people to use a large body of +free software. For example, permission to use the GNU C Library in +non-free programs enables many more people to use the whole GNU +operating system, as well as its variant, the GNU/Linux operating +system. + + Although the Lesser General Public License is Less protective of the +users' freedom, it does ensure that the user of a program that is +linked with the Library has the freedom and the wherewithal to run +that program using a modified version of the Library. + + The precise terms and conditions for copying, distribution and +modification follow. Pay close attention to the difference between a +"work based on the library" and a "work that uses the library". The +former contains code derived from the library, whereas the latter must +be combined with the library in order to run. + + GNU LESSER GENERAL PUBLIC LICENSE + TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION + + 0. This License Agreement applies to any software library or other +program which contains a notice placed by the copyright holder or +other authorized party saying it may be distributed under the terms of +this Lesser General Public License (also called "this License"). +Each licensee is addressed as "you". + + A "library" means a collection of software functions and/or data +prepared so as to be conveniently linked with application programs +(which use some of those functions and data) to form executables. + + The "Library", below, refers to any such software library or work +which has been distributed under these terms. A "work based on the +Library" means either the Library or any derivative work under +copyright law: that is to say, a work containing the Library or a +portion of it, either verbatim or with modifications and/or translated +straightforwardly into another language. (Hereinafter, translation is +included without limitation in the term "modification".) + + "Source code" for a work means the preferred form of the work for +making modifications to it. For a library, complete source code means +all the source code for all modules it contains, plus any associated +interface definition files, plus the scripts used to control compilation +and installation of the library. + + Activities other than copying, distribution and modification are not +covered by this License; they are outside its scope. The act of +running a program using the Library is not restricted, and output from +such a program is covered only if its contents constitute a work based +on the Library (independent of the use of the Library in a tool for +writing it). Whether that is true depends on what the Library does +and what the program that uses the Library does. + + 1. You may copy and distribute verbatim copies of the Library's +complete source code as you receive it, in any medium, provided that +you conspicuously and appropriately publish on each copy an +appropriate copyright notice and disclaimer of warranty; keep intact +all the notices that refer to this License and to the absence of any +warranty; and distribute a copy of this License along with the +Library. + + You may charge a fee for the physical act of transferring a copy, +and you may at your option offer warranty protection in exchange for a +fee. + + 2. You may modify your copy or copies of the Library or any portion +of it, thus forming a work based on the Library, and copy and +distribute such modifications or work under the terms of Section 1 +above, provided that you also meet all of these conditions: + + a) The modified work must itself be a software library. + + b) You must cause the files modified to carry prominent notices + stating that you changed the files and the date of any change. + + c) You must cause the whole of the work to be licensed at no + charge to all third parties under the terms of this License. + + d) If a facility in the modified Library refers to a function or a + table of data to be supplied by an application program that uses + the facility, other than as an argument passed when the facility + is invoked, then you must make a good faith effort to ensure that, + in the event an application does not supply such function or + table, the facility still operates, and performs whatever part of + its purpose remains meaningful. + + (For example, a function in a library to compute square roots has + a purpose that is entirely well-defined independent of the + application. Therefore, Subsection 2d requires that any + application-supplied function or table used by this function must + be optional: if the application does not supply it, the square + root function must still compute square roots.) + +These requirements apply to the modified work as a whole. If +identifiable sections of that work are not derived from the Library, +and can be reasonably considered independent and separate works in +themselves, then this License, and its terms, do not apply to those +sections when you distribute them as separate works. But when you +distribute the same sections as part of a whole which is a work based +on the Library, the distribution of the whole must be on the terms of +this License, whose permissions for other licensees extend to the +entire whole, and thus to each and every part regardless of who wrote +it. + +Thus, it is not the intent of this section to claim rights or contest +your rights to work written entirely by you; rather, the intent is to +exercise the right to control the distribution of derivative or +collective works based on the Library. + +In addition, mere aggregation of another work not based on the Library +with the Library (or with a work based on the Library) on a volume of +a storage or distribution medium does not bring the other work under +the scope of this License. + + 3. You may opt to apply the terms of the ordinary GNU General Public +License instead of this License to a given copy of the Library. To do +this, you must alter all the notices that refer to this License, so +that they refer to the ordinary GNU General Public License, version 2, +instead of to this License. (If a newer version than version 2 of the +ordinary GNU General Public License has appeared, then you can specify +that version instead if you wish.) Do not make any other change in +these notices. + + Once this change is made in a given copy, it is irreversible for +that copy, so the ordinary GNU General Public License applies to all +subsequent copies and derivative works made from that copy. + + This option is useful when you wish to copy part of the code of +the Library into a program that is not a library. + + 4. You may copy and distribute the Library (or a portion or +derivative of it, under Section 2) in object code or executable form +under the terms of Sections 1 and 2 above provided that you accompany +it with the complete corresponding machine-readable source code, which +must be distributed under the terms of Sections 1 and 2 above on a +medium customarily used for software interchange. + + If distribution of object code is made by offering access to copy +from a designated place, then offering equivalent access to copy the +source code from the same place satisfies the requirement to +distribute the source code, even though third parties are not +compelled to copy the source along with the object code. + + 5. A program that contains no derivative of any portion of the +Library, but is designed to work with the Library by being compiled or +linked with it, is called a "work that uses the Library". Such a +work, in isolation, is not a derivative work of the Library, and +therefore falls outside the scope of this License. + + However, linking a "work that uses the Library" with the Library +creates an executable that is a derivative of the Library (because it +contains portions of the Library), rather than a "work that uses the +library". The executable is therefore covered by this License. +Section 6 states terms for distribution of such executables. + + When a "work that uses the Library" uses material from a header file +that is part of the Library, the object code for the work may be a +derivative work of the Library even though the source code is not. +Whether this is true is especially significant if the work can be +linked without the Library, or if the work is itself a library. The +threshold for this to be true is not precisely defined by law. + + If such an object file uses only numerical parameters, data +structure layouts and accessors, and small macros and small inline +functions (ten lines or less in length), then the use of the object +file is unrestricted, regardless of whether it is legally a derivative +work. (Executables containing this object code plus portions of the +Library will still fall under Section 6.) + + Otherwise, if the work is a derivative of the Library, you may +distribute the object code for the work under the terms of Section 6. +Any executables containing that work also fall under Section 6, +whether or not they are linked directly with the Library itself. + + 6. As an exception to the Sections above, you may also combine or +link a "work that uses the Library" with the Library to produce a +work containing portions of the Library, and distribute that work +under terms of your choice, provided that the terms permit +modification of the work for the customer's own use and reverse +engineering for debugging such modifications. + + You must give prominent notice with each copy of the work that the +Library is used in it and that the Library and its use are covered by +this License. You must supply a copy of this License. If the work +during execution displays copyright notices, you must include the +copyright notice for the Library among them, as well as a reference +directing the user to the copy of this License. Also, you must do one +of these things: + + a) Accompany the work with the complete corresponding + machine-readable source code for the Library including whatever + changes were used in the work (which must be distributed under + Sections 1 and 2 above); and, if the work is an executable linked + with the Library, with the complete machine-readable "work that + uses the Library", as object code and/or source code, so that the + user can modify the Library and then relink to produce a modified + executable containing the modified Library. (It is understood + that the user who changes the contents of definitions files in the + Library will not necessarily be able to recompile the application + to use the modified definitions.) + + b) Use a suitable shared library mechanism for linking with the + Library. A suitable mechanism is one that (1) uses at run time a + copy of the library already present on the user's computer system, + rather than copying library functions into the executable, and (2) + will operate properly with a modified version of the library, if + the user installs one, as long as the modified version is + interface-compatible with the version that the work was made with. + + c) Accompany the work with a written offer, valid for at + least three years, to give the same user the materials + specified in Subsection 6a, above, for a charge no more + than the cost of performing this distribution. + + d) If distribution of the work is made by offering access to copy + from a designated place, offer equivalent access to copy the above + specified materials from the same place. + + e) Verify that the user has already received a copy of these + materials or that you have already sent this user a copy. + + For an executable, the required form of the "work that uses the +Library" must include any data and utility programs needed for +reproducing the executable from it. However, as a special exception, +the materials to be distributed need not include anything that is +normally distributed (in either source or binary form) with the major +components (compiler, kernel, and so on) of the operating system on +which the executable runs, unless that component itself accompanies +the executable. + + It may happen that this requirement contradicts the license +restrictions of other proprietary libraries that do not normally +accompany the operating system. Such a contradiction means you cannot +use both them and the Library together in an executable that you +distribute. + + 7. You may place library facilities that are a work based on the +Library side-by-side in a single library together with other library +facilities not covered by this License, and distribute such a combined +library, provided that the separate distribution of the work based on +the Library and of the other library facilities is otherwise +permitted, and provided that you do these two things: + + a) Accompany the combined library with a copy of the same work + based on the Library, uncombined with any other library + facilities. This must be distributed under the terms of the + Sections above. + + b) Give prominent notice with the combined library of the fact + that part of it is a work based on the Library, and explaining + where to find the accompanying uncombined form of the same work. + + 8. You may not copy, modify, sublicense, link with, or distribute +the Library except as expressly provided under this License. Any +attempt otherwise to copy, modify, sublicense, link with, or +distribute the Library is void, and will automatically terminate your +rights under this License. However, parties who have received copies, +or rights, from you under this License will not have their licenses +terminated so long as such parties remain in full compliance. + + 9. You are not required to accept this License, since you have not +signed it. However, nothing else grants you permission to modify or +distribute the Library or its derivative works. These actions are +prohibited by law if you do not accept this License. Therefore, by +modifying or distributing the Library (or any work based on the +Library), you indicate your acceptance of this License to do so, and +all its terms and conditions for copying, distributing or modifying +the Library or works based on it. + + 10. Each time you redistribute the Library (or any work based on the +Library), the recipient automatically receives a license from the +original licensor to copy, distribute, link with or modify the Library +subject to these terms and conditions. You may not impose any further +restrictions on the recipients' exercise of the rights granted herein. +You are not responsible for enforcing compliance by third parties with +this License. + + 11. If, as a consequence of a court judgment or allegation of patent +infringement or for any other reason (not limited to patent issues), +conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot +distribute so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you +may not distribute the Library at all. For example, if a patent +license would not permit royalty-free redistribution of the Library by +all those who receive copies directly or indirectly through you, then +the only way you could satisfy both it and this License would be to +refrain entirely from distribution of the Library. + +If any portion of this section is held invalid or unenforceable under any +particular circumstance, the balance of the section is intended to apply, +and the section as a whole is intended to apply in other circumstances. + +It is not the purpose of this section to induce you to infringe any +patents or other property right claims or to contest validity of any +such claims; this section has the sole purpose of protecting the +integrity of the free software distribution system which is +implemented by public license practices. Many people have made +generous contributions to the wide range of software distributed +through that system in reliance on consistent application of that +system; it is up to the author/donor to decide if he or she is willing +to distribute software through any other system and a licensee cannot +impose that choice. + +This section is intended to make thoroughly clear what is believed to +be a consequence of the rest of this License. + + 12. If the distribution and/or use of the Library is restricted in +certain countries either by patents or by copyrighted interfaces, the +original copyright holder who places the Library under this License may add +an explicit geographical distribution limitation excluding those countries, +so that distribution is permitted only in or among countries not thus +excluded. In such case, this License incorporates the limitation as if +written in the body of this License. + + 13. The Free Software Foundation may publish revised and/or new +versions of the Lesser General Public License from time to time. +Such new versions will be similar in spirit to the present version, +but may differ in detail to address new problems or concerns. + +Each version is given a distinguishing version number. If the Library +specifies a version number of this License which applies to it and +"any later version", you have the option of following the terms and +conditions either of that version or of any later version published by +the Free Software Foundation. If the Library does not specify a +license version number, you may choose any version ever published by +the Free Software Foundation. + + 14. If you wish to incorporate parts of the Library into other free +programs whose distribution conditions are incompatible with these, +write to the author to ask for permission. For software which is +copyrighted by the Free Software Foundation, write to the Free +Software Foundation; we sometimes make exceptions for this. Our +decision will be guided by the two goals of preserving the free status +of all derivatives of our free software and of promoting the sharing +and reuse of software generally. + + NO WARRANTY + + 15. BECAUSE THE LIBRARY IS LICENSED FREE OF CHARGE, THERE IS NO +WARRANTY FOR THE LIBRARY, TO THE EXTENT PERMITTED BY APPLICABLE LAW. +EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR +OTHER PARTIES PROVIDE THE LIBRARY "AS IS" WITHOUT WARRANTY OF ANY +KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE +LIBRARY IS WITH YOU. SHOULD THE LIBRARY PROVE DEFECTIVE, YOU ASSUME +THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN +WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY +AND/OR REDISTRIBUTE THE LIBRARY AS PERMITTED ABOVE, BE LIABLE TO YOU +FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR +CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE +LIBRARY (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING +RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A +FAILURE OF THE LIBRARY TO OPERATE WITH ANY OTHER SOFTWARE), EVEN IF +SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH +DAMAGES. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Libraries + + If you develop a new library, and you want it to be of the greatest +possible use to the public, we recommend making it free software that +everyone can redistribute and change. You can do so by permitting +redistribution under these terms (or, alternatively, under the terms of the +ordinary General Public License). + + To apply these terms, attach the following notices to the library. It is +safest to attach them to the start of each source file to most effectively +convey the exclusion of warranty; and each file should have at least the +"copyright" line and a pointer to where the full notice is found. + + <one line to give the library's name and a brief idea of what it does.> + Copyright (C) <year> <name of author> + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 2.1 of the License, or (at your option) any later version. + + This library is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public + License along with this library; if not, write to the Free Software + Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + +Also add information on how to contact you by electronic and paper mail. + +You should also get your employer (if you work as a programmer) or your +school, if any, to sign a "copyright disclaimer" for the library, if +necessary. Here is a sample; alter the names: + + Yoyodyne, Inc., hereby disclaims all copyright interest in the + library `Frob' (a library for tweaking knobs) written by James Random Hacker. + + <signature of Ty Coon>, 1 April 1990 + Ty Coon, President of Vice + +That's all there is to it! diff --git a/subprojects/libcmatrix/README.md b/subprojects/libcmatrix/README.md new file mode 100644 index 0000000000000000000000000000000000000000..170d2b43bfc2166e6dedbc64c6a8c37f2862978c --- /dev/null +++ b/subprojects/libcmatrix/README.md @@ -0,0 +1,60 @@ +<div align="center"> + <a href="https://puri.sm"> + <img src="https://path/to/image/raw/master/data/icons/icon.png" width="150" /> + </a> + <br> + + <a href="https://puri.sm"><b>libcmatrix</b></a> + <br> + + A matrix protocol library writting in GObjectified C + <br> + + <a href="https://source.puri.sm/Librem5/libcmatrix/pipelines"><img + src="https://source.puri.sm/Librem5/libcmatrix/badges/main/pipeline.svg" /></a> + <a href="https://source.puri.sm/Librem5/libcmatrix/coverage"><img + src="https://source.puri.sm/Librem5/libcmatrix/badges/main/coverage.svg" /></a> +</div> + +--- + +libcmatrix is a [Matrix][matrix] client library written in +GObjectified C library. + +You could use the library if you are writing a matrix client +in C. + +libcmatrix requires GObject and Gio, and the library assumes +that a glib main event loop is running (All GTK apps have one) + +libcmatrix handles all E2EE transparently. The messages/events +are not stored and the client should store them for chat history, +which may change in the future. + +## Dependencies + - glib >= 2.66 + - gio >= 2.66 + - libsoup-2 + - ligbcrypt + - libolm3 + - libsqlite3 >= 3.34 + +Source Repository: [GitLab][gitlab] + +Issues and Feature Requests: [GitLab][issues] + +## Getting started + + See `examples` directory for examples on how to use the library. + You shall have to use libcmatrix as a subproject. Currently + libcmatrix provides no API nor ABI guarantee until it's stable + enough. + + +<!-- Links referenced elsewhere --> +<!-- To be updated --> +[matrix]: https://matrix.org +[home]: https://puri.sm +[coverage]: https://source.puri.sm/Librem5/libcmatrix/coverage +[gitlab]: https://source.puri.sm/Librem5/libcmatrix/ +[issues]: https://source.puri.sm/Librem5/libcmatrix/issues diff --git a/subprojects/libcmatrix/doc/build-howto.md b/subprojects/libcmatrix/doc/build-howto.md new file mode 100644 index 0000000000000000000000000000000000000000..f277fd8a09db8813c574b357358e78585f004674 --- /dev/null +++ b/subprojects/libcmatrix/doc/build-howto.md @@ -0,0 +1,32 @@ +Title: Compiling with libcmatrix +Slug: building + +# Compiling with libcmatrix + +If you need to build libcmatrix, get the source from +[here](https://source.puri.sm/Librem5/libcmatrix/) and see the `README.md` file. + +## Bundling the library + +libcmatrix is not meant to be used as a shared library. It should be embedded in your source +tree as a git submodule instead: + +``` +git submodule add https://source.puri.sm/Librem5/libcmatrix.git subprojects/libcmatrix +``` + +Add this to your `meson.build`: + +```meson +libcmatrix = subproject('libcmatrix', + default_options: [ + 'package_name=' + meson.project_name(), + 'package_version=' + meson.project_version(), + 'pkgdatadir=' + pkgdatadir, + 'pkglibdir=' + pkglibdir, + 'examples=false', + 'gtk_doc=false', + 'tests=false', + ]) +libcmatrix_dep = libcmatrix.get_variable('libcmatrix_dep') +``` diff --git a/subprojects/libcmatrix/doc/libcmatrix.toml.in b/subprojects/libcmatrix/doc/libcmatrix.toml.in new file mode 100644 index 0000000000000000000000000000000000000000..da9dbdd6d7e10ab1e928eabc1146f62c253b9ba2 --- /dev/null +++ b/subprojects/libcmatrix/doc/libcmatrix.toml.in @@ -0,0 +1,34 @@ +[library] +version = "@VERSION@" +description = "A matrix client client library written in GObjectified C" +authors = "Purism SPC" +license = "LGPL-2.1-or-later" +browse_url = "https://source.puri.sm/Librem5/libcmatrix" +repository_url = "https://source.puri.sm/Librem5/libcmatrix.git" +website_url = "https://source.puri.sm/Librem5/libcmatrix" +dependencies = [ + "GObject-2.0", +] +devhelp = true +search_index = true + +[dependencies."GObject-2.0"] +name = "GObject" +description = "The base type system library" +docs_url = "https://developer.gnome.org/gobject/stable" + +[theme] +name = "basic" +show_index_summary = true +show_class_hierarchy = true + +[source-location] +# The base URL for the web UI +base_url = "https://source.puri.sm/Librem5/libcmatrix/-/blob/main/" +# The format for links, using "filename" and "line" for the format +file_format = "{filename}#L{line}" + +[extra] +content_files = [ + "build-howto.md", +] diff --git a/subprojects/libcmatrix/doc/meson.build b/subprojects/libcmatrix/doc/meson.build new file mode 100644 index 0000000000000000000000000000000000000000..4819fb16cface1d6a408fa1f8bab6bc6e445a482 --- /dev/null +++ b/subprojects/libcmatrix/doc/meson.build @@ -0,0 +1,45 @@ +if get_option('gtk_doc') + +expand_content_md_files = [ + 'build-howto.md', +] + +toml_data = configuration_data() +toml_data.set('VERSION', meson.project_version()) + +libcmatrix_toml = configure_file( + input: 'libcmatrix.toml.in', + output: 'libcmatrix.toml', + configuration: toml_data +) + +dependency('gi-docgen', version: '>= 2021.1', + fallback: ['gi-docgen', 'dummy_dep'], + native: true, + required: get_option('gtk_doc')) + +gidocgen = find_program('gi-docgen') + +docs_dir = datadir / 'doc' + +custom_target('libcmatrix-doc', + input: [ libcmatrix_toml, libcmatrix_gir[0] ], + output: 'libcmatrix-0', + command: [ + gidocgen, + 'generate', + '--quiet', + '--add-include-path=@0@'.format(meson.current_build_dir() / '../../src'), + '--config=@INPUT0@', + '--output-dir=@OUTPUT@', + '--no-namespace-dir', + '--content-dir=@0@'.format(meson.current_source_dir()), + '@INPUT1@', + ], + depend_files: [ expand_content_md_files ], + build_by_default: true, + install: true, + install_dir: docs_dir, +) + +endif diff --git a/subprojects/libcmatrix/examples/meson.build b/subprojects/libcmatrix/examples/meson.build new file mode 100644 index 0000000000000000000000000000000000000000..99d402b1133158d169d55d0ab307a627f58bfca1 --- /dev/null +++ b/subprojects/libcmatrix/examples/meson.build @@ -0,0 +1,13 @@ +example_items = [ + 'simple-client', +] + +foreach item: example_items + executable( + item, + item + '.c', + include_directories: src_inc, + link_with: cmatrix_lib, + dependencies: cmatrix_deps, + ) +endforeach diff --git a/subprojects/libcmatrix/examples/simple-client.c b/subprojects/libcmatrix/examples/simple-client.c new file mode 100644 index 0000000000000000000000000000000000000000..8546eda4ba0cf9f72139361d5a3e7e50bd9ddba5 --- /dev/null +++ b/subprojects/libcmatrix/examples/simple-client.c @@ -0,0 +1,194 @@ +/* -*- mode: c; c-basic-offset: 2; indent-tabs-mode: nil; -*- */ +/* client.c + * + * Copyright 2022 Purism SPC + * + * Author(s): + * Mohammed Sadiq <sadiq@sadiqpk.org> + * + * SPDX-License-Identifier: LGPL-2.1-or-later OR CC0-1.0 + */ + +#include <stdio.h> + +#ifndef CMATRIX_USE_EXPERIMENTAL_API +# define CMATRIX_USE_EXPERIMENTAL_API +#endif /* CMATRIX_USE_EXPERIMENTAL_API */ +#include "cmatrix.h" + +CmMatrix *matrix; +CmClient *client; + +static void +simple_account_sync_cb (gpointer object, + CmClient *cm_client, + CmRoom *room, + GPtrArray *events, + GError *err) +{ + puts ("\n\n\n"); + + if (room && events) + { + for (guint i = 0; i < events->len; i++) + { + gpointer event; + + event = events->pdata[i]; + g_warning ("here: %d", cm_event_get_m_type (event)); + + if (CM_IS_ROOM_MESSAGE_EVENT (event) && + cm_room_message_event_get_msg_type (event)) + { + g_warning ("text message: %s", cm_room_message_event_get_body (event)); + } + } + } + + if (err) + g_warning ("client error: %s", err->message); + + puts ("\n\n\n"); +} + +static void +simple_matrix_save_cb (GObject *object, + GAsyncResult *result, + gpointer user_data) +{ + g_autoptr(GError) error = NULL; + + cm_matrix_save_client_finish (matrix, result, &error); + + if (error) + g_warning ("Error saving client: %s", error->message); + + /* Now, enable the client, and the client will start to sync, executing the callback on events */ + cm_client_set_enabled (client, TRUE); +} + +static void +simple_get_homeserver_cb (GObject *object, + GAsyncResult *result, + gpointer user_data) +{ + g_autoptr(GError) error = NULL; + const char *server; + char homeserver[255]; + + server = cm_client_get_homeserver_finish (client, result, &error); + + /* It's okay to not able to get homeserver from username */ + if (error) + g_message ("Failed to guess/verify homeserver: %s", error->message); + else if (server) + g_warning ("autofetched homeserver: %s", server); + + /* Not having a homeserver set means we failed to guess it from provided login id */ + /* So ask user for one. */ + while (!cm_client_get_homeserver (client)) + { + printf ("input your Matrix homeserver address: "); + scanf ("%s", homeserver); + if (!cm_client_set_homeserver (client, homeserver)) + g_warning ("'%s' is not a valid homeserver uri (did you forget to " + "prefix with 'https://')", homeserver); + } + + /* The sync callback runs for every /sync response and other interesting events (like wrong password error) */ + cm_client_set_sync_callback (client, + simple_account_sync_cb, + NULL, NULL); + cm_matrix_save_client_async (matrix, client, + simple_matrix_save_cb, NULL); + +} + +static void +simple_joined_rooms_changed_cb (GListModel *list, + guint position, + guint removed, + guint added, + gpointer user_data) +{ + puts ("\n\n\n"); + + g_warning ("joined rooms changed"); + g_warning ("total number of items: %u", g_list_model_get_n_items (list)); + + for (guint i = 0; i < g_list_model_get_n_items (list); i++) + { + g_autoptr(CmRoom) room = NULL; + + room = g_list_model_get_item (list, i); + + g_warning ("room name: %s, room id: %s", + cm_room_get_name (room), + cm_room_get_id (room)); + } + + puts ("\n\n\n"); +} + +static void +simple_matrix_open_cb (GObject *object, + GAsyncResult *result, + gpointer user_data) +{ + g_autoptr(GError) error = NULL; + char username[255], password[255]; + GListModel *joined_rooms; + CmAccount *account; + + if (!cm_matrix_open_finish (matrix, result, &error)) + g_error ("Error opening db: %s", error->message); + + printf ("input your Matrix username: "); + scanf ("%s", username); + printf ("input your Matrix password: "); + scanf ("%s", password); + puts (""); + + g_message ("username: %s, password: %s", username, password); + + client = cm_matrix_client_new (matrix); + account = cm_client_get_account (client); + joined_rooms = cm_client_get_joined_rooms (client); + g_signal_connect_object (joined_rooms, "items-changed", + G_CALLBACK (simple_joined_rooms_changed_cb), client, + 0); + + if (!cm_account_set_login_id (account, username)) + g_error ("'%s' isn't a valid username", username); + cm_client_set_password (client, password); + cm_client_set_device_name (client, "Example CMatrix"); + + /* try if we can get a valid homeserver from username */ + cm_client_get_homeserver_async (client, NULL, + simple_get_homeserver_cb, NULL); +} + +int +main (void) +{ + GMainLoop *main_loop; + g_autofree char *db_dir = NULL; + g_autofree char *data_dir = NULL; + g_autofree char *cache_dir = NULL; + + /* Initialize the library */ + cm_init (TRUE); + + /* Create a matrix object */ + data_dir = g_build_filename (g_get_user_data_dir (), "CMatrix", "simple-client", NULL); + cache_dir = g_build_filename (g_get_user_cache_dir (), "CMatrix", "simple-client", NULL); + matrix = cm_matrix_new (data_dir, cache_dir, "com.example.CMatrix", FALSE); + + /* Ask matrix to open/create the db which will be used to store keys, session data, etc. */ + db_dir = g_build_filename (g_get_user_data_dir (), "CMatrix", "simple-client", NULL); + cm_matrix_open_async (matrix, db_dir, "matrix.db", NULL, + simple_matrix_open_cb, NULL); + + main_loop = g_main_loop_new (NULL, FALSE); + g_main_loop_run (main_loop); +} diff --git a/subprojects/libcmatrix/meson.build b/subprojects/libcmatrix/meson.build new file mode 100644 index 0000000000000000000000000000000000000000..2c1dcaad4ba17b8d1ee3f478952a4bcabfb80d5b --- /dev/null +++ b/subprojects/libcmatrix/meson.build @@ -0,0 +1,138 @@ +project('libcmatrix', 'c', + version: '0.0.0', + license: 'LGPL-2.1-or-later', + meson_version: '>= 0.56.0', + default_options: [ 'warning_level=2', 'buildtype=debugoptimized', 'c_std=gnu18' ], +) + +gnome = import('gnome') +i18n = import('i18n') + +VERSION = meson.project_version() +ABI_MAJOR = VERSION.split('.')[0] +ABI_MINOR = VERSION.split('.')[1] +ABI_MICRO = VERSION.split('.')[2] + +prefix = get_option('prefix') +bindir = join_paths(prefix, get_option('bindir')) +datadir = join_paths(prefix, get_option('datadir')) +localedir = join_paths(prefix, get_option('localedir')) +libdir = join_paths(prefix, get_option('libdir')) + +cc = meson.get_compiler('c') + +global_c_args = [ + '-DCMATRIX_COMPILATION', +] + +test_c_args = [ + '-Wcast-align', + '-Wdate-time', + '-Wdeclaration-after-statement', + ['-Werror=format-security', '-Werror=format=2'], + '-Wendif-labels', + '-Werror=incompatible-pointer-types', + '-Werror=missing-declarations', + '-Werror=overflow', + '-Werror=return-type', + '-Werror=shift-count-overflow', + '-Werror=shift-overflow=2', + '-Werror=implicit-fallthrough=3', + '-Wfloat-equal', + '-Wformat-nonliteral', + '-Wformat-security', + '-Winit-self', + '-Wmaybe-uninitialized', + '-Wmissing-field-initializers', + '-Wmissing-include-dirs', + '-Wmissing-noreturn', + '-Wnested-externs', + '-Wno-strict-aliasing', + '-Wno-unused-parameter', + '-Wold-style-definition', + '-Wpointer-arith', + '-Wredundant-decls', + '-Wshadow', + '-Wsign-compare', + '-Wstrict-prototypes', + '-Wswitch-default', + '-Wswitch-enum', + '-Wtype-limits', + '-Wundef', + '-Wunused-function', +] +if get_option('buildtype') != 'plain' + test_c_args += '-fstack-protector-strong' +endif + +foreach arg: test_c_args + if cc.has_multi_arguments(arg) + global_c_args += arg + endif +endforeach +add_project_arguments( + global_c_args, + language: 'c' +) + +add_project_arguments([ + '-DHAVE_CONFIG_H', + '-DCMATRIX_COMPILATION', + '-DCMATRIX_USE_EXPERIMENTAL_API', +], language: 'c') + +config_h = configuration_data() +config_h.set10('HAVE_EXPLICIT_BZERO', cc.has_function('explicit_bzero')) +config_h.set_quoted('GETTEXT_PACKAGE', 'libcmatrix') +config_h.set_quoted('LOCALEDIR', localedir) + +libolm_dep = cc.find_library('olm', required: true) + +if (cc.has_function('olm_account_unpublished_fallback_key', dependencies: libolm_dep)) + config_h.set('OLM_ACCOUNT_PICKLE_V4', true) +else + config_h.set('OLM_ACCOUNT_PICKLE_V4', false) +endif + +if (cc.has_function('olm_pk_key_from_private', dependencies: libolm_dep)) + config_h.set('HAVE_OLM3', true) +else + config_h.set('HAVE_OLM2', true) +endif + +configure_file( + output: 'config.h', + configuration: config_h, +) + +root_inc = include_directories('.') +src_inc = include_directories('src') + +gio_dep = dependency('gio-2.0', version: '>= 2.66') + +# libsoup-3 requires glib 2.69.1+, so if we have an older +# glib, assume libsoup-2.4 +if gio_dep.version().version_compare('<= 2.69') or get_option('soup2') + soup_dep = dependency('libsoup-2.4', required: true) +else + soup_dep = dependency('libsoup-3.0', required: true) +endif + +cmatrix_deps = [ + dependency('libgcrypt'), + dependency('libsecret-1'), + dependency('json-glib-1.0'), + dependency('sqlite3', version: '>=3.26.0'), + libolm_dep, + soup_dep, + gio_dep, + cc.find_library('m', required: false), +] + +subdir('src') +subdir('tests') +subdir('doc') + +if (get_option('build-examples')) + subdir('examples') +endif diff --git a/subprojects/libcmatrix/meson_options.txt b/subprojects/libcmatrix/meson_options.txt new file mode 100644 index 0000000000000000000000000000000000000000..ecc897db0d2f15e7b51c24dd52989344fae9d31e --- /dev/null +++ b/subprojects/libcmatrix/meson_options.txt @@ -0,0 +1,10 @@ +# Subproject +option('package_subdir', type: 'string', + description: 'Subdirectory to append to all installed files, for use as subproject' +) +option('build-examples', type: 'boolean', value: true, description : 'Build examples') +option('soup2', type: 'boolean', value: false, description: 'Whether to build with libsoup2') + +option('gtk_doc', + type: 'boolean', value: false, + description: 'Whether to generate the API reference') diff --git a/subprojects/libcmatrix/src/cm-client-private.h b/subprojects/libcmatrix/src/cm-client-private.h new file mode 100644 index 0000000000000000000000000000000000000000..828c763f8678242113beade0ad3b5cb9fe042aaf --- /dev/null +++ b/subprojects/libcmatrix/src/cm-client-private.h @@ -0,0 +1,60 @@ +/* -*- mode: c; c-basic-offset: 2; indent-tabs-mode: nil; -*- */ +/* + * Copyright (C) 2022 Purism SPC + * + * SPDX-License-Identifier: LGPL-2.1-or-later + */ + +#pragma once + +#if !defined(_CMATRIX_TAKEN) && !defined(CMATRIX_COMPILATION) +# error "Only <cmatrix.h> can be included directly." +#endif + +#include <glib-object.h> +#include <gio/gio.h> + +#include "cm-db-private.h" +#include "cm-enc-private.h" +#include "cm-net-private.h" +#include "cm-types.h" +#include "cm-client.h" + +G_BEGIN_DECLS + +void cm_client_enable_as_in_store (CmClient *self); +CmClient *cm_client_new_from_secret (gpointer secret_retrievable, + CmDb *db); +void cm_client_save_secrets_async (CmClient *self, + GAsyncReadyCallback callback, + gpointer user_data); +gboolean cm_client_save_secrets_finish (CmClient *self, + GAsyncResult *result, + GError **error); +void cm_client_delete_secrets_async (CmClient *self, + GAsyncReadyCallback callback, + gpointer user_data); +gboolean cm_client_delete_secrets_finish (CmClient *self, + GAsyncResult *result, + GError **error); +int cm_client_pop_event_id (CmClient *self); +CmDb *cm_client_get_db (CmClient *self); +CmNet *cm_client_get_net (CmClient *self); +CmEnc *cm_client_get_enc (CmClient *self); +void cm_client_set_db (CmClient *self, + CmDb *db); +const char *cm_client_get_filter_id (CmClient *self); +void cm_client_save (CmClient *self); +const char *cm_client_get_next_batch (CmClient *self); +CmUserList *cm_client_get_user_list (CmClient *self); +void cm_client_get_file_async (CmClient *self, + const char *uri, + GCancellable *cancellable, + GFileProgressCallback progress_callback, + gpointer progress_user_data, + GAsyncReadyCallback callback, + gpointer user_data); +GInputStream *cm_client_get_file_finish (CmClient *self, + GAsyncResult *result, + GError **error); +G_END_DECLS diff --git a/subprojects/libcmatrix/src/cm-client.c b/subprojects/libcmatrix/src/cm-client.c new file mode 100644 index 0000000000000000000000000000000000000000..d7f4225c807f75b538b4099529eaa91b7bd3933c --- /dev/null +++ b/subprojects/libcmatrix/src/cm-client.c @@ -0,0 +1,3360 @@ +/* -*- mode: c; c-basic-offset: 2; indent-tabs-mode: nil; -*- */ +/* cm-client.c + * + * Copyright 2022 Purism SPC + * + * Author(s): + * Mohammed Sadiq <sadiq@sadiqpk.org> + * + * SPDX-License-Identifier: LGPL-2.1-or-later + */ + +#define G_LOG_DOMAIN "cm-client" + +#ifdef HAVE_CONFIG_H +# include "config.h" +#endif + +#define GCRYPT_NO_DEPRECATED +#include <gcrypt.h> +#include <libsecret/secret.h> +#include <libsoup/soup.h> + +#include "cm-net-private.h" +#include "cm-utils-private.h" +#include "cm-common.h" +#include "cm-db-private.h" +#include "cm-olm-sas-private.h" +#include "cm-enc-private.h" +#include "cm-enums.h" +#include "events/cm-event-private.h" +#include "users/cm-room-member-private.h" +#include "users/cm-user-private.h" +#include "users/cm-user-list-private.h" +#include "users/cm-account.h" +#include "cm-room-private.h" +#include "cm-room.h" +#include "cm-secret-store-private.h" +#include "cm-client-private.h" +#include "cm-client.h" + +/** + * SECTION: cm-client + * @title: CmClient + * @short_description: + * @include: "cm-client.h" + */ + +#define KEY_TIMEOUT 10000 /* milliseconds */ +#define URI_REQUEST_TIMEOUT 30 /* seconds */ +#define SYNC_TIMEOUT 30000 /* milliseconds */ + +struct _CmClient +{ + GObject parent_instance; + + char *homeserver; + char *password; + char *device_id; + char *device_name; + + CmAccount *cm_account; + CmDb *cm_db; + CmNet *cm_net; + CmEnc *cm_enc; + GSocketAddress *gaddress; + + CmCallback callback; + gpointer cb_data; + GDestroyNotify cb_destroy; + + GCancellable *cancellable; + char *filter_id; + char *next_batch; + char *key; + char *pickle_key; + + CmUserList *user_list; + /* direct_rooms are set on initial sync from 'account_data', + * which will then be moved to joined_rooms later */ + GHashTable *direct_rooms; + GListStore *joined_rooms; + GListStore *invited_rooms; + + GListStore *key_verifications; + + /* for sending events, incremented for each event */ + int event_id; + + guint resync_id; + + gboolean db_migrated; + gboolean room_list_loading; + gboolean room_list_loaded; + gboolean direct_room_list_loading; + gboolean direct_room_list_loaded; + + gboolean db_loading; + gboolean db_loaded; + gboolean client_enabled; + gboolean client_enabled_in_store; + gboolean has_tried_connecting; + gboolean is_logging_in; + /* Set if passsword is right/success using @access_token */ + gboolean login_success; + gboolean is_sync; + gboolean sync_failed; + gboolean is_self_change; + gboolean save_client_pending; + gboolean save_secret_pending; + gboolean is_saving_client; + gboolean is_saving_secret; + gboolean homeserver_verified; +}; + +G_DEFINE_TYPE (CmClient, cm_client, G_TYPE_OBJECT) + +enum { + PROP_0, + PROP_HOME_SERVER, + N_PROPS +}; + +enum { + STATUS_CHANGED, + ACCESS_TOKEN_CHANGED, + N_SIGNALS +}; + +static GParamSpec *properties[N_PROPS]; +static guint signals[N_SIGNALS]; + +const char *filter_json_str = "{ \"room\": { " + " \"timeline\": { \"limit\": 20 }, " + " \"state\": { \"lazy_load_members\": true } " + " }" + "}"; + +static void matrix_start_sync (CmClient *self, + gpointer tsk); +static void matrix_upload_key (CmClient *self); +static gboolean handle_matrix_glitches (CmClient *self, + GError *error); + +static void +cm_set_string_value (char **strp, + const char *value) +{ + g_assert (strp); + + g_free (*strp); + *strp = g_strdup (value); +} + +static CmEvent * +client_find_key_verification (CmClient *self, + CmEvent *event, + gboolean add_if_missing) +{ + CmOlmSas *olm_sas; + GListModel *model; + CmEventType type; + + g_assert (CM_IS_CLIENT (self)); + g_assert (CM_IS_EVENT (event)); + + type = cm_event_get_m_type (event); + model = G_LIST_MODEL (self->key_verifications); + + g_assert (type >= CM_M_KEY_VERIFICATION_ACCEPT && type <= CM_M_KEY_VERIFICATION_START); + + for (guint i = 0; i < g_list_model_get_n_items (model); i++) + { + g_autoptr(CmEvent) item = NULL; + + item = g_list_model_get_item (model, i); + if (item == event) + return event; + + olm_sas = g_object_get_data (G_OBJECT (item), "olm-sas"); + + if (cm_olm_sas_matches_event (olm_sas, event)) + return item; + } + + if (type != CM_M_KEY_VERIFICATION_START && type != CM_M_KEY_VERIFICATION_REQUEST) + return NULL; + + olm_sas = cm_enc_get_sas_for_event (self->cm_enc, event); + cm_olm_sas_set_client (olm_sas, self); + + if (add_if_missing) + g_list_store_append (self->key_verifications, event); + + return event; +} + +/* + * client_mark_for_save: + * @save_client: 1 for TRUE, 0 for FALSE, ignore otherwise + * @save_secret: 1 for TRUE, 0 for FALSE, ignore otherwise + */ +static void +client_mark_for_save (CmClient *self, + int save_client, + int save_secret) +{ + g_assert (CM_IS_CLIENT (self)); + + /* always reset to 0 when asked, as this is done after saving */ + if (save_client == 0) + self->save_client_pending = save_client; + if (save_secret == 0) + self->save_secret_pending = save_secret; + + if (g_object_get_data (G_OBJECT (self), "no-save")) + return; + + if (self->is_self_change) + goto end; + + if (save_client == 1) + self->save_client_pending = save_client; + if (save_secret == 1) + self->save_secret_pending = save_secret; + + end: + cm_client_save (self); +} + +static void +client_set_login_state (CmClient *self, + gboolean logging_in, + gboolean logged_in) +{ + g_assert (CM_IS_CLIENT (self)); + + /* We can't be in process of logging in and logged in the same time */ + if (logging_in) + g_assert (!logged_in); + + if (self->is_logging_in == logging_in && + self->login_success == logged_in) + return; + + self->is_logging_in = logging_in; + self->login_success = logged_in; + g_signal_emit (self, signals[STATUS_CHANGED], 0); +} + +static void +client_reset_state (CmClient *self) +{ + g_assert (CM_IS_CLIENT (self)); + + self->is_sync = FALSE; + g_clear_pointer (&self->next_batch, g_free); + g_clear_pointer (&self->key, g_free); + g_clear_pointer (&self->pickle_key, gcry_free); + g_clear_pointer (&self->filter_id, g_free); + + g_hash_table_remove_all (self->direct_rooms); + g_list_store_remove_all (self->joined_rooms); + g_list_store_remove_all (self->invited_rooms); + cm_net_set_access_token (self->cm_net, NULL); + cm_enc_set_details (self->cm_enc, NULL, NULL); + client_set_login_state (self, FALSE, FALSE); +} + +static void +client_key_verification_done_cb (GObject *obj, + GAsyncResult *result, + gpointer user_data) +{ + CmClient *self; + g_autoptr(GTask) task = user_data; + g_autoptr(JsonObject) object = NULL; + CmEvent *event; + GError *error = NULL; + + g_assert (G_IS_TASK (task)); + + self = g_task_get_source_object (task); + event = g_task_get_task_data (task); + g_assert (CM_IS_CLIENT (self)); + g_assert (CM_IS_EVENT (event)); + + object = g_task_propagate_pointer (G_TASK (result), &error); + g_debug ("(%p) Key verification %p cancel %s", self, + event, CM_LOG_SUCCESS (!error)); + + if (error) + { + g_debug ("(%p) Key verification cancel error: %s", self, error->message); + g_task_return_error (task, error); + } + else + { + cm_utils_remove_list_item (self->key_verifications, event); + g_object_set_data (G_OBJECT (event), "completed", GINT_TO_POINTER (TRUE)); + g_signal_emit_by_name (event, "updated", 0); + g_task_return_boolean (task, TRUE); + } +} + +static void +cm_client_key_verification_done_async (CmClient *self, + CmEvent *verification_event, + GCancellable *cancellable, + GAsyncReadyCallback callback, + gpointer user_data) +{ + CmOlmSas *olm_sas = NULL; + CmEvent *event, *reply_event; + g_autoptr(GTask) task = NULL; + g_autofree char *uri = NULL; + JsonObject *root; + + g_return_if_fail (CM_IS_CLIENT (self)); + g_return_if_fail (!cancellable || G_IS_CANCELLABLE (cancellable)); + g_return_if_fail (CM_IS_EVENT (verification_event)); + + if (!cancellable) + cancellable = self->cancellable; + + task = g_task_new (self, cancellable, callback, user_data); + event = client_find_key_verification (self, verification_event, FALSE); + g_debug ("(%p) Key verification %p cancel", self, event); + + if (event) + olm_sas = g_object_get_data (G_OBJECT (event), "olm-sas"); + + if (!olm_sas) + { + g_debug ("(%p) Key verification %p cancel fail, not in progress", self, verification_event); + g_task_return_new_error (task, G_IO_ERROR, G_IO_ERROR_NOT_INITIALIZED, + "Provided key verification request is not in progress"); + return; + } + + g_task_set_task_data (task, g_object_ref (event), g_object_unref); + + reply_event = cm_olm_sas_get_done_event (olm_sas); + root = cm_event_get_json (reply_event); + + uri = g_strdup_printf ("/_matrix/client/r0/sendToDevice/m.key.verification.done/%s", + cm_event_get_txn_id (reply_event)); + cm_net_send_json_async (self->cm_net, 0, root, uri, SOUP_METHOD_PUT, + NULL, cancellable, + client_key_verification_done_cb, + g_steal_pointer (&task)); +} + +static void +send_json_cb (GObject *obj, + GAsyncResult *result, + gpointer user_data) +{ + g_autoptr(GTask) task = user_data; + g_autoptr(JsonObject) root = NULL; + g_autoptr(GError) error = NULL; + + g_assert (G_IS_TASK (task)); + + root = g_task_propagate_pointer (G_TASK (result), &error); + + if (error) + { + g_task_return_error (task, g_steal_pointer (&error)); + return; + } + + g_task_return_pointer (task, g_steal_pointer (&root), (GDestroyNotify)json_object_unref); +} + +static gboolean +schedule_resync (gpointer user_data) +{ + CmClient *self = user_data; + gboolean sync_now; + + g_assert (CM_IS_CLIENT (self)); + self->resync_id = 0; + + sync_now = cm_client_can_connect (self); + + if (sync_now) + matrix_start_sync (self, NULL); + else + self->resync_id = g_timeout_add_seconds (URI_REQUEST_TIMEOUT, + schedule_resync, self); + + return G_SOURCE_REMOVE; +} + +static gboolean +handle_matrix_glitches (CmClient *self, + GError *error) +{ + if (!error) + return FALSE; + + if (g_error_matches (error, CM_ERROR, CM_ERROR_UNKNOWN_TOKEN) && + self->password) + { + g_debug ("(%p) Handle glitch, unknown token", self); + + client_reset_state (self); + cm_db_delete_client_async (self->cm_db, self, NULL, NULL); + matrix_start_sync (self, NULL); + + return TRUE; + } + + /* + * The G_RESOLVER_ERROR may be suggesting that the hostname is wrong, but we don't + * know if it's network/DNS/Proxy error. So keep retrying. + */ + if (g_error_matches (error, G_IO_ERROR, G_IO_ERROR_NETWORK_UNREACHABLE) || + g_error_matches (error, G_IO_ERROR, G_IO_ERROR_TIMED_OUT) || +#if SOUP_MAJOR_VERSION == 2 + (error->domain == SOUP_HTTP_ERROR && + error->code <= SOUP_STATUS_TLS_FAILED && + error->code > SOUP_STATUS_CANCELLED) || +#else + error->domain == SOUP_TLD_ERROR || + error->domain == G_TLS_ERROR || +#endif + /* Should we handle connection_refused, or just keep it for localhost? */ + g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CONNECTION_REFUSED) || + error->domain == G_RESOLVER_ERROR || + error->domain == JSON_PARSER_ERROR) + { + self->sync_failed = TRUE; + g_signal_emit (self, signals[STATUS_CHANGED], 0); + + if (cm_client_can_connect (self)) + { + CM_TRACE ("(%p) Handle glitch, network error", self); + g_clear_handle_id (&self->resync_id, g_source_remove); + + self->resync_id = g_timeout_add_seconds (URI_REQUEST_TIMEOUT, + schedule_resync, self); + return TRUE; + } + } + + return FALSE; +} + +static void +client_login_with_password_async (CmClient *self, + GCancellable *cancellable, + GAsyncReadyCallback callback, + gpointer user_data) +{ + g_autoptr(GString) str = NULL; + JsonObject *object, *child; + GTask *task; + + g_assert (CM_IS_CLIENT (self)); + g_assert (!cancellable || G_IS_CANCELLABLE (cancellable)); + g_assert (cm_account_get_login_id (self->cm_account) || + cm_user_get_id (CM_USER (self->cm_account))); + g_assert (self->homeserver_verified); + g_assert (self->password && *self->password); + + str = g_string_new (NULL); + g_debug ("(%p) Logging in with '%s'", self, + cm_utils_anonymize (str, cm_account_get_login_id (self->cm_account))); + + /* https://matrix.org/docs/spec/client_server/r0.6.1#post-matrix-client-r0-login */ + object = json_object_new (); + json_object_set_string_member (object, "type", "m.login.password"); + json_object_set_string_member (object, "password", self->password); + json_object_set_string_member (object, "initial_device_display_name", self->device_name ?: "CMatrix"); + + child = json_object_new (); + + if (cm_utils_user_name_is_email (cm_account_get_login_id (self->cm_account))) + { + json_object_set_string_member (child, "type", "m.id.thirdparty"); + json_object_set_string_member (child, "medium", "email"); + json_object_set_string_member (child, "address", cm_account_get_login_id (self->cm_account)); + } + else + { + json_object_set_string_member (child, "type", "m.id.user"); + json_object_set_string_member (child, "user", cm_account_get_login_id (self->cm_account)); + } + + json_object_set_object_member (object, "identifier", child); + + task = g_task_new (self, cancellable, callback, user_data); + cm_net_send_json_async (self->cm_net, 2, object, + "/_matrix/client/r0/login", SOUP_METHOD_POST, + NULL, cancellable, send_json_cb, + task); +} + +static void +cm_upload_filter_cb (GObject *obj, + GAsyncResult *result, + gpointer user_data) +{ + CmClient *self; + g_autoptr(GTask) task = user_data; + g_autoptr(JsonObject) root = NULL; + g_autoptr(GError) error = NULL; + + g_assert (G_IS_TASK (task)); + + self = g_task_get_source_object (task); + g_assert (CM_IS_CLIENT (self)); + + root = g_task_propagate_pointer (G_TASK (result), &error); + g_debug ("(%p) Upload filter %s", self, CM_LOG_SUCCESS (!error)); + + if (error) + { + if (!handle_matrix_glitches (self, error)) + g_warning ("Error uploading filter: %s", error->message); + + g_task_return_error (task, g_steal_pointer (&error)); + return; + } + + g_task_return_pointer (task, NULL, NULL); + self->filter_id = g_strdup (cm_utils_json_object_get_string (root, "filter_id")); + g_debug ("(%p) Upload filter, id: %s", self, self->filter_id); + + client_set_login_state (self, FALSE, TRUE); + + if (!self->filter_id) + self->filter_id = g_strdup (""); + + client_mark_for_save (self, TRUE, -1); + cm_client_save (self); + + matrix_start_sync (self, NULL); +} + +static void +matrix_upload_filter (CmClient *self, + gpointer tsk) +{ + g_autoptr(GTask) task = tsk; + g_autoptr(GError) error = NULL; + GCancellable *cancellable; + g_autofree char *uri = NULL; + g_autoptr(JsonParser) parser = NULL; + JsonObject *filter = NULL; + JsonNode *root = NULL; + + g_debug ("(%p) Upload filter", self); + + parser = json_parser_new (); + json_parser_load_from_data (parser, filter_json_str, -1, &error); + + if (error) + g_warning ("(%p) Error parsing filter file: %s", self, error->message); + + if (!error) + root = json_parser_get_root (parser); + + if (root) + filter = json_node_get_object (root); + + if (error || !root || !filter) + { + g_warning ("(%p) Error getting filter file: %s", self, error ? error->message : ""); + + self->filter_id = g_strdup (""); + /* Even if we have error uploading filter, continue syncing */ + matrix_start_sync (self, NULL); + + return; + } + + cancellable = g_task_get_cancellable (task); + uri = g_strconcat ("/_matrix/client/r0/user/", cm_user_get_id (CM_USER (self->cm_account)), "/filter", NULL); + cm_net_send_json_async (self->cm_net, 2, json_object_ref (filter), + uri, SOUP_METHOD_POST, + NULL, cancellable, cm_upload_filter_cb, + g_steal_pointer (&task)); +} + +static void +cm_client_set_property (GObject *object, + guint prop_id, + const GValue *value, + GParamSpec *pspec) +{ + CmClient *self = (CmClient *)object; + + switch (prop_id) + { + case PROP_HOME_SERVER: + cm_client_set_homeserver (self, g_value_get_string (value)); + break; + + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + } +} + +static void +cm_client_finalize (GObject *object) +{ + CmClient *self = (CmClient *)object; + + if (self->cancellable) + g_cancellable_cancel (self->cancellable); + g_clear_object (&self->cancellable); + + g_clear_object (&self->cm_account); + g_clear_object (&self->cm_net); + + g_list_store_remove_all (self->joined_rooms); + g_clear_object (&self->joined_rooms); + + g_list_store_remove_all (self->key_verifications); + g_clear_object (&self->key_verifications); + + g_hash_table_unref (self->direct_rooms); + + g_free (self->homeserver); + g_free (self->device_id); + g_free (self->device_name); + gcry_free (self->password); + gcry_free (self->pickle_key); + + G_OBJECT_CLASS (cm_client_parent_class)->finalize (object); +} + +static void +cm_client_class_init (CmClientClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + + object_class->set_property = cm_client_set_property; + object_class->finalize = cm_client_finalize; + + properties[PROP_HOME_SERVER] = + g_param_spec_string ("home-server", + "Home Server", + "Matrix Home Server", + NULL, + G_PARAM_WRITABLE | G_PARAM_CONSTRUCT | G_PARAM_STATIC_STRINGS); + + g_object_class_install_properties (object_class, N_PROPS, properties); + + /** + * ChattyItem::status-changed: + * @self: a #ChattyItem + * + * status-changed signal is emitted when the client's + * login and sync status are changed. + */ + signals [STATUS_CHANGED] = + g_signal_new ("status-changed", + G_TYPE_FROM_CLASS (klass), + G_SIGNAL_RUN_LAST, + 0, NULL, NULL, NULL, + G_TYPE_NONE, 0); +} + +static void +cm_client_init (CmClient *self) +{ + self->cm_account = g_object_new (CM_TYPE_ACCOUNT, NULL); + cm_user_set_client (CM_USER (self->cm_account), self); + + self->cm_net = cm_net_new (); + self->user_list = cm_user_list_new (self); + self->cancellable = g_cancellable_new (); + self->joined_rooms = g_list_store_new (CM_TYPE_ROOM); + self->invited_rooms = g_list_store_new (CM_TYPE_ROOM); + self->key_verifications = g_list_store_new (CM_TYPE_EVENT); + self->direct_rooms = g_hash_table_new_full (g_str_hash, g_str_equal, + g_free, g_object_unref); +} + +/** + * cm_client_new: + * + * Returns: (transfer full): A #CmClient + */ +CmClient * +cm_client_new (void) +{ + return g_object_new (CM_TYPE_CLIENT, NULL); +} + +static char * +client_get_value (const char *str, + const char *key) +{ + const char *start, *end; + + if (!str || !*str) + return NULL; + + g_assert (key && *key); + + start = strstr (str, key); + if (start) { + start = start + strlen (key); + while (*start && *start++ != '"') + ; + + end = start - 1; + do { + end++; + end = strchr (end, '"'); + } while (end && *(end - 1) == '\\' && *(end - 2) != '\\'); + + if (end && end > start) + return g_strndup (start, end - start); + } + + return NULL; +} + +void +cm_client_enable_as_in_store (CmClient *self) +{ + g_return_if_fail (CM_IS_CLIENT (self)); + + self->is_self_change = TRUE; + + if (self->client_enabled_in_store) + cm_client_set_enabled (self, TRUE); + + self->client_enabled_in_store = FALSE; + self->is_self_change = FALSE; +} + +CmClient * +cm_client_new_from_secret (gpointer secret_retrievable, + CmDb *db) +{ + CmClient *self = NULL; + g_autoptr(GHashTable) attributes = NULL; + SecretRetrievable *item = secret_retrievable; + g_autoptr(SecretValue) value = NULL; + const char *homeserver, *credentials = NULL; + const char *username, *login_username; + char *password, *token, *device_id; + char *password_str = NULL, *token_str = NULL; + g_autofree char *enabled = NULL; + + g_return_val_if_fail (SECRET_IS_RETRIEVABLE (item), NULL); + g_return_val_if_fail (CM_IS_DB (db), NULL); + + value = secret_retrievable_retrieve_secret_sync (item, NULL, NULL); + + if (value) + credentials = secret_value_get_text (value); + + if (!credentials) + return NULL; + + attributes = secret_retrievable_get_attributes (item); + login_username = g_hash_table_lookup (attributes, CM_USERNAME_ATTRIBUTE); + homeserver = g_hash_table_lookup (attributes, CM_SERVER_ATTRIBUTE); + + device_id = client_get_value (credentials, "\"device-id\""); + username = client_get_value (credentials, "\"username\""); + password = client_get_value (credentials, "\"password\""); + enabled = client_get_value (credentials, "\"enabled\""); + token = client_get_value (credentials, "\"access-token\""); + + if (token) + token_str = g_strcompress (token); + + if (password) + password_str = g_strcompress (password); + + self = g_object_new (CM_TYPE_CLIENT, NULL); + self->is_self_change = TRUE; + cm_client_set_db (self, db); + cm_client_set_homeserver (self, homeserver); + cm_account_set_login_id (self->cm_account, login_username); + cm_client_set_user_id (self, username); + cm_client_set_password (self, password_str); + cm_client_set_device_id (self, device_id); + + if (g_strcmp0 (enabled, "true") == 0) + self->client_enabled_in_store = TRUE; + + cm_client_set_access_token (self, token_str); + + if (token && device_id) { + g_autofree char *pickle = NULL; + + pickle = client_get_value (credentials, "\"pickle-key\""); + cm_client_set_pickle_key (self, pickle); + } + + self->is_self_change = FALSE; + + cm_utils_free_buffer (device_id); + cm_utils_free_buffer (password); + cm_utils_free_buffer (password_str); + cm_utils_free_buffer (token); + cm_utils_free_buffer (token_str); + + return self; +} +static void +save_secrets_cb (GObject *object, + GAsyncResult *result, + gpointer user_data) +{ + CmClient *self; + g_autoptr(GTask) task = user_data; + GError *error = NULL; + gboolean ret; + + g_assert (G_IS_TASK (task)); + + self = g_task_get_source_object (task); + g_assert (CM_IS_CLIENT (self)); + + ret = cm_secret_store_save_finish (result, &error); + + if (error) + { + client_mark_for_save (self, -1, TRUE); + g_task_return_error (task, error); + } + else + { + g_task_return_boolean (task, ret); + } + + /* Unset at the end so that the client won't initiate + * saving the secret immediately on error. + */ + self->is_saving_secret = FALSE; +} + +void +cm_client_save_secrets_async (CmClient *self, + GAsyncReadyCallback callback, + gpointer user_data) +{ + g_autoptr(GTask) task = NULL; + char *pickle_key = NULL; + + g_return_if_fail (CM_IS_CLIENT (self)); + + task = g_task_new (self, NULL, callback, user_data); + + if (g_object_get_data (G_OBJECT (self), "no-save")) + { + g_task_return_new_error (task, G_IO_ERROR, G_IO_ERROR_FAILED, + "Secrets marked not to save"); + return; + } + + if (self->is_saving_secret) + { + g_task_return_new_error (task, G_IO_ERROR, G_IO_ERROR_PENDING, + "Secrets are already being saved"); + return; + } + + self->is_saving_secret = TRUE; + client_mark_for_save (self, -1, FALSE); + + if (self->cm_enc) + pickle_key = cm_enc_get_pickle_key (self->cm_enc); + + cm_secret_store_save_async (self, + g_strdup (cm_client_get_access_token (self)), + pickle_key, NULL, + save_secrets_cb, + g_steal_pointer (&task)); +} + +gboolean +cm_client_save_secrets_finish (CmClient *self, + GAsyncResult *result, + GError **error) +{ + g_return_val_if_fail (CM_IS_CLIENT (self), FALSE); + g_return_val_if_fail (G_IS_TASK (result), FALSE); + + return g_task_propagate_boolean (G_TASK (result), error); +} + +static void +delete_secrets_cb (GObject *object, + GAsyncResult *result, + gpointer user_data) +{ + CmClient *self; + g_autoptr(GTask) task = user_data; + GError *error = NULL; + gboolean ret; + + g_assert (G_IS_TASK (task)); + + self = g_task_get_source_object (task); + g_assert (CM_IS_CLIENT (self)); + + ret = cm_secret_store_delete_finish (result, &error); + + if (error) + { + g_task_return_error (task, error); + } + else + { + g_list_store_remove_all (self->joined_rooms); + g_task_return_boolean (task, ret); + } +} + +void +cm_client_delete_secrets_async (CmClient *self, + GAsyncReadyCallback callback, + gpointer user_data) +{ + GTask *task; + + g_return_if_fail (CM_IS_CLIENT (self)); + + task = g_task_new (self, NULL, callback, user_data); + cm_client_set_enabled (self, FALSE); + cm_secret_store_delete_async (self, NULL, + delete_secrets_cb, task); +} + +gboolean +cm_client_delete_secrets_finish (CmClient *self, + GAsyncResult *result, + GError **error) +{ + g_return_val_if_fail (CM_IS_CLIENT (self), FALSE); + g_return_val_if_fail (G_IS_TASK (result), FALSE); + + return g_task_propagate_boolean (G_TASK (result), error); +} + +CmAccount * +cm_client_get_account (CmClient *self) +{ + g_return_val_if_fail (CM_IS_CLIENT (self), NULL); + + return self->cm_account; +} + +static void +db_load_client_cb (GObject *obj, + GAsyncResult *result, + gpointer user_data) +{ + g_autoptr(GTask) task = user_data; + g_autoptr(GError) error = NULL; + CmClient *self; + guint room_count = 0; + gboolean success; + + g_assert (G_IS_TASK (task)); + + self = g_task_get_source_object (task); + g_assert (CM_IS_CLIENT (self)); + + g_assert (!self->cm_enc); + + self->db_loaded = TRUE; + self->db_loading = FALSE; + + success = cm_db_load_client_finish (self->cm_db, result, &error); + g_debug ("(%p) Load db %s", self, + CM_LOG_SUCCESS (!error || g_error_matches (error, G_IO_ERROR, G_IO_ERROR_NOT_FOUND))); + + if (!success) + { + if (error && !g_error_matches (error, G_IO_ERROR, G_IO_ERROR_NOT_FOUND)) + { + g_autoptr(GString) str = NULL; + + str = g_string_new (NULL); + g_warning ("(%p) Error loading client '%s': %s", self, + cm_utils_anonymize (str, cm_user_get_id (CM_USER (self->cm_account))), + error->message); + } + + /* We can load further even if fail to load from db */ + /* XXX: handle difference between if the user is missing from db and failed fetching data */ + matrix_start_sync (self, g_steal_pointer (&task)); + return; + } + + if (g_object_get_data (G_OBJECT (result), "pickle") && self->pickle_key) + { + const char *pickle; + + pickle = g_object_get_data (G_OBJECT (result), "pickle"); + self->cm_enc = cm_enc_new (self->cm_db, pickle, self->pickle_key); + } + + if (self->cm_enc) + cm_enc_set_details (self->cm_enc, + cm_client_get_user_id (self), + cm_client_get_device_id (self)); + else + g_clear_pointer (&self->pickle_key, gcry_free); + + if (g_object_get_data (G_OBJECT (result), "rooms")) + { + g_autoptr(GPtrArray) rooms = NULL; + + rooms = g_object_steal_data (G_OBJECT (result), "rooms"); + room_count = rooms->len; + + for (guint i = 0; i < rooms->len; i++) + { + CmRoom *room = rooms->pdata[i]; + cm_room_set_client (room, self); + } + + g_list_store_splice (self->joined_rooms, 0, 0, rooms->pdata, rooms->len); + } + + self->db_migrated = GPOINTER_TO_INT (g_object_get_data (G_OBJECT (result), "db-migrated")); + self->filter_id = g_strdup (g_object_get_data (G_OBJECT (result), "filter-id")); + self->next_batch = g_strdup (g_object_get_data (G_OBJECT (result), "batch")); + g_debug ("(%p) Load db, added %u room(s), db migrated: %s, filter-id: %s", + self, room_count, CM_LOG_BOOL (self->db_migrated), self->filter_id); + + matrix_start_sync (self, g_steal_pointer (&task)); +} + +/* + * cm_client_pop_event_id: + * @self: A #CmClient + * + * Get a number to be used as event suffix. + * The number is incremented per request, + * so you always get a different number. + * + * Returns: An integer + */ +int +cm_client_pop_event_id (CmClient *self) +{ + g_return_val_if_fail (CM_IS_CLIENT (self), 0); + + self->event_id++; + + return self->event_id - 1; +} + +/* + * cm_client_get_db: + * @self: A #CmClient + * + * Get the #CmDb associated with the client. + * Can be %NULL if client hasn't loaded yet. + * + * Returns: (transfer none): A #CmDb + */ +CmDb * +cm_client_get_db (CmClient *self) +{ + g_return_val_if_fail (CM_IS_CLIENT (self), NULL); + + return self->cm_db; +} + +/* + * cm_client_get_net: + * @self: A #CmClient + * + * Get the #CmNet associated with the client + * + * Returns: (transfer none): A #CmNet + */ +CmNet * +cm_client_get_net (CmClient *self) +{ + g_return_val_if_fail (CM_IS_CLIENT (self), NULL); + + return self->cm_net; +} + +/* + * cm_client_get_enc: + * @self: A #CmClient + * + * Get the #CmEnc associated with the client, + * Can be %NULL if not loaded/logged in yet. + * + * Returns: (transfer none): A #CmEnc + */ +CmEnc * +cm_client_get_enc (CmClient *self) +{ + g_return_val_if_fail (CM_IS_CLIENT (self), NULL); + + return self->cm_enc; +} + +/* + * cm_client_set_db: + * @self: A #CmClient + * @db: A #CmDb + * + * Set the db object for the client. + * There is at most one db object which + * is shared for every client. + * + * This should be set only after user ID + * and device ID is set. + * + * @self shall load the client info immediately + * after db is set and enable if the account + * if that's how it's set in db. + */ +void +cm_client_set_db (CmClient *self, + CmDb *db) +{ + g_return_if_fail (CM_IS_CLIENT (self)); + g_return_if_fail (CM_IS_DB (db)); + + if (self->cm_db) + return; + + self->cm_db = g_object_ref (db); +} + +const char * +cm_client_get_filter_id (CmClient *self) +{ + g_return_val_if_fail (CM_IS_CLIENT (self), NULL); + + if (self->filter_id && *self->filter_id) + return self->filter_id; + + return NULL; +} + +/** + * cm_client_set_enabled: + * @self: A #CmClient + * @enable: Whether to enable client + * + * Whether to enable the client and begin + * sync with the server. + */ +void +cm_client_set_enabled (CmClient *self, + gboolean enable) +{ + g_return_if_fail (CM_IS_CLIENT (self)); + if (enable) + g_return_if_fail (self->cm_db); + + enable = !!enable; + + if (self->client_enabled == enable) + return; + + g_debug ("(%p) Set enable to %s", self, CM_LOG_BOOL (enable)); + + self->client_enabled = enable; + g_signal_emit (self, signals[STATUS_CHANGED], 0); + + if (self->client_enabled) + { + matrix_start_sync (self, NULL); + } + else + { + cm_client_stop_sync (self); + } + + client_mark_for_save (self, TRUE, TRUE); + cm_client_save (self); +} + +static void +db_save_cb (GObject *object, + GAsyncResult *result, + gpointer user_data) +{ + g_autoptr(CmClient) self = user_data; + g_autoptr(GError) error = NULL; + gboolean status; + + status = cm_db_save_client_finish (self->cm_db, result, &error); + self->is_saving_client = FALSE; + + if (error || !status) + self->save_client_pending = TRUE; + + if (error) + g_warning ("Error saving to db: %s", error->message); + + /* If settings changed when we were saving the current settings, repeat. */ + cm_client_save (self); +} + +/* + * cm_client_set_enabled: + * @self: A #CmClient + * @force: Whether to force saving to db + * + * Save the changes to associated CmDb. + * Set @force to %TRUE to force saving to + * db even when no changes are made + */ +void +cm_client_save (CmClient *self) +{ + g_return_if_fail (CM_IS_CLIENT (self)); + + if (g_object_get_data (G_OBJECT (self), "no-save")) + return; + + if (!cm_account_get_login_id (self->cm_account) && + !cm_user_get_id (CM_USER (self->cm_account))) + return; + + if (self->save_client_pending && !self->is_saving_client && + cm_client_get_device_id (self)) + { + char *pickle = NULL; + + self->is_saving_client = TRUE; + self->save_client_pending = FALSE; + + if (self->cm_enc) + pickle = cm_enc_get_pickle (self->cm_enc); + + cm_db_save_client_async (self->cm_db, self, pickle, + db_save_cb, + g_object_ref (self)); + } + + if (self->save_secret_pending && !self->is_saving_secret) + cm_client_save_secrets_async (self, NULL, NULL); +} + +/** + * cm_client_get_enabled: + * @self: A #CmClient + * + * Wheter @self has been enabled. + * + * Enabled client doesn't necessarily mean the client + * has logged in or is in sync with server. + * Also See cm_client_is_sync(). + * + * Returns: %TRUE if @self has been enabled, %FALSE + * otherwise + */ +gboolean +cm_client_get_enabled (CmClient *self) +{ + g_return_val_if_fail (CM_IS_CLIENT (self), FALSE); + + return self->client_enabled || self->client_enabled_in_store || + GPOINTER_TO_INT (g_object_get_data (G_OBJECT (self), "enable")); +} + +/** + * cm_client_set_sync_callback: + * @self: A #CmClient + * @callback: A #CmCallback + * @callback_data: A #GObject derived object for @callback user_data + * @callback_data_destroy: (nullable): The method to destroy @callback_data + * + * Set the sync callback which shall be executed for the + * events happening in @self. + * + * @callback_data_destroy() shall be executead only if @callback_data + * is not NULL. + */ +void +cm_client_set_sync_callback (CmClient *self, + CmCallback callback, + gpointer callback_data, + GDestroyNotify callback_data_destroy) +{ + g_return_if_fail (CM_IS_CLIENT (self)); + g_return_if_fail (callback); + + if (self->cb_data && + self->cb_destroy) + self->cb_destroy (self->cb_data); + + self->callback = callback; + self->cb_data = callback_data; + self->cb_destroy = callback_data_destroy; +} + +/** + * cm_client_set_user_id: + * @self: A #CmClient + * @matrix_user_id: A fully qualified matrix user ID + * + * Set the client matrix user ID. This can be set only + * before the account has been enabled and never after. + * + * Returns: %TRUE if @matrix_user_id was successfully set, + * %FALSE otherwise. + */ +gboolean +cm_client_set_user_id (CmClient *self, + const char *matrix_user_id) +{ + g_autoptr(GString) str = NULL; + GRefString *new_user_id = NULL; + g_autofree char *user_id = NULL; + + g_return_val_if_fail (CM_IS_CLIENT (self), FALSE); + g_return_val_if_fail (!self->is_logging_in, FALSE); + g_return_val_if_fail (!self->login_success, FALSE); + + if (!cm_utils_user_name_valid (matrix_user_id)) + { + g_debug ("(%p) New user ID: '%s' %s. ID not valid", self, + matrix_user_id, CM_LOG_SUCCESS (FALSE)); + return FALSE; + } + + if (cm_user_get_id (CM_USER (self->cm_account))) + { + g_debug ("(%p) New user ID not set, a user id is already set", self); + return FALSE; + } + + user_id = g_ascii_strdown (matrix_user_id, -1); + new_user_id = g_ref_string_new_intern (user_id); + + cm_user_set_user_id (CM_USER (self->cm_account), new_user_id); + cm_user_list_set_account (self->user_list, self->cm_account); + + str = g_string_new (NULL); + g_debug ("(%p) New user ID set: '%s'", self, + cm_utils_anonymize (str, matrix_user_id)); + + client_mark_for_save (self, TRUE, TRUE); + + return TRUE; +} + +/** + * cm_client_get_user_id: + * @self: A #CmClient + * + * Get the matrix user ID of the client @self. + * user ID may be available only after the + * login has succeeded and may return %NULL + * otherwise. + * + * Returns: (nullable) (transfer none): The matrix user ID of the client + */ +GRefString * +cm_client_get_user_id (CmClient *self) +{ + g_return_val_if_fail (CM_IS_CLIENT (self), NULL); + + return cm_user_get_id (CM_USER (self->cm_account)); +} + +/** + * cm_client_set_homeserver: + * @self: A #CmClient + * @homeserver: The homserver URL + * + * Set the matrix Home server URL for the client. + * + * Returns: %TRUE if @homeserver is valid, + * %FALSE otherwise. + */ +gboolean +cm_client_set_homeserver (CmClient *self, + const char *homeserver) +{ +#if SOUP_MAJOR_VERSION == 2 + g_autoptr(SoupURI) uri = NULL; +#else + g_autoptr(GUri) uri = NULL; +#endif + GString *server; + + g_return_val_if_fail (CM_IS_CLIENT (self), FALSE); + g_return_val_if_fail (!self->is_logging_in, FALSE); + g_return_val_if_fail (!self->login_success, FALSE); + + if (!homeserver || !*homeserver) + return FALSE; + + if (!g_str_has_prefix (homeserver, "http://") && + !g_str_has_prefix (homeserver, "https://")) + return FALSE; + + if (!cm_utils_home_server_valid (homeserver)) + return FALSE; + + server = g_string_new (homeserver); + if (g_str_has_suffix (server->str, "/")) + g_string_truncate (server, server->len - 1); + + if (g_strcmp0 (self->homeserver, server->str) == 0) + { + g_string_free (server, TRUE); + return TRUE; + } + + g_free (self->homeserver); + self->homeserver = g_string_free (server, FALSE); + cm_net_set_homeserver (self->cm_net, homeserver); + client_mark_for_save (self, TRUE, TRUE); + + return TRUE; +} + +/** + * cm_client_get_homeserver: + * @self: A #CmClient + * + * Get the Home server set for the client. + * + * Returns: (nullable): The Home server for the client + */ +const char * +cm_client_get_homeserver (CmClient *self) +{ + g_return_val_if_fail (CM_IS_CLIENT (self), NULL); + + if (self->homeserver && *self->homeserver) + return self->homeserver; + + return NULL; +} + +/** + * cm_client_set_password: + * @self: A #CmClient + * @password: The password for the @self + * + * Set the password for the client. You can't + * set the password once the client is enabled, + * except in the callback when the client failed + * to login due to wrong password. + */ +void +cm_client_set_password (CmClient *self, + const char *password) +{ + g_return_if_fail (CM_IS_CLIENT (self)); + g_return_if_fail (!self->is_logging_in); + g_return_if_fail (!self->login_success); + g_return_if_fail (!self->is_sync); + + g_clear_pointer (&self->password, gcry_free); + + if (password && *password) + { + self->password = gcry_malloc_secure (strlen (password) + 1); + strcpy (self->password, password); + } + + client_mark_for_save (self, -1, TRUE); + + if (self->has_tried_connecting && + cm_client_get_enabled (self)) + { + cm_client_stop_sync (self); + cm_client_start_sync (self); + } +} + +/** + * cm_client_get_password: + * @self: A #CmClient + * + * Get the password set for the @self. + * + * Returns: The password string + */ +const char * +cm_client_get_password (CmClient *self) +{ + g_return_val_if_fail (CM_IS_CLIENT (self), NULL); + + return self->password; +} + +/** + * cm_client_set_access_token: + * @self: A #CmClient + * @access_token: (nullable): The access token + * + * Set the access token to be used for sync. + * If set to %NULL, @self shall login using password + * And use the newly got access token for sync. + */ +void +cm_client_set_access_token (CmClient *self, + const char *access_token) +{ + g_return_if_fail (CM_IS_CLIENT (self)); + g_return_if_fail (!self->is_logging_in); + g_return_if_fail (!self->login_success); + + cm_net_set_access_token (self->cm_net, access_token); +} + +/** + * cm_client_get_access_token: + * @self: A #CmClient + * + * Get the access token of the client @self + * + * Returns: (nullable): The access token set + */ +const char * +cm_client_get_access_token (CmClient *self) +{ + g_return_val_if_fail (CM_IS_CLIENT (self), NULL); + + return cm_net_get_access_token (self->cm_net); +} + +/* + * cm_client_get_next_batch: + * @self: A #CmClient + * + * Get the next batch of the client @self + * for the last /sync request + * + * Returns: (nullable): The next batch if set + */ +const char * +cm_client_get_next_batch (CmClient *self) +{ + g_return_val_if_fail (CM_IS_CLIENT (self), NULL); + + return self->next_batch; +} + +CmUserList * +cm_client_get_user_list (CmClient *self) +{ + g_return_val_if_fail (CM_IS_CLIENT (self), NULL); + + return self->user_list; +} + +/** + * cm_client_set_device_id: + * @self: A #CmClient + * @device_id: The device ID + * + * Set the device ID for @client. + */ +void +cm_client_set_device_id (CmClient *self, + const char *device_id) +{ + g_return_if_fail (CM_IS_CLIENT (self)); + g_return_if_fail (!self->is_logging_in); + g_return_if_fail (!self->login_success); + + g_free (self->device_id); + self->device_id = g_strdup (device_id); +} + +/** + * cm_client_get_device_id: + * @self: A #CmClient + * + * Get the device ID of the client @self. + * Device ID may be available only after + * a successful login. + * + * Returns: (nullable): The Device ID string + */ +const char * +cm_client_get_device_id (CmClient *self) +{ + g_return_val_if_fail (CM_IS_CLIENT (self), NULL); + + return self->device_id; +} + +/** + * cm_client_set_device_name: + * @self: A #CmClient + * @device_name: The device name string + * + * Set the device name which shall be used as + * the human readable identifier for the device + */ +void +cm_client_set_device_name (CmClient *self, + const char *device_name) +{ + g_return_if_fail (CM_IS_CLIENT (self)); + + g_free (self->device_name); + self->device_name = g_strdup (device_name); +} + +/** + * cm_client_get_device_name: + * @self: A #CmClient + * + * Get the device name of the client @self + * as set with cm_client_set_device_name(). + * + * Returns: (nullable): The Device name string + */ +const char * +cm_client_get_device_name (CmClient *self) +{ + g_return_val_if_fail (CM_IS_CLIENT (self), NULL); + + return self->device_name; +} + +/** + * cm_client_set_pickle_key: + * @self: A #CmClient + * @pickle_key: The pickle key + * + * Set the account pickle key. This key is used as + * the password to decrypt the encrypted pickle loaded + * from db, and so, this should be set before the db + * is set. + */ +void +cm_client_set_pickle_key (CmClient *self, + const char *pickle_key) +{ + g_return_if_fail (CM_IS_CLIENT (self)); + g_return_if_fail (!self->pickle_key); + + if (pickle_key && *pickle_key) + { + self->pickle_key = gcry_malloc_secure (strlen (pickle_key) + 1); + strcpy (self->pickle_key, pickle_key); + } +} + +/** + * cm_client_get_pickle_key: + * @self: A #CmClient + * + * Get the pickle password + */ +const char * +cm_client_get_pickle_key (CmClient *self) +{ + g_return_val_if_fail (CM_IS_CLIENT (self), NULL); + + if (self->cm_enc) + return cm_enc_get_pickle_key (self->cm_enc); + + return NULL; +} + +/** + * cm_client_get_ed25519_key: + * @self: A #CmClient + * + * Get the public ed25519 key of own device. + */ +const char * +cm_client_get_ed25519_key (CmClient *self) +{ + g_return_val_if_fail (CM_IS_CLIENT (self), NULL); + + if (self->cm_enc) + return cm_enc_get_ed25519_key (self->cm_enc); + + return NULL; +} + +static void +client_join_room_by_id_cb (GObject *obj, + GAsyncResult *result, + gpointer user_data) +{ + g_autoptr(GTask) task = user_data; + g_autoptr(JsonObject) object = NULL; + GError *error = NULL; + + object = g_task_propagate_pointer (G_TASK (result), &error); + + if (error) + g_task_return_error (task, error); + else + g_task_return_boolean (task, TRUE); +} + +void +cm_client_join_room_by_id_async (CmClient *self, + const char *room_id, + GCancellable *cancellable, + GAsyncReadyCallback callback, + gpointer user_data) +{ + g_autofree char *uri = NULL; + GTask *task; + + g_return_if_fail (CM_IS_CLIENT (self)); + g_return_if_fail (room_id && *room_id == '!'); + g_return_if_fail (!cancellable || G_IS_CANCELLABLE (cancellable)); + + if (!cancellable) + cancellable = self->cancellable; + + task = g_task_new (self, cancellable, callback, user_data); + uri = g_strconcat ("/_matrix/client/r0/join/", room_id, NULL); + cm_net_send_data_async (self->cm_net, 2, NULL, 0, + uri, SOUP_METHOD_POST, NULL, + cancellable, + client_join_room_by_id_cb, + task); +} + +gboolean +cm_client_join_room_by_id_finish (CmClient *self, + GAsyncResult *result, + GError **error) +{ + g_return_val_if_fail (CM_IS_CLIENT (self), FALSE); + g_return_val_if_fail (G_IS_TASK (result), FALSE); + + return g_task_propagate_boolean (G_TASK (result), error); +} + +void +cm_client_get_homeserver_async (CmClient *self, + GCancellable *cancellable, + GAsyncReadyCallback callback, + gpointer user_data) +{ + g_autoptr(GTask) task = NULL; + const char *user_id; + + g_return_if_fail (CM_IS_CLIENT (self)); + + task = g_task_new (self, cancellable, callback, user_data); + g_task_set_source_tag (task, cm_client_get_homeserver_async); + g_debug ("(%p) Get homeserver", self); + + if (self->homeserver_verified && self->homeserver && *(self->homeserver)) + { + g_debug ("(%p) Get homeserver already loaded", self); + g_task_return_pointer (task, self->homeserver, NULL); + return; + } + + user_id = cm_user_get_id (CM_USER (self->cm_account)); + + if (!user_id) + user_id = cm_account_get_login_id (self->cm_account); + + if (!cm_utils_user_name_valid (user_id)) + user_id = NULL; + + if (!user_id && !self->homeserver) + { + g_debug ("(%p) Get homeserver failed, no user id to guess", self); + g_task_return_new_error (task, G_IO_ERROR, G_IO_ERROR_FAILED, + "No user id present in client"); + return; + } + + matrix_start_sync (self, g_steal_pointer (&task)); +} + +const char * +cm_client_get_homeserver_finish (CmClient *self, + GAsyncResult *result, + GError **error) +{ + g_return_val_if_fail (CM_IS_CLIENT (self), NULL); + g_return_val_if_fail (G_IS_TASK (result), NULL); + + return g_task_propagate_pointer (G_TASK (result), error); +} + +static void +verification_load_user_devices_cb (GObject *object, + GAsyncResult *result, + gpointer user_data) +{ + g_autoptr(GTask) task = user_data; + g_autoptr(GPtrArray) users = NULL; + GError *error = NULL; + + users = cm_user_list_load_devices_finish (CM_USER_LIST (object), result, &error); + + if (error) + g_debug ("Load user devices error: %s", error->message); + + if (error) + g_task_return_error (task, error); + else + g_task_return_boolean (task, !users || users->len == 0); +} + +static void +client_key_verification_accept_cb (GObject *obj, + GAsyncResult *result, + gpointer user_data) +{ + CmClient *self; + g_autoptr(GTask) task = user_data; + g_autoptr(JsonObject) object = NULL; + CmEvent *event; + GError *error = NULL; + + g_assert (G_IS_TASK (task)); + + self = g_task_get_source_object (task); + event = g_task_get_task_data (task); + g_assert (CM_IS_CLIENT (self)); + g_assert (CM_IS_EVENT (event)); + + object = g_task_propagate_pointer (G_TASK (result), &error); + g_debug ("(%p) Key verification %p accept %s", self, + event, CM_LOG_SUCCESS (!error)); + + if (error) + { + g_debug ("(%p) Key verification accept error: %s", self, error->message); + g_task_return_error (task, error); + } + else + { + g_autoptr(GPtrArray) users = NULL; + CmUser *user; + + users = g_ptr_array_new_full (1, g_object_unref); + user = cm_event_get_sender (event);; + g_ptr_array_add (users, g_object_ref (user)); + + cm_user_list_load_devices_async (self->user_list, users, + verification_load_user_devices_cb, + g_steal_pointer (&task)); + } +} + +void +cm_client_key_verification_continue_async (CmClient *self, + CmEvent *verification_event, + GCancellable *cancellable, + GAsyncReadyCallback callback, + gpointer user_data) +{ + CmEvent *event, *reply_event; + CmOlmSas *olm_sas = NULL; + g_autofree char *uri = NULL; + JsonObject *root; + GTask *task; + + g_return_if_fail (CM_IS_CLIENT (self)); + g_return_if_fail (!cancellable || G_IS_CANCELLABLE (cancellable)); + g_return_if_fail (CM_IS_EVENT (verification_event)); + + if (!cancellable) + cancellable = self->cancellable; + + task = g_task_new (self, cancellable, callback, user_data); + + event = client_find_key_verification (self, verification_event, FALSE); + g_debug ("(%p) Key verification %p continue", self, event); + + if (event) + olm_sas = g_object_get_data (G_OBJECT (event), "olm-sas"); + + if (!olm_sas) + { + g_debug ("(%p) Key verification %p continue fail, not in progress", self, verification_event); + g_task_return_new_error (task, G_IO_ERROR, G_IO_ERROR_NOT_INITIALIZED, + "Provided key verification request is not in progress"); + return; + } + + g_task_set_task_data (task, g_object_ref (event), g_object_unref); + + reply_event = cm_olm_sas_get_accept_event (olm_sas); + root = cm_event_get_json (reply_event); + uri = g_strdup_printf ("/_matrix/client/r0/sendToDevice/m.key.verification.accept/%s", + cm_event_get_txn_id (reply_event)); + cm_net_send_json_async (self->cm_net, 0, root, uri, SOUP_METHOD_PUT, + NULL, cancellable, + client_key_verification_accept_cb, + g_steal_pointer (&task)); +} + +gboolean +cm_client_key_verification_continue_finish (CmClient *self, + GAsyncResult *result, + GError **error) +{ + g_return_val_if_fail (CM_IS_CLIENT (self), FALSE); + g_return_val_if_fail (G_IS_TASK (result), FALSE); + + return g_task_propagate_boolean (G_TASK (result), error); +} + +static void +client_key_verification_match_cb (GObject *obj, + GAsyncResult *result, + gpointer user_data) +{ + CmClient *self; + g_autoptr(GTask) task = user_data; + g_autoptr(JsonObject) object = NULL; + CmEvent *event; + GError *error = NULL; + + g_assert (G_IS_TASK (task)); + + self = g_task_get_source_object (task); + event = g_task_get_task_data (task); + g_assert (CM_IS_CLIENT (self)); + g_assert (CM_IS_EVENT (event)); + + object = g_task_propagate_pointer (G_TASK (result), &error); + g_debug ("(%p) Key verification %p match %s", self, + event, CM_LOG_SUCCESS (!error)); + + if (error) + { + g_debug ("(%p) Key verification match error: %s", self, error->message); + g_task_return_error (task, error); + } + else + { + g_task_return_boolean (task, TRUE); + } +} + +void +cm_client_key_verification_match_async (CmClient *self, + CmEvent *verification_event, + GCancellable *cancellable, + GAsyncReadyCallback callback, + gpointer user_data) +{ + CmEvent *event, *reply_event; + CmOlmSas *olm_sas = NULL; + g_autofree char *uri = NULL; + JsonObject *root; + GTask *task; + + g_return_if_fail (CM_IS_CLIENT (self)); + g_return_if_fail (!cancellable || G_IS_CANCELLABLE (cancellable)); + g_return_if_fail (CM_IS_EVENT (verification_event)); + + if (!cancellable) + cancellable = self->cancellable; + + task = g_task_new (self, cancellable, callback, user_data); + + event = client_find_key_verification (self, verification_event, FALSE); + g_debug ("(%p) Key verification %p match", self, event); + + if (event) + olm_sas = g_object_get_data (G_OBJECT (event), "olm-sas"); + + if (!olm_sas) + { + g_debug ("(%p) Key verification %p match fail, not in progress", self, verification_event); + g_task_return_new_error (task, G_IO_ERROR, G_IO_ERROR_NOT_INITIALIZED, + "Provided key verification request is not in progress"); + return; + } + + g_task_set_task_data (task, g_object_ref (event), g_object_unref); + + reply_event = cm_olm_sas_get_mac_event (olm_sas); + root = cm_event_get_json (reply_event); + uri = g_strdup_printf ("/_matrix/client/r0/sendToDevice/m.key.verification.mac/%s", + cm_event_get_txn_id (reply_event)); + cm_net_send_json_async (self->cm_net, 0, root, uri, SOUP_METHOD_PUT, + NULL, cancellable, + client_key_verification_match_cb, + g_steal_pointer (&task)); +} + +gboolean +cm_client_key_verification_match_finish (CmClient *self, + GAsyncResult *result, + GError **error) +{ + g_return_val_if_fail (CM_IS_CLIENT (self), FALSE); + g_return_val_if_fail (G_IS_TASK (result), FALSE); + + return g_task_propagate_boolean (G_TASK (result), error); +} + +static void +client_key_verification_cancel_cb (GObject *obj, + GAsyncResult *result, + gpointer user_data) +{ + CmClient *self; + g_autoptr(GTask) task = user_data; + g_autoptr(JsonObject) object = NULL; + CmEvent *event; + GError *error = NULL; + + g_assert (G_IS_TASK (task)); + + self = g_task_get_source_object (task); + event = g_task_get_task_data (task); + g_assert (CM_IS_CLIENT (self)); + g_assert (CM_IS_EVENT (event)); + + object = g_task_propagate_pointer (G_TASK (result), &error); + g_debug ("(%p) Key verification %p cancel %s", self, + event, CM_LOG_SUCCESS (!error)); + + if (error) + { + g_debug ("(%p) Key verification cancel error: %s", self, error->message); + g_task_return_error (task, error); + } + else + { + cm_utils_remove_list_item (self->key_verifications, event); + g_task_return_boolean (task, TRUE); + } +} + +void +cm_client_key_verification_cancel_async (CmClient *self, + CmEvent *verification_event, + GCancellable *cancellable, + GAsyncReadyCallback callback, + gpointer user_data) +{ + CmOlmSas *olm_sas = NULL; + CmEvent *event, *reply_event; + g_autoptr(GTask) task = NULL; + g_autofree char *uri = NULL; + const char *cancel_code; + JsonObject *root; + + g_return_if_fail (CM_IS_CLIENT (self)); + g_return_if_fail (!cancellable || G_IS_CANCELLABLE (cancellable)); + g_return_if_fail (CM_IS_EVENT (verification_event)); + + if (!cancellable) + cancellable = self->cancellable; + + task = g_task_new (self, cancellable, callback, user_data); + event = client_find_key_verification (self, verification_event, FALSE); + g_debug ("(%p) Key verification %p cancel", self, event); + + if (event) + olm_sas = g_object_get_data (G_OBJECT (event), "olm-sas"); + + if (!olm_sas) + { + g_debug ("(%p) Key verification %p cancel fail, not in progress", self, verification_event); + g_task_return_new_error (task, G_IO_ERROR, G_IO_ERROR_NOT_INITIALIZED, + "Provided key verification request is not in progress"); + return; + } + + g_task_set_task_data (task, g_object_ref (event), g_object_unref); + + cancel_code = cm_olm_sas_get_cancel_code (olm_sas); + reply_event = cm_olm_sas_get_cancel_event (olm_sas, cancel_code); + root = cm_event_get_json (reply_event); + + uri = g_strdup_printf ("/_matrix/client/r0/sendToDevice/m.key.verification.cancel/%s", + cm_event_get_txn_id (reply_event)); + cm_net_send_json_async (self->cm_net, 0, root, uri, SOUP_METHOD_PUT, + NULL, cancellable, + client_key_verification_cancel_cb, + g_steal_pointer (&task)); +} + +gboolean +cm_client_key_verification_cancel_finish (CmClient *self, + GAsyncResult *result, + GError **error) +{ + g_return_val_if_fail (CM_IS_CLIENT (self), FALSE); + g_return_val_if_fail (G_IS_TASK (result), FALSE); + + return g_task_propagate_boolean (G_TASK (result), error); +} + +static void +client_verify_homeserver_cb (GObject *obj, + GAsyncResult *result, + gpointer user_data) +{ + CmClient *self; + g_autoptr(GTask) task = user_data; + GError *error = NULL; + + g_assert (G_IS_TASK (task)); + + self = g_task_get_source_object (task); + g_assert (CM_IS_CLIENT (self)); + + client_set_login_state (self, FALSE, FALSE); + self->homeserver_verified = cm_utils_verify_homeserver_finish (result, &error); + g_object_set_data (G_OBJECT (task), "action", "verify-homeserver"); + + g_debug ("(%p) Verify home server %s", self, CM_LOG_SUCCESS (!error)); + + /* Since GTask can't have timeout, We cancel the cancellable to fake timeout */ + if (g_error_matches (error, G_IO_ERROR, G_IO_ERROR_TIMED_OUT)) + { + g_clear_object (&self->cancellable); + self->cancellable = g_cancellable_new (); + } + + g_clear_object (&self->gaddress); + self->gaddress = g_object_steal_data (G_OBJECT (result), "address"); + + self->has_tried_connecting = TRUE; + + if (g_task_get_source_tag (task) != cm_client_get_homeserver_async && + handle_matrix_glitches (self, error)) + return; + + if (self->homeserver_verified) + { + if (g_task_get_source_tag (task) == cm_client_get_homeserver_async) + g_task_return_pointer (task, self->homeserver, NULL); + else + matrix_start_sync (self, g_steal_pointer (&task)); + } + else + { + self->sync_failed = TRUE; + + g_signal_emit (self, signals[STATUS_CHANGED], 0); + g_task_return_new_error (task, G_IO_ERROR, G_IO_ERROR_FAILED, + "Failed to verify homeserver"); + if (self->callback) + self->callback (self->cb_data, self, NULL, NULL, error); + } +} + +static void +client_password_login_cb (GObject *obj, + GAsyncResult *result, + gpointer user_data) +{ + g_autoptr(GTask) task = user_data; + g_autoptr(GError) error = NULL; + g_autoptr(JsonObject) root = NULL; + JsonObject *object = NULL; + const char *value; + CmClient *self; + + g_assert (G_IS_TASK (task)); + + self = g_task_get_source_object (task); + g_assert (CM_IS_CLIENT (self)); + + root = g_task_propagate_pointer (G_TASK (result), &error); + g_debug ("(%p) Login with password %s", self, CM_LOG_SUCCESS (!error)); + + if (error) + { + g_autoptr(GString) str = NULL; + self->sync_failed = TRUE; + + str = g_string_new (NULL); + g_debug ("(%p) Login %s, username: %s, error: %s", self, CM_LOG_SUCCESS (FALSE), + cm_utils_anonymize (str, cm_account_get_login_id (self->cm_account)), + error->message); + if (error->code == CM_ERROR_FORBIDDEN) + error->code = CM_ERROR_BAD_PASSWORD; + + client_set_login_state (self, FALSE, FALSE); + + if (!handle_matrix_glitches (self, error) && self->callback) + self->callback (self->cb_data, self, NULL, NULL, error); + + g_task_return_error (task, g_steal_pointer (&error)); + return; + } + + /* https://matrix.org/docs/spec/client_server/r0.6.1#post-matrix-client-r0-login */ + value = cm_utils_json_object_get_string (root, "user_id"); + self->is_logging_in = FALSE; + cm_client_set_user_id (self, value); + self->is_logging_in = TRUE; + + value = cm_utils_json_object_get_string (root, "access_token"); + cm_net_set_access_token (self->cm_net, value); + + value = cm_utils_json_object_get_string (root, "device_id"); + cm_set_string_value (&self->device_id, value); + + object = cm_utils_json_object_get_object (root, "well_known"); + object = cm_utils_json_object_get_object (object, "m.homeserver"); + value = cm_utils_json_object_get_string (object, "base_url"); + g_clear_object (&self->cm_enc); + self->cm_enc = cm_enc_new (self->cm_db, NULL, NULL); + cm_enc_set_details (self->cm_enc, + cm_client_get_user_id (self), + cm_client_get_device_id (self)); + cm_set_string_value (&self->key, cm_enc_get_device_keys_json (self->cm_enc)); + self->is_logging_in = FALSE; + cm_client_set_homeserver (self, value); + client_set_login_state (self, FALSE, !!cm_net_get_access_token (self->cm_net)); + client_mark_for_save (self, TRUE, TRUE); + cm_client_save (self); + + matrix_start_sync (self, g_steal_pointer (&task)); +} + +static gboolean +room_matches_id (CmRoom *room, + const char *room_id) +{ + const char *item_room_id; + + g_assert (CM_IS_ROOM (room)); + + item_room_id = cm_room_get_id (room); + + if (g_strcmp0 (room_id, item_room_id) == 0) + return TRUE; + + return FALSE; +} + +static CmRoom * +client_find_room (CmClient *self, + const char *room_id, + GListStore *rooms) +{ + guint n_items; + + g_assert (CM_IS_CLIENT (self)); + g_assert (room_id && *room_id); + + n_items = g_list_model_get_n_items (G_LIST_MODEL (rooms)); + + for (guint i = 0; i < n_items; i++) + { + g_autoptr(CmRoom) room = NULL; + + room = g_list_model_get_item (G_LIST_MODEL (rooms), i); + if (room_matches_id (room, room_id)) + return room; + } + + return NULL; +} + +static void +room_loaded_cb (GObject *obj, + GAsyncResult *result, + gpointer user_data) +{ + g_autoptr(CmClient) self = user_data; + g_autoptr(GError) error = NULL; + CmRoom *room = CM_ROOM (obj); + + cm_room_load_finish (room, result, &error); +} + +static gboolean +handle_one_time_keys (CmClient *self, + JsonObject *object) +{ + size_t count, limit; + + g_assert (CM_IS_CLIENT (self)); + + if (!object) + return FALSE; + + count = cm_utils_json_object_get_int (object, "signed_curve25519"); + limit = cm_enc_max_one_time_keys (self->cm_enc) / 2; + + /* If we don't have enough onetime keys add some */ + if (count < limit) + { + /* First, get all already created keys */ + if (!self->key) + self->key = cm_enc_get_one_time_keys_json (self->cm_enc); + + /* If we got no keys, create new ones and get them */ + if (!self->key) + { + g_debug ("(%p) Generating %" G_GSIZE_FORMAT " onetime keys", self, limit - count); + cm_enc_create_one_time_keys (self->cm_enc, limit - count); + self->key = cm_enc_get_one_time_keys_json (self->cm_enc); + } + matrix_upload_key (self); + + return TRUE; + } + + return FALSE; +} + +static void +upload_key_cb (GObject *obj, + GAsyncResult *result, + gpointer user_data) +{ + g_autoptr(CmClient) self = user_data; + g_autoptr(JsonObject) root = NULL; + g_autoptr(GError) error = NULL; + JsonObject *object = NULL; + g_autofree char *json_str = NULL; + + g_assert (CM_IS_CLIENT (self)); + g_assert (G_IS_TASK (result)); + + root = g_task_propagate_pointer (G_TASK (result), &error); + g_debug ("(%p) Upload key %s", self, CM_LOG_SUCCESS (!error)); + + if (error) + { + self->sync_failed = TRUE; + handle_matrix_glitches (self, error); + g_debug ("Error uploading key: %s", error->message); + return; + } + + json_str = cm_utils_json_object_to_string (root, FALSE); + cm_enc_publish_one_time_keys (self->cm_enc); + + object = cm_utils_json_object_get_object (root, "one_time_key_counts"); + + if (!handle_one_time_keys (self, object)) + matrix_start_sync (self, NULL); +} + +static void +matrix_upload_key (CmClient *self) +{ + char *key; + + g_assert (CM_IS_CLIENT (self)); + g_assert (self->key); + + key = g_steal_pointer (&self->key); + + g_debug ("(%p) Upload key", self); + cm_net_send_data_async (self->cm_net, 2, key, strlen (key), + "/_matrix/client/r0/keys/upload", SOUP_METHOD_POST, + NULL, self->cancellable, upload_key_cb, + g_object_ref (self)); +} + +/* Get the list of chat rooms marked as direct */ +static void +parse_direct_rooms (CmClient *self, + JsonObject *root) +{ + g_autoptr(GList) user_ids = NULL; + + g_assert (CM_IS_CLIENT (self)); + + if (!root) + return; + + user_ids = json_object_get_members (root); + + for (GList *user_id = user_ids; user_id; user_id = user_id->next) + { + JsonArray *array; + guint length = 0; + + array = cm_utils_json_object_get_array (root, user_id->data); + if (array) + length = json_array_get_length (array); + + for (guint i = 0; i < length; i++) + { + CmRoom *room; + const char *room_id; + + room_id = json_array_get_string_element (array, i); + + room = g_hash_table_lookup (self->direct_rooms, room_id); + + if (!room) + room = client_find_room (self, room_id, self->joined_rooms); + + if (room) + { + cm_room_set_generated_name (room, user_id->data); + cm_room_set_is_direct (room, TRUE); + + continue; + } + + room = cm_room_new (room_id); + cm_room_set_status (room, CM_STATUS_JOIN); + cm_room_set_client (room, self); + cm_room_set_is_direct (room, TRUE); + cm_room_set_generated_name (room, user_id->data); + + /* This eats the ref on the new room */ + g_hash_table_insert (self->direct_rooms, g_strdup (room_id), room); + } + } + +} + +static void +handle_account_data (CmClient *self, + JsonObject *root) +{ + JsonObject *object, *content; + JsonArray *array; + guint length = 0; + + g_assert (CM_IS_CLIENT (self)); + + if (!root) + return; + + array = cm_utils_json_object_get_array (root, "events"); + if (array) + length = json_array_get_length (array); + + for (guint i = 0; i < length; i++) + { + const char *type; + + object = json_array_get_object_element (array, i); + type = cm_utils_json_object_get_string (object, "type"); + + if (g_strcmp0 (type, "m.direct") != 0) + continue; + + content = cm_utils_json_object_get_object (object, "content"); + + if (!content) + break; + + parse_direct_rooms (self, content); + } +} + +static void +verification_send_key_cb (GObject *object, + GAsyncResult *result, + gpointer user_data) +{ + g_autoptr(CmClient) self = user_data; +} + +static void +handle_to_device (CmClient *self, + JsonObject *root) +{ + JsonObject *object; + JsonArray *array; + guint length = 0; + + g_assert (CM_IS_CLIENT (self)); + + if (!root) + return; + + array = cm_utils_json_object_get_array (root, "events"); + if (array) + length = json_array_get_length (array); + + for (guint i = 0; i < length; i++) + { + g_autoptr(GPtrArray) events = NULL; + g_autoptr(CmEvent) event = NULL; + GRefString *user_id; + CmEventType type; + + object = json_array_get_object_element (array, i); + + event = cm_event_new_from_json (object, NULL); + user_id = cm_event_get_sender_id (event); + if (user_id) + { + CmUser *user; + + user = cm_user_list_find_user (self->user_list, user_id, TRUE); + cm_event_set_sender (event, user); + } + + type = cm_event_get_m_type (event); + events = g_ptr_array_new (); + + if (type == CM_M_ROOM_ENCRYPTED) + { + cm_enc_handle_room_encrypted (self->cm_enc, object); + } + else if (type >= CM_M_KEY_VERIFICATION_ACCEPT && + type <= CM_M_KEY_VERIFICATION_START) + { + CmEvent *key_event; + CmOlmSas *olm_sas; + + /* Don't force add the event now. If the event has to be cancelled + * for any reason cancel it now and don't even report to the user */ + key_event = client_find_key_verification (self, event, FALSE); + olm_sas = g_object_get_data (G_OBJECT (key_event ?: event), "olm-sas"); + + if (cm_olm_sas_get_cancel_code (olm_sas)) + { + cm_client_key_verification_cancel_async (self, event, NULL, NULL, NULL); + return; + } + + /* Now add the event if not already done so */ + key_event = client_find_key_verification (self, event, TRUE); + + if (key_event && type == CM_M_KEY_VERIFICATION_KEY) + { + /* The start/request event (which is `key_event`) shall not be the same as key event */ + if (key_event != event) + { + g_autofree char *uri = NULL; + CmEvent *reply_event; + JsonObject *obj; + + olm_sas = g_object_get_data (G_OBJECT (key_event), "olm-sas"); + reply_event = cm_olm_sas_get_key_event (olm_sas); + + obj = cm_event_get_json (reply_event); + uri = g_strdup_printf ("/_matrix/client/r0/sendToDevice/m.key.verification.key/%s", + cm_event_get_txn_id (reply_event)); + cm_net_send_json_async (self->cm_net, 0, obj, uri, SOUP_METHOD_PUT, + NULL, NULL, + verification_send_key_cb, + g_object_ref (self)); + } + } + + if (key_event && type == CM_M_KEY_VERIFICATION_MAC) { + CmDevice *device; + + device = cm_olm_sas_get_device (olm_sas); + cm_db_update_device (self->cm_db, self, cm_event_get_sender (key_event), device); + cm_client_key_verification_done_async (self, key_event, NULL, NULL, NULL); + } + } + } +} + +static void +handle_room_join (CmClient *self, + JsonObject *root) +{ + g_autoptr(GList) joined_room_ids = NULL; + + g_assert (CM_IS_CLIENT (self)); + + if (!root) + return; + + joined_room_ids = json_object_get_members (root); + + for (GList *room_id = joined_room_ids; room_id; room_id = room_id->next) + { + g_autoptr(GPtrArray) events = NULL; + CmRoom *room; + JsonObject *room_data; + + room = client_find_room (self, room_id->data, self->joined_rooms); + room_data = cm_utils_json_object_get_object (root, room_id->data); + + if (!room) + { + room = g_hash_table_lookup (self->direct_rooms, room_id->data); + + if (room) + { + g_list_store_append (self->joined_rooms, room); + g_hash_table_remove (self->direct_rooms, room_id->data); + } + else + { + room = cm_room_new (room_id->data); + cm_room_set_status (room, CM_STATUS_JOIN); + cm_room_set_client (room, self); + g_list_store_append (self->joined_rooms, room); + g_object_unref (room); + } + } + + cm_room_set_status (room, CM_STATUS_JOIN); + events = cm_room_set_data (room, room_data); + cm_db_add_room_events (self->cm_db, room, events, FALSE); + + if (self->callback) + self->callback (self->cb_data, self, room, events, NULL); + + cm_utils_remove_list_item (self->invited_rooms, room); + + if (cm_room_get_replacement_room (room)) + cm_utils_remove_list_item (self->joined_rooms, room); + } +} + +static void +handle_room_leave (CmClient *self, + JsonObject *root) +{ + g_autoptr(GList) left_room_ids = NULL; + + g_assert (CM_IS_CLIENT (self)); + + if (!root) + return; + + left_room_ids = json_object_get_members (root); + + for (GList *room_id = left_room_ids; room_id; room_id = room_id->next) + { + g_autoptr(GPtrArray) events = NULL; + CmRoom *room; + JsonObject *room_data; + + room = client_find_room (self, room_id->data, self->joined_rooms); + room_data = cm_utils_json_object_get_object (root, room_id->data); + + if (!room) + continue; + + events = cm_room_set_data (room, room_data); + cm_room_set_status (room, CM_STATUS_LEAVE); + cm_db_add_room_events (self->cm_db, room, events, FALSE); + + if (self->callback) + self->callback (self->cb_data, self, room, events, NULL); + + cm_utils_remove_list_item (self->joined_rooms, room); + } +} + +static void +handle_room_invite (CmClient *self, + JsonObject *root) +{ + g_autoptr(GList) invited_room_ids = NULL; + + g_assert (CM_IS_CLIENT (self)); + + if (!root) + return; + + invited_room_ids = json_object_get_members (root); + + for (GList *room_id = invited_room_ids; room_id; room_id = room_id->next) + { + g_autoptr(GPtrArray) events = NULL; + CmRoom *room; + JsonObject *room_data; + + room = client_find_room (self, room_id->data, self->invited_rooms); + room_data = cm_utils_json_object_get_object (root, room_id->data); + + if (!room) + { + room = cm_room_new (room_id->data); + cm_room_set_status (room, CM_STATUS_INVITE); + cm_room_set_client (room, self); + g_list_store_append (self->joined_rooms, room); + g_object_unref (room); + } + + events = cm_room_set_data (room, room_data); + + if (events && events->len) + cm_db_add_room_events (self->cm_db, room, events, FALSE); + + if (self->callback) + self->callback (self->cb_data, self, room, events, NULL); + } +} + +static void +handle_device_list (CmClient *self, + JsonObject *root) +{ + g_autoptr(GPtrArray) users = NULL; + guint n_items; + + if (!root) + return; + + users = g_ptr_array_new_with_free_func (g_object_unref); + n_items = g_list_model_get_n_items (G_LIST_MODEL (self->joined_rooms)); + cm_user_list_device_changed (self->user_list, root, users); + + for (guint i = 0; i < n_items; i++) + { + g_autoptr(CmRoom) room = NULL; + + room = g_list_model_get_item (G_LIST_MODEL (self->joined_rooms), i); + cm_room_user_changed (room, users); + } + + cm_db_mark_user_device_change (self->cm_db, self, users, TRUE, TRUE); +} + +static void +handle_red_pill (CmClient *self, + JsonObject *root) +{ + JsonObject *object; + + g_assert (CM_IS_CLIENT (self)); + + if (!root) + return; + + handle_account_data (self, cm_utils_json_object_get_object (root, "account_data")); + handle_device_list (self, cm_utils_json_object_get_object (root, "device_lists")); + /* to_device should be handled first as it might contain keys to be used + * to decrypt following events */ + handle_to_device (self, cm_utils_json_object_get_object (root, "to_device")); + + object = cm_utils_json_object_get_object (root, "rooms"); + handle_room_join (self, cm_utils_json_object_get_object (object, "join")); + handle_room_leave (self, cm_utils_json_object_get_object (object, "leave")); + handle_room_invite (self, cm_utils_json_object_get_object (object, "invite")); +} + +static void +matrix_take_red_pill_cb (GObject *obj, + GAsyncResult *result, + gpointer user_data) +{ + g_autoptr(CmClient) self = user_data; + g_autoptr(JsonObject) root = NULL; + g_autoptr(GError) error = NULL; + JsonObject *object = NULL; + + g_assert (CM_IS_CLIENT (self)); + g_assert (G_IS_TASK (result)); + + root = g_task_propagate_pointer (G_TASK (result), &error); + + if (error) + { + self->sync_failed = TRUE; + client_set_login_state (self, FALSE, FALSE); + if (!handle_matrix_glitches (self, error)) + self->callback (self->cb_data, self, NULL, NULL, error); + else if (!g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CANCELLED)) + g_debug ("Error syncing with time %s: %s", self->next_batch, error->message); + return; + } + + client_set_login_state (self, FALSE, TRUE); + + g_free (self->next_batch); + self->next_batch = g_strdup (cm_utils_json_object_get_string (root, "next_batch")); + client_mark_for_save (self, TRUE, -1); + cm_client_save (self); + + { + g_autofree char *json_str = NULL; + + json_str = cm_utils_json_object_to_string (root, FALSE); + handle_red_pill (self, root); + + /* update variables only after the result is locally parsed */ + if (self->sync_failed || !self->is_sync) + { + self->sync_failed = FALSE; + self->is_sync = TRUE; + g_signal_emit (self, signals[STATUS_CHANGED], 0); + } + } + + object = cm_utils_json_object_get_object (root, "device_one_time_keys_count"); + if (handle_one_time_keys (self, object)) + return; + + /* Repeat */ + matrix_start_sync (self, NULL); +} + +static void +matrix_take_red_pill (CmClient *self, + gpointer tsk) +{ + g_autoptr(GTask) task = tsk; + GCancellable *cancellable; + GHashTable *query; + + g_assert (CM_IS_CLIENT (self)); + + query = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, g_free); + if (self->login_success) + g_hash_table_insert (query, g_strdup ("timeout"), g_strdup_printf ("%u", SYNC_TIMEOUT)); + else + g_hash_table_insert (query, g_strdup ("timeout"), g_strdup_printf ("%u", SYNC_TIMEOUT / 1000)); + + if (self->filter_id) + g_hash_table_insert (query, g_strdup ("filter"), g_strdup (self->filter_id)); + + if (self->next_batch) + g_hash_table_insert (query, g_strdup ("since"), g_strdup (self->next_batch)); + + cancellable = g_task_get_cancellable (task); + cm_net_send_json_async (self->cm_net, 2, NULL, + "/_matrix/client/r0/sync", SOUP_METHOD_GET, + query, cancellable, matrix_take_red_pill_cb, + g_object_ref (self)); +} + +static void +client_get_homeserver_cb (GObject *obj, + GAsyncResult *result, + gpointer user_data) +{ + CmClient *self; + g_autoptr(GTask) task = user_data; + g_autofree char *homeserver = NULL; + GError *error = NULL; + + g_assert (G_IS_TASK (task)); + + self = g_task_get_source_object (task); + g_assert (CM_IS_CLIENT (self)); + + homeserver = cm_utils_get_homeserver_finish (result, &error); + g_object_set_data (G_OBJECT (task), "action", "get-homeserver"); + g_debug ("(%p) Get home server %s", self, CM_LOG_SUCCESS (!error)); + + client_set_login_state (self, FALSE, FALSE); + + if (error) + { + self->sync_failed = TRUE; + + g_debug ("(%p) Get home server error: %s", self, error->message); + if (g_task_get_source_tag (task) != cm_client_get_homeserver_async) + handle_matrix_glitches (self, error); + g_task_return_error (task, error); + + return; + } + + g_debug ("(%p) Got home server: %s", self, homeserver); + if (!homeserver) + { + self->sync_failed = TRUE; + g_task_return_new_error (task, CM_ERROR, CM_ERROR_NO_HOME_SERVER, + "Couldn't fetch homeserver"); + if (self->callback) + self->callback (self->cb_data, self, NULL, NULL, error); + + return; + } + + cm_client_set_homeserver (self, homeserver); + + if (!self->homeserver) + { + self->sync_failed = TRUE; + g_debug ("(%p) Get home server: '%s' is invalid uri", self, homeserver); + g_task_return_new_error (task, CM_ERROR, CM_ERROR_BAD_HOME_SERVER, + "'%s' is not a valid URI", homeserver); + return; + } + + matrix_start_sync (self, g_steal_pointer (&task)); +} + +gboolean +cm_client_get_logging_in (CmClient *self) +{ + g_return_val_if_fail (CM_IS_CLIENT (self), FALSE); + + return self->is_logging_in; +} + +gboolean +cm_client_get_logged_in (CmClient *self) +{ + g_return_val_if_fail (CM_IS_CLIENT (self), FALSE); + + return self->login_success; +} + +static void +get_joined_rooms_cb (GObject *obj, + GAsyncResult *result, + gpointer user_data) +{ + g_autoptr(CmClient) self = user_data; + g_autoptr(JsonObject) root = NULL; + g_autoptr(GError) error = NULL; + + g_assert (CM_IS_CLIENT (self)); + g_assert (G_IS_TASK (result)); + + self->room_list_loading = FALSE; + root = g_task_propagate_pointer (G_TASK (result), &error); + + g_debug ("(%p) Get joined rooms %s", self, CM_LOG_SUCCESS (!error)); + + if (g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CANCELLED)) + return; + + if (handle_matrix_glitches (self, error)) + return; + + if (!error) { + JsonArray *array; + guint length = 0; + + array = cm_utils_json_object_get_array (root, "joined_rooms"); + + if (array) + length = json_array_get_length (array); + + g_debug ("(%p) Get joined rooms, count: %u", self, length); + + for (guint i = 0; i < length; i++) + { + const char *room_id; + CmRoom *room; + + room_id = json_array_get_string_element (array, i); + room = client_find_room (self, room_id, self->joined_rooms); + + if (!room) + { + room = g_hash_table_lookup (self->direct_rooms, room_id); + if (room) + { + g_list_store_append (self->joined_rooms, room); + g_hash_table_remove (self->direct_rooms, room_id); + } + } + + if (!room) + { + room = cm_room_new (room_id); + cm_room_set_status (room, CM_STATUS_JOIN); + cm_room_set_client (room, self); + g_list_store_append (self->joined_rooms, room); + g_object_unref (room); + } + + cm_room_load_async (room, self->cancellable, + room_loaded_cb, + g_object_ref (self)); + } + + self->room_list_loaded = TRUE; + matrix_start_sync (self, NULL); + } +} + +static void +get_direct_rooms_cb (GObject *obj, + GAsyncResult *result, + gpointer user_data) +{ + g_autoptr(CmClient) self = user_data; + g_autoptr(JsonObject) root = NULL; + g_autoptr(GError) error = NULL; + + g_assert (CM_IS_CLIENT (self)); + g_assert (G_IS_TASK (result)); + + self->direct_room_list_loading = FALSE; + root = g_task_propagate_pointer (G_TASK (result), &error); + g_debug ("(%p) Get direct rooms %s", self, CM_LOG_SUCCESS (!error)); + + if (root) + parse_direct_rooms (self, root); + + if (root || !error) + self->direct_room_list_loaded = TRUE; + + matrix_start_sync (self, NULL); +} + +static void +matrix_start_sync (CmClient *self, + gpointer tsk) +{ + g_autoptr(GTask) task = tsk; + GCancellable *cancellable = NULL; + + g_assert (CM_IS_CLIENT (self)); + + self->sync_failed = FALSE; + + if (!task) + { + task = g_task_new (self, self->cancellable, NULL, NULL); + cancellable = self->cancellable; + } + + cancellable = g_task_get_cancellable (task); + + if (self->db_loading || self->room_list_loading || self->direct_room_list_loading) + return; + + if (!self->db_loaded) + { + self->db_loading = TRUE; + g_debug ("(%p) Load db", self); + cm_db_load_client_async (self->cm_db, self, + cm_client_get_device_id (self), + db_load_client_cb, + g_steal_pointer (&task)); + } + else if (!self->homeserver) + { + const char *user_id; + + user_id = cm_user_get_id (CM_USER (self->cm_account)); + + if (!user_id) + user_id = cm_account_get_login_id (self->cm_account); + + if (!cm_utils_home_server_valid (self->homeserver) && + !cm_utils_user_name_valid (user_id)) + { + g_warning ("(%p) Error: No Homeserver provided", self); + + g_task_return_new_error (task, CM_ERROR, CM_ERROR_NO_HOME_SERVER, + "No Homeserver provided"); + return; + } + + client_set_login_state (self, TRUE, FALSE); + g_debug ("(%p) Getting homeserver", self); + cm_utils_get_homeserver_async (user_id, 30, cancellable, + client_get_homeserver_cb, + g_steal_pointer (&task)); + } + else if (!self->homeserver_verified) + { + client_set_login_state (self, TRUE, FALSE); + g_debug ("(%p) Verify homeserver '%s'", self, self->homeserver); + cm_utils_verify_homeserver_async (self->homeserver, 30, cancellable, + client_verify_homeserver_cb, + g_steal_pointer (&task)); + } + else if (!self->password && !cm_net_get_access_token (self->cm_net)) + { + GError *error; + + g_warning ("(%p) No password provided, nor access token", self); + + error = g_error_new (CM_ERROR, CM_ERROR_BAD_PASSWORD, "No Password provided"); + + if (self->callback) + self->callback (self->cb_data, self, NULL, NULL, error); + + g_task_return_error (task, error); + } + else if (!cm_net_get_access_token (self->cm_net) || !self->cm_enc) + { + g_assert (self->cm_db); + cm_net_set_access_token (self->cm_net, NULL); + client_set_login_state (self, TRUE, FALSE); + g_debug ("(%p) Login with password", self); + client_login_with_password_async (self, cancellable, + client_password_login_cb, + g_steal_pointer (&task)); + } + else if (self->db_migrated && !self->direct_room_list_loaded) + { + g_autofree char *uri = NULL; + + self->direct_room_list_loading = TRUE; + + uri = g_strconcat ("/_matrix/client/r0/user/", + cm_user_get_id (CM_USER (self->cm_account)), + "/account_data/m.direct", NULL); + g_debug ("(%p) Get direct rooms", self); + cm_net_send_json_async (self->cm_net, 0, NULL, uri, SOUP_METHOD_GET, + NULL, NULL, get_direct_rooms_cb, + g_object_ref (self)); + } + else if (self->db_migrated && !self->room_list_loaded) + { + self->room_list_loading = TRUE; + g_debug ("(%p) Get joined rooms", self); + cm_net_send_json_async (self->cm_net, 0, NULL, + "/_matrix/client/r0/joined_rooms", SOUP_METHOD_GET, + NULL, NULL, get_joined_rooms_cb, + g_object_ref (self)); + + } + else if (!self->filter_id) + { + g_assert (self->cm_enc); + g_assert (self->callback); + client_set_login_state (self, TRUE, FALSE); + matrix_upload_filter (self, g_steal_pointer (&task)); + } + else + { + g_assert (self->cm_db); + g_assert (self->callback); + matrix_take_red_pill (self, g_steal_pointer (&task)); + } +} + +/** + * cm_client_can_connect: + * @self: A #CmClient + * + * Check if @self can be connected to homeserver with current + * network state. This function is a bit dumb: returning + * %TRUE shall not ensure that the @self is connectable. + * But if %FALSE is returned, @self shall not be + * able to connect. + * + * Returns: %TRUE if heuristics says @self can be connected. + * %FALSE otherwise. + */ +gboolean +cm_client_can_connect (CmClient *self) +{ + GNetworkMonitor *nm; + GInetAddress *inet; + + g_return_val_if_fail (CM_IS_CLIENT (self), FALSE); + + /* If never tried, assume we can connect */ + if (!self->has_tried_connecting) + return TRUE; + + nm = g_network_monitor_get_default (); + + if (!self->gaddress || !G_IS_INET_SOCKET_ADDRESS (self->gaddress)) + goto end; + + inet = g_inet_socket_address_get_address ((GInetSocketAddress *)self->gaddress); + + if (g_inet_address_get_is_loopback (inet) || + g_inet_address_get_is_site_local (inet)) + return g_network_monitor_can_reach (nm, G_SOCKET_CONNECTABLE (self->gaddress), NULL, NULL); + + end: + /* Distributions may advertise to have full network support event + * when connected only to local network, so this isn't always right */ + return g_network_monitor_get_connectivity (nm) == G_NETWORK_CONNECTIVITY_FULL; +} + +/** + * cm_client_start_sync: + * @self: A #CmClient + * + * Start sync with server. If @self is already + * in sync or in the process to do so, this method + * simply returns. + */ +void +cm_client_start_sync (CmClient *self) +{ + g_return_if_fail (CM_IS_CLIENT (self)); + g_return_if_fail (self->callback); + + if (self->is_sync || self->is_logging_in) + return; + + g_debug ("(%p) Start sync", self); + g_clear_handle_id (&self->resync_id, g_source_remove); + matrix_start_sync (self, NULL); +} + +/** + * cm_client_is_sync: + * @self: A #CmClient + * + * Check whether the client @self is in + * sync with the server or not. + * + * Returns: %TRUE if in sync with server. + * %FALSE otherwise. + */ +gboolean +cm_client_is_sync (CmClient *self) +{ + g_return_val_if_fail (CM_IS_CLIENT (self), FALSE); + + return cm_net_get_access_token (self->cm_net) && + self->login_success && + self->is_sync && !self->sync_failed; +} + +/** + * cm_client_stop_sync: + * @self: A #CmClient + * + * Stop sync with server. + */ +void +cm_client_stop_sync (CmClient *self) +{ + g_return_if_fail (CM_IS_CLIENT (self)); + + if (self->cancellable) + g_cancellable_cancel (self->cancellable); + + self->is_sync = FALSE; + self->sync_failed = FALSE; + self->is_logging_in = FALSE; + self->login_success = FALSE; + + g_clear_handle_id (&self->resync_id, g_source_remove); + g_clear_object (&self->cancellable); + self->cancellable = g_cancellable_new (); + g_debug ("(%p) Stop sync", self); + + g_signal_emit (self, signals[STATUS_CHANGED], 0); +} + +/** + * cm_client_get_joined_rooms: + * @self: A #CmClient + * + * Get the list of joined rooms with + * #CmRoom as the members. + * + * Returns: (transfer none): A #GListModel + */ +GListModel * +cm_client_get_joined_rooms (CmClient *self) +{ + g_return_val_if_fail (CM_IS_CLIENT (self), NULL); + + return G_LIST_MODEL (self->joined_rooms); +} + +/** + * cm_client_get_invited_rooms: + * @self: A #CmClient + * + * Get the list of invited rooms with + * #CmRoom as the members. + * + * Returns: (transfer none): A #GListModel + */ +GListModel * +cm_client_get_invited_rooms (CmClient *self) +{ + g_return_val_if_fail (CM_IS_CLIENT (self), NULL); + + return G_LIST_MODEL (self->invited_rooms); +} + +GListModel * +cm_client_get_key_verifications (CmClient *self) +{ + g_return_val_if_fail (CM_IS_CLIENT (self), NULL); + + return G_LIST_MODEL (self->key_verifications); +} + +static void +get_file_cb (GObject *object, + GAsyncResult *result, + gpointer user_data) +{ + CmClient *self; + g_autoptr(GTask) task = user_data; + GInputStream *istream = NULL; + GError *error = NULL; + + g_assert (G_IS_TASK (task)); + + self = g_task_get_source_object (task); + g_assert (CM_IS_CLIENT (self)); + + istream = cm_net_get_file_finish (self->cm_net, result, &error); + + if (error) + g_task_return_error (task, error); + else + g_task_return_pointer (task, istream, g_object_unref); +} + +static void +find_file_enc_cb (GObject *object, + GAsyncResult *result, + gpointer user_data) +{ + CmClient *self; + g_autoptr(GTask) task = user_data; + CmEncFileInfo *file_info; + GCancellable *cancellable; + char *uri; + + g_assert (G_IS_TASK (task)); + + self = g_task_get_source_object (task); + g_assert (CM_IS_CLIENT (self)); + + file_info = cm_enc_find_file_enc_finish (self->cm_enc, result, NULL); + + cancellable = g_task_get_cancellable (task); + uri = g_task_get_task_data (task); + + cm_net_get_file_async (self->cm_net, + uri, file_info, cancellable, + get_file_cb, + g_steal_pointer (&task)); +} + +void +cm_client_get_file_async (CmClient *self, + const char *uri, + GCancellable *cancellable, + GFileProgressCallback progress_callback, + gpointer progress_user_data, + GAsyncReadyCallback callback, + gpointer user_data) +{ + GTask *task; + + g_return_if_fail (CM_IS_CLIENT (self)); + g_return_if_fail (uri && *uri); + + task = g_task_new (self, cancellable, callback, user_data); + g_task_set_task_data (task, g_strdup (uri), g_free); + + cm_enc_find_file_enc_async (self->cm_enc, uri, + find_file_enc_cb, task); +} + +GInputStream * +cm_client_get_file_finish (CmClient *self, + GAsyncResult *result, + GError **error) +{ + g_return_val_if_fail (CM_IS_CLIENT (self), FALSE); + g_return_val_if_fail (G_IS_TASK (result), FALSE); + g_return_val_if_fail (!error || !*error, FALSE); + + return g_task_propagate_pointer (G_TASK (result), error); +} diff --git a/subprojects/libcmatrix/src/cm-client.h b/subprojects/libcmatrix/src/cm-client.h new file mode 100644 index 0000000000000000000000000000000000000000..c8cd55606f94f95fd14937f59ea826005bde9889 --- /dev/null +++ b/subprojects/libcmatrix/src/cm-client.h @@ -0,0 +1,113 @@ +/* -*- mode: c; c-basic-offset: 2; indent-tabs-mode: nil; -*- */ +/* + * Copyright (C) 2022 Purism SPC + * + * SPDX-License-Identifier: LGPL-2.1-or-later + */ + +#pragma once + +#if !defined(_CMATRIX_TAKEN) && !defined(CMATRIX_COMPILATION) +# error "Only <cmatrix.h> can be included directly." +#endif + +#include <glib-object.h> +#include <gio/gio.h> + +G_BEGIN_DECLS + +#include "cm-enums.h" +#include "cm-types.h" +#include "users/cm-account.h" + +#define CM_TYPE_CLIENT (cm_client_get_type ()) +G_DECLARE_FINAL_TYPE (CmClient, cm_client, CM, CLIENT, GObject) + +typedef void (*CmCallback) (gpointer object, + CmClient *self, + CmRoom *room, + GPtrArray *events, + GError *err); + +CmClient *cm_client_new (void); +CmAccount *cm_client_get_account (CmClient *self); +void cm_client_set_enabled (CmClient *self, + gboolean enable); +gboolean cm_client_get_enabled (CmClient *self); +void cm_client_set_sync_callback (CmClient *self, + CmCallback callback, + gpointer callback_data, + GDestroyNotify callback_data_destroy); +gboolean cm_client_set_user_id (CmClient *self, + const char *matrix_user_id); +GRefString *cm_client_get_user_id (CmClient *self); +gboolean cm_client_set_homeserver (CmClient *self, + const char *homeserver); +const char *cm_client_get_homeserver (CmClient *self); +void cm_client_set_password (CmClient *self, + const char *password); +const char *cm_client_get_password (CmClient *self); +void cm_client_set_access_token (CmClient *self, + const char *access_token); +const char *cm_client_get_access_token (CmClient *self); +void cm_client_set_device_id (CmClient *self, + const char *device_id); +const char *cm_client_get_device_id (CmClient *self); +void cm_client_set_device_name (CmClient *self, + const char *device_name); +const char *cm_client_get_device_name (CmClient *self); +void cm_client_set_pickle_key (CmClient *self, + const char *pickle_key); +const char *cm_client_get_pickle_key (CmClient *self); +const char *cm_client_get_ed25519_key (CmClient *self); + +void cm_client_join_room_by_id_async (CmClient *self, + const char *room_id, + GCancellable *cancellable, + GAsyncReadyCallback callback, + gpointer user_data); +gboolean cm_client_join_room_by_id_finish (CmClient *self, + GAsyncResult *result, + GError **error); +void cm_client_get_homeserver_async (CmClient *self, + GCancellable *cancellable, + GAsyncReadyCallback callback, + gpointer user_data); +const char *cm_client_get_homeserver_finish (CmClient *self, + GAsyncResult *result, + GError **error); +void cm_client_key_verification_continue_async (CmClient *self, + CmEvent *verification_event, + GCancellable *cancellable, + GAsyncReadyCallback callback, + gpointer user_data); +gboolean cm_client_key_verification_continue_finish (CmClient *self, + GAsyncResult *result, + GError **error); +void cm_client_key_verification_cancel_async (CmClient *self, + CmEvent *verification_event, + GCancellable *cancellable, + GAsyncReadyCallback callback, + gpointer user_data); +gboolean cm_client_key_verification_cancel_finish (CmClient *self, + GAsyncResult *result, + GError **error); +void cm_client_key_verification_match_async (CmClient *self, + CmEvent *verification_event, + GCancellable *cancellable, + GAsyncReadyCallback callback, + gpointer user_data); +gboolean cm_client_key_verification_match_finish (CmClient *self, + GAsyncResult *result, + GError **error); +gboolean cm_client_can_connect (CmClient *self); +void cm_client_start_sync (CmClient *self); +gboolean cm_client_is_sync (CmClient *self); +void cm_client_stop_sync (CmClient *self); +gboolean cm_client_get_logging_in (CmClient *self); +gboolean cm_client_get_logged_in (CmClient *self); +GListModel *cm_client_get_joined_rooms (CmClient *self); +GListModel *cm_client_get_invited_rooms (CmClient *self); +GListModel *cm_client_get_key_verifications (CmClient *self); + +G_END_DECLS diff --git a/subprojects/libcmatrix/src/cm-common.c b/subprojects/libcmatrix/src/cm-common.c new file mode 100644 index 0000000000000000000000000000000000000000..8583c23ce207681b68d598093de73dcc8835f179 --- /dev/null +++ b/subprojects/libcmatrix/src/cm-common.c @@ -0,0 +1,21 @@ +/* -*- mode: c; c-basic-offset: 2; indent-tabs-mode: nil; -*- */ +/* cm-common.c + * + * Copyright 2022 Purism SPC + * + * Author(s): + * Mohammed Sadiq <sadiq@sadiqpk.org> + * + * SPDX-License-Identifier: LGPL-2.1-or-later + */ + +#include "cm-common.h" + +/** + * matrix_error_quark: + * + * Get the Matrix Error Quark. + * + * Returns: a #GQuark. + **/ +G_DEFINE_QUARK (cm-error-quark, cm_error) diff --git a/subprojects/libcmatrix/src/cm-common.h b/subprojects/libcmatrix/src/cm-common.h new file mode 100644 index 0000000000000000000000000000000000000000..9549daf59191525d8877ad7713f53f03d4284f45 --- /dev/null +++ b/subprojects/libcmatrix/src/cm-common.h @@ -0,0 +1,17 @@ +/* -*- mode: c; c-basic-offset: 2; indent-tabs-mode: nil; -*- */ +/* cm-common.h + * + * Copyright 2022 Purism SPC + * + * Author(s): + * Mohammed Sadiq <sadiq@sadiqpk.org> + * + * SPDX-License-Identifier: LGPL-2.1-or-later + */ + +#pragma once + +#include <glib-object.h> + +#define CM_ERROR (cm_error_quark ()) +GQuark cm_error_quark (void); diff --git a/subprojects/libcmatrix/src/cm-db-private.h b/subprojects/libcmatrix/src/cm-db-private.h new file mode 100644 index 0000000000000000000000000000000000000000..200b6fca2df47aa487a9f377cbef11ee2748a608 --- /dev/null +++ b/subprojects/libcmatrix/src/cm-db-private.h @@ -0,0 +1,151 @@ +/* -*- mode: c; c-basic-offset: 2; indent-tabs-mode: nil; -*- */ +/* cm-db.h + * + * Copyright 2022 Purism SPC + * + * Author(s): + * Mohammed Sadiq <sadiq@sadiqpk.org> + * + * SPDX-License-Identifier: LGPL-2.1-or-later + */ + +#pragma once + +#include <gio/gio.h> + +#include "cm-enc-private.h" +#include "cm-client.h" +#include "cm-device.h" +#include "cm-room.h" + +G_BEGIN_DECLS + +/* These values shouldn’t be changed. They are used in DB */ +typedef enum { + SESSION_OLM_V1_IN = 1, + SESSION_OLM_V1_OUT = 2, + SESSION_MEGOLM_V1_IN = 3, + SESSION_MEGOLM_V1_OUT = 4, +} CmSessionType; + +#define CMATRIX_ALGORITHM_A256CTR 1 + +#define CMATRIX_KEY_TYPE_OCT 1 + +#define CM_TYPE_DB (cm_db_get_type ()) + +G_DECLARE_FINAL_TYPE (CmDb, cm_db, CM, DB, GObject) + +CmDb *cm_db_new (void); +void cm_db_open_async (CmDb *self, + char *dir, + const char *file_name, + GAsyncReadyCallback callback, + gpointer user_data); +gboolean cm_db_open_finish (CmDb *self, + GAsyncResult *result, + GError **error); +gboolean cm_db_is_open (CmDb *self); +void cm_db_close_async (CmDb *self, + GAsyncReadyCallback callback, + gpointer user_data); +gboolean cm_db_close_finish (CmDb *self, + GAsyncResult *result, + GError **error); +void cm_db_save_client_async (CmDb *db, + CmClient *client, + char *pickle, + GAsyncReadyCallback callback, + gpointer user_data); +gboolean cm_db_save_client_finish (CmDb *self, + GAsyncResult *result, + GError **error); +void cm_db_load_client_async (CmDb *db, + CmClient *client, + const char *device_id, + GAsyncReadyCallback callback, + gpointer user_data); +gboolean cm_db_load_client_finish (CmDb *self, + GAsyncResult *result, + GError **error); +void cm_db_save_room_async (CmDb *self, + CmClient *client, + CmRoom *room, + GAsyncReadyCallback callback, + gpointer user_data); +gboolean cm_db_save_room_finish (CmDb *self, + GAsyncResult *result, + GError **error); +void cm_db_delete_client_async (CmDb *self, + CmClient *client, + GAsyncReadyCallback callback, + gpointer user_data); +gboolean cm_db_delete_client_finish (CmDb *self, + GAsyncResult *result, + GError **error); +void cm_db_save_file_enc_async (CmDb *self, + CmEncFileInfo *file, + GAsyncReadyCallback callback, + gpointer user_data); +gboolean cm_db_save_file_enc_finish (CmDb *self, + GAsyncResult *result, + GError **error); +void cm_db_find_file_enc_async (CmDb *self, + const char *url, + GAsyncReadyCallback callback, + gpointer user_data); +CmEncFileInfo *cm_db_find_file_enc_finish (CmDb *self, + GAsyncResult *result, + GError **error); +gboolean cm_db_add_session (CmDb *self, + gpointer session, + char *pickle); +gpointer cm_db_lookup_session (CmDb *self, + const char *account_id, + const char *account_device, + const char *session_id, + const char *sender_key, + const char *pickle_key, + const char *room_id, + CmSessionType type); +void cm_db_add_room_members (CmDb *self, + CmRoom *cm_room, + GPtrArray *members); +void cm_db_add_room_events (CmDb *self, + CmRoom *room, + GPtrArray *events, + gboolean prepend); +void cm_db_get_past_events_async (CmDb *self, + CmRoom *room, + CmEvent *from, + GAsyncReadyCallback callback, + gpointer user_data); +GPtrArray *cm_db_get_past_events_finish (CmDb *self, + GAsyncResult *result, + GError **error); +gpointer cm_db_lookup_olm_session (CmDb *self, + const char *account_id, + const char *account_device, + const char *sender_curve25519_key, + const char *body, + const char *pickle_key, + CmSessionType type, + size_t message_type, + char **out_plain_text); +void cm_db_mark_user_device_change (CmDb *self, + CmClient *client, + GPtrArray *users, + gboolean outdated, + gboolean is_tracking); +void cm_db_update_user_devices (CmDb *self, + CmClient *client, + CmUser *user, + GPtrArray *added, + GPtrArray *removed, + gboolean force_add); +void cm_db_update_device (CmDb *self, + CmClient *client, + CmUser *user, + CmDevice *device); + +G_END_DECLS diff --git a/subprojects/libcmatrix/src/cm-db.c b/subprojects/libcmatrix/src/cm-db.c new file mode 100644 index 0000000000000000000000000000000000000000..bc6a61e48d75054a24fdb398d2575c3294c0cbb5 --- /dev/null +++ b/subprojects/libcmatrix/src/cm-db.c @@ -0,0 +1,3612 @@ +/* -*- mode: c; c-basic-offset: 2; indent-tabs-mode: nil; -*- */ +/* cm-db.c + * + * Copyright 2022 Purism SPC + * + * Author(s): + * Mohammed Sadiq <sadiq@sadiqpk.org> + * + * SPDX-License-Identifier: LGPL-2.1-or-later + */ + +#define G_LOG_DOMAIN "cm-db" + +#ifdef HAVE_CONFIG_H +# include "config.h" +#endif + +#include <glib.h> +#include <fcntl.h> +#include <sqlite3.h> + +#include "events/cm-event-private.h" +#include "events/cm-room-event-private.h" +#include "events/cm-room-message-event-private.h" +#include "users/cm-user-private.h" +#include "cm-device-private.h" +#include "cm-enc-private.h" +#include "cm-olm-private.h" +#include "cm-client-private.h" +#include "cm-room-private.h" +#include "cm-utils-private.h" +#include "cm-db-private.h" + +#define STRING(arg) STRING_VALUE(arg) +#define STRING_VALUE(arg) #arg + +/* increment when DB changes */ +#define DB_VERSION 2 + +struct _CmDb +{ + GObject parent_instance; + + GAsyncQueue *queue; + GThread *worker_thread; + sqlite3 *db; + char *db_path; +}; + +#define VERIFICATION_UNSET 0 +#define VERIFICATION_KNOWN 1 +#define VERIFICATION_VERIFIED 2 +#define VERIFICATION_BLACKLISTED 3 +#define VERIFICATION_IGNORED 4 +#define VERIFICATION_IS_SELF 5 + +#define EVENT_NOT_ENCRYPTED 0 +#define EVENT_NOT_DECRYPTED 1 +#define EVENT_DECRYPTED 2 +/* We got m.room.encrypted, but with empty content */ +#define EVENT_MAY_BE_DECRYPTED 3 + +/* + * CmDb->db should never be accessed nor modified in main thread + * except for checking if it’s %NULL. Any operation should be done only + * in @worker_thread. Don't reuse the same #CmDb once closed. + * + * Always copy data with g_object_set_data_full() or similar if the data can change + * (regardless of whether the data has changed or not), so as to avoid surprises + * with multi-thread stuff. + */ + +typedef void (*CmDbCallback) (CmDb *self, + GTask *task); + +G_DEFINE_TYPE (CmDb, cm_db, G_TYPE_OBJECT) + +static void +warn_if_sql_error (int status, + const char *message) +{ + if (status == SQLITE_OK || status == SQLITE_ROW || status == SQLITE_DONE) + return; + + g_warning ("Error %s. errno: %d, message: %s", message, status, sqlite3_errstr (status)); +} + +static void +matrix_bind_text (sqlite3_stmt *statement, + guint position, + const char *bind_value, + const char *message) +{ + guint status; + + status = sqlite3_bind_text (statement, position, bind_value, -1, SQLITE_TRANSIENT); + warn_if_sql_error (status, message); +} + +static void +matrix_bind_int (sqlite3_stmt *statement, + guint position, + gint64 bind_value, + const char *message) +{ + guint status; + + status = sqlite3_bind_int64 (statement, position, bind_value); + warn_if_sql_error (status, message); +} + +static int +db_event_state_to_int (CmEventState state) +{ + switch (state) + { + case CM_EVENT_STATE_DRAFT: + return 1; + + case CM_EVENT_STATE_WAITING: + return 2; + + case CM_EVENT_STATE_SENDING: + return 3; + + case CM_EVENT_STATE_SENDING_FAILED: + return 4; + + case CM_EVENT_STATE_SENT: + return 5; + + case CM_EVENT_STATE_RECEIVED: + return 6; + + case CM_EVENT_STATE_UNKNOWN: + default: + return 0; + } + + return 0; +} + +static CmEventState +db_event_state_from_int (int state) +{ + if (state == 0) + return CM_EVENT_STATE_UNKNOWN; + + if (state == 1) + return CM_EVENT_STATE_DRAFT; + + if (state == 2 || state == 3 || state == 4) + return CM_EVENT_STATE_SENDING_FAILED; + + if (state == 5) + return CM_EVENT_STATE_SENT; + + if (state == 6) + return CM_EVENT_STATE_RECEIVED; + + g_return_val_if_reached (CM_EVENT_STATE_UNKNOWN); +} + +static int +db_event_get_decryption_value (CmEvent *event) +{ + gboolean encrypted = 0, decrypted = 0, has_content = 0; + + if (!event) + return EVENT_NOT_ENCRYPTED; + + g_assert (CM_IS_EVENT (event)); + + encrypted = cm_event_is_encrypted (event); + decrypted = cm_event_is_decrypted (event); + has_content = cm_event_has_encrypted_content (event); + + if (encrypted) + { + if (has_content && decrypted) + return EVENT_DECRYPTED; + + if (!has_content) + return EVENT_MAY_BE_DECRYPTED; + + return EVENT_NOT_DECRYPTED; + } + + return EVENT_NOT_ENCRYPTED; +} + +static GPtrArray * +db_get_past_room_events (CmDb *self, + CmRoom *cm_room, + int room_id, + int from_event_id, + int from_sorted_event_id, + int max_count) +{ + GPtrArray *events = NULL; + sqlite3_stmt *stmt; + gboolean skip = FALSE; + + if (from_event_id) + skip = TRUE; + + sqlite3_prepare_v2 (self->db, + "SELECT id,event_state,room_events.json_data FROM room_events " + "WHERE room_id=? AND sorted_id <= ? " + /* Limit to messages until chatty has better events support */ + "AND (event_type=? OR event_type=?)" + "ORDER BY sorted_id DESC, id DESC LIMIT ?", + -1, &stmt, NULL); + + matrix_bind_int (stmt, 1, room_id, "binding when loading past events"); + matrix_bind_int (stmt, 2, from_sorted_event_id, "binding when loading past events"); + matrix_bind_int (stmt, 3, CM_M_ROOM_MESSAGE, "binding when loading past events"); + matrix_bind_int (stmt, 4, CM_M_ROOM_ENCRYPTED, "binding when loading past events"); + matrix_bind_int (stmt, 5, max_count, "binding when loading past events"); + + while (sqlite3_step (stmt) == SQLITE_ROW) + { + JsonObject *encrypted, *root; + g_autoptr(JsonObject) json = NULL; + CmRoomEvent *cm_event; + CmEventState state; + + if (skip) + { + skip = FALSE; + continue; + } + + json = cm_utils_string_to_json_object ((char *)sqlite3_column_text (stmt, 2)); + + if (!json) + continue; + + root = cm_utils_json_object_get_object (json, "json"); + encrypted = cm_utils_json_object_get_object (json, "encrypted"); + cm_event = cm_room_event_new_from_json (cm_room, root, encrypted); + state = db_event_state_from_int (sqlite3_column_int (stmt, 1)); + cm_event_set_state (CM_EVENT (cm_event), state); + + if (!cm_event) + continue; + + if (!events) + events = g_ptr_array_new_full (32, g_object_unref); + + g_ptr_array_add (events, cm_event); + } + + sqlite3_finalize (stmt); + + return events; +} + +/** + user_devices.json_data + - local + - device_display_name + users.json_data + - local + - name + - avatar_url + - status = unknown, known, blocked, verified, + accounts.json_data + - local + - filter + - id = text + - version = int + rooms.json_data + - local + - direct = bool + - state = int (joined, invited, left, unknown) + - name_loaded = bool + - alias = string + - draft = json object + - m.text = string + - encryption = string + - rotation_period_ms = time_t (or may be use μs as provided by server?) + - rotation_count_msgs = int (message count max for key change) + sessions.json_data + - local + - key_added_date = time_t (or may be use μs as provided by server?) + room_members.json_data + - local + - display_name TEXT (set if different from default name) + - avatar_url TEXT (set id different from default avatar) +*/ +static gboolean +cm_db_create_schema (CmDb *self, + GTask *task) +{ + const char *sql; + char *error = NULL; + int status; + + g_assert (CM_IS_DB (self)); + g_assert (G_IS_TASK (task)); + g_assert (g_thread_self () == self->worker_thread); + g_assert (self->db); + + sql = "PRAGMA user_version = " STRING (DB_VERSION) ";" + + "CREATE TABLE IF NOT EXISTS users (" + "id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, " + /* v2 */ + "account_id INTEGER REFERENCES accounts(id) ON DELETE CASCADE, " + /* Version 1: Unique */ + /* v2: Remove Unique */ + "username TEXT NOT NULL, " + /* v2 */ + "tracking INTEGER NOT NULL DEFAULT 0, " + /* Version 1 */ + "outdated INTEGER DEFAULT 1, " + /* Version 1 */ + "json_data TEXT," + /* v2 */ + "UNIQUE (account_id, username));" + + /* Version 1 */ + "CREATE TABLE IF NOT EXISTS user_devices (" + "id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, " + "user_id INTEGER NOT NULL REFERENCES users(id), " + /* xxx: Move device to devices table along with + * curve25519_key and ed25519_key as the same + * can exist in several accounts + */ + "device TEXT NOT NULL, " + "curve25519_key TEXT, " + "ed25519_key TEXT, " + "verification INTEGER DEFAULT 0, " + "json_data TEXT, " + "UNIQUE (user_id, device));" + + "CREATE TABLE IF NOT EXISTS accounts (" + "id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, " + /* Version 1 */ + "user_device_id INTEGER NOT NULL REFERENCES user_devices(id), " + "next_batch TEXT, " + "pickle TEXT, " + "enabled INTEGER DEFAULT 0, " + /* Version 1 */ + "json_data TEXT, " + "UNIQUE (user_device_id));" + + "CREATE TABLE IF NOT EXISTS rooms (" + "id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, " + /* v2: on delete cascade */ + "account_id INTEGER NOT NULL REFERENCES accounts(id) ON DELETE CASCADE, " + "room_name TEXT NOT NULL, " + "prev_batch TEXT, " + /* Version 1 */ + /* Set if the room has tombstone and got replaced by a different room */ + "replacement_room_id INTEGER REFERENCES rooms(id)," + /* v2 */ + "room_state INTEGER NOT NULL DEFAULT 0, " + /* Version 1 */ + "json_data TEXT, " + "UNIQUE (account_id, room_name));" + + /* v2 */ + "CREATE TABLE IF NOT EXISTS room_members (" + "id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, " + "room_id INTEGER NOT NULL REFERENCES rooms(id) ON DELETE CASCADE, " + "user_id INTEGER NOT NULL REFERENCES users(id), " + /* joined, invited, left (we set left instead of deleting as past messages may refer to user id) */ + "user_state INTEGER NOT NULL DEFAULT 0, " + "json_data TEXT, " + "UNIQUE (room_id, user_id));" + + /* v2 */ + "CREATE TABLE IF NOT EXISTS room_events_cache (" + "id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, " + "room_id INTEGER NOT NULL REFERENCES rooms(id) ON DELETE CASCADE, " + "sender_id INTEGER REFERENCES room_members(id), " + "event_uid TEXT NOT NULL, " + "origin_server_ts INTEGER, " + "json_data TEXT, " + "UNIQUE (room_id, event_uid));" + + /* v2 */ + "CREATE TABLE IF NOT EXISTS room_events (" + "id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, " + /* 'id' above only increments, 'sorted_id' increments + * or decrements in the order events has to be placed. + */ + "sorted_id INTEGER NOT NULL, " + "room_id INTEGER NOT NULL REFERENCES rooms(id) ON DELETE CASCADE, " + "sender_id INTEGER NOT NULL REFERENCES room_members(id), " + "event_type INTEGER NOT NULL, " + "event_uid TEXT, " + "txnid TEXT, " + /* If set to 0, this event is a reply to some other, which is not yet in db */ + "replaces_event_id INTEGER REFERENCES room_events(id), " + "replaces_event_cache_id INTEGER REFERENCES room_events_cache(id), " + /* This event has been replaced with the given event id */ + /* This will likely point to the latest replacement id available */ + "replaced_with_id INTEGER REFERENCES room_events(id), " + /* sending, sent, sending failed, */ + "event_state INTEGER, " + "state_key TEXT, " + "origin_server_ts INTEGER NOT NULL, " + /* 0: not encrypted, 1: not decrypted 2: decrypted */ + /* 3: may be decrypted, we got m.room.encrypted but without content */ + "decryption INTEGER NOT NULL DEFAULT 0, " + /* direction int, encrypted int, verified int, txnid */ + "json_data TEXT, " + "UNIQUE (room_id, event_uid));" + + "CREATE TABLE IF NOT EXISTS encryption_keys (" + "id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, " + /* v2 */ + "account_id INTEGER REFERENCES accounts(id) ON DELETE CASCADE, " + "file_url TEXT NOT NULL, " + "file_sha256 TEXT, " + /* Initialization vector: iv in JSON */ + "iv TEXT NOT NULL, " + /* v in JSON */ + "version INT DEFAULT 2 NOT NULL, " + /* alg in JSON */ + "algorithm INT NOT NULL, " + /* k in JSON */ + "key TEXT NOT NULL, " + /* kty in JSON */ + "type INT NOT NULL, " + /* ext in JSON */ + "extractable INT DEFAULT 1 NOT NULL, " + /* Version 1 */ + "json_data TEXT, " + "UNIQUE (account_id, file_url));" + + /* v2: Renamed to sessions from session */ + "CREATE TABLE IF NOT EXISTS sessions (" + "id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, " + "account_id INTEGER NOT NULL REFERENCES accounts(id) ON DELETE CASCADE, " + "sender_key TEXT NOT NULL, " + "session_id TEXT NOT NULL, " + "type INTEGER NOT NULL, " + "pickle TEXT NOT NULL, " + "time INT, " + /* v2 */ + "origin_server_ts INTEGER, " + /* Version 1 */ + "room_id INTEGER REFERENCES rooms(id), " + /* v2 */ + "chain_index INTEGER, " + /* v2 */ + /* 0: usable, 1: rotated, 2: invalidated (on user/device removals) */ + "session_state INTEGER NOT NULL DEFAULT 0, " + /* v1 */ + "json_data TEXT, " + "UNIQUE (account_id, sender_key, session_id));" + + /* v2 */ + "CREATE UNIQUE INDEX IF NOT EXISTS room_event_idx ON room_events (room_id, event_uid);" + "CREATE UNIQUE INDEX IF NOT EXISTS room_event_txn_idx ON room_events (room_id, txnid);" + "CREATE UNIQUE INDEX IF NOT EXISTS user_device_idx ON user_devices (user_id, device);" + "CREATE INDEX IF NOT EXISTS room_event_state_idx ON room_events (state_key);" + "CREATE UNIQUE INDEX IF NOT EXISTS room_event_cache_idx ON room_events_cache (room_id, event_uid);" + "CREATE UNIQUE INDEX IF NOT EXISTS encryption_key_idx ON encryption_keys (account_id, file_url);" + "CREATE INDEX IF NOT EXISTS session_sender_idx ON sessions (account_id, sender_key);" + "CREATE INDEX IF NOT EXISTS user_idx ON users (username);" + + /* v2 */ + "CREATE TRIGGER IF NOT EXISTS insert_replaced_with_id AFTER INSERT " + "ON room_events FOR EACH ROW WHEN NEW.replaces_event_id IS NOT NULL " + "BEGIN " + "UPDATE room_events SET replaced_with_id=NEW.id " + "WHERE id=NEW.replaces_event_id AND (replaced_with_id IS NULL or replaced_with_id < NEW.id); " + "END;" + + /* v2 */ + "CREATE TRIGGER IF NOT EXISTS update_replaced_with_id AFTER UPDATE OF replaces_event_id " + "ON room_events FOR EACH ROW WHEN NEW.replaces_event_id IS NOT NULL " + "BEGIN " + "UPDATE room_events SET replaced_with_id=NEW.id " + "WHERE id=NEW.replaces_event_id AND (replaced_with_id IS NULL or replaced_with_id < NEW.id); " + "END;"; + + status = sqlite3_exec (self->db, sql, NULL, NULL, &error); + + if (status != SQLITE_OK) + { + g_task_return_new_error (task, + G_IO_ERROR, + G_IO_ERROR_FAILED, + "Error creating table. errno: %d, desc: %s. %s", + status, sqlite3_errmsg (self->db), error); + return FALSE; + } + + return TRUE; +} + +static int +cm_db_get_db_version (CmDb *self, + GTask *task) +{ + sqlite3_stmt *stmt; + int status, version = -1; + + g_assert (CM_IS_DB (self)); + g_assert (G_IS_TASK (task)); + g_assert (g_thread_self () == self->worker_thread); + g_assert (self->db); + + sqlite3_prepare_v2 (self->db, "PRAGMA user_version;", -1, &stmt, NULL); + status = sqlite3_step (stmt); + + if (status == SQLITE_ROW) + version = sqlite3_column_int (stmt, 0); + else + g_task_return_new_error (task, + G_IO_ERROR, + G_IO_ERROR_FAILED, + "Couldn't get database version. error: %s", + sqlite3_errmsg (self->db)); + sqlite3_finalize (stmt); + + return version; +} + +static void +cm_db_backup (CmDb *self) +{ + g_autoptr(GFile) backup_db = NULL; + g_autoptr(GFile) old_db = NULL; + g_autofree char *backup_name = NULL; + g_autofree char *time = NULL; + g_autoptr(GDateTime) date = NULL; + g_autoptr(GError) error = NULL; + + date = g_date_time_new_now_local (); + time = g_date_time_format (date, "%Y-%m-%d-%H%M%S"); + backup_name = g_strdup_printf ("%s.%s", self->db_path, time); + g_info ("Copying database for backup"); + + old_db = g_file_new_for_path (self->db_path); + backup_db = g_file_new_for_path (backup_name); + g_file_copy (old_db, backup_db, G_FILE_COPY_NONE, NULL, NULL, NULL, &error); + g_info ("Copying database success: %d", !error); + + if (error && + !g_error_matches (error, G_IO_ERROR, G_IO_ERROR_NOT_FOUND) && + !g_error_matches (error, G_IO_ERROR, G_IO_ERROR_EXISTS)) + g_error ("Error creating DB backup: %s", error->message); +} + +static gboolean +cm_db_migrate_db_v1 (CmDb *self, + GTask *task) +{ + char *error = NULL; + int status; + + g_assert (CM_IS_DB (self)); + g_assert (G_IS_TASK (task)); + g_assert (g_thread_self () == self->worker_thread); + + cm_db_backup (self); + + status = sqlite3_exec (self->db, + "CREATE TABLE IF NOT EXISTS tmp_users (" + "id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, " + "username TEXT NOT NULL UNIQUE, " + "outdated INTEGER DEFAULT 1, " + "json_data TEXT " + ");" + + "CREATE TABLE IF NOT EXISTS user_devices (" + "id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, " + "user_id INTEGER NOT NULL REFERENCES users(id), " + "device TEXT NOT NULL, " + "curve25519_key TEXT, " + "ed25519_key TEXT, " + "verification INTEGER DEFAULT 0, " + "json_data TEXT, " + "UNIQUE (user_id, device));" + + "CREATE TABLE IF NOT EXISTS tmp_accounts (" + "id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, " + "user_device_id INTEGER NOT NULL REFERENCES user_devices(id), " + "next_batch TEXT, " + "pickle TEXT, " + "enabled INTEGER DEFAULT 0, " + "json_data TEXT, " + "UNIQUE (user_device_id));" + + "CREATE TABLE IF NOT EXISTS tmp_users (" + "id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, " + "username TEXT NOT NULL UNIQUE, " + "outdated INTEGER DEFAULT 1, " + "json_data TEXT " + ");" + + "INSERT OR IGNORE INTO tmp_users(username) " + "SELECT DISTINCT username FROM users;" + + "INSERT OR IGNORE INTO user_devices(user_id,device) " + "SELECT tmp_users.id,devices.device FROM tmp_users " + "JOIN users ON users.username=tmp_users.username " + "JOIN devices ON users.device_id=devices.id;" + + "INSERT OR IGNORE INTO tmp_accounts(user_device_id,next_batch,pickle,enabled) " + "SELECT user_devices.id,next_batch,pickle,enabled FROM accounts " + "JOIN users ON users.id=accounts.user_id " + "JOIN devices ON users.device_id=devices.id " + "JOIN user_devices ON user_devices.device=devices.device " + "JOIN tmp_users ON user_devices.user_id=tmp_users.id " + "AND tmp_users.username=users.username;" + + "UPDATE OR IGNORE session SET account_id=(SELECT tmp_accounts.id " + "FROM tmp_accounts " + "INNER JOIN accounts ON accounts.pickle=tmp_accounts.pickle " + "AND session.account_id=accounts.id" + ");" + + "UPDATE OR IGNORE rooms SET account_id=(SELECT tmp_accounts.id " + "FROM tmp_accounts " + "INNER JOIN accounts ON accounts.pickle=tmp_accounts.pickle " + "AND rooms.account_id=accounts.id" + ");" + + "DROP TABLE IF EXISTS users;" + "DROP TABLE IF EXISTS accounts;" + "DROP TABLE IF EXISTS devices;" + + "ALTER TABLE tmp_users RENAME TO users;" + "ALTER TABLE tmp_accounts RENAME TO accounts;" + + "ALTER TABLE rooms ADD COLUMN replacement_room_id " + "INTEGER REFERENCES rooms(id);" + "ALTER TABLE rooms ADD COLUMN json_data TEXT;" + + "ALTER TABLE encryption_keys ADD COLUMN json_data TEXT;" + + "ALTER TABLE session ADD COLUMN room_id " + "INTEGER REFERENCES rooms(id);" + "ALTER TABLE session ADD COLUMN json_data TEXT;" + + "PRAGMA user_version = 1;", + NULL, NULL, &error); + + g_debug ("Migrating db to version 1, success: %d", !error); + + if (status == SQLITE_OK || status == SQLITE_DONE) + return TRUE; + + g_task_return_new_error (task, + G_IO_ERROR, + G_IO_ERROR_FAILED, + "Couldn't migrate to new db. errno: %d. %s", + status, error); + sqlite3_free (error); + + return FALSE; +} + +static gboolean +cm_db_migrate_to_v2 (CmDb *self, + GTask *task) +{ + char *error = NULL; + int status; + + g_assert (CM_IS_DB (self)); + g_assert (G_IS_TASK (task)); + g_assert (g_thread_self () == self->worker_thread); + + cm_db_backup (self); + + status = sqlite3_exec (self->db, + "CREATE TABLE IF NOT EXISTS room_members (" + "id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, " + "room_id INTEGER NOT NULL REFERENCES rooms(id) ON DELETE CASCADE, " + "user_id INTEGER NOT NULL REFERENCES users(id), " + /* joined, invited, left (we set left instead of deleting as past messages may refer to user id) */ + "user_state INTEGER NOT NULL DEFAULT 0, " + "json_data TEXT, " + "UNIQUE (room_id, user_id));" + + /* v2 */ + "CREATE TABLE IF NOT EXISTS room_events_cache (" + "id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, " + "room_id INTEGER NOT NULL REFERENCES rooms(id) ON DELETE CASCADE, " + "sender_id INTEGER REFERENCES room_members(id), " + "event_uid TEXT NOT NULL, " + "origin_server_ts INTEGER, " + "json_data TEXT, " + "UNIQUE (room_id, event_uid));" + + "CREATE TABLE IF NOT EXISTS room_events (" + "id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, " + /* 'id' above only increments, 'sorted_id' increments + * or decrements in the order events has to be placed. + */ + "sorted_id INTEGER NOT NULL, " + "room_id INTEGER NOT NULL REFERENCES rooms(id) ON DELETE CASCADE, " + "sender_id INTEGER NOT NULL REFERENCES room_members(id), " + "event_type INTEGER NOT NULL, " + "event_uid TEXT, " + "txnid TEXT, " + /* If set to 0, this event is a reply to some other, which is not yet in db */ + "replaces_event_id INTEGER REFERENCES room_events(id), " + "replaces_event_cache_id INTEGER REFERENCES room_events_cache(id), " + /* This event has been replaced with the given event id */ + /* This will likely point to the latest replacement id available */ + "replaced_with_id INTEGER REFERENCES room_events(id), " + /* sending, sent, sending failed, */ + "event_state INTEGER, " + "state_key TEXT, " + "origin_server_ts INTEGER NOT NULL, " + /* 0: not encrypted, 1: not decrypted 2: decrypted */ + /* 3: may be decrypted, we got m.room.encrypted but without content */ + "decryption INTEGER NOT NULL DEFAULT 0, " + /* direction int, encrypted int, verified int, txnid */ + "json_data TEXT, " + "UNIQUE (room_id, event_uid));" + + "CREATE TABLE IF NOT EXISTS tmp_encryption_keys (" + "id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, " + /* v2 */ + "account_id INTEGER REFERENCES accounts(id) ON DELETE CASCADE, " + "file_url TEXT NOT NULL, " + "file_sha256 TEXT, " + /* Initialization vector: iv in JSON */ + "iv TEXT NOT NULL, " + /* v in JSON */ + "version INT DEFAULT 2 NOT NULL, " + /* alg in JSON */ + "algorithm INT NOT NULL, " + /* k in JSON */ + "key TEXT NOT NULL, " + /* kty in JSON */ + "type INT NOT NULL, " + /* ext in JSON */ + "extractable INT DEFAULT 1 NOT NULL, " + /* Version 1 */ + "json_data TEXT, " + "UNIQUE (account_id, file_url));" + + "INSERT INTO tmp_encryption_keys(file_url,file_sha256,iv,version,algorithm,key,type,extractable) " + "SELECT DISTINCT file_url,file_sha256,iv,version,algorithm,key,type,extractable FROM encryption_keys;" + + "CREATE TABLE IF NOT EXISTS tmp_users (" + "id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, " + "account_id INTEGER REFERENCES accounts(id) ON DELETE CASCADE, " + "username TEXT NOT NULL, " + "tracking INTEGER NOT NULL DEFAULT 0, " + "outdated INTEGER DEFAULT 1, " + "json_data TEXT, " + "UNIQUE (account_id, username));" + + "INSERT INTO tmp_users(id,username) " + "SELECT DISTINCT id,username FROM users;" + + "CREATE TABLE IF NOT EXISTS tmp_rooms (" + "id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, " + /* v2: on delete cascade */ + "account_id INTEGER NOT NULL REFERENCES accounts(id) ON DELETE CASCADE, " + "room_name TEXT NOT NULL, " + "prev_batch TEXT, " + /* v1 */ + /* Set if the room has tombstone and got replaced by a different room */ + "replacement_room_id INTEGER REFERENCES rooms(id)," + /* v2 */ + "room_state INTEGER NOT NULL DEFAULT 0, " + /* v1 */ + "json_data TEXT, " + "UNIQUE (account_id, room_name));" + + "INSERT OR IGNORE INTO tmp_rooms(id,account_id,room_name,prev_batch) " + "SELECT DISTINCT id,account_id,room_name,prev_batch FROM rooms;" + + "CREATE TABLE IF NOT EXISTS tmp_sessions (" + "id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, " + "account_id INTEGER NOT NULL REFERENCES accounts(id) ON DELETE CASCADE, " + "sender_key TEXT NOT NULL, " + "session_id TEXT NOT NULL, " + "type INTEGER NOT NULL, " + "pickle TEXT NOT NULL, " + "time INT, " + /* v2 */ + "origin_server_ts INTEGER, " + /* Version 1 */ + "room_id INTEGER REFERENCES rooms(id), " + /* v2 */ + "chain_index INTEGER, " + /* v2 */ + /* 0: usable, 1: rotated, 2: invalidated (on user/device removals) */ + "session_state INTEGER NOT NULL DEFAULT 0, " + /* v1 */ + "json_data TEXT, " + "UNIQUE (account_id, sender_key, session_id));" + + "INSERT OR IGNORE INTO tmp_sessions(id,account_id,sender_key,session_id,type,pickle,time) " + "SELECT DISTINCT id,account_id,sender_key,session_id,type,pickle,time FROM session;" + + "DROP TABLE IF EXISTS rooms;" + "DROP TABLE IF EXISTS users;" + "DROP TABLE IF EXISTS session;" + "DROP TABLE IF EXISTS encryption_keys;" + + "ALTER TABLE tmp_rooms RENAME TO rooms;" + "ALTER TABLE tmp_users RENAME TO users;" + "ALTER TABLE tmp_sessions RENAME TO sessions;" + "ALTER TABLE tmp_encryption_keys RENAME TO encryption_keys;" + + "CREATE UNIQUE INDEX IF NOT EXISTS room_event_idx ON room_events (room_id, event_uid);" + "CREATE UNIQUE INDEX IF NOT EXISTS room_event_txn_idx ON room_events (room_id, txnid);" + "CREATE UNIQUE INDEX IF NOT EXISTS user_device_idx ON user_devices (user_id, device);" + "CREATE INDEX IF NOT EXISTS room_event_state_idx ON room_events (state_key);" + "CREATE UNIQUE INDEX IF NOT EXISTS room_event_cache_idx ON room_events_cache (room_id, event_uid);" + "CREATE UNIQUE INDEX IF NOT EXISTS encryption_key_idx ON encryption_keys (account_id, file_url);" + "CREATE INDEX IF NOT EXISTS session_sender_idx ON sessions (account_id, sender_key);" + "CREATE INDEX IF NOT EXISTS user_idx ON users (username);" + + /* v2 */ + "CREATE TRIGGER IF NOT EXISTS insert_replaced_with_id AFTER INSERT " + "ON room_events FOR EACH ROW WHEN NEW.replaces_event_id IS NOT NULL " + "BEGIN " + "UPDATE room_events SET replaced_with_id=NEW.id " + "WHERE id=NEW.replaces_event_id AND (replaced_with_id IS NULL or replaced_with_id < NEW.id); " + "END;" + + /* v2 */ + "CREATE TRIGGER IF NOT EXISTS update_replaced_with_id AFTER UPDATE OF replaces_event_id " + "ON room_events FOR EACH ROW WHEN NEW.replaces_event_id IS NOT NULL " + "BEGIN " + "UPDATE room_events SET replaced_with_id=NEW.id " + "WHERE id=NEW.replaces_event_id AND (replaced_with_id IS NULL or replaced_with_id < NEW.id); " + "END;" + + "PRAGMA user_version = 2;", + NULL, NULL, &error); + + g_debug ("Migrating db to version 2, success: %d", !error); + + if (status == SQLITE_OK || status == SQLITE_DONE) + return TRUE; + + g_task_return_new_error (task, + G_IO_ERROR, + G_IO_ERROR_FAILED, + "Couldn't migrate to new db. errno: %d. %s", + status, error); + sqlite3_free (error); + + return FALSE; +} + +static gboolean +cm_db_migrate (CmDb *self, + GTask *task) +{ + int version; + + g_assert (CM_IS_DB (self)); + g_assert (G_IS_TASK (task)); + g_assert (g_thread_self () == self->worker_thread); + g_assert (self->db); + + version = cm_db_get_db_version (self, task); + + if (version == DB_VERSION) + return TRUE; + + switch (version) { + case -1: /* Error */ + return FALSE; + + case 0: + if (!cm_db_migrate_db_v1 (self, task)) + return FALSE; + /* fallthrough */ + + case 1: + if (!cm_db_migrate_to_v2 (self, task)) + return FALSE; + break; + + default: + g_task_return_new_error (task, + G_IO_ERROR, + G_IO_ERROR_FAILED, + "Failed to migrate from unknown version %d", + version); + return FALSE; + } + + return TRUE; +} + +static int +db_get_room_event_id (CmDb *self, + int room_id, + int *out_sorted_id, + const char *event) +{ + sqlite3_stmt *stmt; + int event_id = 0; + + if (!room_id || !event || !*event) + return 0; + + g_assert (CM_IS_DB (self)); + + sqlite3_prepare_v2 (self->db, + "SELECT id,sorted_id FROM room_events WHERE room_id=? " + "AND event_uid=?", + -1, &stmt, NULL); + matrix_bind_int (stmt, 1, room_id, "binding when selecting event"); + matrix_bind_text (stmt, 2, event, "binding when selecting event"); + if (sqlite3_step (stmt) == SQLITE_ROW) + { + event_id = sqlite3_column_int (stmt, 0); + + if (out_sorted_id) + *out_sorted_id = sqlite3_column_int (stmt, 1); + } + + sqlite3_finalize (stmt); + + return event_id; +} + +static int +db_get_room_cache_event_id (CmDb *self, + int room_id, + const char *event, + gboolean insert_if_missing) +{ + sqlite3_stmt *stmt; + int event_cache_id = 0; + + if (!room_id || !event || !*event) + return 0; + + g_assert (CM_IS_DB (self)); + + sqlite3_prepare_v2 (self->db, + "SELECT id FROM room_events_cache " + "WHERE room_id=? AND event_uid=?", + -1, &stmt, NULL); + matrix_bind_int (stmt, 1, room_id, "binding when selecting cache event"); + matrix_bind_text (stmt, 2, event, "binding when selecting cache event"); + + if (sqlite3_step (stmt) == SQLITE_ROW) + event_cache_id = sqlite3_column_int (stmt, 0); + sqlite3_finalize (stmt); + + if (event_cache_id || !insert_if_missing) + return event_cache_id; + + sqlite3_prepare_v2 (self->db, + "INSERT INTO room_events_cache (room_id,event_uid) VALUES(?1,?2)", + -1, &stmt, NULL); + matrix_bind_int (stmt, 1, room_id, "binding when adding cache event"); + matrix_bind_text (stmt, 2, event, "binding when adding cache event"); + sqlite3_step (stmt); + sqlite3_finalize (stmt); + + event_cache_id = sqlite3_last_insert_rowid (self->db); + + return event_cache_id; +} + +static int +db_get_first_room_event_id (CmDb *self, + int room_id, + int *out_sorted_id) +{ + sqlite3_stmt *stmt; + int event_id = 0; + + g_assert (CM_IS_DB (self)); + + if (!room_id) + return 0; + + sqlite3_prepare_v2 (self->db, + "SELECT id,sorted_id FROM room_events WHERE room_id=? " + "ORDER BY sorted_id ASC LIMIT 1", + -1, &stmt, NULL); + matrix_bind_int (stmt, 1, room_id, "binding when selecting event"); + + if (sqlite3_step (stmt) == SQLITE_ROW) + { + event_id = sqlite3_column_int (stmt, 0); + if (out_sorted_id) + *out_sorted_id = sqlite3_column_int (stmt, 1); + } + + sqlite3_finalize (stmt); + + return event_id; +} + +/* xxx: Merge with above method with a proper name */ +static int +db_get_last_room_event_id (CmDb *self, + int room_id, + int *out_sorted_id) +{ + sqlite3_stmt *stmt; + int event_id = 0; + + g_assert (CM_IS_DB (self)); + + if (!room_id) + return 0; + + sqlite3_prepare_v2 (self->db, + "SELECT id,sorted_id FROM room_events WHERE room_id=? " + "ORDER BY sorted_id DESC LIMIT 1", + -1, &stmt, NULL); + matrix_bind_int (stmt, 1, room_id, "binding when selecting event"); + + if (sqlite3_step (stmt) == SQLITE_ROW) + { + event_id = sqlite3_column_int (stmt, 0); + if (out_sorted_id) + *out_sorted_id = sqlite3_column_int (stmt, 1); + } + + sqlite3_finalize (stmt); + + return event_id; +} + +static int +matrix_db_get_user_id (CmDb *self, + int account_id, + const char *username, + gboolean insert_if_missing) +{ + const char *query; + sqlite3_stmt *stmt; + int user_id = 0; + + if (!username || !*username) + return 0; + + g_assert (CM_IS_DB (self)); + + if (account_id) + query = "SELECT id FROM users WHERE username=? AND account_id=?"; + else + query = "SELECT id FROM users WHERE username=? AND account_id IS NULL"; + + sqlite3_prepare_v2 (self->db, query, -1, &stmt, NULL); + matrix_bind_text (stmt, 1, username, "binding when selecting user"); + if (account_id) + matrix_bind_int (stmt, 2, account_id, "binding when selecting user"); + + if (sqlite3_step (stmt) == SQLITE_ROW) + user_id = sqlite3_column_int (stmt, 0); + sqlite3_finalize (stmt); + + if (user_id || !insert_if_missing) + return user_id; + + if (account_id) + query = "INSERT INTO users(username,account_id) VALUES(?1,?2)"; + else + query = "INSERT INTO users(username) VALUES(?1)"; + + sqlite3_prepare_v2 (self->db, query, -1, &stmt, NULL); + matrix_bind_text (stmt, 1, username, "binding when adding user"); + if (account_id) + matrix_bind_int (stmt, 2, account_id, "binding when adding user"); + + sqlite3_step (stmt); + sqlite3_finalize (stmt); + user_id = sqlite3_last_insert_rowid (self->db); + + return user_id; +} + +static int +matrix_db_get_user_device_id (CmDb *self, + const char *username, + const char *device, + int *out_user_id, + gboolean insert_if_missing, + gboolean is_self) +{ + sqlite3_stmt *stmt; + int user_id = 0, user_device_id = 0; + + if (!username || !*username || !device || !*device) + return 0; + + g_assert (CM_IS_DB (self)); + + user_id = matrix_db_get_user_id (self, 0, username, insert_if_missing); + + if (out_user_id) + *out_user_id = user_id; + + if (!user_id) + return 0; + + if (device && *device) + { + sqlite3_prepare_v2 (self->db, + "SELECT user_devices.id FROM user_devices " + "WHERE user_id=?1 AND user_devices.device=?2", + -1, &stmt, NULL); + matrix_bind_int (stmt, 1, user_id, "binding when getting user device"); + matrix_bind_text (stmt, 2, device, "binding when getting user device"); + } + else + { + sqlite3_prepare_v2 (self->db, + "SELECT user_devices.id FROM user_devices " + "WHERE user_id=?1 AND user_devices.device IS NULL LIMIT 1", + -1, &stmt, NULL); + matrix_bind_int (stmt, 1, user_id, "binding when getting user device"); + } + + if (sqlite3_step (stmt) == SQLITE_ROW) + user_device_id = sqlite3_column_int (stmt, 0); + sqlite3_finalize (stmt); + + if (user_device_id || !insert_if_missing || !device || !*device) + return user_device_id; + + sqlite3_prepare_v2 (self->db, + "INSERT INTO user_devices(user_id, device, verification) " + "VALUES(?1, ?2, ?3)", + -1, &stmt, NULL); + matrix_bind_int (stmt, 1, user_id, "binding when adding user device"); + matrix_bind_text (stmt, 2, device, "binding when adding user device"); + if (is_self) + matrix_bind_int (stmt, 3, VERIFICATION_IS_SELF, "binding when adding user device"); + + sqlite3_step (stmt); + sqlite3_finalize (stmt); + user_device_id = sqlite3_last_insert_rowid (self->db); + + return user_device_id; +} + +static int +matrix_db_get_account_id (CmDb *self, + const char *username, + const char *device, + int *out_user_device_id, + gboolean insert_if_missing) +{ + sqlite3_stmt *stmt; + int user_device_id = 0; + + if (!username || !*username || !device || !*device) + return 0; + + user_device_id = matrix_db_get_user_device_id (self, username, device, NULL, insert_if_missing, TRUE); + + if (out_user_device_id) + *out_user_device_id = user_device_id; + + if (!user_device_id) + return 0; + + sqlite3_prepare_v2 (self->db, + "SELECT accounts.id FROM accounts " + "WHERE user_device_id=?1;", + -1, &stmt, NULL); + matrix_bind_int (stmt, 1, user_device_id, "binding when getting account id"); + if (sqlite3_step (stmt) == SQLITE_ROW) + return sqlite3_column_int (stmt, 0); + + if (!insert_if_missing) + return 0; + + sqlite3_prepare_v2 (self->db, + "INSERT INTO accounts(user_device_id) " + "VALUES(?1)", + -1, &stmt, NULL); + matrix_bind_int (stmt, 1, user_device_id, "binding when updating account"); + sqlite3_step (stmt); + sqlite3_finalize (stmt); + + return sqlite3_last_insert_rowid (self->db); +} + +static int +matrix_db_get_room_id (CmDb *self, + int account_id, + const char *room, + gboolean insert_if_missing) +{ + sqlite3_stmt *stmt; + int room_id = 0; + + if (!room || !*room || !account_id) + return 0; + + sqlite3_prepare_v2 (self->db, + "SELECT rooms.id FROM rooms " + "WHERE account_id=? and room_name=?", + -1, &stmt, NULL); + matrix_bind_int (stmt, 1, account_id, "binding when getting room id"); + matrix_bind_text (stmt, 2, room, "binding when getting room id"); + + if (sqlite3_step (stmt) == SQLITE_ROW) + { + room_id = sqlite3_column_int (stmt, 0); + sqlite3_finalize (stmt); + + return room_id; + } + + if (!insert_if_missing) + return 0; + + sqlite3_prepare_v2 (self->db, + "INSERT INTO rooms(account_id,room_name) " + "VALUES(?1,?2)", + -1, &stmt, NULL); + matrix_bind_int (stmt, 1, account_id, "binding when getting room id"); + matrix_bind_text (stmt, 2, room, "binding when getting room id"); + + sqlite3_step (stmt); + room_id = sqlite3_last_insert_rowid (self->db); + + sqlite3_finalize (stmt); + + return room_id; +} + +static int +db_get_room_member_id (CmDb *self, + int account_id, + int room_id, + const char *member, + int *out_user_id, + gboolean insert_if_missing) +{ + sqlite3_stmt *stmt; + int member_id = 0, user_id = 0; + + if (!member || !*member || !room_id) + return 0; + + sqlite3_prepare_v2 (self->db, + "SELECT user_id,room_members.id FROM room_members " + "INNER JOIN users ON users.username=? AND users.account_id=? " + "WHERE room_id=?", + -1, &stmt, NULL); + matrix_bind_text (stmt, 1, member, "binding when getting room member id"); + matrix_bind_int (stmt, 2, account_id, "binding when getting room member id"); + matrix_bind_int (stmt, 3, room_id, "binding when getting room member id"); + + if (sqlite3_step (stmt) == SQLITE_ROW) + { + if (out_user_id) + *out_user_id = sqlite3_column_int (stmt, 0); + + member_id = sqlite3_column_int (stmt, 1); + sqlite3_finalize (stmt); + + return member_id; + } + + if (!insert_if_missing) + return 0; + + user_id = matrix_db_get_user_id (self, account_id, member, insert_if_missing); + if (!user_id) + return 0; + + if (out_user_id) + *out_user_id = user_id; + + sqlite3_prepare_v2 (self->db, + "INSERT INTO room_members(room_id,user_id) " + "VALUES(?1,?2)", + -1, &stmt, NULL); + matrix_bind_int (stmt, 1, room_id, "binding when getting room member id"); + matrix_bind_int (stmt, 2, user_id, "binding when getting room member id"); + + sqlite3_step (stmt); + member_id = sqlite3_last_insert_rowid (self->db); + + sqlite3_finalize (stmt); + + return member_id; +} + +static void +cm_db_update_user (CmDb *self, + int user_id, + CmUser *user) +{ + g_autofree char *json_str = NULL; + sqlite3_stmt *stmt; + JsonObject *json; + + g_assert (CM_IS_DB (self)); + g_assert (CM_IS_USER (user)); + + if (!user_id) + return; + + json = cm_user_generate_json (user); + if (!json) + return; + + json_str = cm_utils_json_object_to_string (json, FALSE); + + sqlite3_prepare_v2 (self->db, + "UPDATE users SET json_data=?1 " + "WHERE id=?2", + -1, &stmt, NULL); + + matrix_bind_text (stmt, 1, json_str, "binding when updating user"); + matrix_bind_int (stmt, 2, user_id, "binding when updating user"); + + sqlite3_step (stmt); + sqlite3_finalize (stmt); +} + +static void +cm_db_update_user_device (CmDb *self, + int user_id, + CmDevice *device) +{ + const char *curve25519_key, *ed25519_key; + g_autofree char *json_str = NULL; + sqlite3_stmt *stmt; + JsonObject *json; + + g_assert (CM_IS_DB (self)); + g_assert (CM_IS_DEVICE (device)); + + if (!user_id) + return; + + curve25519_key = cm_device_get_curve_key (device); + ed25519_key = cm_device_get_ed_key (device); + json = cm_device_get_json (device); + if (json) + json_str = cm_utils_json_object_to_string (json, FALSE); + + sqlite3_prepare_v2 (self->db, + "INSERT INTO user_devices(user_id,device," + "curve25519_key,ed25519_key,json_data)" + "VALUES(?1,?2,?3,?4,?5) " + "ON CONFLICT(user_id,device) DO UPDATE SET " + "curve25519_key=?3, ed25519_key=?4, json_data=?5", + -1, &stmt, NULL); + + matrix_bind_int (stmt, 1, user_id, "binding when updating user device"); + matrix_bind_text (stmt, 2, cm_device_get_id (device), "binding when updating user device"); + if (curve25519_key && *curve25519_key) + matrix_bind_text (stmt, 3, curve25519_key, "binding when updating user device"); + if (ed25519_key && *ed25519_key) + matrix_bind_text (stmt, 4, ed25519_key, "binding when updating user device"); + matrix_bind_text (stmt, 5, json_str, "binding when updating user device"); + + sqlite3_step (stmt); + sqlite3_finalize (stmt); +} + +static void +matrix_open_db (CmDb *self, + GTask *task) +{ + const char *dir, *file_name; + sqlite3 *db; + int status; + gboolean db_exists; + + g_assert (CM_IS_DB (self)); + g_assert (G_IS_TASK (task)); + g_assert (g_thread_self () == self->worker_thread); + g_assert (!self->db); + + dir = g_object_get_data (G_OBJECT (task), "dir"); + file_name = g_object_get_data (G_OBJECT (task), "file-name"); + g_assert (dir && *dir); + g_assert (file_name && *file_name); + + g_mkdir_with_parents (dir, S_IRWXU); + self->db_path = g_build_filename (dir, file_name, NULL); + + db_exists = g_file_test (self->db_path, G_FILE_TEST_EXISTS); + status = sqlite3_open (self->db_path, &db); + + if (status == SQLITE_OK) { + self->db = db; + + sqlite3_exec (self->db, "PRAGMA foreign_keys = OFF;", NULL, NULL, NULL); + sqlite3_exec (self->db, "BEGIN TRANSACTION;", NULL, NULL, NULL); + if (db_exists) { + if (!cm_db_migrate (self, task)) + { + sqlite3_exec (self->db, "END TRANSACTION;", NULL, NULL, NULL); + return; + } + } else { + if (!cm_db_create_schema (self, task)) + { + sqlite3_exec (self->db, "END TRANSACTION;", NULL, NULL, NULL); + return; + } + } + + sqlite3_exec (self->db, "PRAGMA foreign_keys = ON;", NULL, NULL, NULL); + sqlite3_exec (self->db, "END TRANSACTION;", NULL, NULL, NULL); + g_task_return_boolean (task, TRUE); + } else { + g_task_return_boolean (task, FALSE); + sqlite3_close (db); + } +} + +static void +matrix_close_db (CmDb *self, + GTask *task) +{ + sqlite3 *db; + int status; + + g_assert (CM_IS_DB (self)); + g_assert (G_IS_TASK (task)); + g_assert (g_thread_self () == self->worker_thread); + g_assert (self->db); + + db = self->db; + self->db = NULL; + status = sqlite3_close (db); + + if (status == SQLITE_OK) + { + /* + * We can’t know when will @self associated with the task will + * be unref. So cm_db_get_default() called immediately + * after this may return the @self that is yet to be free. But + * as the worker_thread is exited after closing the database, any + * actions with the same @self will not execute, and so the tasks + * will take ∞ time to complete. + * + * So Instead of relying on GObject to free the object, Let’s + * explicitly run dispose + */ + g_object_run_dispose (G_OBJECT (self)); + g_debug ("Database closed successfully"); + g_task_return_boolean (task, TRUE); + } + else + { + g_task_return_new_error (task, + G_IO_ERROR, + G_IO_ERROR_FAILED, + "Database could not be closed. errno: %d, desc: %s", + status, sqlite3_errmsg (db)); + } +} + +static void +cm_db_save_client (CmDb *self, + GTask *task) +{ + const char *device, *pickle, *username, *batch, *filter; + g_autofree char *json_str = NULL; + JsonObject *root, *obj; + sqlite3_stmt *stmt; + int status, user_device_id = 0, account_id = 0; + gboolean enabled; + + g_assert (CM_IS_DB (self)); + g_assert (G_IS_TASK (task)); + g_assert (g_thread_self () == self->worker_thread); + g_assert (self->db); + + batch = g_object_get_data (G_OBJECT (task), "batch"); + pickle = g_object_get_data (G_OBJECT (task), "pickle"); + device = g_object_get_data (G_OBJECT (task), "device"); + enabled = GPOINTER_TO_INT (g_object_get_data (G_OBJECT (task), "enabled")); + username = g_object_get_data (G_OBJECT (task), "username"); + filter = g_object_get_data (G_OBJECT (task), "filter-id"); + + sqlite3_exec (self->db, "BEGIN TRANSACTION;", NULL, NULL, NULL); + account_id = matrix_db_get_account_id (self, username, device, &user_device_id, TRUE); + + if (!account_id) + { + sqlite3_exec (self->db, "END TRANSACTION;", NULL, NULL, NULL); + g_task_return_new_error (task, G_IO_ERROR, G_IO_ERROR, + "Failed to add account to db"); + return; + } + + if (filter && *filter) + { + root = json_object_new (); + obj = json_object_new (); + json_object_set_object_member (root, "local", obj); + + if (filter && *filter) + json_object_set_string_member (obj, "filter-id", filter); + + json_str = cm_utils_json_object_to_string (root, FALSE); + } + + sqlite3_prepare_v2 (self->db, + "INSERT INTO accounts(user_device_id,pickle," + "next_batch,enabled,json_data) " + "VALUES(?1,?2,?3,?4,?5) " + "ON CONFLICT(user_device_id) " + "DO UPDATE SET pickle=?2, next_batch=?3, enabled=?4, json_data=?5", + -1, &stmt, NULL); + + matrix_bind_int (stmt, 1, user_device_id, "binding when updating account"); + if (pickle && *pickle) + matrix_bind_text (stmt, 2, pickle, "binding when updating account"); + matrix_bind_text (stmt, 3, batch, "binding when updating account"); + matrix_bind_int (stmt, 4, enabled, "binding when updating account"); + matrix_bind_text (stmt, 5, json_str, "binding when updating account"); + + status = sqlite3_step (stmt); + sqlite3_finalize (stmt); + sqlite3_exec (self->db, "END TRANSACTION;", NULL, NULL, NULL); + + if (status == SQLITE_DONE) + g_task_return_boolean (task, TRUE); + else + g_task_return_new_error (task, + G_IO_ERROR, + G_IO_ERROR_FAILED, + "Error saving account. errno: %d, desc: %s", + status, sqlite3_errmsg (self->db)); +} + +static int +cm_db_get_room_id (CmDb *self, + GTask *task, + const char *room, + int account_id) +{ + sqlite3_stmt *stmt; + const char *error; + int status; + + g_assert (room && *room); + g_assert (account_id); + + sqlite3_prepare_v2 (self->db, + "SELECT rooms.id FROM rooms " + "WHERE room_name=? and account_id=?", + -1, &stmt, NULL); + matrix_bind_text (stmt, 1, room, "binding when getting room id"); + matrix_bind_int (stmt, 2, account_id, "binding when getting room id"); + + status = sqlite3_step (stmt); + if (status == SQLITE_ROW) + return sqlite3_column_int (stmt, 0); + + if (status == SQLITE_DONE) + error = "Room not found in db"; + else + error = sqlite3_errmsg (self->db); + + g_task_return_new_error (task, + G_IO_ERROR, + G_IO_ERROR_FAILED, + "Couldn't find room %s. error: %s", + room, error); + return 0; +} + +static GPtrArray * +cm_db_get_rooms (CmDb *self, + int account_id, + const char *account_next_batch) +{ + g_autoptr(GPtrArray) rooms = NULL; + sqlite3_stmt *stmt; + + g_assert (CM_IS_DB (self)); + g_assert (account_id); + + sqlite3_prepare_v2 (self->db, + "SELECT id,room_name,prev_batch,json_data,room_state FROM rooms " + "WHERE account_id=? AND replacement_room_id IS NULL " + "AND room_state != ?", + -1, &stmt, NULL); + matrix_bind_int (stmt, 1, account_id, "binding when getting rooms"); + matrix_bind_int (stmt, 2, CM_STATUS_LEAVE, "binding when getting rooms"); + + while (sqlite3_step (stmt) == SQLITE_ROW) + { + g_autoptr(GPtrArray) events = NULL; + char *room_name, *prev_batch, *json_str; + JsonObject *json = NULL; + CmRoom *room; + int room_id, event_id, sorted_event_id = 0; + + if (!rooms) + rooms = g_ptr_array_new_full (32, g_object_unref); + + room_id = sqlite3_column_int (stmt, 0); + room_name = (char *)sqlite3_column_text (stmt, 1); + prev_batch = (char *)sqlite3_column_text (stmt, 2); + json_str = (char *)sqlite3_column_text (stmt, 3); + json = cm_utils_string_to_json_object (json_str); + + room = cm_room_new_from_json (room_name, json, NULL); + g_object_set_data (G_OBJECT (room), "-cm-room-id", GINT_TO_POINTER (room_id)); + cm_room_set_prev_batch (room, prev_batch); + cm_room_set_status (room, sqlite3_column_int (stmt, 4)); + + event_id = db_get_last_room_event_id (self, room_id, &sorted_event_id); + if (event_id) + events = db_get_past_room_events (self, room, room_id, 0, sorted_event_id, 1); + else + cm_room_set_prev_batch (room, account_next_batch); + + if (events) + cm_room_add_events (room, events, TRUE); + + g_ptr_array_add (rooms, room); + } + + sqlite3_finalize (stmt); + + return g_steal_pointer (&rooms); +} + +static void +cm_db_load_client (CmDb *self, + GTask *task) +{ + sqlite3_stmt *stmt; + char *username, *device_id; + int status, account_id = 0; + + g_assert (CM_IS_DB (self)); + g_assert (G_IS_TASK (task)); + g_assert (g_thread_self () == self->worker_thread); + g_assert (self->db); + + username = g_object_get_data (G_OBJECT (task), "username"); + device_id = g_object_get_data (G_OBJECT (task), "device"); + + account_id = matrix_db_get_account_id (self, username, device_id, NULL, FALSE); + + if (!account_id) + { + g_task_return_new_error (task, G_IO_ERROR, G_IO_ERROR_NOT_FOUND, + "Account not in db"); + return; + } + + status = sqlite3_prepare_v2 (self->db, + "SELECT pickle,next_batch,json_data " + "FROM accounts WHERE accounts.id=?", + -1, &stmt, NULL); + + matrix_bind_int (stmt, 1, account_id, "binding when loading account"); + status = sqlite3_step (stmt); + + if (status == SQLITE_ROW) + { + const char *filter; + GObject *object = G_OBJECT (task); + g_autoptr(JsonObject) json = NULL; + JsonObject *child; + GPtrArray *rooms; + + g_object_set_data_full (object, "pickle", g_strdup ((char *)sqlite3_column_text (stmt, 0)), g_free); + g_object_set_data_full (object, "batch", g_strdup ((char *)sqlite3_column_text (stmt, 1)), g_free); + + json = cm_utils_string_to_json_object ((char *)sqlite3_column_text (stmt, 2)); + child = cm_utils_json_object_get_object (json, "local"); + filter = cm_utils_json_object_get_string (child, "filter-id"); + + /* If we don't have json_data the db was just migrated from older version */ + if (sqlite3_column_text (stmt, 2) == NULL) + g_object_set_data (object, "db-migrated", GINT_TO_POINTER (TRUE)); + + if (filter && *filter) + g_object_set_data_full (object, "filter-id", g_strdup (filter), g_free); + + rooms = cm_db_get_rooms (self, account_id, (char *)sqlite3_column_text (stmt, 1)); + g_object_set_data_full (object, "rooms", rooms, (GDestroyNotify)g_ptr_array_unref); + } + + sqlite3_finalize (stmt); + g_task_return_boolean (task, status == SQLITE_ROW); +} + +static void +cm_db_save_room (CmDb *self, + GTask *task) +{ + CmRoom *room; + const char *username, *client_device, *prev_batch; + const char *replacement, *json = NULL; + sqlite3_stmt *stmt; + int account_id, room_id = 0, replacement_id = 0; + int room_status; + + g_assert (CM_IS_DB (self)); + g_assert (G_IS_TASK (task)); + g_assert (g_thread_self () == self->worker_thread); + g_assert (self->db); + + username = g_object_get_data (G_OBJECT (task), "username"); + client_device = g_object_get_data (G_OBJECT (task), "client-device"); + room = g_object_get_data (G_OBJECT (task), "room"); + json = g_object_get_data (G_OBJECT (task), "json"); + prev_batch = g_object_get_data (G_OBJECT (task), "prev-batch"); + replacement = g_object_get_data (G_OBJECT (task), "replacement"); + room_status = GPOINTER_TO_INT (g_object_get_data (G_OBJECT (task), "status")); + + sqlite3_exec (self->db, "BEGIN TRANSACTION;", NULL, NULL, NULL); + account_id = matrix_db_get_account_id (self, username, client_device, NULL, FALSE); + + if (!account_id) + { + sqlite3_exec (self->db, "END TRANSACTION;", NULL, NULL, NULL); + g_task_return_new_error (task, G_IO_ERROR, G_IO_ERROR, + "Error getting account id"); + return; + } + + if (replacement) + replacement_id = matrix_db_get_room_id (self, account_id, + replacement, TRUE); + room_id = matrix_db_get_room_id (self, account_id, cm_room_get_id (room), TRUE); + + if (!room_id) + { + sqlite3_exec (self->db, "END TRANSACTION;", NULL, NULL, NULL); + g_task_return_new_error (task, G_IO_ERROR, G_IO_ERROR, + "Error getting room id"); + return; + } + + if (!cm_room_has_state_sync (room)) + json = NULL; + + sqlite3_prepare_v2 (self->db, + "UPDATE rooms SET prev_batch=?1,json_data=?2, " + "replacement_room_id=iif(?3 = 0, null, ?3),room_state=?4 " + "WHERE id=?5", + -1, &stmt, NULL); + + matrix_bind_text (stmt, 1, prev_batch, "binding when saving room"); + matrix_bind_text (stmt, 2, json, "binding when saving room"); + matrix_bind_int (stmt, 3, replacement_id, "binding when saving room"); + matrix_bind_int (stmt, 4, room_status, "binding when saving room"); + matrix_bind_int (stmt, 5, room_id, "binding when saving room"); + + sqlite3_step (stmt); + sqlite3_finalize (stmt); + sqlite3_exec (self->db, "END TRANSACTION;", NULL, NULL, NULL); + + g_task_return_boolean (task, TRUE); +} + +static void +cm_db_delete_client (CmDb *self, + GTask *task) +{ + sqlite3_stmt *stmt; + char *username, *device_id; + int account_id; + int status; + + g_assert (CM_IS_DB (self)); + g_assert (G_IS_TASK (task)); + g_assert (g_thread_self () == self->worker_thread); + g_assert (self->db); + + username = g_object_get_data (G_OBJECT (task), "username"); + device_id = g_object_get_data (G_OBJECT (task), "device-id"); + + sqlite3_exec (self->db, "BEGIN TRANSACTION;", NULL, NULL, NULL); + account_id = matrix_db_get_account_id (self, username, device_id, NULL, FALSE); + + if (!account_id) + { + sqlite3_exec (self->db, "END TRANSACTION;", NULL, NULL, NULL); + g_task_return_new_error (task, G_IO_ERROR, G_IO_ERROR, + "Error getting account id"); + return; + } + + status = sqlite3_prepare_v2 (self->db, + "DELETE FROM sessions " + "WHERE sessions.account_id=?1; ", + -1, &stmt, NULL); + matrix_bind_int (stmt, 1, account_id, "binding when deleting account"); + sqlite3_step (stmt); + sqlite3_finalize (stmt); + + status = sqlite3_prepare_v2 (self->db, + "DELETE FROM rooms " + "WHERE rooms.account_id=?1; ", + -1, &stmt, NULL); + matrix_bind_int (stmt, 1, account_id, "binding when deleting account"); + sqlite3_step (stmt); + sqlite3_finalize (stmt); + + status = sqlite3_prepare_v2 (self->db, + "DELETE FROM accounts " + "WHERE accounts.id=?1; ", + -1, &stmt, NULL); + + matrix_bind_int (stmt, 1, account_id, "binding when deleting account"); + + status = sqlite3_step (stmt); + sqlite3_finalize (stmt); + sqlite3_exec (self->db, "END TRANSACTION;", NULL, NULL, NULL); + + g_task_return_boolean (task, status == SQLITE_ROW); +} + +static void +db_add_session (CmDb *self, + GTask *task) +{ + CmOlm *session; + sqlite3_stmt *stmt; + const char *username, *account_device, *session_id, *sender_key, *pickle, *room; + CmSessionType type; + CmOlmState state; + int status, account_id, room_id = 0; + + g_assert (CM_IS_DB (self)); + g_assert (G_IS_TASK (task)); + g_assert (g_thread_self () == self->worker_thread); + g_assert (self->db); + + session = g_object_get_data (G_OBJECT (task), "session"); + g_assert (CM_IS_OLM (session)); + + room = cm_olm_get_room_id (session); + type = cm_olm_get_session_type (session); + username = cm_olm_get_account_id (session); + session_id = cm_olm_get_session_id (session); + sender_key = cm_olm_get_sender_key (session); + account_device = cm_olm_get_account_device (session); + pickle = g_object_get_data (G_OBJECT (task), "pickle"); + state = GPOINTER_TO_INT (g_object_get_data (G_OBJECT (task), "state")); + + account_id = matrix_db_get_account_id (self, username, account_device, NULL, FALSE); + + if (!account_id) + { + g_task_return_new_error (task, G_IO_ERROR, G_IO_ERROR, + "Error getting account id"); + return; + } + + if (room) + room_id = cm_db_get_room_id (self, task, room, account_id); + + status = sqlite3_prepare_v2 (self->db, + /* 1 2 3 */ + "INSERT INTO sessions(account_id,sender_key,session_id," + /* 4 5 6 7 8 9 10 */ + "type,pickle,room_id,time,session_state,origin_server_ts,json_data) " + "VALUES(?1,?2,?3,?4,?5,?6,?7,?8,?9,?10) " + "ON CONFLICT(account_id, sender_key, session_id) DO UPDATE SET " + "pickle=?5, session_state=?8", + -1, &stmt, NULL); + + matrix_bind_int (stmt, 1, account_id, "binding when adding session"); + matrix_bind_text (stmt, 2, sender_key, "binding when adding session"); + matrix_bind_text (stmt, 3, session_id, "binding when adding session"); + matrix_bind_int (stmt, 4, type, "binding when adding session"); + matrix_bind_text (stmt, 5, pickle, "binding when adding session"); + if (room_id) + matrix_bind_int (stmt, 6, room_id, "binding when adding session"); + /* Save time in milliseconds */ + matrix_bind_int (stmt, 7, time (NULL) * 1000, "binding when adding session"); + matrix_bind_int (stmt, 8, state, "binding when adding session"); + + if (!g_object_get_data (G_OBJECT (session), "-cm-db-id")) + { + g_autofree char *json_str = NULL; + JsonObject *json, *child; + + g_object_set_data (G_OBJECT (session), "-cm-db-id", + GINT_TO_POINTER (sqlite3_last_insert_rowid (self->db))); + json = json_object_new (); + json_object_set_object_member (json, "local", json_object_new ()); + child = cm_utils_json_object_get_object (json, "local"); + json_object_set_string_member (child, "first_pickle", pickle); + json_str = cm_utils_json_object_to_string (json, FALSE); + + matrix_bind_text (stmt, 10, json_str, "binding when adding session"); + } + + status = sqlite3_step (stmt); + sqlite3_finalize (stmt); + + if (status != SQLITE_DONE) + g_task_return_new_error (task, G_IO_ERROR, G_IO_ERROR_FAILED, + "%s", sqlite3_errmsg (self->db)); + else + g_task_return_boolean (task, TRUE); +} + +static void +cm_db_save_file_enc (CmDb *self, + GTask *task) +{ + CmEncFileInfo *file; + sqlite3_stmt *stmt; + int status, algorithm = 0, version = 0, type = 0; + + g_assert (CM_IS_DB (self)); + g_assert (G_IS_TASK (task)); + g_assert (g_thread_self () == self->worker_thread); + g_assert (self->db); + + file = g_object_get_data (G_OBJECT (task), "file"); + g_assert (file && file->mxc_uri); + + if (g_strcmp0 (file->algorithm, "A256CTR") == 0) + algorithm = CMATRIX_ALGORITHM_A256CTR; + + if (g_strcmp0 (file->kty, "oct") == 0) + type = CMATRIX_KEY_TYPE_OCT; + + if (g_strcmp0 (file->version, "v2") == 0) + version = 2; + + status = sqlite3_prepare_v2 (self->db, + "INSERT INTO encryption_keys(file_url,file_sha256," + "iv,version,algorithm,key,type,extractable) " + "VALUES(?1,?2,?3,?4,?5,?6,?7,?8)", + -1, &stmt, NULL); + + matrix_bind_text (stmt, 1, file->mxc_uri, "binding when adding file url"); + matrix_bind_text (stmt, 2, file->sha256_base64, "binding when adding file url"); + matrix_bind_text (stmt, 3, file->aes_iv_base64, "binding when adding file url"); + matrix_bind_int (stmt, 4, version, "binding when adding file url"); + matrix_bind_int (stmt, 5, algorithm, "binding when adding file url"); + matrix_bind_text (stmt, 6, file->aes_key_base64, "binding when adding file url"); + matrix_bind_int (stmt, 7, type, "binding when adding file url"); + matrix_bind_int (stmt, 8, file->extractable, "binding when adding file url"); + + status = sqlite3_step (stmt); + sqlite3_finalize (stmt); + + g_task_return_boolean (task, status == SQLITE_DONE); +} + +static void +cm_db_find_file_enc (CmDb *self, + GTask *task) +{ + sqlite3_stmt *stmt; + CmEncFileInfo *file = NULL; + char *uri; + + g_assert (CM_IS_DB (self)); + g_assert (G_IS_TASK (task)); + g_assert (g_thread_self () == self->worker_thread); + g_assert (self->db); + + uri = g_object_get_data (G_OBJECT (task), "uri"); + g_assert (uri && *uri); + + sqlite3_prepare_v2 (self->db, + "SELECT file_sha256,iv,key " + "FROM encryption_keys WHERE file_url=?1", + -1, &stmt, NULL); + matrix_bind_text (stmt, 1, uri, "binding when looking up file encryption"); + + if (sqlite3_step (stmt) == SQLITE_ROW) + { + file = g_new0 (CmEncFileInfo, 1); + file->mxc_uri = g_strdup (uri); + file->sha256_base64 = g_strdup ((char *)sqlite3_column_text (stmt, 0)); + file->aes_iv_base64 = g_strdup ((char *)sqlite3_column_text (stmt, 1)); + file->aes_key_base64 = g_strdup ((char *)sqlite3_column_text (stmt, 2)); + + if (!g_str_has_prefix (uri, "mxc://")) + g_clear_pointer (&file->mxc_uri, g_free); + } + + g_task_return_pointer (task, file, cm_enc_file_info_free); +} + +static void +db_lookup_session (CmDb *self, + GTask *task) +{ + sqlite3_stmt *stmt; + const char *username, *account_device, *session_id, *sender_key; + char *pickle_key, *room; + CmOlm *session = NULL; + CmSessionType type; + int status, account_id, room_id = 0; + + g_assert (CM_IS_DB (self)); + g_assert (g_thread_self () == self->worker_thread); + g_assert (G_IS_TASK (task)); + + type = GPOINTER_TO_INT (g_object_get_data (G_OBJECT (task), "type")); + + room = g_object_get_data (G_OBJECT (task), "room-id"); + username = g_object_get_data (G_OBJECT (task), "account-id"); + session_id = g_object_get_data (G_OBJECT (task), "session-id"); + sender_key = g_object_get_data (G_OBJECT (task), "sender-key"); + pickle_key = g_object_get_data (G_OBJECT (task), "pickle-key"); + account_device = g_object_get_data (G_OBJECT (task), "account-device"); + + account_id = matrix_db_get_account_id (self, username, account_device, NULL, FALSE); + + if (!account_id) + { + g_task_return_new_error (task, G_IO_ERROR, G_IO_ERROR, + "Error getting account id"); + return; + } + + /* If no session-id is given, only MegOlm out session can be requested */ + if (!session_id && type != SESSION_MEGOLM_V1_OUT) + { + g_task_return_new_error (task, G_IO_ERROR, G_IO_ERROR, + "Requested session without session id"); + return; + } + + if (room) + room_id = matrix_db_get_room_id (self, account_id, room, FALSE); + + if (session_id) + sqlite3_prepare_v2 (self->db, + "SELECT id,pickle FROM sessions " + "WHERE account_id=? AND sender_key=? AND type=? " + "AND session_id=? AND session_state=0", + -1, &stmt, NULL); + else + sqlite3_prepare_v2 (self->db, + "SELECT id,pickle FROM sessions " + "WHERE account_id=? AND sender_key=? AND type=?" + "AND room_id=? AND session_state=0 " + "ORDER BY id DESC LIMIT 1", + -1, &stmt, NULL); + + + matrix_bind_int (stmt, 1, account_id, "binding when looking up session"); + matrix_bind_text (stmt, 2, sender_key, "binding when looking up session"); + matrix_bind_int (stmt, 3, type, "binding when looking up session"); + if (session_id) + matrix_bind_text (stmt, 4, session_id, "binding when looking up session"); + else if (room_id) + matrix_bind_int (stmt, 4, room_id, "binding when looking up session"); + + status = sqlite3_step (stmt); + + if (status == SQLITE_ROW) + { + g_autofree char *pickle = NULL; + int id; + + pickle = g_strdup ((const char *)sqlite3_column_text (stmt, 1)); + session = cm_olm_new_from_pickle (pickle, pickle_key, sender_key, type); + id = sqlite3_column_int (stmt, 0); + + if (session) + g_object_set_data (G_OBJECT (session), "-cm-db-id", GINT_TO_POINTER (id)); + } + + sqlite3_finalize (stmt); + g_task_return_pointer (task, session, g_object_unref); +} + +static void +db_lookup_olm_session (CmDb *self, + GTask *task) +{ + sqlite3_stmt *stmt; + const char *username, *account_device, *sender_curve_key, *body; + char *pickle_key, *plain_text = NULL; + gpointer session = NULL; + CmSessionType type; + size_t message_type; + int account_id; + + g_assert (CM_IS_DB (self)); + g_assert (g_thread_self () == self->worker_thread); + g_assert (G_IS_TASK (task)); + + type = GPOINTER_TO_INT (g_object_get_data (G_OBJECT (task), "type")); + message_type = GPOINTER_TO_UINT (g_object_get_data (G_OBJECT (task), "message-type")); + + username = g_object_get_data (G_OBJECT (task), "account-id"); + sender_curve_key = g_object_get_data (G_OBJECT (task), "sender-key"); + account_device = g_object_get_data (G_OBJECT (task), "account-device"); + body = g_object_get_data (G_OBJECT (task), "body"); + pickle_key = g_strdup (g_object_get_data (G_OBJECT (task), "pickle-key")); + + account_id = matrix_db_get_account_id (self, username, account_device, NULL, FALSE); + + if (!account_id) + { + g_task_return_new_error (task, G_IO_ERROR, G_IO_ERROR, + "Error getting account id"); + return; + } + + sqlite3_prepare_v2 (self->db, + "SELECT pickle FROM sessions " + "WHERE account_id=? AND sender_key=? AND type=?", + -1, &stmt, NULL); + + matrix_bind_int (stmt, 1, account_id, "binding when looking up olm session"); + matrix_bind_text (stmt, 2, sender_curve_key, "binding when looking up olm session"); + matrix_bind_int (stmt, 3, type, "binding when looking up olm session"); + + while (sqlite3_step (stmt) == SQLITE_ROW) + { + char *pickle; + + pickle = (char *)sqlite3_column_text (stmt, 0); + session = cm_olm_match_olm_session (body, strlen (body), message_type, + pickle, pickle_key, sender_curve_key, + type, &plain_text); + + if (session) + break; + } + + cm_utils_free_buffer (pickle_key); + sqlite3_finalize (stmt); + + g_object_set_data_full (G_OBJECT (task), "plaintext", plain_text, + (GDestroyNotify)cm_utils_free_buffer); + g_task_return_pointer (task, session, g_object_unref); +} + +static void +db_update_user_tracking (CmDb *self, + int user_id, + gboolean outdated, + gboolean is_tracking) +{ + sqlite3_stmt *stmt; + + g_assert (CM_IS_DB (self)); + g_assert (g_thread_self () == self->worker_thread); + g_assert (user_id); + + sqlite3_prepare_v2 (self->db, + "UPDATE users SET tracking=?1, outdated=?2 " + "WHERE id=?3", + -1, &stmt, NULL); + + matrix_bind_int (stmt, 1, is_tracking, "binding add user device"); + matrix_bind_int (stmt, 2, outdated, "binding add user device"); + matrix_bind_int (stmt, 3, user_id, "binding add user device"); + sqlite3_step (stmt); + sqlite3_finalize (stmt); +} + +static void +db_mark_user_device_change (CmDb *self, + GTask *task) +{ + GPtrArray *users; + const char *username, *account_device; + gboolean outdated, is_tracking; + int account_id; + + g_assert (CM_IS_DB (self)); + g_assert (g_thread_self () == self->worker_thread); + g_assert (G_IS_TASK (task)); + + is_tracking = GPOINTER_TO_INT (g_object_get_data (G_OBJECT (task), "tracking")); + outdated = GPOINTER_TO_INT (g_object_get_data (G_OBJECT (task), "outdated")); + username = g_object_get_data (G_OBJECT (task), "account-id"); + users = g_object_get_data (G_OBJECT (task), "users"); + account_device = g_object_get_data (G_OBJECT (task), "account-device"); + g_assert (users && users->len); + + sqlite3_exec (self->db, "BEGIN TRANSACTION;", NULL, NULL, NULL); + account_id = matrix_db_get_account_id (self, username, account_device, NULL, FALSE); + + if (!account_id) + { + sqlite3_exec (self->db, "END TRANSACTION;", NULL, NULL, NULL); + g_task_return_new_error (task, G_IO_ERROR, G_IO_ERROR, + "Error getting account id"); + return; + } + + for (guint i = 0; i < users->len; i++) + { + int user_id; + + username = cm_user_get_id (users->pdata[i]); + user_id = matrix_db_get_user_id (self, account_id, username, TRUE); + db_update_user_tracking (self, user_id, outdated, is_tracking); + } + sqlite3_exec (self->db, "END TRANSACTION;", NULL, NULL, NULL); + + g_task_return_boolean (task, TRUE); +} + +static void +db_update_user_devices (CmDb *self, + GTask *task) +{ + const char *account_username, *account_device, *username; + GPtrArray *added, *removed; + int account_id, user_id, force_add; + + g_assert (CM_IS_DB (self)); + g_assert (g_thread_self () == self->worker_thread); + g_assert (G_IS_TASK (task)); + + account_username = g_object_get_data (G_OBJECT (task), "account-id"); + account_device = g_object_get_data (G_OBJECT (task), "account-device"); + force_add = GPOINTER_TO_INT (g_object_get_data (G_OBJECT (task), "force-add")); + username = g_object_get_data (G_OBJECT (task), "username"); + removed = g_object_get_data (G_OBJECT (task), "removed"); + added = g_object_get_data (G_OBJECT (task), "added"); + g_assert (added || removed); + + sqlite3_exec (self->db, "BEGIN TRANSACTION;", NULL, NULL, NULL); + account_id = matrix_db_get_account_id (self, account_username, account_device, NULL, FALSE); + + if (!account_id) + { + sqlite3_exec (self->db, "END TRANSACTION;", NULL, NULL, NULL); + g_task_return_new_error (task, G_IO_ERROR, G_IO_ERROR, + "Error getting account id"); + return; + } + + user_id = matrix_db_get_user_id (self, account_id, username, force_add); + + if (!user_id) + { + sqlite3_exec (self->db, "END TRANSACTION;", NULL, NULL, NULL); + g_task_return_new_error (task, G_IO_ERROR, G_IO_ERROR_NOT_FOUND, + "User not in db"); + return; + } + + for (guint i = 0; added && i < added->len; i++) + { + sqlite3_stmt *stmt; + CmDevice *device; + gboolean verified; + + device = added->pdata[i]; + verified = cm_device_is_verified (device); + sqlite3_prepare_v2 (self->db, + "INSERT INTO user_devices(user_id,device,curve25519_key,ed25519_key,verification) " + "VALUES(?1,?2,?3,?4,?5) ON CONFLICT(user_id,device) DO UPDATE SET " + "verification=?5", + -1, &stmt, NULL); + + matrix_bind_int (stmt, 1, user_id, "binding add user device"); + matrix_bind_text (stmt, 2, cm_device_get_id (device), "binding add user device"); + matrix_bind_text (stmt, 3, cm_device_get_curve_key (device), "binding add user device"); + matrix_bind_text (stmt, 4, cm_device_get_ed_key (device), "binding add user device"); + if (verified) + matrix_bind_int (stmt, 5, VERIFICATION_VERIFIED, "binding add user device"); + + sqlite3_step (stmt); + sqlite3_finalize (stmt); + } + + for (guint i = 0; removed && i < removed->len; i++) + { + sqlite3_stmt *stmt; + CmDevice *device; + + device = removed->pdata[i]; + sqlite3_prepare_v2 (self->db, + "DELETE FROM user_devices WHERE user_id=?1 AND device=?2", + -1, &stmt, NULL); + + matrix_bind_int (stmt, 1, user_id, "binding add user device"); + matrix_bind_text (stmt, 2, cm_device_get_id (device), "binding add user device"); + sqlite3_step (stmt); + sqlite3_finalize (stmt); + } + + db_update_user_tracking (self, user_id, FALSE, TRUE); + sqlite3_exec (self->db, "END TRANSACTION;", NULL, NULL, NULL); + + g_task_return_boolean (task, TRUE); +} + +static void +db_update_user_device (CmDb *self, + GTask *task) +{ + const char *account_username, *account_device, *username; + sqlite3_stmt *stmt; + CmDevice *device; + int account_id, user_id; + gboolean verified; + + g_assert (CM_IS_DB (self)); + g_assert (g_thread_self () == self->worker_thread); + g_assert (G_IS_TASK (task)); + + account_username = g_object_get_data (G_OBJECT (task), "account-id"); + account_device = g_object_get_data (G_OBJECT (task), "account-device"); + username = g_object_get_data (G_OBJECT (task), "username"); + device = g_object_get_data (G_OBJECT (task), "device"); + g_assert (device); + + sqlite3_exec (self->db, "BEGIN TRANSACTION;", NULL, NULL, NULL); + account_id = matrix_db_get_account_id (self, account_username, account_device, NULL, FALSE); + + if (!account_id) + { + sqlite3_exec (self->db, "END TRANSACTION;", NULL, NULL, NULL); + g_task_return_new_error (task, G_IO_ERROR, G_IO_ERROR, + "Error getting account id"); + return; + } + + user_id = matrix_db_get_user_id (self, account_id, username, FALSE); + + if (!user_id) + { + sqlite3_exec (self->db, "END TRANSACTION;", NULL, NULL, NULL); + g_task_return_new_error (task, G_IO_ERROR, G_IO_ERROR_NOT_FOUND, + "User not in db"); + return; + } + + verified = cm_device_is_verified (device); + + sqlite3_prepare_v2 (self->db, + "INSERT INTO user_devices(user_id,device,curve25519_key,ed25519_key,verification) " + "VALUES(?1,?2,?3,?4,?5) ON CONFLICT(user_id,device) DO UPDATE SET " + "verification=?5", + -1, &stmt, NULL); + + matrix_bind_int (stmt, 1, user_id, "binding add user device"); + matrix_bind_text (stmt, 2, cm_device_get_id (device), "binding add user device"); + matrix_bind_text (stmt, 3, cm_device_get_curve_key (device), "binding add user device"); + matrix_bind_text (stmt, 4, cm_device_get_ed_key (device), "binding add user device"); + if (verified) + matrix_bind_int (stmt, 5, VERIFICATION_VERIFIED, "binding add user device"); + + sqlite3_step (stmt); + sqlite3_finalize (stmt); + + sqlite3_exec (self->db, "END TRANSACTION;", NULL, NULL, NULL); + + g_task_return_boolean (task, TRUE); +} + +static void +cm_db_delete_event_with_txn_id (CmDb *self, + int room_id, + const char *txnid) +{ + sqlite3_stmt *stmt; + + g_assert (CM_IS_DB (self)); + + if (!room_id || !txnid || !*txnid) + return; + + sqlite3_prepare_v2 (self->db, + "DELETE FROM room_events " + "WHERE room_id=? AND txnid=? AND event_uid IS NULL", + -1, &stmt, NULL); + matrix_bind_int (stmt, 1, room_id, "binding when deleting room event txnid"); + matrix_bind_text (stmt, 2, txnid, "binding when deleting room event txnid"); + sqlite3_step (stmt); + sqlite3_finalize (stmt); +} + +static void +db_add_room_members (CmDb *self, + GTask *task) +{ + const char *username, *device, *room; + GPtrArray *members; + int room_id, account_id; + + g_assert (CM_IS_DB (self)); + g_assert (G_IS_TASK (task)); + g_assert (g_thread_self () == self->worker_thread); + g_assert (self->db); + + username = g_object_get_data (G_OBJECT (task), "username"); + members = g_object_get_data (G_OBJECT (task), "members"); + device = g_object_get_data (G_OBJECT (task), "device"); + room = g_object_get_data (G_OBJECT (task), "room"); + g_assert (members && members->len); + + account_id = matrix_db_get_account_id (self, username, device, NULL, FALSE); + room_id = matrix_db_get_room_id (self, account_id, room, FALSE); + + if (!room_id) + { + g_task_return_new_error (task, G_IO_ERROR, G_IO_ERROR_NOT_FOUND, + "Account or Room not found in db"); + return; + } + + /* todo: Look into sqlite transactions */ + for (guint i = 0; i < members->len; i++) + { + CmUser *cm_user = members->pdata[i]; + GListModel *devices; + guint n_items; + int member_id, user_id = 0; + + username = cm_user_get_id (cm_user); + member_id = db_get_room_member_id (self, account_id, room_id, username, &user_id, TRUE); + + if (!member_id) + continue; + + cm_db_update_user (self, user_id, cm_user); + devices = cm_user_get_devices (cm_user); + n_items = g_list_model_get_n_items (devices); + + for (guint j = 0; j < n_items; j++) + { + g_autoptr(CmDevice) cm_device = NULL; + + cm_device = g_list_model_get_item (devices, j); + cm_db_update_user_device (self, user_id, cm_device); + } + } + + g_task_return_boolean (task, TRUE); +} + +static void +db_add_room_events (CmDb *self, + GTask *task) +{ + const char *username, *device, *room; + sqlite3_stmt *stmt; + GPtrArray *events; + int room_id, account_id, sorted_event_id = 0, match_id = 0; + gboolean prepend; + + g_assert (CM_IS_DB (self)); + g_assert (G_IS_TASK (task)); + g_assert (g_thread_self () == self->worker_thread); + g_assert (self->db); + + username = g_object_get_data (G_OBJECT (task), "username"); + device = g_object_get_data (G_OBJECT (task), "device"); + room = g_object_get_data (G_OBJECT (task), "room"); + events = g_object_get_data (G_OBJECT (task), "events"); + prepend = GPOINTER_TO_INT (g_object_get_data (G_OBJECT (task), "prepend")); + g_assert (events && events->len); + + sqlite3_exec (self->db, "BEGIN TRANSACTION;", NULL, NULL, NULL); + account_id = matrix_db_get_account_id (self, username, device, NULL, FALSE); + room_id = matrix_db_get_room_id (self, account_id, room, FALSE); + + if (prepend) + match_id = db_get_first_room_event_id (self, room_id, &sorted_event_id); + else + match_id = db_get_last_room_event_id (self, room_id, &sorted_event_id); + + if (match_id) + prepend ? (--sorted_event_id) : (++sorted_event_id); + + if (!room_id) + { + sqlite3_exec (self->db, "END TRANSACTION;", NULL, NULL, NULL); + g_task_return_new_error (task, G_IO_ERROR, G_IO_ERROR_NOT_FOUND, + "Account or Room not found in db"); + return; + } + + for (guint i = 0; i < events->len; i++) + { + g_autoptr(JsonObject) encrypted = NULL; + g_autoptr(JsonObject) json_obj = NULL; + g_autoptr(JsonObject) json = NULL; + JsonObject *local = NULL; + CmEvent *event = events->pdata[i]; + g_autofree char *json_str = NULL; + const char *sender; + int member_id, replaces_id, replaces_cache_id = 0; + int event_state, status; + + json = cm_event_get_json (event); + encrypted = cm_event_get_encrypted_json (event); + sender = cm_event_get_sender_id (event); + + member_id = db_get_room_member_id (self, account_id, room_id, sender, NULL, TRUE); + + /* Delete existing ones as we add them below so that the sort order is right */ + if (cm_event_get_txn_id (event)) + cm_db_delete_event_with_txn_id (self, room_id, cm_event_get_txn_id (event)); + + if (!member_id) + continue; + + replaces_id = db_get_room_event_id (self, room_id, NULL, + cm_event_get_replaces_id (event)); + if (cm_event_get_replaces_id (event) && !replaces_id) + replaces_cache_id = db_get_room_cache_event_id (self, room_id, + cm_event_get_replaces_id (event), TRUE); + + json_obj = json_object_new (); + if (json) + json_object_set_object_member (json_obj, "json", g_steal_pointer (&json)); + if (encrypted) + json_object_set_object_member (json_obj, "encrypted", g_steal_pointer (&encrypted)); + + if (cm_event_get_txn_id (event)) + { + local = json_object_new (); + json_object_set_string_member (local, "txnid", + cm_event_get_txn_id (event)); + } + + if (local) + json_object_set_object_member (json_obj, "local", local); + + json_str = cm_utils_json_object_to_string (json_obj, FALSE); + event_state = db_event_state_to_int (cm_event_get_state (event)); + + sqlite3_prepare_v2 (self->db, + /* 1 2 3 */ + "INSERT INTO room_events(sorted_id,room_id,sender_id," + /* 4 5 6 7 8 */ + "event_type,event_uid,txnid,replaces_event_id,replaces_event_cache_id," + /* 9 10 11 12 13*/ + "event_state,state_key,origin_server_ts,decryption,json_data) " + "VALUES(?1,?2,?3,?4,?5,?6,?7,?8,?9,?10,?11,?12,?13)", + -1, &stmt, NULL); + matrix_bind_int (stmt, 1, sorted_event_id, "binding when adding event"); + matrix_bind_int (stmt, 2, room_id, "binding when adding event"); + matrix_bind_int (stmt, 3, member_id, "binding when adding event"); + matrix_bind_int (stmt, 4, cm_event_get_m_type (event), "binding when adding event"); + matrix_bind_text (stmt, 5, cm_event_get_id (event), "binding when adding event"); + if (cm_event_get_txn_id (event)) + matrix_bind_text (stmt, 6, cm_event_get_txn_id (event), "binding when adding event"); + if (replaces_id) + matrix_bind_int (stmt, 7, replaces_id, "binding when adding event"); + if (replaces_cache_id) + matrix_bind_int (stmt, 8, replaces_cache_id, "binding when adding event"); + matrix_bind_int (stmt, 9, event_state, "binding when adding event"); + matrix_bind_text (stmt, 10, cm_event_get_state_key (event), "binding when adding event"); + matrix_bind_int (stmt, 11, cm_event_get_time_stamp (event), "binding when adding event"); + matrix_bind_int (stmt, 12, db_event_get_decryption_value (event), "binding when adding event"); + matrix_bind_text (stmt, 13, json_str, "binding when adding event"); + status = sqlite3_step (stmt); + sqlite3_finalize (stmt); + + if (status == SQLITE_DONE) + { + int event_id, event_cache_id; + + event_id = sqlite3_last_insert_rowid (self->db); + event_cache_id = db_get_room_cache_event_id (self, room_id, + cm_event_get_id (event), FALSE); + + if (event_id && event_cache_id) + { + sqlite3_prepare_v2 (self->db, + "UPDATE room_events SET replaces_event_id=?" + "WHERE replaces_event_cache_id=?;", + -1, &stmt, NULL); + matrix_bind_int (stmt, 1, event_id, "binding when adding event"); + matrix_bind_int (stmt, 2, event_cache_id, "binding when adding event"); + sqlite3_step (stmt); + sqlite3_finalize (stmt); + } + } + else + { + g_warning ("Failed to save event: %s, error: %s", + cm_event_get_id (event), + sqlite3_errmsg (self->db)); + } + + prepend ? (--sorted_event_id) : (++sorted_event_id); + } + sqlite3_exec (self->db, "END TRANSACTION;", NULL, NULL, NULL); + + g_task_return_boolean (task, TRUE); +} + +static void +db_get_past_events (CmDb *self, + GTask *task) +{ + const char *username, *device, *room, *event; + GPtrArray *events = NULL; + CmRoom *cm_room; + int room_id, account_id, event_id, sorted_event_id = 0; + + g_assert (CM_IS_DB (self)); + g_assert (G_IS_TASK (task)); + g_assert (g_thread_self () == self->worker_thread); + g_assert (self->db); + + room = g_object_get_data (G_OBJECT (task), "room"); + event = g_object_get_data (G_OBJECT (task), "event"); + device = g_object_get_data (G_OBJECT (task), "device"); + cm_room = g_object_get_data (G_OBJECT (task), "cm-room"); + username = g_object_get_data (G_OBJECT (task), "username"); + + account_id = matrix_db_get_account_id (self, username, device, NULL, FALSE); + room_id = matrix_db_get_room_id (self, account_id, room, FALSE); + + if (event) + event_id = db_get_room_event_id (self, room_id, &sorted_event_id, event); + else + event_id = db_get_last_room_event_id (self, room_id, &sorted_event_id); + + if (!event_id) + { + g_task_return_new_error (task, G_IO_ERROR, G_IO_ERROR_NOT_FOUND, + "Couldn't find event in db"); + return; + } + + events = db_get_past_room_events (self, cm_room, room_id, + event ? event_id : 0, + sorted_event_id, 30); + g_task_return_pointer (task, events, (GDestroyNotify)g_ptr_array_unref); +} + +static gpointer +cm_db_worker (gpointer user_data) +{ + CmDb *self = user_data; + GTask *task; + + g_assert (CM_IS_DB (self)); + + while ((task = g_async_queue_pop (self->queue))) + { + CmDbCallback callback; + + g_assert (task); + callback = g_task_get_task_data (task); + callback (self, task); + g_object_unref (task); + + if (callback == matrix_close_db) + break; + } + + return NULL; +} + +static void +ma_finish_cb (GObject *object, + GAsyncResult *result, + gpointer user_data) +{ + g_autoptr(GError) error = NULL; + + g_task_propagate_boolean (G_TASK (result), &error); + + if (error) + g_warning ("Error: %s", error->message); + + g_task_return_boolean (G_TASK (user_data), !error); +} + +static void +cm_db_close (CmDb *self) +{ + g_autoptr(GTask) task = NULL; + + g_return_if_fail (CM_IS_DB (self)); + + if (!self->db) + return; + + task = g_task_new (NULL, NULL, NULL, NULL); + cm_db_close_async (self, ma_finish_cb, task); + + /* Wait until the task is completed */ + while (!g_task_get_completed (task)) + g_main_context_iteration (NULL, TRUE); +} + +static void +cm_db_dispose (GObject *object) +{ + CmDb *self = (CmDb *)object; + + cm_db_close (self); + + g_clear_pointer (&self->worker_thread, g_thread_unref); + + G_OBJECT_CLASS (cm_db_parent_class)->dispose (object); +} + +static void +cm_db_finalize (GObject *object) +{ + CmDb *self = (CmDb *)object; + + if (self->db) + g_warning ("Database not closed"); + + g_clear_pointer (&self->queue, g_async_queue_unref); + g_free (self->db_path); + + G_OBJECT_CLASS (cm_db_parent_class)->finalize (object); +} + +static void +cm_db_class_init (CmDbClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + + object_class->dispose = cm_db_dispose; + object_class->finalize = cm_db_finalize; +} + +static void +cm_db_init (CmDb *self) +{ + self->queue = g_async_queue_new (); +} + +CmDb * +cm_db_new (void) +{ + return g_object_new (CM_TYPE_DB, NULL); +} + +/** + * cm_db_open_async: + * @self: a #CmDb + * @dir: (transfer full): The database directory + * @file_name: The file name of database + * @callback: a #GAsyncReadyCallback, or %NULL + * @user_data: closure data for @callback + * + * Open the database file @file_name from path @dir. + * Complete with cm_db_open_finish() to get + * the result. + */ +void +cm_db_open_async (CmDb *self, + char *dir, + const char *file_name, + GAsyncReadyCallback callback, + gpointer user_data) +{ + GTask *task; + + g_return_if_fail (CM_IS_DB (self)); + g_return_if_fail (dir || !*dir); + g_return_if_fail (file_name || !*file_name); + + if (self->db) + { + g_warning ("A DataBase is already open"); + return; + } + + if (!self->worker_thread) + self->worker_thread = g_thread_new ("matrix-db-worker", + cm_db_worker, + self); + + task = g_task_new (self, NULL, callback, user_data); + g_task_set_source_tag (task, cm_db_open_async); + g_task_set_task_data (task, matrix_open_db, NULL); + g_object_set_data_full (G_OBJECT (task), "dir", dir, g_free); + g_object_set_data_full (G_OBJECT (task), "file-name", g_strdup (file_name), g_free); + + g_async_queue_push (self->queue, task); +} + +/** + * cm_db_open_finish: + * @self: a #CmDb + * @result: a #GAsyncResult provided to callback + * @error: a location for a #GError or %NULL + * + * Completes opening a database started with + * cm_db_open_async(). + * + * Returns: %TRUE if database was opened successfully. + * %FALSE otherwise with @error set. + */ +gboolean +cm_db_open_finish (CmDb *self, + GAsyncResult *result, + GError **error) +{ + g_return_val_if_fail (CM_IS_DB (self), FALSE); + g_return_val_if_fail (G_IS_TASK (result), FALSE); + g_return_val_if_fail (!error || !*error, FALSE); + + return g_task_propagate_boolean (G_TASK (result), error); +} + +/** + * cm_db_is_open: + * @self: a #CmDb + * + * Get if the database is open or not + * + * Returns: %TRUE if a database is open. + * %FALSE otherwise. + */ +gboolean +cm_db_is_open (CmDb *self) +{ + g_return_val_if_fail (CM_IS_DB (self), FALSE); + + return !!self->db; +} + +/** + * cm_db_close_async: + * @self: a #CmDb + * @callback: a #GAsyncReadyCallback, or %NULL + * @user_data: closure data for @callback + * + * Close the database opened. + * Complete with cm_db_close_finish() to get + * the result. + */ +void +cm_db_close_async (CmDb *self, + GAsyncReadyCallback callback, + gpointer user_data) +{ + GTask *task; + + g_return_if_fail (CM_IS_DB (self)); + + task = g_task_new (self, NULL, callback, user_data); + g_task_set_source_tag (task, cm_db_close_async); + g_task_set_task_data (task, matrix_close_db, NULL); + + g_async_queue_push (self->queue, task); +} + +/** + * cm_db_close_finish: + * @self: a #CmDb + * @result: a #GAsyncResult provided to callback + * @error: a location for a #GError or %NULL + * + * Completes closing a database started with + * cm_db_close_async(). @self is + * g_object_unref() if closing succeeded. + * So @self will be freed if you haven’t kept + * your own reference on @self. + * + * Returns: %TRUE if database was closed successfully. + * %FALSE otherwise with @error set. + */ +gboolean +cm_db_close_finish (CmDb *self, + GAsyncResult *result, + GError **error) +{ + g_return_val_if_fail (CM_IS_DB (self), FALSE); + g_return_val_if_fail (G_IS_TASK (result), FALSE); + g_return_val_if_fail (!error || !*error, FALSE); + + return g_task_propagate_boolean (G_TASK (result), error); +} + +void +cm_db_save_client_async (CmDb *self, + CmClient *client, + char *pickle, + GAsyncReadyCallback callback, + gpointer user_data) +{ + GObject *object; + GTask *task; + const char *username; + + g_return_if_fail (CM_IS_DB (self)); + g_return_if_fail (CM_IS_CLIENT (client)); + + task = g_task_new (self, NULL, callback, user_data); + g_task_set_source_tag (task, cm_db_save_client_async); + g_task_set_task_data (task, cm_db_save_client, NULL); + + object = G_OBJECT (task); + username = cm_client_get_user_id (client); + + if (g_application_get_default ()) + g_application_hold (g_application_get_default ()); + + if (!username || !*username || !cm_client_get_device_id (client)) + { + g_task_return_boolean (task, FALSE); + return; + } + + g_object_set_data (object, "enabled", GINT_TO_POINTER (cm_client_get_enabled (client))); + g_object_set_data_full (object, "pickle", pickle, g_free); + g_object_set_data_full (object, "device", + g_strdup (cm_client_get_device_id (client)), g_free); + g_object_set_data_full (object, "batch", + g_strdup (cm_client_get_next_batch (client)), g_free); + g_object_set_data_full (object, "username", g_strdup (username), g_free); + g_object_set_data_full (object, "account", g_object_ref (client), g_object_unref); + g_object_set_data_full (object, "filter-id", + g_strdup (cm_client_get_filter_id (client)), g_free); + + g_async_queue_push (self->queue, task); +} + +gboolean +cm_db_save_client_finish (CmDb *self, + GAsyncResult *result, + GError **error) +{ + g_return_val_if_fail (CM_IS_DB (self), FALSE); + g_return_val_if_fail (G_IS_TASK (result), FALSE); + + if (g_application_get_default ()) + g_application_release (g_application_get_default ()); + + return g_task_propagate_boolean (G_TASK (result), error); +} + +void +cm_db_load_client_async (CmDb *self, + CmClient *client, + const char *device_id, + GAsyncReadyCallback callback, + gpointer user_data) +{ + GTask *task; + const char *username; + + g_return_if_fail (!device_id || *device_id); + g_return_if_fail (CM_IS_DB (self)); + g_return_if_fail (CM_IS_CLIENT (client)); + + task = g_task_new (self, NULL, callback, user_data); + g_task_set_source_tag (task, cm_db_load_client_async); + g_task_set_task_data (task, cm_db_load_client, NULL); + + username = cm_client_get_user_id (client); + + if (!username || !*username || !device_id) + { + g_task_return_boolean (task, FALSE); + return; + } + + g_object_set_data_full (G_OBJECT (task), "device", g_strdup (device_id), g_free); + g_object_set_data_full (G_OBJECT (task), "username", g_strdup (username), g_free); + g_object_set_data_full (G_OBJECT (task), "client", g_object_ref (client), g_object_unref); + + g_async_queue_push_front (self->queue, task); +} + +gboolean +cm_db_load_client_finish (CmDb *self, + GAsyncResult *result, + GError **error) +{ + g_return_val_if_fail (CM_IS_DB (self), FALSE); + g_return_val_if_fail (G_IS_TASK (result), FALSE); + + return g_task_propagate_boolean (G_TASK (result), error); +} + +void +cm_db_save_room_async (CmDb *self, + CmClient *client, + CmRoom *room, + GAsyncReadyCallback callback, + gpointer user_data) +{ + const char *username, *device_id, *prev_batch; + const char *replacement; + CmStatus room_status; + GTask *task; + + g_return_if_fail (CM_IS_DB (self)); + g_return_if_fail (CM_IS_CLIENT (client)); + + task = g_task_new (self, NULL, callback, user_data); + g_task_set_source_tag (task, cm_db_save_room_async); + g_task_set_task_data (task, cm_db_save_room, NULL); + + username = cm_client_get_user_id (client); + device_id = cm_client_get_device_id (client); + prev_batch = cm_room_get_prev_batch (room); + replacement = cm_room_get_replacement_room (room); + room_status = cm_room_get_status (room); + + g_object_set_data_full (G_OBJECT (task), "username", g_strdup (username), g_free); + g_object_set_data_full (G_OBJECT (task), "room", g_object_ref (room), g_object_unref); + g_object_set_data_full (G_OBJECT (task), "json", cm_room_get_json (room), g_free); + g_object_set_data_full (G_OBJECT (task), "prev-batch", g_strdup (prev_batch), g_free); + g_object_set_data_full (G_OBJECT (task), "client", g_object_ref (client), g_object_unref); + g_object_set_data_full (G_OBJECT (task), "client-device", g_strdup (device_id), g_free); + g_object_set_data_full (G_OBJECT (task), "replacement", g_strdup (replacement), g_free); + g_object_set_data (G_OBJECT (task), "status", GINT_TO_POINTER (room_status)); + + g_async_queue_push (self->queue, task); +} + +gboolean +cm_db_save_room_finish (CmDb *self, + GAsyncResult *result, + GError **error) +{ + g_return_val_if_fail (CM_IS_DB (self), FALSE); + g_return_val_if_fail (G_IS_TASK (result), FALSE); + + return g_task_propagate_boolean (G_TASK (result), error); +} + +void +cm_db_delete_client_async (CmDb *self, + CmClient *client, + GAsyncReadyCallback callback, + gpointer user_data) +{ + const char *username, *device_id; + GTask *task; + + g_return_if_fail (CM_IS_DB (self)); + g_return_if_fail (CM_IS_CLIENT (client)); + + task = g_task_new (self, NULL, callback, user_data); + g_task_set_source_tag (task, cm_db_delete_client_async); + g_task_set_task_data (task, cm_db_delete_client, NULL); + + username = cm_client_get_user_id (client); + device_id = cm_client_get_device_id (client); + + g_object_set_data_full (G_OBJECT (task), "username", g_strdup (username), g_free); + g_object_set_data_full (G_OBJECT (task), "device-id", g_strdup (device_id), g_free); + g_object_set_data_full (G_OBJECT (task), "client", g_object_ref (client), g_object_unref); + + g_async_queue_push (self->queue, task); +} + +gboolean +cm_db_delete_client_finish (CmDb *self, + GAsyncResult *result, + GError **error) +{ + g_return_val_if_fail (CM_IS_DB (self), FALSE); + g_return_val_if_fail (G_IS_TASK (result), FALSE); + + return g_task_propagate_boolean (G_TASK (result), error); +} + +gboolean +cm_db_add_session (CmDb *self, + gpointer session, + char *pickle) +{ + g_autoptr(GTask) task = NULL; + g_autoptr(GError) error = NULL; + GObject *object; + gboolean success; + + g_return_val_if_fail (CM_IS_DB (self), FALSE); + g_return_val_if_fail (CM_IS_OLM (session), FALSE); + g_return_val_if_fail (pickle && *pickle, FALSE); + + task = g_task_new (self, NULL, NULL, NULL); + g_object_ref (task); + g_task_set_source_tag (task, cm_db_add_session); + g_task_set_task_data (task, db_add_session, NULL); + object = G_OBJECT (task); + + g_object_set_data_full (object, "session", g_object_ref (session), g_object_unref); + g_object_set_data_full (object, "pickle", pickle, g_free); + g_object_set_data (object, "state", GINT_TO_POINTER (cm_olm_get_state (session))); + if (cm_olm_get_session_type (session) == SESSION_MEGOLM_V1_OUT) + g_object_set_data (object, "chain-index", GINT_TO_POINTER (cm_olm_get_message_index (session))); + + g_async_queue_push_front (self->queue, task); + + while (!g_task_get_completed (task)) + g_main_context_iteration (NULL, TRUE); + + success = g_task_propagate_boolean (task, &error); + + if (error) + g_warning ("Failed to save olm session with id: %s, error: %s", + cm_olm_get_session_id (session), error->message); + + return success; +} + +void +cm_db_save_file_enc_async (CmDb *self, + CmEncFileInfo *file, + GAsyncReadyCallback callback, + gpointer user_data) +{ + GTask *task; + + g_return_if_fail (CM_IS_DB (self)); + g_return_if_fail (file && file->mxc_uri); + + task = g_task_new (self, NULL, callback, user_data); + g_task_set_source_tag (task, cm_db_save_file_enc_async); + g_task_set_task_data (task, cm_db_save_file_enc, NULL); + g_object_set_data (G_OBJECT (task), "file", file); + + g_async_queue_push (self->queue, task); +} + +gboolean +cm_db_save_file_enc_finish (CmDb *self, + GAsyncResult *result, + GError **error) +{ + g_return_val_if_fail (CM_IS_DB (self), FALSE); + g_return_val_if_fail (G_IS_TASK (result), FALSE); + g_return_val_if_fail (!error || !*error, FALSE); + + return g_task_propagate_boolean (G_TASK (result), error); +} + +void +cm_db_find_file_enc_async (CmDb *self, + const char *uri, + GAsyncReadyCallback callback, + gpointer user_data) +{ + GTask *task; + + g_return_if_fail (CM_IS_DB (self)); + g_return_if_fail (uri && *uri); + + task = g_task_new (self, NULL, callback, user_data); + g_task_set_source_tag (task, cm_db_find_file_enc_async); + g_task_set_task_data (task, cm_db_find_file_enc, NULL); + + g_object_set_data_full (G_OBJECT (task), "uri", g_strdup (uri), g_free); + + g_async_queue_push (self->queue, task); +} + +CmEncFileInfo * +cm_db_find_file_enc_finish (CmDb *self, + GAsyncResult *result, + GError **error) +{ + g_return_val_if_fail (CM_IS_DB (self), NULL); + g_return_val_if_fail (G_IS_TASK (result), NULL); + g_return_val_if_fail (!error || !*error, NULL); + + return g_task_propagate_pointer (G_TASK (result), error); +} + +gpointer +cm_db_lookup_session (CmDb *self, + const char *account_id, + const char *account_device, + const char *session_id, + const char *sender_key, + const char *pickle_key, + const char *room_id, + CmSessionType type) +{ + g_autoptr(GTask) task = NULL; + g_autoptr(GError) error = NULL; + GObject *object; + gpointer session; + + g_return_val_if_fail (CM_IS_DB (self), NULL); + g_return_val_if_fail (account_id && *account_id, NULL); + g_return_val_if_fail (account_device && *account_device, NULL); + g_return_val_if_fail (sender_key && *sender_key, NULL); + + task = g_task_new (self, NULL, NULL, NULL); + g_object_ref (task); + + g_task_set_source_tag (task, cm_db_lookup_session); + g_task_set_task_data (task, db_lookup_session, NULL); + object = G_OBJECT (task); + + g_object_set_data_full (object, "account-id", g_strdup (account_id), g_free); + g_object_set_data_full (object, "account-device", g_strdup (account_device), g_free); + g_object_set_data_full (object, "session-id", g_strdup (session_id), g_free); + g_object_set_data_full (object, "sender-key", g_strdup (sender_key), g_free); + g_object_set_data_full (object, "room-id", g_strdup (room_id), g_free); + g_object_set_data_full (object, "pickle-key", g_strdup (pickle_key), + (GDestroyNotify)cm_utils_free_buffer); + g_object_set_data (object, "type", GINT_TO_POINTER (type)); + g_async_queue_push (self->queue, task); + g_assert (task); + + /* Wait until the task is completed */ + while (!g_task_get_completed (task)) + g_main_context_iteration (NULL, TRUE); + + session = g_task_propagate_pointer (task, &error); + + if (error) + g_debug ("Error getting session: %s", error->message); + + return session; +} + +gpointer +cm_db_lookup_olm_session (CmDb *self, + const char *account_id, + const char *account_device, + const char *sender_curve25519_key, + const char *body, + const char *pickle_key, + CmSessionType type, + size_t message_type, + char **out_plain_text) +{ + g_autoptr(GTask) task = NULL; + g_autoptr(GError) error = NULL; + GObject *object; + char *pickle; + + g_return_val_if_fail (CM_IS_DB (self), NULL); + g_return_val_if_fail (account_id && *account_id, NULL); + g_return_val_if_fail (account_device && *account_device, NULL); + g_return_val_if_fail (sender_curve25519_key && *sender_curve25519_key, NULL); + g_return_val_if_fail (body && *body, NULL); + g_return_val_if_fail (pickle_key && *pickle_key, NULL); + g_return_val_if_fail (out_plain_text, NULL); + + task = g_task_new (self, NULL, NULL, NULL); + g_object_ref (task); + + g_task_set_source_tag (task, cm_db_lookup_olm_session); + g_task_set_task_data (task, db_lookup_olm_session, NULL); + object = G_OBJECT (task); + + g_object_set_data_full (object, "account-id", g_strdup (account_id), g_free); + g_object_set_data_full (object, "account-device", g_strdup (account_device), g_free); + g_object_set_data_full (object, "sender-key", g_strdup (sender_curve25519_key), g_free); + g_object_set_data_full (object, "body", g_strdup (body), g_free); + g_object_set_data_full (object, "pickle-key", g_strdup (pickle_key), + (GDestroyNotify)cm_utils_free_buffer); + g_object_set_data (object, "type", GINT_TO_POINTER (type)); + g_object_set_data (object, "message-type", GUINT_TO_POINTER (message_type)); + + /* Push to end as we may have to match items inserted immediately before */ + g_async_queue_push (self->queue, task); + g_assert (task); + + /* Wait until the task is completed */ + while (!g_task_get_completed (task)) + g_main_context_iteration (NULL, TRUE); + + pickle = g_task_propagate_pointer (task, &error); + *out_plain_text = g_object_steal_data (G_OBJECT (task), "plaintext"); + + if (error) + g_debug ("Error getting session: %s", error->message); + + return pickle; +} + +void +cm_db_mark_user_device_change (CmDb *self, + CmClient *client, + GPtrArray *users, + gboolean outdated, + gboolean is_tracking) +{ + const char *account_id, *device; + g_autoptr(GTask) task = NULL; + g_autoptr(GError) error = NULL; + GObject *object; + + g_return_if_fail (CM_IS_DB (self)); + g_return_if_fail (CM_IS_CLIENT (client)); + g_return_if_fail (users && users->len); + + task = g_task_new (self, NULL, NULL, NULL); + g_object_ref (task); + + g_task_set_source_tag (task, cm_db_mark_user_device_change); + g_task_set_task_data (task, db_mark_user_device_change, NULL); + account_id = cm_client_get_user_id (client); + device = cm_client_get_device_id (client); + object = G_OBJECT (task); + + g_debug ("Saving user device change: %p", users); + g_object_set_data_full (object, "users", g_ptr_array_ref (users), + (GDestroyNotify)g_ptr_array_unref); + g_object_set_data_full (object, "account-id", g_strdup (account_id), g_free); + g_object_set_data_full (object, "account-device", g_strdup (device), g_free); + g_object_set_data (object, "tracking", GINT_TO_POINTER (is_tracking)); + g_object_set_data (object, "outdated", GINT_TO_POINTER (outdated)); + + g_async_queue_push (self->queue, task); + g_assert (task); + + /* Wait until the task is completed */ + while (!g_task_get_completed (task)) + g_main_context_iteration (NULL, TRUE); + + g_task_propagate_pointer (task, &error); + g_debug ("Saving user device change %s", CM_LOG_SUCCESS (!error)); + + if (error) + g_debug ("Error marking user device changed: %s", error->message); +} + +void +cm_db_update_user_devices (CmDb *self, + CmClient *client, + CmUser *user, + GPtrArray *added, + GPtrArray *removed, + gboolean force_add) +{ + const char *account_id, *device; + g_autoptr(GTask) task = NULL; + g_autoptr(GError) error = NULL; + GObject *object; + + g_return_if_fail (CM_IS_DB (self)); + g_return_if_fail (CM_IS_CLIENT (client)); + g_return_if_fail (CM_IS_USER (user)); + g_return_if_fail ((added && added->len) || (removed && removed->len)); + + task = g_task_new (self, NULL, NULL, NULL); + g_object_ref (task); + + g_task_set_source_tag (task, cm_db_update_user_devices); + g_task_set_task_data (task, db_update_user_devices, NULL); + account_id = cm_client_get_user_id (client); + device = cm_client_get_device_id (client); + object = G_OBJECT (task); + + if (g_application_get_default ()) + g_application_hold (g_application_get_default ()); + + if (added) + g_ptr_array_ref (added); + + if (removed) + g_ptr_array_ref (removed); + + g_debug ("Updating user devices. user %p, added: %u, removed: %u", + user, added ? added->len : 0, removed ? removed->len : 0); + g_object_set_data_full (object, "client", g_object_ref (client), g_object_unref); + g_object_set_data_full (object, "user", g_object_ref (user), g_object_unref); + g_object_set_data_full (object, "added", added, (GDestroyNotify)g_ptr_array_unref); + g_object_set_data_full (object, "removed", removed, (GDestroyNotify)g_ptr_array_unref); + g_object_set_data_full (object, "account-id", g_strdup (account_id), g_free); + g_object_set_data_full (object, "username", g_strdup (cm_user_get_id (user)), g_free); + g_object_set_data_full (object, "account-device", g_strdup (device), g_free); + g_object_set_data (object, "force-add", GINT_TO_POINTER (force_add)); + + g_async_queue_push (self->queue, task); + g_assert (task); + + /* Wait until the task is completed */ + while (!g_task_get_completed (task)) + g_main_context_iteration (NULL, TRUE); + + if (g_application_get_default ()) + g_application_release (g_application_get_default ()); + + g_task_propagate_boolean (task, &error); + + g_debug ("Updating user devices. user %p %s", user, CM_LOG_SUCCESS (!error)); + + if (g_error_matches (error, G_IO_ERROR, G_IO_ERROR_NOT_FOUND)) + g_debug ("Updating user devices. user %p skipped, we are not tracking the user", user); + else if (error) + g_warning ("Error updating user devices, user %p: %s", user, error->message); +} + +void +cm_db_update_device (CmDb *self, + CmClient *client, + CmUser *user, + CmDevice *device) +{ + const char *account_id, *device_id; + g_autoptr(GTask) task = NULL; + g_autoptr(GError) error = NULL; + GObject *object; + + g_return_if_fail (CM_IS_DB (self)); + g_return_if_fail (CM_IS_CLIENT (client)); + g_return_if_fail (CM_IS_DEVICE (device)); + g_return_if_fail (CM_IS_USER (user)); + g_return_if_fail (cm_device_get_user (device) == user); + + task = g_task_new (self, NULL, NULL, NULL); + g_object_ref (task); + + g_task_set_source_tag (task, cm_db_update_user_device); + g_task_set_task_data (task, db_update_user_device, NULL); + account_id = cm_client_get_user_id (client); + device_id = cm_client_get_device_id (client); + object = G_OBJECT (task); + + if (g_application_get_default ()) + g_application_hold (g_application_get_default ()); + + g_debug ("Updating device. user %p, device: %p ", user, device); + g_object_set_data_full (object, "client", g_object_ref (client), g_object_unref); + g_object_set_data_full (object, "user", g_object_ref (user), g_object_unref); + g_object_set_data_full (object, "device", g_object_ref (device), g_object_unref); + g_object_set_data_full (object, "account-id", g_strdup (account_id), g_free); + g_object_set_data_full (object, "username", g_strdup (cm_user_get_id (user)), g_free); + g_object_set_data_full (object, "account-device", g_strdup (device_id), g_free); + + g_async_queue_push (self->queue, task); + g_assert (task); + + /* Wait until the task is completed */ + while (!g_task_get_completed (task)) + g_main_context_iteration (NULL, TRUE); + + if (g_application_get_default ()) + g_application_release (g_application_get_default ()); + + g_task_propagate_boolean (task, &error); + + g_debug ("Updating user device. user: %p, device: %p, %s", user, device, CM_LOG_SUCCESS (!error)); + + if (error) + g_warning ("Error updating user devices, user: %p, device: %p: %s", user, device, error->message); +} + +void +cm_db_add_room_members (CmDb *self, + CmRoom *cm_room, + GPtrArray *members) +{ + const char *username, *device, *room; + g_autoptr(GTask) task = NULL; + g_autoptr(GError) error = NULL; + CmClient *client; + + g_return_if_fail (CM_IS_DB (self)); + g_return_if_fail (CM_IS_ROOM (cm_room)); + + if (!members || !members->len) + return; + + if (g_application_get_default ()) + g_application_hold (g_application_get_default ()); + + client = cm_room_get_client (cm_room); + username = cm_client_get_user_id (client); + device = cm_client_get_device_id (client); + room = cm_room_get_id (cm_room); + + task = g_task_new (self, NULL, NULL, NULL); + g_object_ref (task); + g_object_ref (cm_room); + g_ptr_array_ref (members); + g_task_set_task_data (task, db_add_room_members, NULL); + g_object_set_data_full (G_OBJECT (task), "members", members, (GDestroyNotify)g_ptr_array_unref); + g_object_set_data_full (G_OBJECT (task), "cm-room", cm_room, g_object_unref); + g_object_set_data_full (G_OBJECT (task), "room", g_strdup (room), g_free); + g_object_set_data_full (G_OBJECT (task), "username", g_strdup (username), g_free); + g_object_set_data_full (G_OBJECT (task), "device", g_strdup (device), g_free); + + g_async_queue_push (self->queue, task); + + /* Wait until the task is completed */ + while (!g_task_get_completed (task)) + g_main_context_iteration (NULL, TRUE); + + if (g_application_get_default ()) + g_application_release (g_application_get_default ()); + + g_task_propagate_boolean (task, &error); + + if (error) + g_warning ("Error saving room members: %s", error->message); +} + +/* + * cm_db_add_room_events: + * @prepend: Whether the events should be + * added before or after already saved + * events. Set %TRUE to add before. %FALSE + * otherwise. + */ +void +cm_db_add_room_events (CmDb *self, + CmRoom *cm_room, + GPtrArray *events, + gboolean prepend) +{ + const char *username, *device, *room; + g_autoptr(GTask) task = NULL; + g_autoptr(GError) error = NULL; + CmClient *client; + + g_return_if_fail (CM_IS_DB (self)); + g_return_if_fail (CM_IS_ROOM (cm_room)); + + if (!events || !events->len) + return; + + if (g_application_get_default ()) + g_application_hold (g_application_get_default ()); + + client = cm_room_get_client (cm_room); + username = cm_client_get_user_id (client); + device = cm_client_get_device_id (client); + room = cm_room_get_id (cm_room); + + task = g_task_new (self, NULL, NULL, NULL); + g_object_ref (task); + g_object_ref (cm_room); + g_ptr_array_ref (events); + g_task_set_task_data (task, db_add_room_events, NULL); + g_object_set_data_full (G_OBJECT (task), "events", events, (GDestroyNotify)g_ptr_array_unref); + g_object_set_data_full (G_OBJECT (task), "cm-room", cm_room, g_object_unref); + g_object_set_data_full (G_OBJECT (task), "room", g_strdup (room), g_free); + g_object_set_data_full (G_OBJECT (task), "username", g_strdup (username), g_free); + g_object_set_data_full (G_OBJECT (task), "device", g_strdup (device), g_free); + g_object_set_data (G_OBJECT (task), "prepend", GINT_TO_POINTER (!!prepend)); + + g_async_queue_push (self->queue, task); + + /* Wait until the task is completed */ + while (!g_task_get_completed (task)) + g_main_context_iteration (NULL, TRUE); + + if (g_application_get_default ()) + g_application_release (g_application_get_default ()); + + g_task_propagate_boolean (task, &error); + + if (error) + g_debug ("Error getting session: %s", error->message); +} + +void +cm_db_get_past_events_async (CmDb *self, + CmRoom *room, + CmEvent *from, + GAsyncReadyCallback callback, + gpointer user_data) +{ + const char *room_name, *username, *device, *event = NULL; + CmClient *client; + GTask *task; + + g_return_if_fail (CM_IS_DB (self)); + g_return_if_fail (CM_IS_ROOM (room)); + g_return_if_fail (!from || CM_IS_EVENT (from)); + + g_object_ref (room); + if (from) + { + g_object_ref (from); + event = cm_event_get_id (from); + } + + client = cm_room_get_client (room); + room_name = cm_room_get_id (room); + username = cm_client_get_user_id (client); + device = cm_client_get_device_id (client); + g_return_if_fail (device && *device); + + task = g_task_new (self, NULL, callback, user_data); + g_object_set_data_full (G_OBJECT (task), "cm-room", room, g_object_unref); + g_object_set_data_full (G_OBJECT (task), "cm-event", from, g_object_unref); + g_object_set_data_full (G_OBJECT (task), "room", g_strdup (room_name), g_free); + g_object_set_data_full (G_OBJECT (task), "event", g_strdup (event), g_free); + g_object_set_data_full (G_OBJECT (task), "username", g_strdup (username), g_free); + g_object_set_data_full (G_OBJECT (task), "device", g_strdup (device), g_free); + g_task_set_source_tag (task, cm_db_get_past_events_async); + g_task_set_task_data (task, db_get_past_events, NULL); + + g_async_queue_push (self->queue, task); +} + +GPtrArray * +cm_db_get_past_events_finish (CmDb *self, + GAsyncResult *result, + GError **error) +{ + g_return_val_if_fail (CM_IS_DB (self), NULL); + g_return_val_if_fail (G_IS_TASK (result), NULL); + g_return_val_if_fail (!error || !*error, NULL); + + return g_task_propagate_pointer (G_TASK (result), error); +} diff --git a/subprojects/libcmatrix/src/cm-device-private.h b/subprojects/libcmatrix/src/cm-device-private.h new file mode 100644 index 0000000000000000000000000000000000000000..3c6f7c5330a6ec41abed2f94030d84c1969cb92c --- /dev/null +++ b/subprojects/libcmatrix/src/cm-device-private.h @@ -0,0 +1,32 @@ +/* -*- mode: c; c-basic-offset: 2; indent-tabs-mode: nil; -*- */ +/* + * Copyright (C) 2022 Purism SPC + * + * SPDX-License-Identifier: LGPL-2.1-or-later + */ + +#pragma once + +#if !defined(_CMATRIX_TAKEN) && !defined(CMATRIX_COMPILATION) +# error "Only <cmatrix.h> can be included directly." +#endif + +#include <glib-object.h> +#include <gio/gio.h> +#include <json-glib/json-glib.h> + +#include "cm-types.h" +#include "cm-device.h" + +G_BEGIN_DECLS + +CmDevice *cm_device_new (CmUser *user, + CmClient *client, + JsonObject *root); +void cm_device_set_verified (CmDevice *self, + gboolean verified); +gboolean cm_device_is_verified (CmDevice *self); +CmUser *cm_device_get_user (CmDevice *self); +JsonObject *cm_device_get_json (CmDevice *self); + +G_END_DECLS diff --git a/subprojects/libcmatrix/src/cm-device.c b/subprojects/libcmatrix/src/cm-device.c new file mode 100644 index 0000000000000000000000000000000000000000..c8df30afff0ed58a215814199f3082de27b836bd --- /dev/null +++ b/subprojects/libcmatrix/src/cm-device.c @@ -0,0 +1,192 @@ +/* -*- mode: c; c-basic-offset: 2; indent-tabs-mode: nil; -*- */ +/* + * Copyright (C) 2022 Purism SPC + * + * SPDX-License-Identifier: LGPL-2.1-or-later + */ + +#define G_LOG_DOMAIN "cm-device" + +#ifdef HAVE_CONFIG_H +# include "config.h" +#endif + +#include "cm-client.h" +#include "cm-client-private.h" +#include "cm-utils-private.h" +#include "cm-enc-private.h" +#include "cm-device-private.h" +#include "cm-device.h" + +struct _CmDevice +{ + GObject parent_instance; + + CmClient *client; + JsonObject *json; + CmUser *user; + char *device_id; + char *device_name; + char *ed_key; + char *curve_key; + + gboolean meagolm_v1; + gboolean olm_v1; + gboolean signature_failed; + gboolean verified; +}; + +G_DEFINE_TYPE (CmDevice, cm_device, G_TYPE_OBJECT) + +static void +cm_device_finalize (GObject *object) +{ + CmDevice *self = (CmDevice *)object; + + g_clear_object (&self->client); + g_free (self->device_id); + g_free (self->device_name); + g_free (self->ed_key); + g_free (self->curve_key); + g_clear_pointer (&self->json, json_object_unref); + + G_OBJECT_CLASS (cm_device_parent_class)->finalize (object); +} + +static void +cm_device_class_init (CmDeviceClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + + object_class->finalize = cm_device_finalize; +} + +static void +cm_device_init (CmDevice *self) +{ +} + +CmDevice * +cm_device_new (CmUser *user, + CmClient *client, + JsonObject *root) +{ + JsonObject *object; + JsonArray *array; + CmDevice *self; + const char *text; + char *key_name; + + g_return_val_if_fail (CM_IS_USER (user), NULL); + g_return_val_if_fail (CM_IS_CLIENT (client), NULL); + g_return_val_if_fail (root, NULL); + + if (g_strcmp0 (cm_user_get_id (user), + cm_utils_json_object_get_string (root, "user_id")) != 0) + g_return_val_if_reached (NULL); + + self = g_object_new (CM_TYPE_DEVICE, NULL); + self->json = json_object_ref (root); + g_set_weak_pointer (&self->user, user); + self->client = g_object_ref (client); + + text = cm_utils_json_object_get_string (root, "device_id"); + self->device_id = g_strdup (text); + g_return_val_if_fail (text && *text, NULL); + + object = cm_utils_json_object_get_object (root, "unsigned"); + text = cm_utils_json_object_get_string (object, "device_display_name"); + self->device_name = g_strdup (text); + + key_name = g_strconcat ("ed25519:", self->device_id, NULL); + object = cm_utils_json_object_get_object (root, "keys"); + text = cm_utils_json_object_get_string (object, key_name); + self->ed_key = g_strdup (text); + g_free (key_name); + + if (!cm_enc_verify (cm_client_get_enc (self->client), root, + cm_user_get_id (user), + self->device_id, self->ed_key)) + { + /* DEBUG */ + g_warning ("Signature failed"); + self->signature_failed = TRUE; + return self; + } + + key_name = g_strconcat ("curve25519:", self->device_id, NULL); + object = cm_utils_json_object_get_object (root, "keys"); + text = cm_utils_json_object_get_string (object, key_name); + self->curve_key = g_strdup (text); + g_free (key_name); + + array = cm_utils_json_object_get_array (root, "algorithms"); + for (guint i = 0; array && i < json_array_get_length (array); i++) { + const char *algorithm; + + algorithm = json_array_get_string_element (array, i); + if (g_strcmp0 (algorithm, ALGORITHM_MEGOLM) == 0) + self->meagolm_v1 = TRUE; + else if (g_strcmp0 (algorithm, ALGORITHM_OLM) == 0) + self->olm_v1 = TRUE; + } + + return self; +} + +void +cm_device_set_verified (CmDevice *self, + gboolean verified) +{ + g_return_if_fail (CM_IS_DEVICE (self)); + + self->verified = !!verified; +} + +gboolean +cm_device_is_verified (CmDevice *self) +{ + g_return_val_if_fail (CM_IS_DEVICE (self), FALSE); + + return !self->signature_failed && self->verified; +} + +JsonObject * +cm_device_get_json (CmDevice *self) +{ + g_return_val_if_fail (CM_IS_DEVICE (self), NULL); + + return self->json; +} + +CmUser * +cm_device_get_user (CmDevice *self) +{ + g_return_val_if_fail (CM_IS_DEVICE (self), NULL); + + return self->user; +} + +const char * +cm_device_get_id (CmDevice *self) +{ + g_return_val_if_fail (CM_IS_DEVICE (self), NULL); + + return self->device_id; +} + +const char * +cm_device_get_ed_key (CmDevice *self) +{ + g_return_val_if_fail (CM_IS_DEVICE (self), NULL); + + return self->ed_key; +} + +const char * +cm_device_get_curve_key (CmDevice *self) +{ + g_return_val_if_fail (CM_IS_DEVICE (self), NULL); + + return self->curve_key; +} diff --git a/subprojects/libcmatrix/src/cm-device.h b/subprojects/libcmatrix/src/cm-device.h new file mode 100644 index 0000000000000000000000000000000000000000..488cf16791de72c00a5454c16a07a39103d05065 --- /dev/null +++ b/subprojects/libcmatrix/src/cm-device.h @@ -0,0 +1,27 @@ +/* -*- mode: c; c-basic-offset: 2; indent-tabs-mode: nil; -*- */ +/* + * Copyright (C) 2022 Purism SPC + * + * SPDX-License-Identifier: LGPL-2.1-or-later + */ + +#pragma once + +#if !defined(_CMATRIX_TAKEN) && !defined(CMATRIX_COMPILATION) +# error "Only <cmatrix.h> can be included directly." +#endif + +#include <glib-object.h> +#include <gio/gio.h> + +G_BEGIN_DECLS + +#define CM_TYPE_DEVICE (cm_device_get_type ()) + +G_DECLARE_FINAL_TYPE (CmDevice, cm_device, CM, DEVICE, GObject) + +const char *cm_device_get_id (CmDevice *self); +const char *cm_device_get_ed_key (CmDevice *self); +const char *cm_device_get_curve_key (CmDevice *self); + +G_END_DECLS diff --git a/subprojects/libcmatrix/src/cm-enc-private.h b/subprojects/libcmatrix/src/cm-enc-private.h new file mode 100644 index 0000000000000000000000000000000000000000..9cb054a4f9ba270ed0fc538f0852cdb6bc3df2c1 --- /dev/null +++ b/subprojects/libcmatrix/src/cm-enc-private.h @@ -0,0 +1,113 @@ +/* -*- mode: c; c-basic-offset: 2; indent-tabs-mode: nil; -*- */ +/* cm-enc.h + * + * Copyright 2022 Purism SPC + * + * Author(s): + * Mohammed Sadiq <sadiq@sadiqpk.org> + * + * SPDX-License-Identifier: LGPL-2.1-or-later + */ + +#pragma once + +#define GCRYPT_NO_DEPRECATED +#include <gcrypt.h> +#include <json-glib/json-glib.h> +#include <glib-object.h> + +#include "events/cm-event.h" +#include "cm-types.h" + +G_BEGIN_DECLS + +typedef struct _CmEncFileInfo CmEncFileInfo; +typedef void * cm_gcry_t; + + +struct _CmEncFileInfo { + char *mxc_uri; + + char *aes_iv_base64; + char *aes_key_base64; + char *sha256_base64; + + char *algorithm; + char *version; + char *kty; + + gboolean extractable; +}; + +#define ALGORITHM_MEGOLM "m.megolm.v1.aes-sha2" +#define ALGORITHM_OLM "m.olm.v1.curve25519-aes-sha2" +#define CURVE25519_SIZE 43 /* when base64 encoded */ +#define ED25519_SIZE 43 /* when base64 encoded */ + +#define CM_TYPE_ENC (cm_enc_get_type ()) + +G_DECLARE_FINAL_TYPE (CmEnc, cm_enc, CM, ENC, GObject) + +CmEnc *cm_enc_new (gpointer matrix_db, + const char *pickle, + const char *key); +gpointer cm_enc_get_sas_for_event (CmEnc *self, + CmEvent *event); +void cm_enc_set_details (CmEnc *self, + GRefString *user_id, + const char *device_id); +char *cm_enc_get_pickle (CmEnc *self); +char *cm_enc_get_pickle_key (CmEnc *self); +char *cm_enc_sign_string (CmEnc *self, + const char *str, + size_t len); +gboolean cm_enc_verify (CmEnc *self, + JsonObject *object, + const char *matrix_id, + const char *device_id, + const char *ed_key); +size_t cm_enc_max_one_time_keys (CmEnc *self); +size_t cm_enc_create_one_time_keys (CmEnc *self, + size_t count); +void cm_enc_publish_one_time_keys (CmEnc *self); +JsonObject *cm_enc_get_one_time_keys (CmEnc *self); +char *cm_enc_get_one_time_keys_json (CmEnc *self); +char *cm_enc_get_device_keys_json (CmEnc *self); +void cm_enc_handle_room_encrypted (CmEnc *self, + JsonObject *object); +char *cm_enc_handle_join_room_encrypted (CmEnc *self, + CmRoom *room, + JsonObject *object); +JsonObject *cm_enc_encrypt_for_chat (CmEnc *self, + CmRoom *room, + const char *message); +JsonObject *cm_enc_create_out_group_keys (CmEnc *self, + CmRoom *room, + GPtrArray *one_time_keys, + gpointer *out_session); +gboolean cm_enc_has_room_group_key (CmEnc *self, + CmRoom *room); +void cm_enc_set_room_group_key (CmEnc *self, + CmRoom *room, + gpointer out_session); +void cm_enc_rm_room_group_key (CmEnc *self, + CmRoom *room); +void cm_enc_find_file_enc_async (CmEnc *self, + const char *uri, + GAsyncReadyCallback callback, + gpointer user_data); +CmEncFileInfo *cm_enc_find_file_enc_finish (CmEnc *self, + GAsyncResult *result, + GError **error); +void cm_enc_file_info_free (gpointer data); + +G_DEFINE_AUTOPTR_CLEANUP_FUNC (CmEncFileInfo, cm_enc_file_info_free) +/* G_DEFINE_AUTOPTR_CLEANUP_FUNC (cm_gcry_t, gcry_free) */ + +/* For tests */ +GRefString *cm_enc_get_user_id (CmEnc *self); +const char *cm_enc_get_device_id (CmEnc *self); +const char *cm_enc_get_curve25519_key (CmEnc *self); +const char *cm_enc_get_ed25519_key (CmEnc *self); + +G_END_DECLS diff --git a/subprojects/libcmatrix/src/cm-enc.c b/subprojects/libcmatrix/src/cm-enc.c new file mode 100644 index 0000000000000000000000000000000000000000..e7e7b5986d920004c933110b74025b7a2c704141 --- /dev/null +++ b/subprojects/libcmatrix/src/cm-enc.c @@ -0,0 +1,1560 @@ +/* -*- mode: c; c-basic-offset: 2; indent-tabs-mode: nil; -*- */ +/* cm-enc.c + * + * Copyright 2022 Purism SPC + * + * Author(s): + * Mohammed Sadiq <sadiq@sadiqpk.org> + * + * SPDX-License-Identifier: LGPL-2.1-or-later + */ + +#define G_LOG_DOMAIN "cm-enc" + +#ifdef HAVE_CONFIG_H +# include "config.h" +#endif + +#include <json-glib/json-glib.h> +#include <olm/olm.h> +#include <sys/random.h> + +#include "cm-utils-private.h" +#include "users/cm-user-private.h" +#include "users/cm-user-list-private.h" +#include "users/cm-room-member-private.h" +#include "cm-room-private.h" +#include "cm-device.h" +#include "cm-device-private.h" +#include "cm-db-private.h" +#include "cm-olm-private.h" +#include "cm-olm-sas-private.h" +#include "cm-enc-private.h" + +#define KEY_LABEL_SIZE 6 +#define STRING_ALLOCATION 512 + +/* + * SECTION: cm-enc + * @title: CmEnc + * @short_description: An abstraction for E2EE + * @include: "cm-enc.h" + */ + +/* + * Documentations: + * https://matrix.org/docs/guides/end-to-end-encryption-implementation-guide + * + * Other: + * * We use g_malloc(size) instead of g_malloc(size * sizeof(type)) for all ‘char’ + * and ‘[u]int8_t’, unless there is a possibility that the type can change. + */ +struct _CmEnc +{ + GObject parent_instance; + + CmDb *cm_db; + + OlmAccount *account; + OlmUtility *utility; + char *pickle_key; + + /* FIXME: is there a way to limit hashtable entries to-say-1000, + * and when new items are added older ones are deleted? + * Or any other data structure with fast lookup? + */ + GHashTable *enc_files; + GHashTable *in_olm_sessions; + GHashTable *out_olm_sessions; + GHashTable *in_group_sessions; + GHashTable *out_group_sessions; + GHashTable *out_group_room_session; + + GRefString *user_id; + char *device_id; + + char *curve_key; /* Public part of Curve25519 identity key */ + char *ed_key; /* Public part of Ed25519 fingerprint key */ +}; + +G_DEFINE_TYPE (CmEnc, cm_enc, G_TYPE_OBJECT) + +static void +free_all_details (CmEnc *self) +{ + if (self->account) + olm_clear_account (self->account); + + g_clear_pointer (&self->account, g_free); + g_hash_table_remove_all (self->in_olm_sessions); + g_hash_table_remove_all (self->out_olm_sessions); + g_hash_table_remove_all (self->in_group_sessions); + g_hash_table_remove_all (self->out_group_sessions); + g_hash_table_remove_all (self->out_group_room_session); +} + +static CmOlm * +ma_enc_lookup_out_group_session (CmEnc *self, + CmRoom *room, + const char **out_session_id) +{ + const char *session_id; + + g_assert (CM_IS_ENC (self)); + g_assert (CM_IS_ROOM (room)); + + session_id = g_hash_table_lookup (self->out_group_room_session, room); + if (!session_id) + return NULL; + + if (out_session_id) + *out_session_id = session_id; + + return g_hash_table_lookup (self->out_group_sessions, session_id); +} + +static CmOlm * +ma_create_olm_out_session (CmEnc *self, + const char *curve_key, + const char *one_time_key, + const char *room_id) +{ + CmOlm *session; + + g_assert (CM_ENC (self)); + + session = cm_olm_outbound_new (self->account, curve_key, one_time_key, room_id); + + if (!session) + return NULL; + + cm_olm_set_db (session, self->cm_db); + cm_olm_set_key (session, self->pickle_key); + cm_olm_set_sender_details (session, room_id, self->user_id); + cm_olm_set_account_details (session, self->user_id, self->device_id); + cm_olm_save (session); + + return session; +} + +/* + * cm_enc_load_identity_keys: + * @self: A #CmEnc + * + * Load the public part of Ed25519 fingerprint + * key pair and Curve25519 identity key pair. + */ +static gboolean +cm_enc_load_identity_keys (CmEnc *self) +{ + g_autoptr(JsonParser) parser = NULL; + g_autoptr(GError) error = NULL; + g_autofree char *key = NULL; + JsonObject *object; + JsonNode *node; + size_t length, err; + + length = olm_account_identity_keys_length (self->account); + key = malloc (length + 1); + err = olm_account_identity_keys (self->account, key, length); + key[length] = '\0'; + + if (err == olm_error ()) + { + g_warning ("error getting identity keys: %s", olm_account_last_error (self->account)); + return FALSE; + } + + parser = json_parser_new (); + json_parser_load_from_data (parser, key, length, &error); + + if (error) + { + g_warning ("error parsing keys: %s", error->message); + return FALSE; + } + + node = json_parser_get_root (parser); + object = json_node_get_object (node); + + g_free (self->curve_key); + g_free (self->ed_key); + + self->curve_key = g_strdup (json_object_get_string_member (object, "curve25519")); + self->ed_key = g_strdup (json_object_get_string_member (object, "ed25519")); + + return TRUE; +} + +static void +create_new_details (CmEnc *self) +{ + char *pickle_key; + cm_gcry_t buffer; + size_t length, err; + + g_assert (CM_ENC (self)); + + g_debug ("(%p) Creating new encryption keys", self); + + free_all_details (self); + + self->account = g_malloc (olm_account_size ()); + olm_account (self->account); + + gcry_free (self->pickle_key); + buffer = gcry_random_bytes_secure (64, GCRY_STRONG_RANDOM); + pickle_key = g_base64_encode (buffer, 64); + gcry_free (buffer); + + /* Copy and free pickle_key as it's not an mlock() memory */ + self->pickle_key = gcry_malloc_secure (strlen (pickle_key) + 1); + strcpy (self->pickle_key, pickle_key); + cm_utils_free_buffer (pickle_key); + + length = olm_create_account_random_length (self->account); + if (length) + buffer = gcry_random_bytes (length, GCRY_STRONG_RANDOM); + err = olm_create_account (self->account, buffer, length); + gcry_free (buffer); + if (err == olm_error ()) + g_warning ("Error creating account: %s", olm_account_last_error (self->account)); +} + +static void +cm_enc_sign_json_object (CmEnc *self, + JsonObject *object) +{ + g_autoptr(GString) str = NULL; + g_autofree char *signature = NULL; + g_autofree char *label = NULL; + JsonObject *sign, *child; + + g_assert (CM_IS_ENC (self)); + g_assert (object); + + /* The JSON is in canonical form. Required for signing */ + /* https://matrix.org/docs/spec/appendices#signing-json */ + str = cm_utils_json_get_canonical (object, NULL); + signature = cm_enc_sign_string (self, str->str, str->len); + + sign = json_object_new (); + label = g_strconcat ("ed25519:", self->device_id, NULL); + json_object_set_string_member (sign, label, signature); + + child = json_object_new (); + json_object_set_object_member (child, self->user_id, sign); + json_object_set_object_member (object, "signatures", child); +} + +static void +cm_enc_finalize (GObject *object) +{ + CmEnc *self = (CmEnc *)object; + + olm_clear_account (self->account); + g_free (self->account); + + olm_clear_utility (self->utility); + g_free (self->utility); + + g_hash_table_unref (self->enc_files); + g_hash_table_unref (self->in_olm_sessions); + g_hash_table_unref (self->out_olm_sessions); + g_hash_table_unref (self->in_group_sessions); + g_hash_table_unref (self->out_group_sessions); + g_hash_table_unref (self->out_group_room_session); + + g_clear_pointer (&self->user_id, g_ref_string_release); + g_free (self->device_id); + gcry_free (self->pickle_key); + cm_utils_free_buffer (self->curve_key); + cm_utils_free_buffer (self->ed_key); + g_clear_object (&self->cm_db); + + G_OBJECT_CLASS (cm_enc_parent_class)->finalize (object); +} + + +static void +cm_enc_class_init (CmEncClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + + object_class->finalize = cm_enc_finalize; +} + + +static void +cm_enc_init (CmEnc *self) +{ + self->utility = g_malloc (olm_utility_size ()); + olm_utility (self->utility); + + self->enc_files = g_hash_table_new_full (g_str_hash, g_str_equal, + g_free, cm_enc_file_info_free); + /* We use hashtable of hashtables, each value of hashtable indexed with + sender's curve25519 key */ + /* in_olm_sessions = g_hash_table_new (g_str_hash, g_str_equal, */ + /* g_free, free_olm_session); */ + self->in_olm_sessions = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, + (GDestroyNotify)g_hash_table_unref); + self->out_olm_sessions = g_hash_table_new_full (g_str_hash, g_str_equal, + g_free, g_object_unref); + self->in_group_sessions = g_hash_table_new_full (g_str_hash, g_str_equal, + g_free, g_object_unref); + self->out_group_sessions = g_hash_table_new_full (g_str_hash, g_str_equal, + g_free, g_object_unref); + self->out_group_room_session = g_hash_table_new_full (g_direct_hash, g_direct_equal, + g_object_unref, g_free); +} + +/** + * cm_enc_new: + * @pickle: (nullable): The account pickle + * @key: @pickle key, can be %NULL if @pickle is %NULL + * + * If @pickle is non-null, the olm account is created + * using the pickled data. Otherwise a new olm account + * is created. If @pickle is non-null and invalid + * %NULL is returned. + * + * For @self to be ready for use, the details of @self + * should be set with cm_enc_set_details(). + * + * Also see cm_enc_get_pickle(). + * + * Returns: (transfer full) (nullable): A new #CmEnc. + * Free with g_object_unref() + */ +CmEnc * +cm_enc_new (gpointer matrix_db, + const char *pickle, + const char *key) +{ + g_autoptr(CmEnc) self = NULL; + + g_return_val_if_fail (!pickle || (*pickle && key && *key), NULL); + + self = g_object_new (CM_TYPE_ENC, NULL); + g_set_object (&self->cm_db, matrix_db); + + /* Deserialize the pickle to create the account */ + if (pickle && *pickle) + { + g_autofree char *duped = NULL; + size_t err; + + g_debug ("(%p) Create from pickle", self); + self->pickle_key = gcry_malloc_secure (strlen (pickle) + 1); + strcpy (self->pickle_key, key); + self->account = g_malloc (olm_account_size ()); + olm_account (self->account); + + duped = g_strdup (pickle); + err = olm_unpickle_account (self->account, key, strlen (key), + duped, strlen (duped)); + + if (err == olm_error ()) + { + g_warning ("Error account unpickle: %s", olm_account_last_error (self->account)); + return NULL; + } + } + else + { + create_new_details (self); + } + + if (!cm_enc_load_identity_keys (self)) + return NULL; + + return g_steal_pointer (&self); +} + +gpointer +cm_enc_get_sas_for_event (CmEnc *self, + CmEvent *event) +{ + CmOlmSas *sas; + + g_return_val_if_fail (CM_IS_ENC (self), NULL); + g_return_val_if_fail (CM_IS_EVENT (event), NULL); + + sas = g_object_get_data (G_OBJECT (event), "olm-sas"); + + if (sas) + return sas; + + sas = cm_olm_sas_new (); + cm_olm_sas_set_key_verification (sas, event); + g_object_set_data_full (G_OBJECT (event), "olm-sas", sas, g_object_unref); + + return sas; +} + +/** + * cm_enc_set_details: + * @self: A #CmEnc + * @user_id: (nullable): Fully qualified Matrix user ID + * @device_id: (nullable): The device id string + * + * Set user id and device id of @self. @user_id + * should be fully qualified Matrix user ID + * (ie, @user:example.com) + */ +void +cm_enc_set_details (CmEnc *self, + GRefString *user_id, + const char *device_id) +{ + g_autoptr(GRefString) old_user = NULL; + g_autofree char *old_device = NULL; + + g_return_if_fail (CM_IS_ENC (self)); + g_return_if_fail (!user_id || *user_id == '@'); + + old_user = g_steal_pointer (&self->user_id); + old_device = self->device_id; + + if (user_id) + self->user_id = g_ref_string_acquire (user_id); + self->device_id = g_strdup (device_id); + + if (self->user_id && old_device && + g_strcmp0 (device_id, old_device) == 0) + { + create_new_details (self); + cm_enc_load_identity_keys (self); + } +} + +char * +cm_enc_get_pickle (CmEnc *self) +{ + g_autofree char *pickle = NULL; + size_t length, err; + + g_return_val_if_fail (CM_IS_ENC (self), NULL); + + length = olm_pickle_account_length (self->account); + pickle = malloc (length + 1); + err = olm_pickle_account (self->account, self->pickle_key, + strlen (self->pickle_key), pickle, length); + pickle[length] = '\0'; + + if (err == olm_error ()) + { + g_warning ("Error getting account pickle: %s", olm_account_last_error (self->account)); + + return NULL; + } + + return g_steal_pointer (&pickle); +} + +char * +cm_enc_get_pickle_key (CmEnc *self) +{ + g_return_val_if_fail (CM_ENC (self), NULL); + + return g_strdup (self->pickle_key); +} + +/** + * cm_enc_sign_string: + * @self: A #CmEnc + * @str: A string to sign + * @len: The length of @str, or -1 + * + * Sign @str and return the signature. + * Returns %NULL on error. + * + * Returns: (transfer full): The signature string. + * Free with g_free() + */ +char * +cm_enc_sign_string (CmEnc *self, + const char *str, + size_t len) +{ + char *signature; + size_t length, err; + + g_return_val_if_fail (CM_IS_ENC (self), NULL); + g_return_val_if_fail (str, NULL); + g_return_val_if_fail (*str, NULL); + + if (len == (size_t) -1) + len = strlen (str); + + length = olm_account_signature_length (self->account); + signature = malloc (length + 1); + err = olm_account_sign (self->account, str, len, signature, length); + signature[length] = '\0'; + + if (err == olm_error ()) + { + g_warning ("Error signing data: %s", olm_account_last_error (self->account)); + + return NULL; + } + + return signature; +} + +/** + * cm_enc_verify: + * @self: A #CmEnc + * @object: A #JsonObject + * @matrix_id: A Fully qualified Matrix ID + * @device_id: The device id string. + * @ed_key: The ED25519 key of @matrix_id + * + * Verify if the content in @object is signed by + * the user @matrix_id with device @device_id. + * + * This function may modify @object by removing + * "signatures" and "unsigned" members. + * + * Returns; %TRUE if verification succeeded. Or + * %FALSE otherwise. + */ +gboolean +cm_enc_verify (CmEnc *self, + JsonObject *object, + const char *matrix_id, + const char *device_id, + const char *ed_key) +{ + JsonNode *signatures, *non_signed; + g_autoptr(GString) json_str = NULL; + g_autofree char *signature = NULL; + g_autofree char *key_name = NULL; + JsonObject *child; + size_t error; + + if (!object) + return FALSE; + + g_return_val_if_fail (CM_IS_ENC (self), FALSE); + g_return_val_if_fail (matrix_id && *matrix_id == '@', FALSE); + g_return_val_if_fail (device_id && *device_id, FALSE); + g_return_val_if_fail (ed_key && *ed_key, FALSE); + + /* https://matrix.org/docs/spec/appendices#checking-for-a-signature */ + key_name = g_strconcat ("ed25519:", device_id, NULL); + child = cm_utils_json_object_get_object (object, "signatures"); + child = cm_utils_json_object_get_object (child, matrix_id); + signature = g_strdup (cm_utils_json_object_get_string (child, key_name)); + + if (!signature) + return FALSE; + + signatures = json_object_dup_member (object, "signatures"); + non_signed = json_object_dup_member (object, "signatures"); + /* Remove the non signed members before verification */ + json_object_remove_member (object, "signatures"); + json_object_remove_member (object, "unsigned"); + + json_str = cm_utils_json_get_canonical (object, NULL); + + /* Revert the changes we made to the JSON object */ + if (signatures) + json_object_set_member (object, "signatures", signatures); + if (non_signed) + json_object_set_member (object, "unsigned", non_signed); + + error = olm_ed25519_verify (self->utility, + ed_key, strlen (ed_key), + json_str->str, json_str->len, + signature, strlen (signature)); + + if (error == olm_error ()) + { + g_debug ("Error verifying signature: %s", olm_utility_last_error (self->utility)); + return FALSE; + } + + return TRUE; +} + +/** + * cm_enc_max_one_time_keys: + * @self: A #CmEnc + * + * Get the maximum number of one time keys Olm + * library can handle. + * + * Returns: The number of maximum one-time keys. + */ +size_t +cm_enc_max_one_time_keys (CmEnc *self) +{ + g_return_val_if_fail (CM_IS_ENC (self), 0); + + return olm_account_max_number_of_one_time_keys (self->account); +} + +/** + * cm_enc_create_one_time_keys: + * @self: A #CmEnc + * @count: A non-zero number + * + * Generate @count number of curve25519 one time keys. + * @count is capped to the half of what Olm library + * can handle. + * + * Returns: The number of one-time keys generated. + * It will be <= @count. + */ +size_t +cm_enc_create_one_time_keys (CmEnc *self, + size_t count) +{ + cm_gcry_t buffer = NULL; + size_t length, err; + + g_return_val_if_fail (CM_IS_ENC (self), 0); + g_return_val_if_fail (count, 0); + + /* doc: The maximum number of active keys supported by libolm + is returned by olm_account_max_number_of_one_time_keys. + The client should try to maintain about half this number on the homeserver. */ + count = MIN (count, olm_account_max_number_of_one_time_keys (self->account) / 2); + + length = olm_account_generate_one_time_keys_random_length (self->account, count); + if (length) + buffer = gcry_random_bytes (length, GCRY_STRONG_RANDOM); + err = olm_account_generate_one_time_keys (self->account, count, buffer, length); + + if (err == olm_error ()) + { + g_warning ("Error creating one time keys: %s", olm_account_last_error (self->account)); + + return 0; + } + + return count; +} + +/** + * cm_enc_publish_one_time_keys: + * @self: A #CmEnc + * + * Mark current set of one-time keys as published, + * So that they won't be returned again when requested + * with cm_enc_get_one_time_keys() or so. + */ +void +cm_enc_publish_one_time_keys (CmEnc *self) +{ + g_return_if_fail (CM_IS_ENC (self)); + + olm_account_mark_keys_as_published (self->account); +} + +/** + * cm_enc_get_one_time_keys: + * @self: A #CmEnc + * + * Get public part of unpublished Curve25519 one-time keys in @self. + * + * The returned data is a JSON-formatted object with the single + * property curve25519, which is itself an object mapping key id + * to base64-encoded Curve25519 key. For example: + * + * { + * "curve25519": { + * "AAAAAA": "wo76WcYtb0Vk/pBOdmduiGJ0wIEjW4IBMbbQn7aSnTo", + * "AAAAAB": "LRvjo46L1X2vx69sS9QNFD29HWulxrmW11Up5AfAjgU" + * } + * } + * + * Returns: (nullable) (transfer full): The unpublished one time keys. + * Free with g_free() + */ +JsonObject * +cm_enc_get_one_time_keys (CmEnc *self) +{ + g_autoptr(JsonParser) parser = NULL; + g_autoptr(GError) error = NULL; + g_autofree char *buffer = NULL; + size_t length, err; + + g_return_val_if_fail (CM_IS_ENC (self), NULL); + + length = olm_account_one_time_keys_length (self->account); + buffer = g_malloc (length + 1); + err = olm_account_one_time_keys (self->account, buffer, length); + buffer[length] = '\0'; + + if (err == olm_error ()) + { + g_warning ("Error getting one time keys: %s", olm_account_last_error (self->account)); + + return NULL; + } + + /* Return NULL if there are no keys */ + if (g_str_equal (buffer, "{\"curve25519\":{}}")) + return NULL; + + parser = json_parser_new (); + json_parser_load_from_data (parser, buffer, length, &error); + + if (error) + { + g_warning ("error parsing keys: %s", error->message); + return NULL; + } + + return json_node_dup_object (json_parser_get_root (parser)); +} + +/** + * cm_enc_get_one_time_keys_json: + * @self: A #CmEnc + * + * Get the signed Curve25519 one-time keys JSON. The JSON shall + * be in the following format: + * + * { + * "signed_curve25519:AAAAHg": { + * "key": "zKbLg+NrIjpnagy+pIY6uPL4ZwEG2v+8F9lmgsnlZzs", + * "signatures": { + * "@alice:example.com": { + * "ed25519:JLAFKJWSCS": "FLWxXqGbwrb8SM3Y795eB6OA8bwBcoMZFXBqnTn58AYWZSqiD45tlBVcDa2L7RwdKXebW/VzDlnfVJ+9jok1Bw" + * } + * } + * }, + * "signed_curve25519:AAAAHQ": { + * "key": "j3fR3HemM16M7CWhoI4Sk5ZsdmdfQHsKL1xuSft6MSw", + * "signatures": { + * "@alice:example.com": { + * "ed25519:JLAFKJWSCS": "IQeCEPb9HFk217cU9kw9EOiusC6kMIkoIRnbnfOh5Oc63S1ghgyjShBGpu34blQomoalCyXWyhaaT3MrLZYQAA" + * } + * } + * } + * } + * + * Returns: (transfer full): A JSON encoded string. + * Free with g_free() + */ +char * +cm_enc_get_one_time_keys_json (CmEnc *self) +{ + g_autoptr(JsonObject) object = NULL; + g_autoptr(JsonObject) root = NULL; + g_autoptr(GList) members = NULL; + JsonObject *keys, *child; + + g_return_val_if_fail (CM_IS_ENC (self), NULL); + + object = cm_enc_get_one_time_keys (self); + + if (!object) + return NULL; + + keys = json_object_new (); + object = json_object_get_object_member (object, "curve25519"); + members = json_object_get_members (object); + + for (GList *item = members; item; item = item->next) + { + g_autofree char *label = NULL; + const char *value; + + child = json_object_new (); + value = json_object_get_string_member (object, item->data); + json_object_set_string_member (child, "key", value); + cm_enc_sign_json_object (self, child); + + label = g_strconcat ("signed_curve25519:", item->data, NULL); + json_object_set_object_member (keys, label, child); + } + + root = json_object_new (); + json_object_set_object_member (root, "one_time_keys", keys); + + return cm_utils_json_object_to_string (root, FALSE); +} + +/** + * cm_enc_get_device_keys_json: + * @self: A #CmEnc + * + * Get the signed device key JSON. The JSON shall + * be in the following format: + * + * { + * "user_id": "@alice:example.com", + * "device_id": "JLAFKJWSCS", + * "algorithms": [ + * "m.olm.curve25519-aes-sha256", + * "m.megolm.v1.aes-sha2" + * ], + * "keys": { + * "curve25519:JLAFKJWSCS": "3C5BFWi2Y8MaVvjM8M22DBmh24PmgR0nPvJOIArzgyI", + * "ed25519:JLAFKJWSCS": "lEuiRJBit0IG6nUf5pUzWTUEsRVVe/HJkoKuEww9ULI" + * }, + * "signatures": { + * "@alice:example.com": { + * "ed25519:JLAFKJWSCS": "dSO80A01XiigH3uBiDVx/EjzaoycHcjq9lfQX0uWsqxl2giMIiSPR8a4d291W1ihKJL/a+myXS367WT6NAIcBA" + * } + * } + * } + * + * Returns: (nullable): A JSON encoded string. + * Free with g_free() + */ +char * +cm_enc_get_device_keys_json (CmEnc *self) +{ + g_autoptr(JsonObject) root = NULL; + JsonObject *keys, *device_keys; + JsonArray *array; + char *label; + + g_return_val_if_fail (CM_IS_ENC (self), NULL); + g_return_val_if_fail (self->user_id, NULL); + g_return_val_if_fail (self->device_id, NULL); + + device_keys = json_object_new (); + json_object_set_string_member (device_keys, "user_id", self->user_id); + json_object_set_string_member (device_keys, "device_id", self->device_id); + + array = json_array_new (); + json_array_add_string_element (array, ALGORITHM_OLM); + json_array_add_string_element (array, ALGORITHM_MEGOLM); + json_object_set_array_member (device_keys, "algorithms", array); + + keys = json_object_new (); + + label = g_strconcat ("curve25519:", self->device_id, NULL); + json_object_set_string_member (keys, label, self->curve_key); + g_free (label); + + label = g_strconcat ("ed25519:", self->device_id, NULL); + json_object_set_string_member (keys, label, self->ed_key); + g_free (label); + + json_object_set_object_member (device_keys, "keys", keys); + cm_enc_sign_json_object (self, device_keys); + + root = json_object_new (); + json_object_set_object_member (root, "device_keys", device_keys); + + return cm_utils_json_object_to_string (root, FALSE); +} + +static gboolean +in_olm_matches (gpointer key, + gpointer value, + gpointer user_data) +{ + g_autofree char *body = NULL; + size_t match; + + body = g_strdup (user_data); + match = olm_matches_inbound_session (value, body, strlen (body)); + + if (match == olm_error ()) + g_warning ("Error matching inbound session: %s", olm_session_last_error (key)); + + return match == 1; +} + +static void +handle_m_room_key (CmEnc *self, + JsonObject *root, + const char *sender_key) +{ + CmOlm *session; + JsonObject *object; + const char *session_key, *session_id, *room_id; + + g_assert (CM_IS_ENC (self)); + g_assert (root); + + object = cm_utils_json_object_get_object (root, "content"); + session_key = cm_utils_json_object_get_string (object, "session_key"); + session_id = cm_utils_json_object_get_string (object, "session_id"); + room_id = cm_utils_json_object_get_string (object, "room_id"); + + /* The documentation recommends to look if the session already exists */ + if (!session_key || + g_hash_table_lookup (self->in_group_sessions, session_id)) + return; + + session = cm_olm_in_group_new (session_key, sender_key, session_id); + g_debug ("(%p) Create new in group olm session %p", self, session); + cm_olm_set_sender_details (session, room_id, self->user_id); + cm_olm_set_account_details (session, self->user_id, self->device_id); + cm_olm_set_key (session, self->pickle_key); + cm_olm_set_db (session, self->cm_db); + cm_olm_save (session); + g_hash_table_insert (self->in_group_sessions, g_strdup (session_id), session); +} + +void +cm_enc_handle_room_encrypted (CmEnc *self, + JsonObject *object) +{ + g_autoptr(GRefString) sender = NULL; + const char *algorithm, *sender_key; + g_autofree char *plaintext = NULL; + g_autofree char *body = NULL; + CmOlm *session = NULL; + size_t type; + gboolean force_save = FALSE; + + g_return_if_fail (CM_IS_ENC (self)); + g_return_if_fail (object); + + if (cm_utils_json_object_get_string (object, "sender")) + sender = g_ref_string_new_intern (cm_utils_json_object_get_string (object, "sender")); + object = cm_utils_json_object_get_object (object, "content"); + algorithm = cm_utils_json_object_get_string (object, "algorithm"); + /* sender_key is the Curve25519 identity key of the sender */ + sender_key = cm_utils_json_object_get_string (object, "sender_key"); + + if (!algorithm || !sender_key || !sender) + g_return_if_reached (); + + if (!g_str_equal (algorithm, ALGORITHM_MEGOLM) && + !g_str_equal (algorithm, ALGORITHM_OLM)) + g_return_if_reached (); + + object = cm_utils_json_object_get_object (object, "ciphertext"); + object = cm_utils_json_object_get_object (object, self->curve_key); + + body = g_strdup (cm_utils_json_object_get_string (object, "body")); + type = (size_t)cm_utils_json_object_get_int (object, "type"); + + if (!body) + return; + + if (self->cm_db) + session = cm_db_lookup_olm_session (self->cm_db, self->user_id, self->device_id, + sender_key, body, self->pickle_key, + SESSION_OLM_V1_IN, type, &plaintext); + + if (!session && type == OLM_MESSAGE_TYPE_MESSAGE) + session = cm_db_lookup_olm_session (self->cm_db, self->user_id, self->device_id, + sender_key, body, self->pickle_key, + SESSION_OLM_V1_OUT, type, &plaintext); + + if (!session && type == OLM_MESSAGE_TYPE_PRE_KEY) + { + GHashTable *in_olm_sessions; + + in_olm_sessions = g_hash_table_lookup (self->in_olm_sessions, sender_key); + if (in_olm_sessions) + session = g_hash_table_find (in_olm_sessions, in_olm_matches, body); + g_debug ("(%p) Message with pre-key received, has session: %p", self, session); + + if (!session) + { + session = cm_olm_inbound_new (self->account, sender_key, body); + g_debug ("(%p) New inbound session created %p", self, session); + cm_olm_set_db (session, self->cm_db); + cm_olm_set_key (session, self->pickle_key); + + force_save = TRUE; + } + } + + g_debug ("(%p) Handle decrypted, session: %p", self, session); + + if (!session) + return; + + if (!plaintext) + plaintext = cm_olm_decrypt (session, type, body); + + { + g_autoptr(JsonObject) content = NULL; + JsonObject *data; + const char *message_type; + + content = cm_utils_string_to_json_object (plaintext); + message_type = cm_utils_json_object_get_string (content, "type"); + + g_debug ("(%p) Message decrypted. type: %s", self, message_type); + + if (g_strcmp0 (sender, cm_utils_json_object_get_string (content, "sender")) != 0) + { + g_warning ("(%p) Sender mismatch in encrypted content", self); + return; + } + + /* The content is not meant for us */ + if (g_strcmp0 (self->user_id, cm_utils_json_object_get_string (content, "recipient")) != 0) + return; + + data = cm_utils_json_object_get_object (content, "recipient_keys"); + if (g_strcmp0 (self->ed_key, cm_utils_json_object_get_string (data, "ed25519")) != 0) + { + g_warning ("(%p) ed25519 in content doesn't match to ours", self); + return; + } + + if (force_save) + { + GHashTable *in_olm_sessions; + const char *id, *room_id; + + data = cm_utils_json_object_get_object (content, "content"); + room_id = cm_utils_json_object_get_string (data, "room_id"); + in_olm_sessions = g_hash_table_lookup (self->in_olm_sessions, sender_key); + + if (!in_olm_sessions) + { + in_olm_sessions = g_hash_table_new_full (g_str_hash, g_str_equal, + g_free, g_object_unref); + g_hash_table_insert (self->in_olm_sessions, g_strdup (sender_key), + in_olm_sessions); + } + + g_debug ("(%p) Save in olm session %p", self, session); + + id = cm_olm_get_session_id (session); + g_hash_table_insert (in_olm_sessions, g_strdup (id), session); + cm_olm_set_sender_details (session, room_id, sender); + cm_olm_set_account_details (session, self->user_id, self->device_id); + cm_olm_save (session); + } + + if (g_strcmp0 (message_type, "m.room_key") == 0) + handle_m_room_key (self, content, sender_key); + } +} + +static CmEncFileInfo * +cm_enc_get_json_file_enc_info (JsonObject *root) +{ + g_autoptr(CmEncFileInfo) file = NULL; + JsonObject *child; + + if (!root) + return NULL; + + file = g_new0 (CmEncFileInfo, 1); + file->mxc_uri = g_strdup (cm_utils_json_object_get_string (root, "url")); + file->version = g_strdup (cm_utils_json_object_get_string (root, "v")); + file->aes_iv_base64 = g_strdup (cm_utils_json_object_get_string (root, "iv")); + + child = cm_utils_json_object_get_object (root, "hashes"); + file->sha256_base64 = g_strdup (cm_utils_json_object_get_string (child, "sha256")); + + child = cm_utils_json_object_get_object (root, "key"); + file->algorithm = g_strdup (cm_utils_json_object_get_string (child, "alg")); + file->extractable = cm_utils_json_object_get_bool (child, "ext"); + file->kty = g_strdup (cm_utils_json_object_get_string (child, "kty")); + file->aes_key_base64 = g_strdup (cm_utils_json_object_get_string (child, "k")); + + if (file->mxc_uri && g_str_has_prefix (file->mxc_uri, "mxc://") && + file->aes_key_base64) + return g_steal_pointer (&file); + + return NULL; +} + +static void +cm_enc_save_file_enc (CmEnc *self, + const char *json_str) +{ + g_autoptr(JsonObject) root = NULL; + CmEncFileInfo *file_info; + JsonObject *child; + + g_assert (CM_IS_ENC (self)); + + root = cm_utils_string_to_json_object (json_str); + + if (!root) + return; + + child = cm_utils_json_object_get_object (root, "content"); + child = cm_utils_json_object_get_object (child, "file"); + file_info = cm_enc_get_json_file_enc_info (child); + + if (file_info && file_info->mxc_uri && + !g_hash_table_contains (self->enc_files, file_info->mxc_uri)) + { + g_debug ("(%p) Save file keys", self); + g_hash_table_insert (self->enc_files, g_strdup (file_info->mxc_uri), file_info); + cm_db_save_file_enc_async (self->cm_db, file_info, NULL, NULL); + } + + /* todo: handle encrypted thumbnails */ +} + +char * +cm_enc_handle_join_room_encrypted (CmEnc *self, + CmRoom *room, + JsonObject *object) +{ + CmOlm *session = NULL; + const char *sender_key; + const char *ciphertext, *session_id; + g_autofree char *plaintext = NULL; + + g_return_val_if_fail (CM_IS_ENC (self), NULL); + g_return_val_if_fail (object, NULL); + + sender_key = cm_utils_json_object_get_string (object, "sender_key"); + + ciphertext = cm_utils_json_object_get_string (object, "ciphertext"); + session_id = cm_utils_json_object_get_string (object, "session_id"); + + /* the ciphertext can be absent, eg: in redacted events */ + if (!ciphertext) + return NULL; + + if (session_id) + session = g_hash_table_lookup (self->in_group_sessions, session_id); + + g_debug ("(%p) Got room encrypted, room: %p. session: %p", self, room, session); + + if (!session && self->cm_db) + { + session = cm_db_lookup_session (self->cm_db, self->user_id, + self->device_id, session_id, + sender_key, self->pickle_key, + cm_room_get_id (room), + SESSION_MEGOLM_V1_IN); + + g_debug ("(%p) Got in group session %p from matrix db", self, session); + + if (session) + g_hash_table_insert (self->in_group_sessions, g_strdup (session_id), session); + } + + if (!session) + return NULL; + + g_return_val_if_fail (session, NULL); + + plaintext = cm_olm_decrypt (session, 0, ciphertext); + + if (strstr (plaintext, "\"key_ops\"")) + cm_enc_save_file_enc (self, plaintext); + + return g_steal_pointer (&plaintext); +} + +JsonObject * +cm_enc_encrypt_for_chat (CmEnc *self, + CmRoom *room, + const char *message) +{ + CmOlm *session; + g_autofree char *encrypted = NULL; + const char *session_id; + JsonObject *root; + + g_return_val_if_fail (CM_IS_ENC (self), NULL); + g_return_val_if_fail (CM_IS_ROOM (room), NULL); + g_return_val_if_fail (message && *message, NULL); + + session = ma_enc_lookup_out_group_session (self, room, NULL); + g_return_val_if_fail (session, NULL); + + encrypted = cm_olm_encrypt (session, message); + g_debug ("(%p) Enrypt for room %p, session: %p, chain-index: %zu", + self, room, session, + cm_olm_get_message_index (session)); + + cm_olm_update_validity (session, + cm_room_get_encryption_msg_count (room), + cm_room_get_encryption_rotation_time (room)); + cm_olm_save (session); + session_id = cm_olm_get_session_id (session); + + root = json_object_new (); + json_object_set_string_member (root, "algorithm", ALGORITHM_MEGOLM); + json_object_set_string_member (root, "sender_key", self->curve_key); + json_object_set_string_member (root, "ciphertext", encrypted); + json_object_set_string_member (root, "session_id", session_id); + json_object_set_string_member (root, "device_id", self->device_id); + + return root; +} + +JsonObject * +cm_enc_create_out_group_keys (CmEnc *self, + CmRoom *room, + GPtrArray *one_time_keys, + gpointer *out_session) +{ + CmOlm *session = NULL; + const char *session_key, *session_id; + JsonObject *root, *child; + + g_return_val_if_fail (CM_IS_ENC (self), FALSE); + g_return_val_if_fail (CM_IS_ROOM (room), FALSE); + g_return_val_if_fail (one_time_keys && one_time_keys->len, NULL); + g_return_val_if_fail (out_session, NULL); + + session = ma_enc_lookup_out_group_session (self, room, NULL); + + if (!session) + { + session = cm_olm_out_group_new (self->curve_key); + cm_olm_set_account_details (session, self->user_id, self->device_id); + cm_olm_set_sender_details (session, cm_room_get_id (room), self->user_id); + cm_olm_set_key (session, self->pickle_key); + cm_olm_set_db (session, self->cm_db); + g_debug ("(%p) Create out group keys, room: %p, session: %p", self, room, session); + } + + if (!session) + g_return_val_if_reached (NULL); + + session_id = cm_olm_get_session_id (session); + session_key = cm_olm_get_session_key (session); + *out_session = session; + + root = json_object_new (); + + /* https://matrix.org/docs/spec/client_server/r0.6.1#m-room-key */ + for (guint i = 0; i < one_time_keys->len; i++) + { + CmUser *member; + const char *curve_key; + JsonObject *user; + CmUserKey *key; + + key = one_time_keys->pdata[i]; + member = key->user; + + user = json_object_new (); + json_object_set_object_member (root, cm_user_get_id (member), user); + + for (guint j = 0; j < key->devices->len; j++) + { + CmDevice *device; + CmOlm *olm_session = NULL; + char *one_time_key = NULL; + JsonObject *content; + + device = key->devices->pdata[j]; + curve_key = cm_device_get_curve_key (device); + + one_time_key = key->keys->pdata[j]; + olm_session = ma_create_olm_out_session (self, curve_key, one_time_key, + cm_room_get_id (room)); + + if (!one_time_key || !curve_key || !olm_session) + continue; + + /* xxx: Do we want to store only the keys */ + /* g_hash_table_insert (self->out_olm_sessions, g_strdup (curve_key), olm_session); */ + + /* Create per device object */ + child = json_object_new (); + json_object_set_object_member (user, cm_device_get_id (device), child); + + json_object_set_string_member (child, "algorithm", ALGORITHM_OLM); + json_object_set_string_member (child, "sender_key", self->curve_key); + json_object_set_object_member (child, "ciphertext", json_object_new ()); + + content = json_object_new (); + child = json_object_get_object_member (child, "ciphertext"); + g_assert (child); + json_object_set_object_member (child, curve_key, content); + + /* Body to be encrypted */ + { + g_autoptr(JsonObject) object = NULL; + g_autofree char *encrypted = NULL; + g_autofree char *data = NULL; + + /* Create a json object with common data */ + object = json_object_new (); + json_object_set_string_member (object, "type", "m.room_key"); + json_object_set_string_member (object, "sender", self->user_id); + json_object_set_string_member (object, "sender_device", self->device_id); + + child = json_object_new (); + json_object_set_string_member (child, "ed25519", self->ed_key); + json_object_set_object_member (object, "keys", child); + + child = json_object_new (); + json_object_set_string_member (child, "algorithm", "m.megolm.v1.aes-sha2"); + json_object_set_string_member (child, "room_id", cm_room_get_id (room)); + json_object_set_string_member (child, "session_id", session_id); + json_object_set_string_member (child, "session_key", session_key); + json_object_set_int_member (child, "chain_index", cm_olm_get_message_index (session)); + json_object_set_object_member (object, "content", child); + + /* User specific data */ + json_object_set_string_member (object, "recipient", cm_user_get_id (member)); + + /* Device specific data */ + child = json_object_new (); + json_object_set_string_member (child, "ed25519", cm_device_get_ed_key (device)); + json_object_set_object_member (object, "recipient_keys", child); + + /* Now encrypt the above JSON */ + data = cm_utils_json_object_to_string (object, FALSE); + encrypted = cm_olm_encrypt (olm_session, data); + + /* Add the encrypted data as the content */ + json_object_set_int_member (content, "type", cm_olm_get_message_type (olm_session)); + json_object_set_string_member (content, "body", encrypted); + } + } + } + + return root; +} + +/** + * cm_enc_has_room_group_key: + * @self: A #CmEnc + * @room_id: A matrix room id + * + * Check if any valid outgoing session is present + * for the given room @room_id. This should be + * checked before sending each message as @self + * may rotate the key after certain count or time + * + * Returns: %TRUE if a valid group session is + * present. %FALSE otherwise. + */ +gboolean +cm_enc_has_room_group_key (CmEnc *self, + CmRoom *room) +{ + CmOlm *session; + const char *session_id = NULL; + + g_return_val_if_fail (CM_IS_ENC (self), FALSE); + g_return_val_if_fail (CM_IS_ROOM (room), FALSE); + + session = ma_enc_lookup_out_group_session (self, room, &session_id); + + if (!session_id && self->cm_db && + !g_object_get_data (G_OBJECT (room), "olm-checked")) + { + session = cm_db_lookup_session (self->cm_db, self->user_id, + self->device_id, NULL, + self->curve_key, self->pickle_key, + cm_room_get_id (room), + SESSION_MEGOLM_V1_OUT); + + g_object_set_data (G_OBJECT (room), "olm-checked", GINT_TO_POINTER (TRUE)); + g_debug ("(%p) Got out group session %p from matrix db", self, session); + + if (session) + { + CmOlm *in_session; + + cm_olm_set_db (session, self->cm_db); + cm_olm_set_sender_details (session, cm_room_get_id (room), self->user_id); + cm_olm_set_account_details (session, self->user_id, self->device_id); + + session_id = cm_olm_get_session_id (session); + + g_hash_table_insert (self->out_group_room_session, + g_object_ref (room), g_strdup (session_id)); + g_hash_table_insert (self->out_group_sessions, + g_strdup (session_id), g_object_ref (session)); + + in_session = cm_olm_in_group_new_from_out (session, self->curve_key); + g_hash_table_insert (self->in_group_sessions, + g_strdup (session_id), in_session); + } + } + + return !!session; +} + +/** + * cm_enc_set_room_group_key: + * @self: A #CmEnc + * @room_id: The room id the session should be added to + * @out_session: A megolm #CmOlm + * + * Set the outgoing group encryption session for the room + * @room_id. All future messages shall be encrypted with + * @out_session for the given room until it's invalidated + */ +void +cm_enc_set_room_group_key (CmEnc *self, + CmRoom *room, + gpointer out_session) +{ + CmOlm *in_session = NULL; + const char *session_id; + + g_return_if_fail (CM_IS_ENC (self)); + g_return_if_fail (CM_IS_ROOM (room)); + g_return_if_fail (CM_IS_OLM (out_session)); + g_return_if_fail (cm_olm_get_session_type (out_session) == SESSION_MEGOLM_V1_OUT); + + if (ma_enc_lookup_out_group_session (self, room, NULL) == out_session) + return; + + /* There should be no existing sessions for the room */ + g_warn_if_fail (!g_hash_table_contains (self->out_group_room_session, room)); + + g_debug ("(%p) Set out group key, room: %p, session: %p", self, room, out_session); + + session_id = cm_olm_get_session_id (out_session); + in_session = cm_olm_in_group_new_from_out (out_session, self->curve_key); + g_hash_table_insert (self->out_group_room_session, + g_object_ref (room), g_strdup (session_id)); + g_hash_table_insert (self->out_group_sessions, + g_strdup (session_id), g_object_ref (out_session)); + g_hash_table_insert (self->in_group_sessions, + g_strdup (session_id), in_session); + cm_olm_save (out_session); + cm_olm_save (in_session); +} + +/** + * cm_enc_rm_room_group_key: + * @self: A #CmEnc + * @room_id: The room id for which the session should be removed + * + * Invalidate any out group session for the given room. + * The in session pair of the same is not removed as it + * may be useful to decrypt yet to receive messages. + */ +void +cm_enc_rm_room_group_key (CmEnc *self, + CmRoom *room) +{ + g_autoptr(CmOlm) session = NULL; + const char *session_id = NULL; + + g_return_if_fail (CM_IS_ENC (self)); + g_return_if_fail (CM_IS_ROOM (room)); + + session = ma_enc_lookup_out_group_session (self, room, &session_id); + g_debug ("(%p) Remove out group key, room: %p, session: %p", self, room, session); + + if (session) + { + g_object_ref (session); + cm_olm_set_state (session, OLM_STATE_INVALIDATED); + g_hash_table_remove (self->out_group_sessions, session_id); + cm_olm_save (session); + } + + g_hash_table_remove (self->out_group_room_session, room); +} + +static void +enc_find_file_enc_cb (GObject *obj, + GAsyncResult *result, + gpointer user_data) +{ + CmEnc *self; + g_autoptr(GTask) task = user_data; + CmEncFileInfo *file; + + g_assert (G_IS_TASK (task)); + + self = g_task_get_source_object (task); + + file = cm_db_find_file_enc_finish (CM_DB (obj), result, NULL); + g_debug ("(%p) Find file key done, has key: %s", self, CM_LOG_SUCCESS (!!file)); + + g_task_return_pointer (task, file, cm_enc_file_info_free); +} + +void +cm_enc_find_file_enc_async (CmEnc *self, + const char *uri, + GAsyncReadyCallback callback, + gpointer user_data) +{ + g_autoptr(GTask) task = NULL; + CmEncFileInfo *file; + + g_return_if_fail (uri && *uri); + + task = g_task_new (self, NULL, callback, user_data); + file = g_hash_table_lookup (self->enc_files, uri); + g_debug ("(%p) Find file key", self); + + if (file) + { + g_debug ("(%p) Find file key %s from cache", self, CM_LOG_SUCCESS (TRUE)); + g_task_return_pointer (task, file, NULL); + return; + } + + cm_db_find_file_enc_async (self->cm_db, uri, + enc_find_file_enc_cb, + g_steal_pointer (&task)); +} + +CmEncFileInfo * +cm_enc_find_file_enc_finish (CmEnc *self, + GAsyncResult *result, + GError **error) +{ + g_return_val_if_fail (CM_IS_ENC (self), FALSE); + g_return_val_if_fail (G_IS_TASK (result), FALSE); + g_return_val_if_fail (!error || !*error, FALSE); + + return g_task_propagate_pointer (G_TASK (result), error); +} + +void +cm_enc_file_info_free (gpointer data) +{ + CmEncFileInfo *file = data; + + if (!file) + return; + + cm_utils_free_buffer (file->aes_iv_base64); + cm_utils_free_buffer (file->aes_key_base64); + cm_utils_free_buffer (file->sha256_base64); + + cm_utils_free_buffer (file->mxc_uri); + cm_utils_free_buffer (file->algorithm); + cm_utils_free_buffer (file->version); + cm_utils_free_buffer (file->kty); + + g_free (file); +} + +GRefString * +cm_enc_get_user_id (CmEnc *self) +{ + g_return_val_if_fail (CM_IS_ENC (self), NULL); + + return self->user_id; +} + +const char * +cm_enc_get_device_id (CmEnc *self) +{ + g_return_val_if_fail (CM_IS_ENC (self), NULL); + + return self->device_id; +} + +const char * +cm_enc_get_curve25519_key (CmEnc *self) +{ + g_return_val_if_fail (CM_IS_ENC (self), NULL); + + return self->curve_key; +} + +const char * +cm_enc_get_ed25519_key (CmEnc *self) +{ + g_return_val_if_fail (CM_IS_ENC (self), NULL); + + return self->ed_key; +} diff --git a/subprojects/libcmatrix/src/cm-enums.h b/subprojects/libcmatrix/src/cm-enums.h new file mode 100644 index 0000000000000000000000000000000000000000..431ff5e255cb95a120eb768271a3755a14a89cb6 --- /dev/null +++ b/subprojects/libcmatrix/src/cm-enums.h @@ -0,0 +1,174 @@ +/* -*- mode: c; c-basic-offset: 2; indent-tabs-mode: nil; -*- */ +/* cm-enums.h + * + * Copyright 2022 Purism SPC + * + * Author(s): + * Mohammed Sadiq <sadiq@sadiqpk.org> + * + * SPDX-License-Identifier: LGPL-2.1-or-later + */ + +#pragma once + + +/** + * CmError: + * + * The Error returned by the Matrix Server + * See https://matrix.org/docs/spec/client_server/r0.6.1#api-standards + * for details. + */ +/* The order and value of members here and the error strings + * in the array error_codes in cm-utils.c should match. + */ +typedef enum { + CM_ERROR_FORBIDDEN = 1, + CM_ERROR_UNKNOWN_TOKEN, + CM_ERROR_MISSING_TOKEN, + CM_ERROR_BAD_JSON, + CM_ERROR_NOT_JSON, + CM_ERROR_NOT_FOUND, + CM_ERROR_LIMIT_EXCEEDED, + CM_ERROR_UNKNOWN, + CM_ERROR_UNRECOGNIZED, + CM_ERROR_UNAUTHORIZED, + CM_ERROR_USER_DEACTIVATED, + CM_ERROR_USER_IN_USE, + CM_ERROR_INVALID_USERNAME, + CM_ERROR_ROOM_IN_USE, + CM_ERROR_INVALID_ROOM_STATE, + CM_ERROR_THREEPID_IN_USE, + CM_ERROR_THREEPID_NOT_FOUND, + CM_ERROR_THREEPID_AUTH_FAILED, + CM_ERROR_THREEPID_DENIED, + CM_ERROR_SERVER_NOT_TRUSTED, + CM_ERROR_UNSUPPORTED_ROOM_VERSION, + CM_ERROR_INCOMPATIBLE_ROOM_VERSION, + CM_ERROR_BAD_STATE, + CM_ERROR_GUEST_ACCESS_FORBIDDEN, + CM_ERROR_CAPTCHA_NEEDED, + CM_ERROR_CAPTCHA_INVALID, + CM_ERROR_MISSING_PARAM, + CM_ERROR_INVALID_PARAM, + CM_ERROR_TOO_LARGE, + CM_ERROR_EXCLUSIVE, + CM_ERROR_RESOURCE_LIMIT_EXCEEDED, + CM_ERROR_CANNOT_LEAVE_SERVER_NOTICE_ROOM, + + /* Local errors */ + CM_ERROR_BAD_PASSWORD, + CM_ERROR_NO_HOME_SERVER, + CM_ERROR_BAD_HOME_SERVER, + CM_ERROR_USER_DEVICE_CHANGED, +} CmError; + +typedef enum { + CM_STATUS_UNKNOWN, + CM_STATUS_JOIN, + CM_STATUS_LEAVE, + CM_STATUS_INVITE, + CM_STATUS_BAN, + CM_STATUS_KNOCK +} CmStatus; + +typedef enum { + CM_CONTENT_TYPE_UNKNOWN, + CM_CONTENT_TYPE_TEXT, + CM_CONTENT_TYPE_EMOTE, + CM_CONTENT_TYPE_NOTICE, + CM_CONTENT_TYPE_IMAGE, + CM_CONTENT_TYPE_FILE, + CM_CONTENT_TYPE_LOCATION, + CM_CONTENT_TYPE_AUDIO, + CM_CONTENT_TYPE_VIDEO, + CM_CONTENT_TYPE_SERVER_NOTICE, +} CmContentType; + +/* + * The order of the enum items SHOULD NEVER + * be changed as they are used in database. + * New items should be appended to the end. + */ +typedef enum { + CM_M_UNKNOWN, + CM_M_CALL_ANSWER, + CM_M_CALL_ASSERTED_IDENTITY, + CM_M_CALL_ASSERTED_IDENTITY_PREFIX, + CM_M_CALL_CANDIDATES, + CM_M_CALL_HANGUP, + CM_M_CALL_INVITE, + CM_M_CALL_NEGOTIATE, + CM_M_CALL_REJECT, + CM_M_CALL_REPLACES, + CM_M_CALL_SELECT_ANSWER, + CM_M_DIRECT, + CM_M_DUMMY, + CM_M_FORWARDED_ROOM_KEY, + CM_M_FULLY_READ, + CM_M_IGNORED_USER_LIST, + CM_M_KEY_VERIFICATION_ACCEPT, + CM_M_KEY_VERIFICATION_CANCEL, + CM_M_KEY_VERIFICATION_DONE, + CM_M_KEY_VERIFICATION_KEY, + CM_M_KEY_VERIFICATION_MAC, + CM_M_KEY_VERIFICATION_READY, + CM_M_KEY_VERIFICATION_REQUEST, + CM_M_KEY_VERIFICATION_START, + CM_M_PRESENCE, + CM_M_PUSH_RULES, + CM_M_REACTION, + CM_M_RECEIPT, + CM_M_ROOM_ALIASES, + CM_M_ROOM_AVATAR, + CM_M_ROOM_BOT_OPTIONS, + CM_M_ROOM_CANONICAL_ALIAS, + CM_M_ROOM_CREATE, + CM_M_ROOM_ENCRYPTED, + CM_M_ROOM_ENCRYPTION, + CM_M_ROOM_GUEST_ACCESS, + CM_M_ROOM_HISTORY_VISIBILITY, + CM_M_ROOM_JOIN_RULES, + CM_M_ROOM_KEY, + CM_M_ROOM_KEY_REQUEST, + CM_M_ROOM_MEMBER, + CM_M_ROOM_MESSAGE, + CM_M_ROOM_MESSAGE_FEEDBACK, + CM_M_ROOM_NAME, + CM_M_ROOM_PINNED_EVENTS, + CM_M_ROOM_PLUMBING, + CM_M_ROOM_POWER_LEVELS, + CM_M_ROOM_REDACTION, + CM_M_ROOM_RELATED_GROUPS, + CM_M_ROOM_SERVER_ACL, + CM_M_ROOM_THIRD_PARTY_INVITE, + CM_M_ROOM_TOMBSTONE, + CM_M_ROOM_TOPIC, + CM_M_SECRET_REQUEST, + CM_M_SECRET_SEND, + CM_M_SECRET_STORAGE_DEFAULT_KEY, + CM_M_SPACE_CHILD, + CM_M_SPACE_PARENT, + CM_M_STICKER, + CM_M_TAG, + CM_M_TYPING, + + /* Custom */ + CM_M_USER_STATUS = 256, + CM_M_ROOM_INVITE, + CM_M_ROOM_BAN, + CM_M_ROOM_KICK, +} CmEventType; + +typedef enum +{ + CM_EVENT_STATE_UNKNOWN, + CM_EVENT_STATE_DRAFT, + /* Messages that are queued to be sent */ + CM_EVENT_STATE_WAITING, + /* When saving to db consider this as failed until sent? */ + CM_EVENT_STATE_SENDING, + CM_EVENT_STATE_SENDING_FAILED, + CM_EVENT_STATE_SENT, + CM_EVENT_STATE_RECEIVED, +} CmEventState; diff --git a/subprojects/libcmatrix/src/cm-input-stream-private.h b/subprojects/libcmatrix/src/cm-input-stream-private.h new file mode 100644 index 0000000000000000000000000000000000000000..c8dc3fc27bc15eb829f60a4429e6508450bc44f3 --- /dev/null +++ b/subprojects/libcmatrix/src/cm-input-stream-private.h @@ -0,0 +1,37 @@ +/* -*- mode: c; c-basic-offset: 2; indent-tabs-mode: nil; -*- */ +/* cm-input-stream-private.h + * + * Copyright 2022 Purism SPC + * + * Author(s): + * Mohammed Sadiq <sadiq@sadiqpk.org> + * + * SPDX-License-Identifier: LGPL-2.1-or-later + */ + +#pragma once + +#include <gio/gio.h> + +#include "cm-enc-private.h" + +G_BEGIN_DECLS + +#define CM_TYPE_INPUT_STREAM (cm_input_stream_get_type ()) + +G_DECLARE_FINAL_TYPE (CmInputStream, cm_input_stream, CM, INPUT_STREAM, GFilterInputStream) + +CmInputStream *cm_input_stream_new (GInputStream *base_stream); +CmInputStream *cm_input_stream_new_from_file (GFile *file, + gboolean encrypt, + GCancellable *cancellable, + GError **error); +void cm_input_stream_set_file_enc (CmInputStream *self, + CmEncFileInfo *file); +void cm_input_stream_set_encrypt (CmInputStream *self); +char *cm_input_stream_get_sha256 (CmInputStream *self); +const char *cm_input_stream_get_content_type (CmInputStream *self); +goffset cm_input_stream_get_size (CmInputStream *self); +JsonObject *cm_input_stream_get_file_json (CmInputStream *self); + +G_END_DECLS diff --git a/subprojects/libcmatrix/src/cm-input-stream.c b/subprojects/libcmatrix/src/cm-input-stream.c new file mode 100644 index 0000000000000000000000000000000000000000..683740ba8363fbfa18b7658827e0b577c3cb9d98 --- /dev/null +++ b/subprojects/libcmatrix/src/cm-input-stream.c @@ -0,0 +1,426 @@ +/* -*- mode: c; c-basic-offset: 2; indent-tabs-mode: nil; -*- */ +/* cm-input-stream.c + * + * Copyright 2022 Purism SPC + * + * Author(s): + * Mohammed Sadiq <sadiq@sadiqpk.org> + * + * SPDX-License-Identifier: LGPL-2.1-or-later + */ + +#define G_LOG_DOMAIN "cm-input-stream" + +#ifdef HAVE_CONFIG_H +# include "config.h" +#endif + +#define GCRYPT_NO_DEPRECATED +#include <gcrypt.h> + +#include "cm-utils-private.h" +#include "cm-input-stream-private.h" + +struct _CmInputStream +{ + GFilterInputStream parent_instance; + + gcry_cipher_hd_t cipher_hd; + + char *aes_key_base64; + char *aes_iv_base64; + char *sha256_base64; + + /* For files that will be used to upload */ + GFile *file; + GFileInfo *file_info; + GChecksum *checksum; + gboolean checksum_complete; + gboolean encrypt; + + char *buffer; + int buffer_len; + + gcry_error_t gcr_error; +}; + +G_DEFINE_TYPE (CmInputStream, cm_input_stream, G_TYPE_FILTER_INPUT_STREAM) + +static char * +value_to_unpadded_base64 (const guchar *data, + gsize data_len, + gboolean url_safe) +{ + char *base64; + + g_assert (data); + g_assert (data_len); + + base64 = g_base64_encode (data, data_len); + g_strdelimit (base64, "=", '\0'); + + if (url_safe) + { + g_strdelimit (base64, "/", '_'); + g_strdelimit (base64, "+", '-'); + } + + return base64; +} + +static void +parse_base64_value (const char *unpadded_base64, + guchar **out, + gsize *out_len) +{ + g_autofree char *base64 = NULL; + gsize len, padded_len; + + g_assert (out); + g_assert (out_len); + + if (!unpadded_base64) + return; + + len = strlen (unpadded_base64); + /* base64 is always multiple of 4, so add space for padding */ + if (len % 4) + padded_len = len + 4 - len % 4; + else + padded_len = len; + base64 = malloc (padded_len + 1); + strcpy (base64, unpadded_base64); + memset (base64 + len, '=', padded_len - len); + base64[padded_len] = '\0'; + + *out = g_base64_decode (base64, out_len); +} + +static gssize +cm_input_stream_read_fn (GInputStream *stream, + void *buffer, + gsize count, + GCancellable *cancellable, + GError **error) +{ + CmInputStream *self = (CmInputStream *)stream; + gssize n_read = -1; + + if (self->gcr_error) + goto end; + + n_read = G_INPUT_STREAM_CLASS (cm_input_stream_parent_class)->read_fn (stream, buffer, count, + cancellable, error); + + if (self->cipher_hd && n_read > 0) + { + /* We need sha256 checksums only for encrypted/to be encrypted files */ + if (!self->checksum) + self->checksum = g_checksum_new (G_CHECKSUM_SHA256); + + if (G_UNLIKELY (self->buffer_len < n_read)) + { + self->buffer_len = MAX (n_read, 1024 * 8); + self->buffer = g_realloc (self->buffer, self->buffer_len); + } + } + + /* Since it's CTR mode, the encrypted and decrypted always have the same size */ + if (self->cipher_hd && n_read > 0) + { + if (self->encrypt) + { + self->gcr_error = gcry_cipher_encrypt (self->cipher_hd, self->buffer, + n_read, buffer, n_read); + /* we are encrypting, calculate the checksum after encryption */ + if (!self->gcr_error) + g_checksum_update (self->checksum, (gpointer)self->buffer, n_read); + } + else + { + /* we are decrypting, calculate the checksum before decryption */ + g_checksum_update (self->checksum, buffer, n_read); + self->gcr_error = gcry_cipher_decrypt (self->cipher_hd, self->buffer, + n_read, buffer, n_read); + } + + if (!self->gcr_error) + memcpy (buffer, self->buffer, n_read); + } + + if (!self->gcr_error && self->cipher_hd && n_read == 0) + self->checksum_complete = TRUE; + + end: + if (self->gcr_error) + { + g_set_error (error, G_IO_ERROR, G_IO_ERROR_FAILED, + "Failed decrypting buffer: %s", gcry_strerror (self->gcr_error)); + return -1; + } + + return n_read; +} + +static void +cm_input_stream_finalize (GObject *object) +{ + CmInputStream *self = (CmInputStream *)object; + + if (self->cipher_hd) + gcry_cipher_close (self->cipher_hd); + + if (self->checksum) + g_checksum_free (self->checksum); + + g_free (self->buffer); + + g_clear_object (&self->file); + g_clear_object (&self->file_info); + + G_OBJECT_CLASS (cm_input_stream_parent_class)->finalize (object); +} + +static void +cm_input_stream_class_init (CmInputStreamClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + GInputStreamClass *input_stream_class = G_INPUT_STREAM_CLASS (klass); + + object_class->finalize = cm_input_stream_finalize; + + input_stream_class->read_fn = cm_input_stream_read_fn; +} + +static void +cm_input_stream_init (CmInputStream *self) +{ +} + +CmInputStream * +cm_input_stream_new (GInputStream *base_stream) +{ + return g_object_new (CM_TYPE_INPUT_STREAM, + "base-stream", base_stream, + NULL); +} + +CmInputStream * +cm_input_stream_new_from_file (GFile *file, + gboolean encrypt, + GCancellable *cancellable, + GError **error) +{ + CmInputStream *self; + GInputStream *stream; + GFileInfo *file_info; + + g_return_val_if_fail (G_IS_FILE (file), NULL); + g_return_val_if_fail (!cancellable || G_IS_CANCELLABLE (cancellable), NULL); + + stream = G_INPUT_STREAM (g_file_read (file, cancellable, error)); + + if (!stream) + return NULL; + + file_info = g_file_query_info (file, + G_FILE_ATTRIBUTE_STANDARD_SIZE","G_FILE_ATTRIBUTE_STANDARD_CONTENT_TYPE, + G_FILE_QUERY_INFO_NONE, + cancellable, + error); + if (!file_info) + return NULL; + + self = cm_input_stream_new (stream); + self->file_info = file_info; + self->file = g_object_ref (file); + self->encrypt = !!encrypt; + + if (encrypt) + cm_input_stream_set_encrypt (self); + + return self; +} + +void +cm_input_stream_set_file_enc (CmInputStream *self, + CmEncFileInfo *file) +{ + gcry_cipher_hd_t cipher_hd; + gsize len; + + g_return_if_fail (CM_IS_INPUT_STREAM (self)); + + if (!file) + return; + + g_return_if_fail (file->mxc_uri); + g_return_if_fail (file->aes_key_base64); + g_return_if_fail (!self->cipher_hd); + + self->gcr_error = gcry_cipher_open (&cipher_hd, GCRY_CIPHER_AES256, GCRY_CIPHER_MODE_CTR, 0); + + if (!self->gcr_error) + self->cipher_hd = cipher_hd; + + if (!self->gcr_error) + { + g_autofree char *key_base64 = NULL; + g_autofree guchar *key = NULL; + + /* uses unpadded base64url */ + key_base64 = g_strdup (file->aes_key_base64); + g_strdelimit (key_base64, "_", '/'); + g_strdelimit (key_base64, "-", '+'); + parse_base64_value (key_base64, &key, &len); + self->gcr_error = gcry_cipher_setkey (cipher_hd, key, len); + + cm_utils_clear ((char *)key, len); + cm_utils_clear (key_base64, -1); + } + + if (!self->gcr_error) + { + g_autofree char *iv_base64 = NULL; + g_autofree guchar *iv = NULL; + + iv_base64 = g_strdup (file->aes_iv_base64); + /* uses unpadded base64 */ + parse_base64_value (iv_base64, &iv, &len); + self->gcr_error = gcry_cipher_setctr (cipher_hd, iv, len); + + cm_utils_clear ((char *)iv, len); + cm_utils_clear (iv_base64, -1); + } +} + +void +cm_input_stream_set_encrypt (CmInputStream *self) +{ + gcry_cipher_hd_t cipher_hd; + + g_return_if_fail (CM_IS_INPUT_STREAM (self)); + g_return_if_fail (!self->cipher_hd); + + self->gcr_error = gcry_cipher_open (&cipher_hd, GCRY_CIPHER_AES256, GCRY_CIPHER_MODE_CTR, 0); + + if (!self->gcr_error) + self->cipher_hd = cipher_hd; + + if (!self->gcr_error) + { + g_autofree guchar *key = NULL; + + key = gcry_random_bytes (32, GCRY_STRONG_RANDOM); + self->aes_key_base64 = value_to_unpadded_base64 (key, 32, TRUE); + self->gcr_error = gcry_cipher_setkey (cipher_hd, key, 32); + + cm_utils_clear ((char *)key, 32); + } + + if (!self->gcr_error) + { + g_autofree guchar *iv = NULL; + + /* The first 8 bytes has to be random, and the rest (counter) has to be 0 */ + iv = g_malloc0 (16); + gcry_randomize (iv, 8, GCRY_STRONG_RANDOM); + self->aes_iv_base64 = value_to_unpadded_base64 (iv, 16, FALSE); + self->gcr_error = gcry_cipher_setctr (cipher_hd, iv, 16); + + cm_utils_clear ((char *)iv, 16); + } +} + +char * +cm_input_stream_get_sha256 (CmInputStream *self) +{ + guint8 *buffer; + gsize digest_len; + + g_return_val_if_fail (CM_IS_INPUT_STREAM (self), NULL); + + if (!self->checksum || !self->checksum_complete) + return NULL; + + digest_len = g_checksum_type_get_length (G_CHECKSUM_SHA256); + buffer = g_malloc (digest_len); + g_checksum_get_digest (self->checksum, buffer, &digest_len); + + return value_to_unpadded_base64 (buffer, digest_len, FALSE); +} + +const char * +cm_input_stream_get_content_type (CmInputStream *self) +{ + const char *content_type; + + g_return_val_if_fail (CM_IS_INPUT_STREAM (self), NULL); + + if (!self->file_info) + return NULL; + + content_type = g_file_info_get_content_type (self->file_info); + + if (content_type && !self->encrypt) + return content_type; + + return "application/octect-stream"; +} + +goffset +cm_input_stream_get_size (CmInputStream *self) +{ + g_return_val_if_fail (CM_IS_INPUT_STREAM (self), 0); + + if (!self->file_info) + return 0; + + return g_file_info_get_size (self->file_info); +} + +JsonObject * +cm_input_stream_get_file_json (CmInputStream *self) +{ + g_autofree char *sha256 = NULL; + JsonObject *root, *child; + JsonArray *array; + const char *url; + + g_return_val_if_fail (CM_IS_INPUT_STREAM (self), NULL); + + /* Return JSON only if the stream is used to encrypt and after file + * has been read completely + */ + if (!self->encrypt || !self->checksum_complete || !self->cipher_hd) + return NULL; + + /* The mxc url should have set somewhere else */ + if (!g_object_get_data (G_OBJECT (self), "uri")) + return NULL; + + url = g_object_get_data (G_OBJECT (self), "uri"); + root = json_object_new (); + json_object_set_string_member (root, "v", "v2"); + json_object_set_string_member (root, "url", url); + json_object_set_string_member (root, "iv", self->aes_iv_base64); + + sha256 = cm_input_stream_get_sha256 (self); + child = json_object_new (); + json_object_set_string_member (child, "sha256", sha256); + json_object_set_object_member (root, "hashes", child); + + array = json_array_new (); + json_array_add_string_element (array, "encrypt"); + json_array_add_string_element (array, "decrypt"); + + child = json_object_new (); + json_object_set_array_member (child, "key_ops", array); + json_object_set_string_member (child, "alg", "A256CTR"); + json_object_set_string_member (child, "kty", "oct"); + json_object_set_string_member (child, "k", self->aes_key_base64); + json_object_set_boolean_member (child, "ext", TRUE); + json_object_set_object_member (root, "key", child); + + return root; +} diff --git a/subprojects/libcmatrix/src/cm-matrix-private.h b/subprojects/libcmatrix/src/cm-matrix-private.h new file mode 100644 index 0000000000000000000000000000000000000000..664affcc402d0c1dacf7c74ee9f13d45710784f3 --- /dev/null +++ b/subprojects/libcmatrix/src/cm-matrix-private.h @@ -0,0 +1,26 @@ +/* -*- mode: c; c-basic-offset: 2; indent-tabs-mode: nil; -*- */ +/* + * Copyright (C) 2022 Purism SPC + * + * SPDX-License-Identifier: LGPL-2.1-or-later + */ + +#pragma once + +#include <gio/gio.h> +#include "cm-matrix.h" +#include "cm-db-private.h" + +#if !defined(_CMATRIX_TAKEN) && !defined(CMATRIX_COMPILATION) +# error "Only <cmatrix.h> can be included directly." +#endif + +G_BEGIN_DECLS + +const char *cm_matrix_get_data_dir (void); +const char *cm_matrix_get_app_id (void); + +/* To be used only for tests */ +CmDb *cm_matrix_get_db (CmMatrix *self); + +G_END_DECLS diff --git a/subprojects/libcmatrix/src/cm-matrix.c b/subprojects/libcmatrix/src/cm-matrix.c new file mode 100644 index 0000000000000000000000000000000000000000..9e2ec0d1648c39baea8181cc9162a4d527de3e60 --- /dev/null +++ b/subprojects/libcmatrix/src/cm-matrix.c @@ -0,0 +1,902 @@ +/* -*- mode: c; c-basic-offset: 2; indent-tabs-mode: nil; -*- */ +/* + * Copyright (C) 2022 Purism SPC + * + * SPDX-License-Identifier: LGPL-2.1-or-later + */ + +#define G_LOG_DOMAIN "cm-matrix" + +#ifdef HAVE_CONFIG_H +# include "config.h" +#endif + +#define GCRYPT_NO_DEPRECATED +#include <gcrypt.h> +#include <libsecret/secret.h> +#include <glib.h> +#include <glib/gstdio.h> +#include <fcntl.h> +#include <sys/types.h> +#include <sys/stat.h> + +#include "cm-db-private.h" +#include "cm-utils-private.h" +#include "cm-secret-store-private.h" +#include "cm-client.h" +#include "cm-client-private.h" +#include "cm-matrix.h" +#include "cm-matrix-private.h" + +/** + * SECTION: cm-matrix + * @title: CmMatrix + * @short_description: + * @include: "cm-matrix.h" + */ + +struct _CmMatrix +{ + GObject parent_instance; + + char *db_path; + char *db_name; + + char *data_dir; + char *cache_dir; + + CmDb *cm_db; + + GListStore *clients_list; + GHashTable *clients_to_save; + + guint network_change_id; + + gboolean secrets_loaded; + gboolean db_loaded; + gboolean is_opening; + gboolean loading_accounts; + gboolean disable_auto_login; +}; + + +#define RECONNECT_TIMEOUT 500 /* milliseconds */ + +char *cmatrix_data_dir, *cmatrix_app_id; + +G_DEFINE_TYPE (CmMatrix, cm_matrix, G_TYPE_OBJECT) + +enum { + PROP_0, + PROP_READY, + N_PROPS +}; + +static GParamSpec *properties[N_PROPS]; + +static gboolean +matrix_has_client (CmMatrix *self, + CmClient *client, + gboolean check_pending) +{ + const char *login_name, *user_name; + CmAccount *account; + GListModel *model; + guint n_items; + + g_assert (CM_IS_MATRIX (self)); + g_assert (CM_IS_CLIENT (client)); + + model = G_LIST_MODEL (self->clients_list); + n_items = g_list_model_get_n_items (model); + account = cm_client_get_account (client); + user_name = cm_client_get_user_id (client); + login_name = cm_account_get_login_id (account); + + /* For the time being, let's ignore the fact that the same username + * can exist in different homeservers + */ + for (guint i = 0; i < n_items; i++) + { + g_autoptr(CmClient) item = NULL; + CmAccount *item_account; + + item = g_list_model_get_item (model, i); + item_account = cm_client_get_account (item); + + if (login_name && + g_strcmp0 (login_name, cm_account_get_login_id (item_account)) == 0) + return TRUE; + + if (user_name && + g_strcmp0 (user_name, cm_client_get_user_id (item)) == 0) + return TRUE; + + if (user_name && + g_strcmp0 (user_name, cm_account_get_login_id (item_account)) == 0) + return TRUE; + } + + if (check_pending) + return g_hash_table_contains (self->clients_to_save, login_name); + + return FALSE; +} + +static gboolean +matrix_reconnect (gpointer user_data) +{ + CmMatrix *self = user_data; + GListModel *model; + guint n_items; + + self->network_change_id = 0; + + model = G_LIST_MODEL (self->clients_list); + n_items = g_list_model_get_n_items (model); + + for (guint i = 0; i < n_items; i++) { + g_autoptr(CmClient) client = NULL; + + client = g_list_model_get_item (model, i); + + if (cm_client_can_connect (client) && + cm_client_get_enabled (client)) + cm_client_start_sync (client); + else + cm_client_stop_sync (client); + } + + return G_SOURCE_REMOVE; +} + +static void +matrix_network_changed_cb (CmMatrix *self, + gboolean network_available, + GNetworkMonitor *network_monitor) +{ + g_assert (CM_IS_MATRIX (self)); + g_assert (G_IS_NETWORK_MONITOR (network_monitor)); + + if (!cm_matrix_is_ready (self)) + return; + + g_clear_handle_id (&self->network_change_id, g_source_remove); + self->network_change_id = g_timeout_add (RECONNECT_TIMEOUT, + matrix_reconnect, self); +} + +static void +matrix_stop (CmMatrix *self) +{ + GListModel *model; + guint n_items; + + g_assert (CM_IS_MATRIX (self)); + + model = G_LIST_MODEL (self->clients_list); + n_items = g_list_model_get_n_items (model); + + for (guint i = 0; i < n_items; i++) + { + g_autoptr(CmClient) client = NULL; + + client = g_list_model_get_item (model, i); + + cm_client_stop_sync (client); + } +} + +static void +cm_matrix_get_property (GObject *object, + guint prop_id, + GValue *value, + GParamSpec *pspec) +{ + CmMatrix *self = (CmMatrix *)object; + + switch (prop_id) + { + case PROP_READY: + g_value_set_boolean (value, cm_matrix_is_ready (self)); + break; + + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + } +} + +static void +cm_matrix_finalize (GObject *object) +{ + CmMatrix *self = (CmMatrix *)object; + + g_clear_handle_id (&self->network_change_id, g_source_remove); + + matrix_stop (self); + g_list_store_remove_all (self->clients_list); + g_clear_object (&self->clients_list); + g_hash_table_unref (self->clients_to_save); + + g_free (self->db_path); + g_free (self->db_name); + + g_free (self->data_dir); + g_free (self->cache_dir); + + G_OBJECT_CLASS (cm_matrix_parent_class)->finalize (object); +} + +static void +cm_matrix_class_init (CmMatrixClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + + object_class->finalize = cm_matrix_finalize; + object_class->get_property = cm_matrix_get_property; + + /** + * CmMatrix:ready: + * + * Whether matrix is enabled and usable + */ + properties[PROP_READY] = + g_param_spec_boolean ("ready", + "matrix is ready", + "matrix is ready", + FALSE, + G_PARAM_READABLE | G_PARAM_STATIC_STRINGS); + + g_object_class_install_properties (object_class, N_PROPS, properties); +} + +static void +cm_matrix_init (CmMatrix *self) +{ + if (!gcry_control (GCRYCTL_INITIALIZATION_FINISHED_P)) + g_error ("libgcrypt has not been initialized, did you run cm_init()?"); + + self->clients_list = g_list_store_new (CM_TYPE_CLIENT); + self->clients_to_save = g_hash_table_new_full (g_str_hash, g_str_equal, + g_free, g_object_unref); + + g_signal_connect_object (g_network_monitor_get_default (), + "network-changed", + G_CALLBACK (matrix_network_changed_cb), self, + G_CONNECT_AFTER | G_CONNECT_SWAPPED); +} + +/** + * cm_matrix_new: + * @data_dir: The data directory + * @cache_dir: The cache directory + * @app_id: The app id string (unused) + * @disable_auto_login: Disable auto login + * + * Create a new #CmMatrix with the provided details + * + * @data_dir is used to store downloaded files, + * avatars, and thumbnails. The content shall not + * be encrypted even if that was the case when + * received over the wire. + * + * @app_id should be a valid string when validated + * with g_application_id_is_valid() + * + * The same values should be provided every time + * #CmMatrix is created as these info are used + * to store data. + * + * Returns: (transfer full): A #CmMatrix + */ +/* + * @cache_dir may be used to store files temporarily + * when needed (eg: when resizing images) + */ +CmMatrix * +cm_matrix_new (const char *data_dir, + const char *cache_dir, + const char *app_id, + gboolean disable_auto_login) +{ + CmMatrix *self; + char *dir; + + g_return_val_if_fail (data_dir && *data_dir, NULL); + g_return_val_if_fail (cache_dir && *cache_dir, NULL); + g_return_val_if_fail (g_application_id_is_valid (app_id), NULL); + + self = g_object_new (CM_TYPE_MATRIX, NULL); + self->disable_auto_login = !!disable_auto_login; + self->data_dir = g_build_filename (data_dir, "cmatrix", NULL); + cmatrix_data_dir = g_strdup (self->data_dir); + cmatrix_app_id = g_strdup (app_id); + self->cache_dir = g_build_filename (cache_dir, "cmatrix", NULL); + + dir = cm_utils_get_path_for_m_type (self->data_dir, CM_M_ROOM_MESSAGE, TRUE, NULL); + g_mkdir_with_parents (dir, S_IRWXU); + g_free (dir); + + dir = cm_utils_get_path_for_m_type (self->data_dir, CM_M_ROOM_MEMBER, TRUE, NULL); + g_mkdir_with_parents (dir, S_IRWXU); + g_free (dir); + + dir = cm_utils_get_path_for_m_type (self->data_dir, CM_M_ROOM_AVATAR, TRUE, NULL); + g_mkdir_with_parents (dir, S_IRWXU); + g_free (dir); + + g_debug ("(%p) New matrix, data: %s, cache: %s", self, self->data_dir, self->cache_dir); + + return self; +} + +/** + * cm_init: + * @init_gcrypt: Whether to initialize gcrypt + * + * This function should be called to initialize the library. + * You may call this in main() + * + * If you don't initialize gcrypt, you should do it yourself + */ +void +cm_init (gboolean init_gcrypt) +{ + /* Force HTTP1 as we have issues with HTTP/2 implementation in libsoup3 + * Like https://gitlab.gnome.org/GNOME/libsoup/-/issues/302, + * https://gitlab.gnome.org/GNOME/libsoup/-/issues/296, etc */ + /* todo: Remove once we have better HTTP/2 support */ + g_setenv ("SOUP_FORCE_HTTP1", "1", FALSE); + + if (init_gcrypt) + { + /* Version check should be the very first call because it + makes sure that important subsystems are initialized. */ + if (!gcry_check_version (GCRYPT_VERSION)) + { + g_critical ("libgcrypt version mismatch"); + exit (2); + } + gcry_control (GCRYCTL_SUSPEND_SECMEM_WARN); + gcry_control (GCRYCTL_INIT_SECMEM, 512 * 1024, 0); + gcry_control (GCRYCTL_RESUME_SECMEM_WARN); + gcry_control (GCRYCTL_INITIALIZATION_FINISHED, 0); + } +} + +static void +load_accounts_from_secrets (CmMatrix *self, + GPtrArray *accounts) +{ + g_assert (CM_IS_MATRIX (self)); + + if (!accounts || !accounts->len) + return; + + g_debug ("(%p) Load %u account secrets", self, accounts->len); + + g_assert (SECRET_IS_RETRIEVABLE (accounts->pdata[0])); + + for (guint i = 0; i < accounts->len; i++) + { + g_autoptr(CmClient) client = NULL; + + client = cm_client_new_from_secret (accounts->pdata[i], self->cm_db); + g_list_store_append (self->clients_list, client); + if (!self->disable_auto_login) + cm_client_enable_as_in_store (client); + } +} + +static void +db_open_cb (GObject *obj, + GAsyncResult *result, + gpointer user_data) +{ + g_autoptr(GTask) task = user_data; + GError *error = NULL; + CmMatrix *self; + + g_assert (G_IS_TASK (task)); + + self = g_task_get_source_object (task); + g_assert (CM_IS_MATRIX (self)); + + self->db_loaded = cm_db_open_finish (self->cm_db, result, &error); + self->is_opening = FALSE; + g_debug ("(%p) Open DB %s", self, CM_LOG_SUCCESS (self->db_loaded)); + + if (!self->db_loaded) + { + g_clear_object (&self->cm_db); + g_warning ("(%p) Open DB error: %s", self, error ? error->message : ""); + g_task_return_error (task, error); + return; + } + else + { + GPtrArray *accounts; + + accounts = g_task_get_task_data (task); + load_accounts_from_secrets (self, accounts); + g_object_notify_by_pspec (G_OBJECT (self), properties[PROP_READY]); + g_task_return_boolean (task, self->db_loaded && self->secrets_loaded); + } +} + +static void +matrix_store_load_cb (GObject *object, + GAsyncResult *result, + gpointer user_data) +{ + CmMatrix *self; + g_autoptr(GTask) task = user_data; + g_autoptr(GPtrArray) accounts = NULL; + GError *error = NULL; + + g_assert (G_IS_TASK (task)); + + self = g_task_get_source_object (task); + g_assert (CM_IS_MATRIX (self)); + + accounts = cm_secret_store_load_finish (result, &error); + self->is_opening = FALSE; + if (!error) + self->secrets_loaded = TRUE; + + g_debug ("(%p) Load secrets %s", self, CM_LOG_SUCCESS (!error)); + + if (error) + { + g_warning ("(%p) Load secrets error: %s", self, error->message); + g_task_return_error (task, error); + return; + } + + if (self->db_loaded) + { + load_accounts_from_secrets (self, accounts); + g_object_notify_by_pspec (G_OBJECT (self), properties[PROP_READY]); + g_task_return_boolean (task, TRUE); + } + else + { + self->is_opening = TRUE; + if (accounts) + g_task_set_task_data (task, g_steal_pointer (&accounts), + (GDestroyNotify)g_ptr_array_unref); + + self->cm_db = cm_db_new (); + + g_debug ("(%p) Open DB", self); + cm_db_open_async (self->cm_db, + g_strdup (self->db_path), self->db_name, + db_open_cb, g_steal_pointer (&task)); + } +} + +/** + * cm_matrix_open_async: + * @db_path: The path where db is (to be) stored + * @db_name: The name of database + * @cancellable: (nullable): A #GCancellable + * @callback: The callback to run when ready + * @user_data: user data for @callback + * + * Open the matrix E2EE db which shall be used by clients + * when required. + * + * Run cm_matrix_open_finish() to get the result. + */ +void +cm_matrix_open_async (CmMatrix *self, + const char *db_path, + const char *db_name, + GCancellable *cancellable, + GAsyncReadyCallback callback, + gpointer user_data) +{ + g_autoptr(GTask) task = NULL; + + g_return_if_fail (CM_IS_MATRIX (self)); + g_return_if_fail (db_path && *db_path); + g_return_if_fail (db_name && *db_name); + g_return_if_fail (!cancellable || G_IS_CANCELLABLE (cancellable)); + g_return_if_fail (!cm_matrix_is_ready (self)); + + task = g_task_new (self, cancellable, callback, user_data); + + if (self->is_opening) + { + g_debug ("(%p) Open matrix already in progress", self); + g_task_return_new_error (task, G_IO_ERROR, G_IO_ERROR_FAILED, + "Opening db in progress"); + return; + } + + if (cm_matrix_is_ready (self)) + { + g_debug ("(%p) Open matrix already succeeded", self); + g_task_return_boolean (task, TRUE); + return; + } + + self->is_opening = TRUE; + + if (!self->db_path) + self->db_path = g_strdup (db_path); + + if (!self->db_name) + self->db_name = g_strdup (db_name); + + /* Don't load libsecret in tests as password request requires X11 */ + if (g_test_initialized ()) + self->secrets_loaded = TRUE; + + if (!self->secrets_loaded) + { + g_debug ("(%p) Load secrets", self); + cm_secret_store_load_async (cancellable, + matrix_store_load_cb, + g_steal_pointer (&task)); + } + else if (!self->db_loaded) + { + self->cm_db = cm_db_new (); + + g_debug ("(%p) Open DB", self); + cm_db_open_async (self->cm_db, g_strdup (db_path), db_name, + db_open_cb, + g_steal_pointer (&task)); + } + else + g_assert_not_reached (); +} + +gboolean +cm_matrix_open_finish (CmMatrix *self, + GAsyncResult *result, + GError **error) +{ + g_return_val_if_fail (CM_IS_MATRIX (self), FALSE); + g_return_val_if_fail (G_IS_TASK (result), FALSE); + g_return_val_if_fail (!error || !*error, FALSE); + + return g_task_propagate_boolean (G_TASK (result), error); +} + +gboolean +cm_matrix_is_ready (CmMatrix *self) +{ + g_return_val_if_fail (CM_IS_MATRIX (self), FALSE); + + return self->db_loaded || self->secrets_loaded; +} + +GListModel * +cm_matrix_get_clients_list (CmMatrix *self) +{ + g_return_val_if_fail (CM_IS_MATRIX (self), FALSE); + + return G_LIST_MODEL (self->clients_list); +} + +gboolean +cm_matrix_has_client_with_id (CmMatrix *self, + const char *user_id) +{ + GListModel *model; + guint n_items; + + g_return_val_if_fail (CM_IS_MATRIX (self), FALSE); + g_return_val_if_fail (user_id && *user_id, FALSE); + + model = G_LIST_MODEL (self->clients_list); + n_items = g_list_model_get_n_items (model); + + for (guint i = 0; i < n_items; i++) + { + g_autoptr(CmClient) item = NULL; + CmAccount *item_account; + + item = g_list_model_get_item (model, i); + item_account = cm_client_get_account (item); + + if (g_strcmp0 (user_id, cm_account_get_login_id (item_account)) == 0) + return TRUE; + + if (g_strcmp0 (user_id, cm_client_get_user_id (item)) == 0) + return TRUE; + } + + return g_hash_table_contains (self->clients_to_save, user_id); +} + +/** + * cm_matrix_client_new: + * @self: A #CmMatrix + * + * Create a new #CmClient. It's an error + * to create a new client before opening + the db with cm_matrix_open_async() + * + * Returns: (transfer full): A #CmClient + */ +CmClient * +cm_matrix_client_new (CmMatrix *self) +{ + CmClient *client; + + g_return_val_if_fail (CM_IS_MATRIX (self), NULL); + + if (!cm_matrix_is_ready (self)) + g_error ("(%p) DB not open, See cm_matrix_open_async()", self); + + client = g_object_new (CM_TYPE_CLIENT, NULL); + /* Mark the client as not to save automatically unless asked explicitly + * with cm_matrix_save_client_async() at least once. + */ + g_object_set_data (G_OBJECT (client), "no-save", GINT_TO_POINTER (TRUE)); + cm_client_set_db (client, self->cm_db); + + g_debug ("(%p) New client %p created", self, client); + + return client; +} + +static void +matrix_save_client_cb (GObject *object, + GAsyncResult *result, + gpointer user_data) +{ + CmMatrix *self; + g_autoptr(GTask) task = user_data; + GError *error = NULL; + gboolean ret; + + g_assert (G_IS_TASK (task)); + + self = g_task_get_source_object (task); + g_assert (CM_IS_MATRIX (self)); + + ret = cm_client_save_secrets_finish (CM_CLIENT (object), result, &error); + g_debug ("(%p) Save client %p %s", self, object, CM_LOG_SUCCESS (self->db_loaded)); + + if (error) + { + g_warning ("(%p) Save client %p error: %s", self, object, error->message); + g_task_return_error (task, error); + } + else + { + CmAccount *account; + + account = cm_client_get_account (CM_CLIENT (object)); + g_list_store_append (self->clients_list, object); + g_hash_table_remove (self->clients_to_save, + cm_account_get_login_id (account)); + g_task_return_boolean (task, ret); + } + + g_object_set_data (object, "enable", GINT_TO_POINTER (FALSE)); +} + +void +cm_matrix_save_client_async (CmMatrix *self, + CmClient *client, + GAsyncReadyCallback callback, + gpointer user_data) +{ + const char *login_id; + CmAccount *account; + GTask *task; + + g_return_if_fail (CM_IS_MATRIX (self)); + g_return_if_fail (CM_IS_CLIENT (client)); + + account = cm_client_get_account (client); + g_return_if_fail (cm_account_get_login_id (account)); + /* user id is set after login, which should be set only by cmatrix */ + g_return_if_fail (!cm_client_get_user_id (client)); + g_return_if_fail (cm_client_get_homeserver (client)); + + task = g_task_new (self, NULL, callback, user_data); + g_object_set_data (G_OBJECT (client), "no-save", GINT_TO_POINTER (FALSE)); + g_debug ("(%p) Save client %p", self, client); + + if (matrix_has_client (self, client, TRUE)) + { + g_debug ("(%p) Save client %p error, user exists", self, client); + g_task_return_new_error (task, G_IO_ERROR, G_IO_ERROR_EXISTS, + "User already exists"); + return; + } + + login_id = cm_account_get_login_id (account); + g_hash_table_insert (self->clients_to_save, g_strdup (login_id), g_object_ref (client)); + g_object_set_data (G_OBJECT (client), "enable", GINT_TO_POINTER (TRUE)); + cm_client_save_secrets_async (client, + matrix_save_client_cb, + task); +} + +gboolean +cm_matrix_save_client_finish (CmMatrix *self, + GAsyncResult *result, + GError **error) +{ + g_return_val_if_fail (CM_IS_MATRIX (self), FALSE); + g_return_val_if_fail (G_IS_TASK (result), FALSE); + + return g_task_propagate_boolean (G_TASK (result), error); +} + +static void +matrix_delete_client_cb (GObject *object, + GAsyncResult *result, + gpointer user_data) +{ + CmMatrix *self; + g_autoptr(GTask) task = user_data; + GError *error = NULL; + gboolean ret; + + g_assert (G_IS_TASK (task)); + + self = g_task_get_source_object (task); + g_assert (CM_IS_MATRIX (self)); + + ret = cm_client_delete_secrets_finish (CM_CLIENT (object), result, &error); + g_debug ("(%p) Delete client %p %s", self, object, CM_LOG_SUCCESS (!error)); + + if (error) + { + g_warning ("(%p) Delete client %p error: %s", self, object, error->message); + g_task_return_error (task, error); + } + else + { + CmClient *client; + + client = g_task_get_task_data (task); + cm_utils_remove_list_item (self->clients_list, client); + g_task_return_boolean (task, ret); + } +} + +void +cm_matrix_delete_client_async (CmMatrix *self, + CmClient *client, + GAsyncReadyCallback callback, + gpointer user_data) +{ + GTask *task; + + g_return_if_fail (CM_IS_MATRIX (self)); + g_return_if_fail (CM_IS_CLIENT (client)); + + g_debug ("(%p) Delete client %p", self, client); + task = g_task_new (self, NULL, callback, user_data); + g_task_set_task_data (task, g_object_ref (client), g_object_unref); + + cm_client_delete_secrets_async (client, + matrix_delete_client_cb, + task); +} + +gboolean +cm_matrix_delete_client_finish (CmMatrix *self, + GAsyncResult *result, + GError **error) +{ + g_return_val_if_fail (CM_IS_MATRIX (self), FALSE); + g_return_val_if_fail (G_IS_TASK (result), FALSE); + + return g_task_propagate_boolean (G_TASK (result), error); +} + +CmDb * +cm_matrix_get_db (CmMatrix *self) +{ + g_return_val_if_fail (CM_IS_MATRIX (self), NULL); + + return self->cm_db; +} + +const char * +cm_matrix_get_data_dir (void) +{ + return cmatrix_data_dir; +} + +const char * +cm_matrix_get_app_id (void) +{ + return cmatrix_app_id; +} + +static void +matrix_save_client (GObject *object, + GAsyncResult *result, + gpointer user_data) +{ + CmMatrix *self; + g_autoptr(GTask) task = user_data; + GPtrArray *clients; + CmClient *client; + + g_assert (G_IS_TASK (task)); + + self = g_task_get_source_object (task); + g_assert (CM_IS_MATRIX (self)); + + clients = g_task_get_task_data (task); + client = (CmClient *)object; + + if (client && + cm_client_save_secrets_finish (client, result, NULL)) + { + g_debug ("(%p) Save client %p done", self, CM_LOG_SUCCESS (TRUE)); + g_list_store_append (self->clients_list, CM_CLIENT (object)); + cm_client_enable_as_in_store (client); + } + + if (!clients || !clients->len) + { + g_task_return_boolean (task, TRUE); + return; + } + + client = g_ptr_array_steal_index (clients, 0); + g_object_set_data_full (user_data, "client", client, g_object_unref); + + g_debug ("(%p) Save client %p, %u left to save", self, client, clients->len); + cm_client_save_secrets_async (client, + matrix_save_client, + g_steal_pointer (&task)); +} + +void +cm_matrix_add_clients_async (CmMatrix *self, + GPtrArray *secrets, + GAsyncReadyCallback callback, + gpointer user_data) +{ + GPtrArray *clients; + GTask *task; + + g_return_if_fail (CM_IS_MATRIX (self)); + g_return_if_fail (secrets && secrets->len); + g_return_if_fail (SECRET_IS_RETRIEVABLE (secrets->pdata[0])); + g_return_if_fail (cm_matrix_is_ready (self)); + + clients = g_ptr_array_new_full (secrets->len, g_object_unref); + task = g_task_new (self, NULL, callback, user_data); + g_task_set_task_data (task, clients, (GDestroyNotify)g_ptr_array_unref); + + for (guint i = 0; i < secrets->len; i++) + { + SecretRetrievable *secret = secrets->pdata[i]; + CmClient *client; + + client = cm_client_new_from_secret (secret, self->cm_db); + if (client) + g_ptr_array_add (clients, client); + else + g_warning ("(%p) Failed to create client from secret", self); + } + + g_debug ("(%p) Save clients, count: %u", self, secrets->len); + matrix_save_client (NULL, NULL, task); +} + +gboolean +cm_matrix_add_clients_finish (CmMatrix *self, + GAsyncResult *result, + GError **error) +{ + g_return_val_if_fail (CM_IS_MATRIX (self), FALSE); + g_return_val_if_fail (G_IS_TASK (result), FALSE); + + return g_task_propagate_boolean (G_TASK (result), error); +} diff --git a/subprojects/libcmatrix/src/cm-matrix.h b/subprojects/libcmatrix/src/cm-matrix.h new file mode 100644 index 0000000000000000000000000000000000000000..2e902b1fc3fa6c2fe33952f271de94cd07de1778 --- /dev/null +++ b/subprojects/libcmatrix/src/cm-matrix.h @@ -0,0 +1,63 @@ +/* -*- mode: c; c-basic-offset: 2; indent-tabs-mode: nil; -*- */ +/* + * Copyright (C) 2022 Purism SPC + * + * SPDX-License-Identifier: LGPL-2.1-or-later + */ + +#pragma once + +#include <gio/gio.h> +#include "cm-client.h" + +#if !defined(_CMATRIX_TAKEN) && !defined(CMATRIX_COMPILATION) +# error "Only <cmatrix.h> can be included directly." +#endif + +G_BEGIN_DECLS + +#define CM_TYPE_MATRIX (cm_matrix_get_type ()) +G_DECLARE_FINAL_TYPE (CmMatrix, cm_matrix, CM, MATRIX, GObject) + +CmMatrix *cm_matrix_new (const char *data_dir, + const char *cache_dir, + const char *app_id, + gboolean disable_auto_login); +void cm_init (gboolean init_gcrypt); +void cm_matrix_open_async (CmMatrix *self, + const char *db_path, + const char *db_name, + GCancellable *cancellable, + GAsyncReadyCallback callback, + gpointer user_data); +gboolean cm_matrix_open_finish (CmMatrix *self, + GAsyncResult *result, + GError **error); +gboolean cm_matrix_is_ready (CmMatrix *self); +GListModel *cm_matrix_get_clients_list (CmMatrix *self); +gboolean cm_matrix_has_client_with_id (CmMatrix *self, + const char *user_id); +CmClient *cm_matrix_client_new (CmMatrix *self); +void cm_matrix_save_client_async (CmMatrix *self, + CmClient *client, + GAsyncReadyCallback callback, + gpointer user_data); +gboolean cm_matrix_save_client_finish (CmMatrix *self, + GAsyncResult *result, + GError **error); +void cm_matrix_delete_client_async (CmMatrix *self, + CmClient *client, + GAsyncReadyCallback callback, + gpointer user_data); +gboolean cm_matrix_delete_client_finish (CmMatrix *self, + GAsyncResult *result, + GError **error); +void cm_matrix_add_clients_async (CmMatrix *self, + GPtrArray *secrets, + GAsyncReadyCallback callback, + gpointer user_data); +gboolean cm_matrix_add_clients_finish (CmMatrix *self, + GAsyncResult *result, + GError **error); + +G_END_DECLS diff --git a/subprojects/libcmatrix/src/cm-net-private.h b/subprojects/libcmatrix/src/cm-net-private.h new file mode 100644 index 0000000000000000000000000000000000000000..bce04964c417c55a7d81a09fdbedfa29e63ac2a7 --- /dev/null +++ b/subprojects/libcmatrix/src/cm-net-private.h @@ -0,0 +1,71 @@ +/* -*- mode: c; c-basic-offset: 2; indent-tabs-mode: nil; -*- */ +/* cm-net-private.h + * + * Copyright 2022 Purism SPC + * + * Author(s): + * Mohammed Sadiq <sadiq@sadiqpk.org> + * + * SPDX-License-Identifier: LGPL-2.1-or-later + */ + +#pragma once + +#include <glib-object.h> +#include <json-glib/json-glib.h> + +#include "cm-enc-private.h" + +G_BEGIN_DECLS + +#define CM_TYPE_NET (cm_net_get_type ()) + +G_DECLARE_FINAL_TYPE (CmNet, cm_net, CM, NET, GObject) + +CmNet *cm_net_new (void); +void cm_net_set_homeserver (CmNet *self, + const char *homeserver); +void cm_net_set_access_token (CmNet *self, + const char *access_token); +const char *cm_net_get_access_token (CmNet *self); +void cm_net_send_data_async (CmNet *self, + int priority, + char *data, + gsize size, + const char *uri_path, + const char *method, /* interned */ + GHashTable *query, + GCancellable *cancellable, + GAsyncReadyCallback callback, + gpointer user_data); +void cm_net_send_json_async (CmNet *self, + int priority, + JsonObject *object, + const char *uri_path, + const char *method, /* interned */ + GHashTable *query, + GCancellable *cancellable, + GAsyncReadyCallback callback, + gpointer user_data); +void cm_net_get_file_async (CmNet *self, + const char *uri, + CmEncFileInfo *file_info, + GCancellable *cancellable, + GAsyncReadyCallback callback, + gpointer user_data); +GInputStream *cm_net_get_file_finish (CmNet *self, + GAsyncResult *result, + GError **error); +void cm_net_put_file_async (CmNet *self, + GFile *file, + gboolean encrypt, + GFileProgressCallback progress_callback, + gpointer progress_user_data, + GCancellable *cancellable, + GAsyncReadyCallback callback, + gpointer user_data); +char *cm_net_put_file_finish (CmNet *self, + GAsyncResult *result, + GError **error); + +G_END_DECLS diff --git a/subprojects/libcmatrix/src/cm-net.c b/subprojects/libcmatrix/src/cm-net.c new file mode 100644 index 0000000000000000000000000000000000000000..8bedaa689d40057163c8746a3456c3ee5ca22faf --- /dev/null +++ b/subprojects/libcmatrix/src/cm-net.c @@ -0,0 +1,724 @@ +/* -*- mode: c; c-basic-offset: 2; indent-tabs-mode: nil; -*- */ +/* + * Copyright (C) 2022 Purism SPC + * + * SPDX-License-Identifier: LGPL-2.1-or-later + */ + +#define G_LOG_DOMAIN "cm-net" + +#ifdef HAVE_CONFIG_H +# include "config.h" +#endif + +#define GCRYPT_NO_DEPRECATED +#include <gcrypt.h> +#include <libsoup/soup.h> +#include <json-glib/json-glib.h> + +#include "cm-common.h" +#include "cm-utils-private.h" +#include "cm-enums.h" +#include "cm-enc-private.h" +#include "cm-input-stream-private.h" +#include "cm-net-private.h" + +/** + * SECTION: cm-net + * @title: CmNet + * @short_description: Matrix Network related methods + * @include: "cm-net.h" + */ + +#define MAX_CONNECTIONS 4 + +struct _CmNet +{ + GObject parent_instance; + + SoupSession *soup_session; + SoupSession *file_session; + GCancellable *cancellable; + char *homeserver; + char *access_token; +}; + + +G_DEFINE_TYPE (CmNet, cm_net, G_TYPE_OBJECT) + + +static void +net_get_file_stream_cb (GObject *obj, + GAsyncResult *result, + gpointer user_data) +{ + CmNet *self; + g_autoptr(GTask) task = user_data; + GInputStream *stream; + GError *error = NULL; + + g_assert (G_IS_TASK (task)); + + self = g_task_get_source_object (task); + g_assert (CM_IS_NET (self)); + + stream = soup_session_send_finish (SOUP_SESSION (obj), result, &error); + + if (error) + { + g_task_return_error (task, error); + } + else + { + CmInputStream *cm_stream; + CmEncFileInfo *enc_file; + + cm_stream = cm_input_stream_new (stream); + + enc_file = g_object_get_data (user_data, "file"); + cm_input_stream_set_file_enc (cm_stream, enc_file); + + g_task_return_pointer (task, cm_stream, g_object_unref); + } +} + +static void +net_load_from_stream_cb (GObject *object, + GAsyncResult *result, + gpointer user_data) +{ + CmNet *self; + JsonParser *parser = JSON_PARSER (object); + g_autoptr(GTask) task = user_data; + JsonNode *root = NULL; + GError *error = NULL; + + g_assert (JSON_IS_PARSER (parser)); + g_assert (G_IS_TASK (task)); + + self = g_task_get_source_object (task); + g_assert (CM_IS_NET (self)); + + json_parser_load_from_stream_finish (parser, result, &error); + + if (!error) { + root = json_parser_get_root (parser); + error = cm_utils_json_node_get_error (root); + } + + if (error) { + if (g_error_matches (error, CM_ERROR, CM_ERROR_LIMIT_EXCEEDED) && + root && + JSON_NODE_HOLDS_OBJECT (root)) { + JsonObject *obj; + guint retry = 0; + + obj = json_node_get_object (root); + retry = cm_utils_json_object_get_int (obj, "retry_after_ms"); + g_object_set_data (G_OBJECT (task), "retry-after", GINT_TO_POINTER (retry)); + } else { + g_debug ("Error loading from stream: %s", error->message); + } + + g_task_return_error (task, error); + return; + } + + if (JSON_NODE_HOLDS_OBJECT (root)) + g_task_return_pointer (task, json_node_dup_object (root), + (GDestroyNotify)json_object_unref); + else if (JSON_NODE_HOLDS_ARRAY (root)) + g_task_return_pointer (task, json_node_dup_array (root), + (GDestroyNotify)json_array_unref); + else + g_task_return_new_error (task, G_IO_ERROR, G_IO_ERROR_INVALID_DATA, + "Received invalid data"); +} + +static void +session_send_cb (GObject *object, + GAsyncResult *result, + gpointer user_data) +{ + CmNet *self; + g_autoptr(GTask) task = user_data; + g_autoptr(GInputStream) stream = NULL; + g_autoptr(JsonParser) parser = NULL; + GCancellable *cancellable; + GError *error = NULL; + + g_assert (G_IS_TASK (task)); + + self = g_task_get_source_object (task); + g_assert (CM_IS_NET (self)); + + stream = soup_session_send_finish (SOUP_SESSION (object), result, &error); + + if (error) { + if (!g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CANCELLED)) + g_debug ("Error session send: %s", error->message); + g_task_return_error (task, error); + return; + } + + cancellable = g_task_get_cancellable (task); + parser = json_parser_new (); + json_parser_load_from_stream_async (parser, stream, cancellable, + net_load_from_stream_cb, + g_steal_pointer (&task)); +} + +/* + * queue_data: + * @data: (transfer full) + * @size: non-zero if @data is not %NULL + * @task: (transfer full) + */ +static void +queue_data (CmNet *self, + char *data, + gsize size, + const char *uri_path, + const char *method, /* interned */ + GHashTable *query, + GTask *task) +{ + g_autoptr(SoupMessage) message = NULL; +#if SOUP_MAJOR_VERSION == 2 + g_autoptr(SoupURI) uri = NULL; +#else + g_autoptr(GUri) uri = NULL; + GUri *old_uri; + g_autoptr(GBytes) content_data = NULL; +#endif + GCancellable *cancellable; + SoupMessagePriority msg_priority; + int priority = 0; + + g_assert (CM_IS_NET (self)); + g_assert (uri_path && *uri_path); + g_assert (method && *method); + g_return_if_fail (self->homeserver && *self->homeserver); + + g_assert (method == SOUP_METHOD_GET || + method == SOUP_METHOD_POST || + method == SOUP_METHOD_PUT); + +#if SOUP_MAJOR_VERSION == 2 + uri = soup_uri_new (self->homeserver); + soup_uri_set_path (uri, uri_path); +#else + uri = g_uri_parse (self->homeserver, SOUP_HTTP_URI_FLAGS, NULL); + old_uri = uri; + uri = soup_uri_copy (old_uri, SOUP_URI_PATH, uri_path, SOUP_URI_NONE); + g_clear_pointer (&old_uri, g_uri_unref); +#endif + + if (self->access_token) { + if (!query) + query = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, g_free); + + g_hash_table_replace (query, g_strdup ("access_token"), g_strdup (self->access_token)); +#if SOUP_MAJOR_VERSION == 2 + soup_uri_set_query_from_form (uri, query); +#else + old_uri = uri; + uri = soup_uri_copy (old_uri, SOUP_URI_QUERY, soup_form_encode_hash (query), SOUP_URI_NONE); + g_clear_pointer (&old_uri, g_uri_unref); +#endif + g_hash_table_unref (query); + } + + message = soup_message_new_from_uri (method, uri); +#if SOUP_MAJOR_VERSION == 2 + soup_message_headers_append (message->request_headers, "Accept-Encoding", "gzip"); +#else + soup_message_headers_append (soup_message_get_request_headers (message), "Accept-Encoding", "gzip"); +#endif + + priority = GPOINTER_TO_INT (g_object_get_data (G_OBJECT (task), "priority")); + + if (priority <= -2) + msg_priority = SOUP_MESSAGE_PRIORITY_VERY_LOW; + else if (priority == -1) + msg_priority = SOUP_MESSAGE_PRIORITY_LOW; + else if (priority == 1) + msg_priority = SOUP_MESSAGE_PRIORITY_HIGH; + else if (priority >= 2) + msg_priority = SOUP_MESSAGE_PRIORITY_VERY_HIGH; + else + msg_priority = SOUP_MESSAGE_PRIORITY_NORMAL; + + soup_message_set_priority (message, msg_priority); + + if (data) + { +#if SOUP_MAJOR_VERSION == 2 + soup_message_set_request (message, "application/json", SOUP_MEMORY_TAKE, data, size); +#else + content_data = g_bytes_new_take (data, size); + soup_message_set_request_body_from_bytes (message, "application/json", content_data); +#endif + } + + cancellable = g_task_get_cancellable (task); + g_task_set_task_data (task, g_object_ref (message), g_object_unref); + +#if SOUP_MAJOR_VERSION == 2 + soup_session_send_async (self->soup_session, message, cancellable, + session_send_cb, task); +#else + soup_session_send_async (self->soup_session, message, msg_priority, cancellable, + session_send_cb, task); +#endif +} + +static void +cm_net_finalize (GObject *object) +{ + CmNet *self = (CmNet *)object; + + if (self->cancellable) + g_cancellable_cancel (self->cancellable); + + soup_session_abort (self->soup_session); + soup_session_abort (self->file_session); + + g_clear_object (&self->cancellable); + g_clear_object (&self->soup_session); + g_clear_object (&self->file_session); + + g_free (self->homeserver); + g_clear_pointer (&self->access_token, gcry_free); + + G_OBJECT_CLASS (cm_net_parent_class)->finalize (object); +} + +static void +cm_net_class_init (CmNetClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + + object_class->finalize = cm_net_finalize; +} + +static void +cm_net_init (CmNet *self) +{ + self->soup_session = g_object_new (SOUP_TYPE_SESSION, + "max-conns-per-host", MAX_CONNECTIONS, + NULL); + self->file_session = g_object_new (SOUP_TYPE_SESSION, + "max-conns-per-host", MAX_CONNECTIONS, + NULL); + self->cancellable = g_cancellable_new (); +} + +CmNet * +cm_net_new (void) +{ + return g_object_new (CM_TYPE_NET, NULL); +} + +void +cm_net_set_homeserver (CmNet *self, + const char *homeserver) +{ + g_return_if_fail (CM_IS_NET (self)); + g_return_if_fail (homeserver && *homeserver); + + g_free (self->homeserver); + self->homeserver = g_strdup (homeserver); +} + +void +cm_net_set_access_token (CmNet *self, + const char *access_token) +{ + g_return_if_fail (CM_IS_NET (self)); + + g_clear_pointer (&self->access_token, gcry_free); + + if (access_token && *access_token) + { + self->access_token = gcry_malloc_secure (strlen (access_token) + 1); + strcpy (self->access_token, access_token); + } +} + +const char * +cm_net_get_access_token (CmNet *self) +{ + g_return_val_if_fail (CM_IS_NET (self), NULL); + + return self->access_token; +} + +/** + * cm_net_send_data_async: + * @self: A #CmNet + * @priority: The priority of request, 0 for default + * @data: (nullable) (transfer full): The data to send + * @size: The @data size in bytes + * @uri_path: A string of the matrix uri path + * @method: An interned string for GET, PUT, POST, etc. + * @query: (nullable): A query to pass to internal #GUri + * @cancellable: (nullable): A #GCancellable + * @callback: The callback to run when completed + * @user_data: user data for @callback + * + * Send a JSON data @object to the @uri_path endpoint. + * @method should be one of %SOUP_METHOD_GET, %SOUP_METHOD_PUT + * or %SOUP_METHOD_POST. + * If @cancellable is %NULL, the internal cancellable + * shall be used + */ +void +cm_net_send_data_async (CmNet *self, + int priority, + char *data, + gsize size, + const char *uri_path, + const char *method, /* interned */ + GHashTable *query, + GCancellable *cancellable, + GAsyncReadyCallback callback, + gpointer user_data) +{ + GTask *task; + + g_return_if_fail (CM_IS_NET (self)); + g_return_if_fail (uri_path && *uri_path); + g_return_if_fail (method && *method); + g_return_if_fail (!cancellable || G_IS_CANCELLABLE (cancellable)); + g_return_if_fail (callback); + g_return_if_fail (self->homeserver && *self->homeserver); + + if (data && *data) + g_return_if_fail (size); + + if (!cancellable) + cancellable = self->cancellable; + + task = g_task_new (self, cancellable, callback, user_data); + g_object_set_data (G_OBJECT (task), "priority", GINT_TO_POINTER (priority)); + + queue_data (self, data, size, uri_path, method, query, task); +} + +/** + * cm_net_send_json_async: + * @self: A #CmNet + * @priority: The priority of request, 0 for default + * @object: (nullable) (transfer full): The data to send + * @uri_path: A string of the matrix uri path + * @method: An interned string for GET, PUT, POST, etc. + * @query: (nullable): A query to pass to internal #GUri + * @cancellable: (nullable): A #GCancellable + * @callback: The callback to run when completed + * @user_data: user data for @callback + * + * Send a JSON data @object to the @uri_path endpoint. + * @method should be one of %SOUP_METHOD_GET, %SOUP_METHOD_PUT + * or %SOUP_METHOD_POST. + * If @cancellable is %NULL, the internal cancellable + * shall be used + */ +void +cm_net_send_json_async (CmNet *self, + int priority, + JsonObject *object, + const char *uri_path, + const char *method, /* interned */ + GHashTable *query, + GCancellable *cancellable, + GAsyncReadyCallback callback, + gpointer user_data) +{ + GTask *task; + char *data = NULL; + gsize size = 0; + + g_return_if_fail (CM_IS_NET (self)); + g_return_if_fail (uri_path && *uri_path); + g_return_if_fail (method && *method); + g_return_if_fail (!cancellable || G_IS_CANCELLABLE (cancellable)); + g_return_if_fail (callback); + g_return_if_fail (self->homeserver && *self->homeserver); + + if (object) + { + data = cm_utils_json_object_to_string (object, FALSE); + json_object_unref (object); + } + + if (data && *data) + size = strlen (data); + + if (!cancellable) + cancellable = self->cancellable; + + task = g_task_new (self, cancellable, callback, user_data); + g_object_set_data (G_OBJECT (task), "priority", GINT_TO_POINTER (priority)); + + queue_data (self, data, size, uri_path, method, query, task); +} + +/** + * cm_net_get_file_async: + * @self: A #CmNet + * @message: (nullable) (transfer full): A #ChattyMessage + * @file: A #ChattyFileInfo + * @cancellable: (nullable): A #GCancellable + * @progress_callback: (nullable): A #GFileProgressCallback + * @callback: The callback to run when completed + * @user_data: user data for @callback + * + * Download the file @file. @file path shall be updated + * after download is completed, and if @file is encrypted + * and has keys to decrypt the file, the file shall be + * stored decrypted. + */ +void +cm_net_get_file_async (CmNet *self, + const char *uri, + CmEncFileInfo *enc_file, + GCancellable *cancellable, + GAsyncReadyCallback callback, + gpointer user_data) +{ + g_autofree char *url = NULL; + SoupMessage *msg; + GTask *task; + + g_return_if_fail (CM_IS_NET (self)); + g_return_if_fail (uri && *uri); + + if (!cancellable) + cancellable = self->cancellable; + + if (g_str_has_prefix (uri, "mxc://")) { + const char *file_url; + + file_url = uri + strlen ("mxc://"); + url = g_strconcat (self->homeserver, + "/_matrix/media/r0/download/", file_url, NULL); + } + + if (!url) + url = g_strdup (uri); + + msg = soup_message_new (SOUP_METHOD_GET, url); + + task = g_task_new (self, cancellable, callback, user_data); + g_object_set_data_full (G_OBJECT (task), "url", g_strdup (url), g_free); + g_object_set_data (G_OBJECT (task), "file", enc_file); + g_object_set_data_full (G_OBJECT (task), "msg", msg, g_object_unref); + +#if SOUP_MAJOR_VERSION == 2 + soup_session_send_async (self->soup_session, msg, cancellable, + net_get_file_stream_cb, task); +#else + soup_session_send_async (self->file_session, msg, 0, cancellable, + net_get_file_stream_cb, task); +#endif +} + +GInputStream * +cm_net_get_file_finish (CmNet *self, + GAsyncResult *result, + GError **error) +{ + g_return_val_if_fail (CM_IS_NET (self), NULL); + g_return_val_if_fail (G_IS_TASK (result), NULL); + g_return_val_if_fail (!error || !*error, NULL); + + return g_task_propagate_pointer (G_TASK (result), error); +} + +static void +put_file_async_cb (GObject *obj, + GAsyncResult *result, + gpointer user_data) +{ + CmNet *self = user_data; + g_autoptr(JsonObject) root = NULL; + GError *error = NULL; + GTask *task, *local_task; + + local_task = G_TASK (result); + g_assert (G_IS_TASK (local_task)); + + self = g_task_get_source_object (local_task); + task = g_task_get_task_data (local_task); + + g_assert (G_IS_TASK (task)); + g_assert (CM_IS_NET (self)); + + root = g_task_propagate_pointer (G_TASK (result), &error); + g_assert_no_error (error); + + if (root) + { + const char *file_url; + + file_url = cm_utils_json_object_get_string (root, "content_uri"); + g_task_return_pointer (task, g_strdup (file_url), g_free); + } + else + { + if (error) + g_task_return_error (task, error); + else + g_task_return_pointer (task, NULL, NULL); + } +} + +#if SOUP_MAJOR_VERSION == 2 +static void +put_file_chunk (GTask *task, + SoupMessage *msg) +{ + CmNet *self; + GInputStream *stream; + char buffer[8 * 1024]; + gssize n_read; + + g_assert (G_IS_TASK (task)); + g_assert (SOUP_IS_MESSAGE (msg)); + + self = g_task_get_source_object (task); + g_assert (CM_IS_NET (self)); + + stream = g_object_get_data (G_OBJECT (task), "stream"); + n_read = g_input_stream_read (stream, buffer, 8 * 1024, NULL, NULL); + + if (n_read == 0) + { + soup_message_body_complete (msg->request_body); + } + else if (n_read == -1) + { + soup_session_cancel_message (self->file_session, msg, SOUP_STATUS_CANCELLED); + } + else + { + soup_message_body_append (msg->request_body, SOUP_MEMORY_COPY, buffer, n_read); + } +} +#endif + +static void +wrote_body_data_cb (GTask *task, + SoupMessage *msg, +#if SOUP_MAJOR_VERSION == 2 + SoupBuffer *chunk +#else + guint chunk_size +#endif + ) +{ + GFileProgressCallback progress_cb; + gpointer progress_user_data; + + progress_cb = g_object_get_data (G_OBJECT (task), "progress-cb"); + progress_user_data = g_object_get_data (G_OBJECT (task), "progress-cb-data"); + g_assert (progress_cb); + + progress_cb (0, 0, progress_user_data); +} + +void +cm_net_put_file_async (CmNet *self, + GFile *file, + gboolean encrypt, + GFileProgressCallback progress_callback, + gpointer progress_user_data, + GCancellable *cancellable, + GAsyncReadyCallback callback, + gpointer user_data) +{ + g_autoptr(GHashTable) query = NULL; + g_autoptr(GError) error = NULL; + g_autofree char *url = NULL; + CmInputStream *cm_stream; + GTask *task, *local_task; + SoupMessage *msg; + + if (!cancellable) + cancellable = self->cancellable; + + cm_stream = cm_input_stream_new_from_file (file, encrypt, cancellable, &error); + task = g_task_new (self, cancellable, callback, user_data); + + if (!cm_stream) + { + g_task_return_new_error (task, G_IO_ERROR, G_IO_ERROR_FAILED, + "Failed to create stream: %s", error->message ?: ""); + return; + } + + query = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, g_free); + g_hash_table_replace (query, g_strdup ("filename"), g_file_get_basename (file)); + g_hash_table_replace (query, g_strdup ("access_token"), g_strdup (self->access_token)); + + url = g_strconcat (self->homeserver, "/_matrix/media/r0/upload", NULL); + msg = soup_message_new (SOUP_METHOD_POST, url); + +#if SOUP_MAJOR_VERSION == 2 + soup_uri_set_query_from_form (soup_message_get_uri (msg), query); + soup_message_headers_set_encoding (msg->request_headers, SOUP_ENCODING_CHUNKED); + soup_message_body_set_accumulate (msg->request_body, FALSE); + soup_message_headers_set_content_length (msg->request_headers, + cm_input_stream_get_size (cm_stream)); + soup_message_headers_set_content_type (msg->request_headers, + cm_input_stream_get_content_type (cm_stream), NULL); +#else + soup_message_set_uri (msg, soup_uri_copy (soup_message_get_uri (msg), SOUP_URI_QUERY, + soup_form_encode_hash (query), SOUP_URI_NONE)); + + /* We're uploading files in chunk */ + soup_message_set_request_body (msg, + cm_input_stream_get_content_type (cm_stream), + G_INPUT_STREAM (cm_stream), + cm_input_stream_get_size (cm_stream)); +#endif + + g_task_set_task_data (task, g_object_ref (file), g_object_unref); + g_object_set_data_full (G_OBJECT (task), "msg", msg, g_object_unref); + g_object_set_data_full (G_OBJECT (task), "stream", cm_stream, g_object_unref); + + local_task = g_task_new (self, cancellable, put_file_async_cb, self); + g_task_set_task_data (local_task, task, g_object_unref); + +#if SOUP_MAJOR_VERSION == 2 + g_signal_connect_object (msg, "wrote-headers", + G_CALLBACK (put_file_chunk), task, G_CONNECT_SWAPPED); + g_signal_connect_object (msg, "wrote-chunk", + G_CALLBACK (put_file_chunk), task, G_CONNECT_SWAPPED); +#endif + + if (progress_callback) + g_signal_connect_object (msg, "wrote-body-data", + G_CALLBACK (wrote_body_data_cb), task, G_CONNECT_SWAPPED); + +#if SOUP_MAJOR_VERSION == 2 + soup_session_send_async (self->soup_session, msg, cancellable, + session_send_cb, task); +#else + soup_session_send_async (self->file_session, msg, 0, cancellable, + session_send_cb, local_task); +#endif +} + +char * +cm_net_put_file_finish (CmNet *self, + GAsyncResult *result, + GError **error) +{ + g_return_val_if_fail (CM_IS_NET (self), FALSE); + g_return_val_if_fail (G_IS_TASK (result), FALSE); + g_return_val_if_fail (!error || !*error, FALSE); + + return g_task_propagate_pointer (G_TASK (result), error); +} diff --git a/subprojects/libcmatrix/src/cm-olm-private.h b/subprojects/libcmatrix/src/cm-olm-private.h new file mode 100644 index 0000000000000000000000000000000000000000..63c8ce20478f5583ac3b0a276ae332d9f6db46b7 --- /dev/null +++ b/subprojects/libcmatrix/src/cm-olm-private.h @@ -0,0 +1,95 @@ +/* -*- mode: c; c-basic-offset: 2; indent-tabs-mode: nil; -*- */ +/* cm-olm-private.h + * + * Copyright 2022 Purism SPC + * + * Author(s): + * Mohammed Sadiq <sadiq@sadiqpk.org> + * + * SPDX-License-Identifier: LGPL-2.1-or-later + */ + +#pragma once + +#include <glib-object.h> + +#include "cm-db-private.h" + +G_BEGIN_DECLS + +/* The value of the items shouldn't be changed as + * they are used in db */ +typedef enum { + OLM_STATE_USABLE = 0, + OLM_STATE_ROTATED = 1, + OLM_STATE_INVALIDATED = 2, + OLM_STATE_NOT_SET = 8, +} CmOlmState; + +#define CM_TYPE_OLM (cm_olm_get_type ()) + +G_DECLARE_FINAL_TYPE (CmOlm, cm_olm, CM, OLM, GObject) + +CmOlm *cm_olm_new_from_pickle (char *pickle, + const char *pickle_key, + const char *sender_identity_key, + CmSessionType session_type); +CmOlm *cm_olm_outbound_new (gpointer olm_account, + const char *curve_key, + const char *one_time_key, + const char *room_id); +CmOlm *cm_olm_inbound_new (gpointer olm_account, + const char *sender_identity_key, + const char *one_time_key_message); +CmOlm *cm_olm_in_group_new (const char *session_key, + const char *sender_identity_key, + const char *session_id); +CmOlm *cm_olm_in_group_new_from_out (CmOlm *out_group, + const char *sender_identity_key); +CmOlm *cm_olm_out_group_new (const char *sender_identity_key); + +CmSessionType cm_olm_get_session_type (CmOlm *self); +size_t cm_olm_get_message_index (CmOlm *self); +gint64 cm_olm_get_created_time (CmOlm *self); +void cm_olm_set_state (CmOlm *self, + CmOlmState state); +void cm_olm_update_validity (CmOlm *self, + guint count, + gint64 duration); +CmOlmState cm_olm_get_state (CmOlm *self); + +void cm_olm_set_sender_details (CmOlm *self, + const char *room_id, + GRefString *sender_id); +void cm_olm_set_account_details (CmOlm *self, + GRefString *account_user_id, + const char *account_device_id); +void cm_olm_set_db (CmOlm *self, + gpointer cm_db); +void cm_olm_set_key (CmOlm *self, + const char *key); +gboolean cm_olm_save (CmOlm *self); +char *cm_olm_encrypt (CmOlm *self, + const char *plain_text); +char *cm_olm_decrypt (CmOlm *self, + size_t type, + const char *message); +size_t cm_olm_get_message_type (CmOlm *self); + +const char *cm_olm_get_session_id (CmOlm *self); +const char *cm_olm_get_session_key (CmOlm *self); +const char *cm_olm_get_room_id (CmOlm *self); +const char *cm_olm_get_sender_key (CmOlm *self); +GRefString *cm_olm_get_account_id (CmOlm *self); +const char *cm_olm_get_account_device (CmOlm *self); + +gpointer cm_olm_match_olm_session (const char *body, + gsize body_len, + size_t message_type, + const char *pickle, + const char *pickle_key, + const char *sender_identify_key, + CmSessionType session_type, + char **out_decrypted); + +G_END_DECLS diff --git a/subprojects/libcmatrix/src/cm-olm-sas-private.h b/subprojects/libcmatrix/src/cm-olm-sas-private.h new file mode 100644 index 0000000000000000000000000000000000000000..63d1ac5bc13cec22dd95af9c854827b14839d197 --- /dev/null +++ b/subprojects/libcmatrix/src/cm-olm-sas-private.h @@ -0,0 +1,43 @@ +/* -*- mode: c; c-basic-offset: 2; indent-tabs-mode: nil; -*- */ +/* cm-olm-sas-private.h + * + * Copyright 2022 Purism SPC + * + * Author(s): + * Mohammed Sadiq <sadiq@sadiqpk.org> + * + * SPDX-License-Identifier: LGPL-2.1-or-later + */ + +#pragma once + +#include <glib-object.h> + +#include "cm-device.h" +#include "events/cm-event.h" + +G_BEGIN_DECLS + +#define CM_TYPE_OLM_SAS (cm_olm_sas_get_type ()) + +G_DECLARE_FINAL_TYPE (CmOlmSas, cm_olm_sas, CM, OLM_SAS, GObject) + +CmOlmSas *cm_olm_sas_new (void); +void cm_olm_sas_set_client (CmOlmSas *self, + gpointer cm_client); +void cm_olm_sas_set_key_verification (CmOlmSas *self, + CmEvent *event); +gboolean cm_olm_sas_matches_event (CmOlmSas *self, + CmEvent *event); +const char *cm_olm_sas_get_cancel_code (CmOlmSas *self); +CmEvent *cm_olm_sas_get_cancel_event (CmOlmSas *self, + const char *cancel_code); +CmEvent *cm_olm_sas_get_accept_event (CmOlmSas *self); +CmEvent *cm_olm_sas_get_key_event (CmOlmSas *self); +GPtrArray *cm_olm_sas_get_emojis (CmOlmSas *self); +CmEvent *cm_olm_sas_get_mac_event (CmOlmSas *self); +CmEvent *cm_olm_sas_get_done_event (CmOlmSas *self); +gboolean cm_olm_sas_is_verified (CmOlmSas *self); +CmDevice *cm_olm_sas_get_device (CmOlmSas *self); + +G_END_DECLS diff --git a/subprojects/libcmatrix/src/cm-olm-sas.c b/subprojects/libcmatrix/src/cm-olm-sas.c new file mode 100644 index 0000000000000000000000000000000000000000..e2d5f39c7aa5305007bb50ae9cd1100352d9fd91 --- /dev/null +++ b/subprojects/libcmatrix/src/cm-olm-sas.c @@ -0,0 +1,874 @@ +/* -*- mode: c; c-basic-offset: 2; indent-tabs-mode: nil; -*- */ +/* cm-olm-sas.c + * + * Copyright 2022 Purism SPC + * + * Author(s): + * Mohammed Sadiq <sadiq@sadiqpk.org> + * + * SPDX-License-Identifier: LGPL-2.1-or-later + */ + +#define G_LOG_DOMAIN "cm-olm" + +#ifdef HAVE_CONFIG_H +# include "config.h" +#endif + +#define GCRYPT_NO_DEPRECATED +#include <gcrypt.h> +#include <olm/olm.h> +#include <olm/sas.h> + +#include "events/cm-event-private.h" +#include "cm-enc-private.h" +#include "cm-client.h" +#include "cm-client-private.h" +#include "cm-device-private.h" +#include "users/cm-user-private.h" +#include "cm-utils-private.h" +#include "cm-olm-sas-private.h" + +/* The order shouldn't be changed */ +/* https://github.com/matrix-org/matrix-spec-proposals/blob/old_master/data-definitions/sas-emoji.json */ +static const char *emojis[] = { + "ðŸ¶", /* "Dog", "U+1F436" */ + "ðŸ±", /* "Cat", "U+1F431" */ + "ðŸ¦", /* "Lion", "U+1F981" */ + "ðŸŽ", /* "Horse", "U+1F40E" */ + "🦄", /* "Unicorn", "U+1F984" */ + "ðŸ·", /* "Pig", "U+1F437" */ + "ðŸ˜", /* "Elephant", "U+1F418" */ + "ðŸ°", /* "Rabbit", "U+1F430" */ + "ðŸ¼", /* "Panda", "U+1F43C" */ + "ðŸ“", /* "Rooster", "U+1F413" */ + "ðŸ§", /* "Penguin", "U+1F427" */ + "ðŸ¢", /* "Turtle", "U+1F422" */ + "ðŸŸ", /* "Fish", "U+1F41F" */ + "ðŸ™", /* "Octopus", "U+1F419" */ + "🦋", /* "Butterfly", "U+1F98B" */ + "🌷", /* "Flower", "U+1F337" */ + "🌳", /* "Tree", "U+1F333" */ + "🌵", /* "Cactus", "U+1F335" */ + "ðŸ„", /* "Mushroom", "U+1F344" */ + "ðŸŒ", /* "Globe", "U+1F30F" */ + "🌙", /* "Moon", "U+1F319" */ + "â˜ï¸", /* "Cloud", "U+2601U+FE0F" */ + "🔥", /* "Fire", "U+1F525" */ + "ðŸŒ", /* "Banana", "U+1F34C" */ + "ðŸŽ", /* "Apple", "U+1F34E" */ + "ðŸ“", /* "Strawberry", "U+1F353" */ + "🌽", /* "Corn", "U+1F33D" */ + "ðŸ•", /* "Pizza", "U+1F355" */ + "🎂", /* "Cake", "U+1F382" */ + "â¤ï¸", /* "Heart", "U+2764U+FE0F" */ + "😀", /* "Smiley", "U+1F600" */ + "🤖", /* "Robot", "U+1F916" */ + "🎩", /* "Hat", "U+1F3A9" */ + "👓", /* "Glasses", "U+1F453" */ + "🔧", /* "Spanner", "U+1F527" */ + "🎅", /* "Santa", "U+1F385" */ + "ðŸ‘", /* "Thumbs Up", "U+1F44D" */ + "☂ï¸", /* "Umbrella", "U+2602U+FE0F" */ + "⌛", /* "Hourglass", "U+231B" */ + "â°", /* "Clock", "U+23F0" */ + "ðŸŽ", /* "Gift", "U+1F381" */ + "💡", /* "Light Bulb", "U+1F4A1" */ + "📕", /* "Book", "U+1F4D5" */ + "âœï¸", /* "Pencil", "U+270FU+FE0F" */ + "📎", /* "Paperclip", "U+1F4CE" */ + "✂ï¸", /* "Scissors", "U+2702U+FE0F" */ + "🔒", /* "Lock", "U+1F512" */ + "🔑", /* "Key", "U+1F511" */ + "🔨", /* "Hammer", "U+1F528" */ + "☎ï¸", /* "Telephone", "U+260EU+FE0F" */ + "ðŸ", /* "Flag", "U+1F3C1" */ + "🚂", /* "Train", "U+1F682" */ + "🚲", /* "Bicycle", "U+1F6B2" */ + "✈ï¸", /* "Aeroplane", "U+2708U+FE0F" */ + "🚀", /* "Rocket", "U+1F680" */ + "ðŸ†", /* "Trophy", "U+1F3C6" */ + "âš½", /* "Ball", "U+26BD" */ + "🎸", /* "Guitar", "U+1F3B8" */ + "🎺", /* "Trumpet", "U+1F3BA" */ + "🔔", /* "Bell", "U+1F514" */ + "âš“", /* "Anchor", "U+2693" */ + "🎧", /* "Headphones", "U+1F3A7" */ + "ðŸ“", /* "Folder", "U+1F4C1" */ + "📌", /* "Pin", "U+1F4CC" */ +}; + +#define NUM_SAS_BYTES (6) + +struct _CmOlmSas +{ + GObject parent_instance; + + CmClient *cm_client; + + OlmSAS *olm_sas; + char *our_pub_key; + char *their_pub_key; + + char *their_user_id; + char *their_device_id; + CmDevice *their_device; + + char *cancel_code; + + CmEvent *key_verification_event; + CmEvent *key_verification_cancel; + CmEvent *key_verification_accept; + CmEvent *key_verification_mac; + CmEvent *key_verification_done; + CmEvent *verification_key; + GString *commitment_str; + + guint8 *sas_bytes; + guint8 *sas_emoji_indices; + GPtrArray *sas_emojis; + guint16 *sas_decimals; + + gboolean verified; +}; + +G_DEFINE_TYPE (CmOlmSas, cm_olm_sas, G_TYPE_OBJECT) + +static char * +calculate_mac (CmOlmSas *self, + const char *input, + const char *info, + size_t info_len) +{ + char *mac; + size_t len; + + g_assert (CM_IS_OLM_SAS (self)); + + len = olm_sas_mac_length (self->olm_sas); + mac = g_malloc (len + 1); + olm_sas_calculate_mac (self->olm_sas, input, strlen (input), + info, info_len, mac, len); + mac[len] = '\0'; + + return mac; +} + +static void +cm_olm_sas_generate_bytes (CmOlmSas *self) +{ + g_autoptr(GString) sas_info = NULL; + g_autofree char *their_info = NULL; + g_autofree char *our_info = NULL; + const char *user_id, *device_id, *transaction_id; + guint8 *bytes; + + if (self->sas_bytes) + return; + + user_id = cm_client_get_user_id (self->cm_client); + device_id = cm_client_get_device_id (self->cm_client); + our_info = g_strdup_printf ("%s|%s|%s", user_id, device_id, self->our_pub_key); + + user_id = self->their_user_id; + device_id = self->their_device_id; + their_info = g_strdup_printf ("%s|%s|%s", user_id, device_id, self->their_pub_key); + + sas_info = g_string_sized_new (1024); + transaction_id = cm_event_get_transaction_id (self->key_verification_event); + + g_string_append_printf (sas_info, "MATRIX_KEY_VERIFICATION_SAS|%s|%s|%s", + their_info, our_info, transaction_id); + + /* Always generate 6 bytes even if we may use decimal verification */ + /* for which we'll ignore the last byte as it requires only 5 */ + self->sas_bytes = g_malloc (NUM_SAS_BYTES); + bytes = self->sas_bytes; + olm_sas_generate_bytes (self->olm_sas, sas_info->str, sas_info->len, self->sas_bytes, NUM_SAS_BYTES); + + /* We have 7 items of 6 bit each */ + self->sas_emoji_indices = g_malloc0 (7); + + /* The indices are of 6 bits, so iterate over every byte and extract + * those 6 bit indices and store as bytes + */ + /* Don't complicate by using loops */ + self->sas_emoji_indices[0] = bytes[0] >> 2; + self->sas_emoji_indices[1] = (bytes[0] & 0b11) << 4 | bytes[1] >> 4; + self->sas_emoji_indices[2] = (bytes[1] & 0b1111) << 2 | bytes[2] >> 6; + self->sas_emoji_indices[3] = bytes[2] & 0b111111; + self->sas_emoji_indices[4] = bytes[3] >> 2; + self->sas_emoji_indices[5] = (bytes[3] & 0b11) << 4 | bytes[4] >> 4; + self->sas_emoji_indices[6] = (bytes[4] & 0b1111) << 2 | bytes[5] >> 6; + + /* There are 3 numbers of 13 bits */ + self->sas_decimals = g_malloc0 (4 * sizeof(guint16)); + self->sas_decimals[0] = (bytes[0] << 5 | bytes[1] >> 3) + 1000; + self->sas_decimals[1] = ((bytes[1] & 0b111) << 10 | bytes[2] << 2 | bytes[3] >> 6) + 1000; + self->sas_decimals[2] = ((bytes[3] & 0b111111) << 7 | bytes[4] >> 1) + 1000; +} + +static JsonObject * +olm_sas_get_message_json (CmOlmSas *self, + JsonObject **content) +{ + JsonObject *root, *child; + const char *value; + + root = json_object_new (); + + child = json_object_new (); + json_object_set_object_member (root, "messages", child); + + value = cm_event_get_sender_id (self->key_verification_event); + json_object_set_object_member (child, value, json_object_new ()); + child = cm_utils_json_object_get_object (child, value); + + value = cm_event_get_sender_device_id (self->key_verification_event); + json_object_set_object_member (child, value, json_object_new ()); + child = cm_utils_json_object_get_object (child, value); + + value = cm_event_get_transaction_id (self->key_verification_event); + json_object_set_string_member (child, "transaction_id", value); + + if (content) + *content = child; + + return root; +} + +static void +cm_olm_sas_create_commitment (CmOlmSas *self) +{ + g_autofree OlmUtility *olm_util = NULL; + g_autofree char *sha256 = NULL; + g_autoptr(JsonObject) json = NULL; + g_autoptr(GString) str = NULL; + JsonObject *content; + CmEvent *event; + size_t len; + + g_return_if_fail (CM_IS_OLM_SAS (self)); + g_return_if_fail (self->key_verification_event); + + if (self->commitment_str->len) + return; + + if (cm_event_get_m_type (self->key_verification_event) == CM_M_KEY_VERIFICATION_REQUEST) + event = g_object_get_data (G_OBJECT (self->key_verification_event), "start"); + else + event = self->key_verification_event; + + /* We should have an m.key.verification.start event to get commitment */ + g_return_if_fail (event); + + str = g_string_sized_new (1024); + if (!self->our_pub_key) + { + len = olm_sas_pubkey_length (self->olm_sas); + self->our_pub_key = g_malloc (len + 1); + olm_sas_get_pubkey (self->olm_sas, self->our_pub_key, len); + self->our_pub_key[len] = '\0'; + } + + g_string_append_len (str, self->our_pub_key, strlen (self->our_pub_key)); + json = cm_event_get_json (event); + content = cm_utils_json_object_get_object (json, "content"); + cm_utils_json_get_canonical (content, str); + + olm_util = g_malloc (olm_utility_size ()); + olm_utility (olm_util); + + len = olm_sha256_length (olm_util); + sha256 = g_malloc (len); + olm_sha256 (olm_util, str->str, str->len, sha256, len); + g_string_append_len (self->commitment_str, sha256, len); +} + +static void +cm_olm_sas_finalize (GObject *object) +{ + CmOlmSas *self = (CmOlmSas *)object; + + g_clear_object (&self->key_verification_event); + olm_clear_sas (self->olm_sas); + g_free (self->olm_sas); + + if (self->commitment_str) + g_string_free (self->commitment_str, TRUE); + + g_free (self->our_pub_key); + g_free (self->their_pub_key); + g_free (self->their_user_id); + g_free (self->their_device_id); + + g_free (self->sas_bytes); + + G_OBJECT_CLASS (cm_olm_sas_parent_class)->finalize (object); +} + +static void +cm_olm_sas_class_init (CmOlmSasClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + + object_class->finalize = cm_olm_sas_finalize; +} + +static void +cm_olm_sas_init (CmOlmSas *self) +{ + size_t len; + + self->olm_sas = g_malloc (olm_sas_size ()); + self->commitment_str = g_string_sized_new (256); + olm_sas (self->olm_sas); + + len = olm_create_sas_random_length (self->olm_sas); + if (len) + { + cm_gcry_t buffer; + + buffer = gcry_random_bytes (len, GCRY_STRONG_RANDOM); + olm_create_sas (self->olm_sas, buffer, len); + gcry_free (buffer); + } +} + +CmOlmSas * +cm_olm_sas_new (void) +{ + return g_object_new (CM_TYPE_OLM_SAS, NULL); +} + +void +cm_olm_sas_set_client (CmOlmSas *self, + gpointer cm_client) +{ + g_return_if_fail (CM_IS_OLM_SAS (self)); + g_return_if_fail (CM_IS_CLIENT (cm_client)); + + if (!self->cm_client) + g_set_weak_pointer (&self->cm_client, cm_client); +} + +static gboolean +cm_olm_array_has_string (JsonObject *content, + const char *array_str, + const char *value) +{ + JsonArray *array; + const char *element; + + if (!content) + return FALSE; + + g_assert (array_str && *array_str); + g_assert (value && *value); + + array = cm_utils_json_object_get_array (content, array_str); + for (guint i = 0; i < json_array_get_length (array); i++) + { + element = json_array_get_string_element (array, i); + + if (g_strcmp0 (element, value) == 0) + return TRUE; + } + + return FALSE; +} + +static void +cm_olm_sas_parse_verification_start (CmOlmSas *self, + CmEvent *event) +{ + g_autoptr(JsonObject) root = NULL; + JsonObject *content; + + g_assert (CM_IS_OLM_SAS (self)); + g_assert (CM_IS_EVENT (event)); + + root = cm_event_get_json (event); + content = cm_utils_json_object_get_object (root, "content"); + + if (g_strcmp0 (cm_utils_json_object_get_string (content, "method"), "m.sas.v1") != 0 || + !cm_olm_array_has_string (content, "key_agreement_protocols", "curve25519-hkdf-sha256") || + !cm_olm_array_has_string (content, "hashes", "sha256") || + !cm_olm_array_has_string (content, "message_authentication_codes", "hkdf-hmac-sha256") || + !cm_olm_array_has_string (content, "short_authentication_string", "decimal")) + self->cancel_code = g_strdup ("m.unknown_method"); +} + +static void +cm_olm_sas_parse_verification_mac (CmOlmSas *self, + CmEvent *event) +{ + g_autoptr(GString) base_info = NULL; + g_autoptr(GString) key_ids = NULL; + g_autoptr(GList) members = NULL; + g_autofree char *mac = NULL; + JsonObject *root, *content, *mac_json; + + g_assert (CM_IS_OLM_SAS (self)); + g_assert (CM_IS_EVENT (event)); + + if (!g_object_get_data (G_OBJECT (self->key_verification_event), "key") && + !self->cancel_code) + { + self->cancel_code = g_strdup ("m.unexpected_message"); + return; + } + + if (self->verified || self->cancel_code) + return; + + root = cm_event_get_json (event); + content = cm_utils_json_object_get_object (root, "content"); + mac_json = cm_utils_json_object_get_object (content, "mac"); + + if (!mac_json) + { + self->cancel_code = g_strdup ("m.key_mismatch"); + return; + } + + key_ids = g_string_sized_new (1024); + members = json_object_get_members (mac_json); + + members = g_list_sort (members, (GCompareFunc)g_strcmp0); + + for (GList *item = members; item && item->data; item = item->next) + { + g_string_append (key_ids, (char *)item->data); + g_string_append_c (key_ids, ','); + } + + if (!key_ids->len) + { + self->cancel_code = g_strdup ("m.key_mismatch"); + return; + } + + /* Remove the trailing ',' */ + g_string_set_size (key_ids, key_ids->len - 1); + + base_info = g_string_sized_new (1024); + g_string_printf (base_info, "MATRIX_KEY_VERIFICATION_MAC%s%s%s%s%s", + self->their_user_id, self->their_device_id, + cm_client_get_user_id (self->cm_client), + cm_client_get_device_id (self->cm_client), + cm_event_get_transaction_id (self->key_verification_event)); + g_string_append (base_info, "KEY_IDS"); + + mac = calculate_mac (self, key_ids->str, base_info->str, base_info->len); + + if (g_strcmp0 (mac, cm_utils_json_object_get_string (content, "keys")) != 0) + { + g_debug ("(%p) key mismatch, mac != keys", self); + self->cancel_code = g_strdup ("m.key_mismatch"); + return; + } + + for (GList *item = members; item && item->data; item = item->next) + { + g_auto(GStrv) strv = NULL; + const char *key_mac; + CmDevice *device; + CmUser *user; + + strv = g_strsplit (item->data, ":", -1); + + if (g_strcmp0 (strv[0], "ed25519") != 0) + { + g_debug ("(%p) key mismatch, '%s' is not ed25519", self, strv[0]); + self->cancel_code = g_strdup ("m.key_mismatch"); + return; + } + + if (g_strcmp0 (strv[1], self->their_device_id) != 0) + continue; + + key_mac = cm_utils_json_object_get_string (mac_json, item->data); + user = cm_event_get_sender (self->key_verification_event); + device = cm_user_find_device (user, self->their_device_id); + + if (!device || !key_mac || !cm_device_get_ed_key (device)) + { + self->cancel_code = g_strdup ("m.key_mismatch"); + return; + } + + g_free (mac); + g_string_truncate (base_info, base_info->len - strlen ("KEY_IDS")); + g_string_append (base_info, item->data); + + mac = calculate_mac (self, cm_device_get_ed_key (device), + base_info->str, base_info->len); + + /* Currently we handle only one device */ + if (g_strcmp0 (key_mac, mac) != 0) + { + self->cancel_code = g_strdup ("m.key_mismatch"); + return; + } + + self->verified = TRUE; + self->their_device = g_object_ref (device); + cm_device_set_verified (device, TRUE); + } +} + +void +cm_olm_sas_set_key_verification (CmOlmSas *self, + CmEvent *event) +{ + g_autoptr(JsonObject) json = NULL; + CmEventType type; + gint64 minutes; + + g_return_if_fail (CM_IS_OLM_SAS (self)); + g_return_if_fail (CM_IS_EVENT (event)); + g_return_if_fail (!self->key_verification_event); + + type = cm_event_get_m_type (event); + g_return_if_fail (type == CM_M_KEY_VERIFICATION_REQUEST || + type == CM_M_KEY_VERIFICATION_START); + self->key_verification_event = g_object_ref (event); + + /* fixme: We now only accepts verification requests */ + self->their_user_id = g_strdup (cm_event_get_sender_id (event)); + self->their_device_id = g_strdup (cm_event_get_sender_device_id (event)); + + minutes = (time (NULL) - cm_event_get_time_stamp (event) / 1000) % 60; + + if (type == CM_M_KEY_VERIFICATION_START) + cm_olm_sas_parse_verification_start (self, event); + + if (self->cancel_code) + return; + + /* Cancel if request is 10+ minutes from the past or 5+ minutes from the future */ + if (minutes < -5 || minutes > 10) + self->cancel_code = g_strdup ("m.timeout"); +} + +gboolean +cm_olm_sas_matches_event (CmOlmSas *self, + CmEvent *event) +{ + GObject *obj = (GObject *)self->key_verification_event; + const char *item_txn_id, *event_txn_id; + CmEventType type; + + g_return_val_if_fail (CM_IS_OLM_SAS (self), FALSE); + g_return_val_if_fail (CM_IS_EVENT (event), FALSE); + g_return_val_if_fail (self->key_verification_event, FALSE); + + if (event == self->key_verification_event) + return TRUE; + + item_txn_id = cm_event_get_transaction_id (event); + event_txn_id = cm_event_get_transaction_id (self->key_verification_event); + + if (g_strcmp0 (item_txn_id, event_txn_id) != 0) + return FALSE; + + g_object_ref (event); + type = cm_event_get_m_type (event); + + if (type == CM_M_KEY_VERIFICATION_KEY) + { + g_autofree char *key = NULL; + + key = g_strdup (cm_event_get_verification_key (event)); + + if (olm_sas_is_their_key_set (self->olm_sas)) + { + g_warning ("Key was already set"); + } + else + { + self->their_pub_key = g_strdup (key); + olm_sas_set_their_key (self->olm_sas, key, strlen (key)); + } + } + + if (type == CM_M_KEY_VERIFICATION_CANCEL) + g_object_set_data_full (obj, "cancel", event, g_object_unref); + else if (type == CM_M_KEY_VERIFICATION_DONE) + g_object_set_data_full (obj, "done", event, g_object_unref); + else if (type == CM_M_KEY_VERIFICATION_KEY) + g_object_set_data_full (obj, "key", event, g_object_unref); + else if (type == CM_M_KEY_VERIFICATION_MAC) + g_object_set_data_full (obj, "mac", event, g_object_unref); + else if (type == CM_M_KEY_VERIFICATION_READY) + g_object_set_data_full (obj, "ready", event, g_object_unref); + else if (type == CM_M_KEY_VERIFICATION_REQUEST) + g_object_set_data_full (obj, "request", event, g_object_unref); + else if (type == CM_M_KEY_VERIFICATION_START) + g_object_set_data_full (obj, "start", event, g_object_unref); + + if (type == CM_M_KEY_VERIFICATION_START) + cm_olm_sas_parse_verification_start (self, event); + + if (type == CM_M_KEY_VERIFICATION_MAC) + cm_olm_sas_parse_verification_mac (self, event); + + if (type == CM_M_KEY_VERIFICATION_CANCEL && !self->cancel_code) + self->cancel_code = g_strdup ("m.timeout"); + + /* generate emojis */ + if (type == CM_M_KEY_VERIFICATION_KEY) + cm_olm_sas_get_emojis (self); + + g_signal_emit_by_name (obj, "updated", 0); + + return TRUE; +} + +/** + * cm_olm_sas_get_cancel_reason: + * @self: A #CmOlmSas + * + * Get the error 'code' to be used for m.key.verification.cancel. + * This is to be checked after every update and the verification + * should be cancelled if a non-%NULL value is return. + * + * Returns: (nullable): A string + */ +const char * +cm_olm_sas_get_cancel_code (CmOlmSas *self) +{ + g_return_val_if_fail (CM_IS_OLM_SAS (self), NULL); + + return self->cancel_code; +} + +CmEvent * +cm_olm_sas_get_cancel_event (CmOlmSas *self, + const char *cancel_code) +{ + JsonObject *root, *child; + CmEvent *event; + + g_return_val_if_fail (CM_IS_OLM_SAS (self), NULL); + g_return_val_if_fail (self->key_verification_event, NULL); + g_return_val_if_fail (self->cm_client, NULL); + + if (self->key_verification_cancel) + return self->key_verification_cancel; + + if (!cancel_code) + cancel_code = "m.user"; + + if (g_strcmp0 (cancel_code, "m.user") != 0 && + g_strcmp0 (cancel_code, "m.timeout") != 0 && + g_strcmp0 (cancel_code, "m.unknown_method") != 0 && + g_strcmp0 (cancel_code, "m.key_mismatch") != 0 && + g_strcmp0 (cancel_code, "m.user_mismatch") != 0 && + g_strcmp0 (cancel_code, "m.unexpected_message") != 0) + g_return_val_if_reached (NULL); + + event = cm_event_new (CM_M_KEY_VERIFICATION_CANCEL); + cm_event_create_txn_id (event, cm_client_pop_event_id (self->cm_client)); + self->key_verification_cancel = event; + + root = olm_sas_get_message_json (self, &child); + cm_event_set_json (event, root, NULL); + + json_object_set_string_member (child, "code", cancel_code); + + return self->key_verification_cancel; +} + +CmEvent * +cm_olm_sas_get_accept_event (CmOlmSas *self) +{ + JsonObject *root, *child; + JsonArray *array; + CmEvent *event; + + g_return_val_if_fail (CM_IS_OLM_SAS (self), NULL); + g_return_val_if_fail (self->key_verification_event, NULL); + + if (self->key_verification_accept) + return self->key_verification_accept; + + if (cm_event_get_m_type (self->key_verification_event) == CM_M_KEY_VERIFICATION_REQUEST) + event = g_object_get_data (G_OBJECT (self->key_verification_event), "start"); + else + event = self->key_verification_event; + + /* We should have an m.key.verification.start event to get commitment */ + g_return_val_if_fail (event, NULL); + cm_olm_sas_create_commitment (self); + + event = cm_event_new (CM_M_KEY_VERIFICATION_ACCEPT); + cm_event_create_txn_id (event, cm_client_pop_event_id (self->cm_client)); + self->key_verification_accept = event; + + root = olm_sas_get_message_json (self, &child); + cm_event_set_json (event, root, NULL); + + json_object_set_string_member (child, "hash", "sha256"); + json_object_set_string_member (child, "method", "m.sas.v1"); + json_object_set_string_member (child, "key_agreement_protocol", "curve25519-hkdf-sha256"); + json_object_set_string_member (child, "commitment", self->commitment_str->str); + json_object_set_string_member (child, "message_authentication_code", "hkdf-hmac-sha256"); + + array = json_array_new (); + json_array_add_string_element (array, "emoji"); + json_array_add_string_element (array, "decimal"); + json_object_set_array_member (child, "short_authentication_string", array); + + return self->key_verification_accept; +} + +CmEvent * +cm_olm_sas_get_key_event (CmOlmSas *self) +{ + JsonObject *root, *child; + CmEvent *event; + + g_return_val_if_fail (CM_IS_OLM_SAS (self), NULL); + g_return_val_if_fail (self->key_verification_event, NULL); + + if (self->verification_key) + return self->verification_key; + + if (cm_event_get_m_type (self->key_verification_event) == CM_M_KEY_VERIFICATION_REQUEST) + event = g_object_get_data (G_OBJECT (self->key_verification_event), "start"); + else + event = self->key_verification_event; + + /* We should have an m.key.verification.start event to get key event */ + g_return_val_if_fail (event, NULL); + cm_olm_sas_create_commitment (self); + + event = cm_event_new (CM_M_KEY_VERIFICATION_KEY); + cm_event_create_txn_id (event, cm_client_pop_event_id (self->cm_client)); + self->verification_key = event; + + root = olm_sas_get_message_json (self, &child); + cm_event_set_json (event, root, NULL); + + json_object_set_string_member (child, "key", self->our_pub_key); + + return self->verification_key; +} + +GPtrArray * +cm_olm_sas_get_emojis (CmOlmSas *self) +{ + g_return_val_if_fail (CM_IS_OLM_SAS (self), NULL); + g_return_val_if_fail (self->key_verification_event, NULL); + + if (!g_object_get_data (G_OBJECT (self->key_verification_event), "key")) + return NULL; + + /* We need to have both keys to have a drink */ + g_return_val_if_fail (self->our_pub_key, NULL); + g_return_val_if_fail (self->their_pub_key, NULL); + + cm_olm_sas_generate_bytes (self); + + if (!self->sas_emojis) + { + self->sas_emojis = g_ptr_array_sized_new (7); + + for (guint i = 0; i < 7; i++) + g_ptr_array_add (self->sas_emojis, g_strdup (emojis[self->sas_emoji_indices[i]])); + + g_object_set_data (G_OBJECT (self->key_verification_event), "emoji", self->sas_emojis); + g_object_set_data (G_OBJECT (self->key_verification_event), "decimal", self->sas_decimals); + } + + return self->sas_emojis; +} + +CmEvent * +cm_olm_sas_get_mac_event (CmOlmSas *self) +{ + g_autoptr(GString) base_info = NULL; + g_autofree char *key_id = NULL; + g_autofree char *keys = NULL; + g_autofree char *mac = NULL; + JsonObject *root, *content, *mac_json; + const char *ed25519; + CmEvent *event; + + g_return_val_if_fail (CM_IS_OLM_SAS (self), NULL); + g_return_val_if_fail (self->key_verification_event, NULL); + g_return_val_if_fail (self->verification_key, NULL); + + if (self->key_verification_mac) + return self->key_verification_mac; + + base_info = g_string_sized_new (1024); + g_string_printf (base_info, "MATRIX_KEY_VERIFICATION_MAC%s%s%s%s%s", + cm_client_get_user_id (self->cm_client), + cm_client_get_device_id (self->cm_client), + self->their_user_id, self->their_device_id, + cm_event_get_transaction_id (self->key_verification_event)); + key_id = g_strconcat ("ed25519:", cm_client_get_device_id (self->cm_client), NULL); + g_string_append (base_info, key_id); + + ed25519 = cm_client_get_ed25519_key (self->cm_client); + mac = calculate_mac (self, ed25519, base_info->str, base_info->len); + + g_string_truncate (base_info, base_info->len - strlen (key_id)); + g_string_append (base_info, "KEY_IDS"); + keys = calculate_mac (self, key_id, base_info->str, base_info->len); + + event = cm_event_new (CM_M_KEY_VERIFICATION_MAC); + cm_event_create_txn_id (event, cm_client_pop_event_id (self->cm_client)); + self->key_verification_mac = event; + + root = olm_sas_get_message_json (self, &content); + mac_json = json_object_new (); + json_object_set_string_member (mac_json, key_id, mac); + json_object_set_object_member (content, "mac", mac_json); + json_object_set_string_member (content, "keys", keys); + cm_event_set_json (event, root, NULL); + + return self->key_verification_mac; +} + +CmEvent * +cm_olm_sas_get_done_event (CmOlmSas *self) +{ + JsonObject *root; + CmEvent *event; + + g_return_val_if_fail (CM_IS_OLM_SAS (self), NULL); + g_return_val_if_fail (self->key_verification_event, NULL); + g_return_val_if_fail (self->cm_client, NULL); + + if (self->key_verification_done) + return self->key_verification_done; + + event = cm_event_new (CM_M_KEY_VERIFICATION_DONE); + cm_event_create_txn_id (event, cm_client_pop_event_id (self->cm_client)); + self->key_verification_done = event; + + root = olm_sas_get_message_json (self, NULL); + cm_event_set_json (event, root, NULL); + + return self->key_verification_done; +} + +gboolean +cm_olm_sas_is_verified (CmOlmSas *self) +{ + g_return_val_if_fail (CM_IS_OLM_SAS (self), FALSE); + + return self->verified; +} + +CmDevice * +cm_olm_sas_get_device (CmOlmSas *self) +{ + g_return_val_if_fail (CM_IS_OLM_SAS (self), NULL); + + return self->their_device; +} diff --git a/subprojects/libcmatrix/src/cm-olm.c b/subprojects/libcmatrix/src/cm-olm.c new file mode 100644 index 0000000000000000000000000000000000000000..cf8e70493b982baf1b65f1c9e577eb2c5526bfaf --- /dev/null +++ b/subprojects/libcmatrix/src/cm-olm.c @@ -0,0 +1,856 @@ +/* -*- mode: c; c-basic-offset: 2; indent-tabs-mode: nil; -*- */ +/* cm-olm.c + * + * Copyright 2022 Purism SPC + * + * Author(s): + * Mohammed Sadiq <sadiq@sadiqpk.org> + * + * SPDX-License-Identifier: LGPL-2.1-or-later + */ + +#define G_LOG_DOMAIN "cm-olm" + +#ifdef HAVE_CONFIG_H +# include "config.h" +#endif + +#define GCRYPT_NO_DEPRECATED +#include <gcrypt.h> +#include <olm/olm.h> + +#include "cm-utils-private.h" +#include "cm-db-private.h" +#include "cm-olm-private.h" + +struct _CmOlm +{ + GObject parent_instance; + + CmDb *cm_db; + char *room_id; + GRefString *sender_id; + char *device_id; + char *account_user_id; + char *account_device_id; + OlmAccount *account; + + char *curve_key; + char *pickle_key; + char *session_id; + char *session_key; + uint8_t *current_session_key; + OlmInboundGroupSession *in_gp_session; + OlmOutboundGroupSession *out_gp_session; + OlmSession *olm_session; + + gint64 created_time; + CmSessionType type; + CmOlmState state; +}; + +G_DEFINE_TYPE (CmOlm, cm_olm, G_TYPE_OBJECT) + + +static char * +cm_olm_get_olm_session_pickle (CmOlm *self) +{ + g_autofree char *pickle = NULL; + size_t len; + + g_return_val_if_fail (self->pickle_key, NULL); + + if (self->olm_session) + { + len = olm_pickle_session_length (self->olm_session); + pickle = g_malloc (len + 1); + olm_pickle_session (self->olm_session, self->pickle_key, + strlen (self->pickle_key), + pickle, len); + } + else if (self->in_gp_session) + { + len = olm_pickle_inbound_group_session_length (self->in_gp_session); + pickle = g_malloc (len + 1); + olm_pickle_inbound_group_session (self->in_gp_session, self->pickle_key, + strlen (self->pickle_key), + pickle, len); + } + else if (self->out_gp_session) + { + len = olm_pickle_outbound_group_session_length (self->out_gp_session); + pickle = g_malloc (len + 1); + olm_pickle_outbound_group_session (self->out_gp_session, self->pickle_key, + strlen (self->pickle_key), + pickle, len); + } + else + g_return_val_if_reached (NULL); + + pickle[len] = '\0'; + + return g_steal_pointer (&pickle); +} + +static void +cm_olm_finalize (GObject *object) +{ + CmOlm *self = (CmOlm *)object; + + g_free (self->pickle_key); + g_free (self->curve_key); + g_clear_pointer (&self->sender_id, g_ref_string_release); + g_free (self->device_id); + g_free (self->room_id); + + g_clear_pointer (&self->account_user_id, g_ref_string_release); + g_free (self->account_device_id); + + if (self->olm_session) + olm_clear_session (self->olm_session); + if (self->in_gp_session) + olm_clear_inbound_group_session (self->in_gp_session); + if (self->out_gp_session) + olm_clear_outbound_group_session (self->out_gp_session); + + g_free (self->olm_session); + g_free (self->in_gp_session); + g_free (self->out_gp_session); + + cm_utils_free_buffer (self->session_key); + cm_utils_free_buffer (self->session_id); + + g_clear_object (&self->cm_db); + + G_OBJECT_CLASS (cm_olm_parent_class)->finalize (object); +} + +static void +cm_olm_class_init (CmOlmClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + + object_class->finalize = cm_olm_finalize; +} + +static void +cm_olm_init (CmOlm *self) +{ +} + +CmOlm * +cm_olm_new_from_pickle (char *pickle, + const char *pickle_key, + const char *sender_identity_key, + CmSessionType session_type) +{ + CmOlm *self; + g_autofree OlmInboundGroupSession *in_session = NULL; + g_autofree OlmOutboundGroupSession *out_session = NULL; + g_autofree OlmSession *session = NULL; + g_autofree uint8_t *session_key = NULL; + size_t err, len; + + if (session_type == SESSION_MEGOLM_V1_IN) + { + in_session = g_malloc (olm_inbound_group_session_size ()); + err = olm_unpickle_inbound_group_session (in_session, pickle_key, + strlen (pickle_key), + pickle, strlen (pickle)); + if (err == olm_error ()) + { + g_debug ("Error in group unpickle: %s", + olm_inbound_group_session_last_error (in_session)); + + return NULL; + } + } + else if (session_type == SESSION_MEGOLM_V1_OUT) + { + out_session = g_malloc (olm_outbound_group_session_size ()); + err = olm_unpickle_outbound_group_session (out_session, pickle_key, + strlen (pickle_key), + pickle, strlen (pickle)); + if (err == olm_error ()) + { + g_debug ("Error in group unpickle: %s", + olm_outbound_group_session_last_error (out_session)); + + return NULL; + } + + len = olm_outbound_group_session_key_length (out_session); + session_key = g_malloc (len + 1); + len = olm_outbound_group_session_key (out_session, session_key, len); + if (len == olm_error ()) + { + g_warning ("Error getting session key: %s", + olm_outbound_group_session_last_error (out_session)); + + return NULL; + } + session_key[len] = '\0'; + } + else + { + session = g_malloc (olm_session_size ()); + olm_session (session); + err = olm_unpickle_session (session, pickle_key, strlen (pickle_key), + pickle, strlen (pickle)); + if (err == olm_error ()) + return NULL; + } + + self = g_object_new (CM_TYPE_OLM, NULL); + self->olm_session = g_steal_pointer (&session); + self->in_gp_session = g_steal_pointer (&in_session); + self->out_gp_session = g_steal_pointer (&out_session); + self->session_key = (char *)g_steal_pointer (&session_key); + self->curve_key = g_strdup (sender_identity_key); + self->pickle_key = g_strdup (pickle_key); + self->type = session_type; + + return self; +} + +CmOlm * +cm_olm_outbound_new (gpointer olm_account, + const char *curve_key, + const char *one_time_key, + const char *room_id) +{ + CmOlm *self; + g_autofree OlmSession *session = NULL; + cm_gcry_t buffer = NULL; + size_t length, error; + + g_return_val_if_fail (olm_account, NULL); + + if (!curve_key || !one_time_key) + return NULL; + + session = g_malloc (olm_session_size ()); + olm_session (session); + + length = olm_create_outbound_session_random_length (session); + if (length) + buffer = gcry_random_bytes (length, GCRY_STRONG_RANDOM); + + error = olm_create_outbound_session (session, + olm_account, + curve_key, strlen (curve_key), + one_time_key, strlen (one_time_key), + buffer, length); + gcry_free (buffer); + + if (error == olm_error ()) + { + g_warning ("Error creating outbound olm session: %s", + olm_session_last_error (session)); + return NULL; + } + + self = g_object_new (CM_TYPE_OLM, NULL); + self->olm_session = g_steal_pointer (&session); + self->curve_key = g_strdup (curve_key); + self->account = olm_account; + self->type = SESSION_OLM_V1_OUT; + /* time in milliseconds */ + self->created_time = time (NULL) * 1000; + + return self; +} + +CmOlm * +cm_olm_inbound_new (gpointer olm_account, + const char *sender_identity_key, + const char *one_time_key_message) +{ + CmOlm *self = NULL; + OlmSession *session = NULL; + g_autofree char *message_copy = NULL; + size_t err; + + message_copy = g_strdup (one_time_key_message); + + session = g_malloc (olm_session_size ()); + olm_session (session); + + err = olm_create_inbound_session_from (session, olm_account, + sender_identity_key, strlen (sender_identity_key), + message_copy, strlen (message_copy)); + if (err == olm_error ()) + { + g_warning ("Error creating session: %s", olm_session_last_error (session)); + olm_clear_session (session); + g_free (session); + + return NULL; + } + + /* Remove one time keys that are used */ + err = olm_remove_one_time_keys (olm_account, session); + if (err == olm_error ()) + g_warning ("Error removing key: %s", olm_account_last_error (olm_account)); + + self = g_object_new (CM_TYPE_OLM, NULL); + self->olm_session = g_steal_pointer (&session); + self->curve_key = g_strdup (sender_identity_key); + self->account = olm_account; + self->type = SESSION_OLM_V1_IN; + + return self; +} + +CmOlm * +cm_olm_in_group_new (const char *session_key, + const char *sender_identity_key, + const char *session_id) +{ + CmOlm *self; + g_autofree OlmInboundGroupSession *session = NULL; + size_t err; + + session = g_malloc (olm_inbound_group_session_size ()); + olm_inbound_group_session (session); + + err = olm_init_inbound_group_session (session, (gpointer)session_key, + strlen (session_key)); + if (err == olm_error ()) + { + g_warning ("Error creating group session from key: %s", + olm_inbound_group_session_last_error (session)); + return NULL; + } + + self = g_object_new (CM_TYPE_OLM, NULL); + self->in_gp_session = g_steal_pointer (&session); + self->curve_key = g_strdup (sender_identity_key); + self->session_id = g_strdup (session_id); + self->session_key = g_strdup (session_key); + self->type = SESSION_MEGOLM_V1_IN; + + return self; +} + +CmOlm * +cm_olm_in_group_new_from_out (CmOlm *out_group, + const char *sender_identity_key) +{ + CmOlm *self; + + g_assert (CM_IS_OLM (out_group)); + g_assert (out_group->out_gp_session); + + self = cm_olm_in_group_new (out_group->session_key, + sender_identity_key, + out_group->session_id); + cm_olm_set_account_details (self, out_group->account_user_id, + out_group->account_device_id); + cm_olm_set_sender_details (self, out_group->room_id, out_group->sender_id); + cm_olm_set_key (self, out_group->pickle_key); + cm_olm_set_db (self, out_group->cm_db); + self->created_time = out_group->created_time; + + return self; +} + +CmOlm * +cm_olm_out_group_new (const char *sender_identity_key) +{ + CmOlm *self; + g_autofree OlmOutboundGroupSession *session = NULL; + g_autofree uint8_t *session_key = NULL; + g_autofree uint8_t *session_id = NULL; + uint8_t *random = NULL; + size_t length, error; + + /* Initialize session */ + session = g_malloc (olm_outbound_group_session_size ()); + olm_outbound_group_session (session); + + /* Feed in random bits */ + length = olm_init_outbound_group_session_random_length (session); + if (length) + random = gcry_random_bytes (length, GCRY_STRONG_RANDOM); + error = olm_init_outbound_group_session (session, random, length); + gcry_free (random); + + if (error == olm_error ()) + { + g_warning ("Error init out group session: %s", olm_outbound_group_session_last_error (session)); + + return NULL; + } + + /* Get session key */ + length = olm_outbound_group_session_key_length (session); + session_key = g_malloc (length + 1); + length = olm_outbound_group_session_key (session, session_key, length); + if (length == olm_error ()) + { + g_warning ("Error getting session key: %s", olm_outbound_group_session_last_error (session)); + + return NULL; + } + session_key[length] = '\0'; + + self = g_object_new (CM_TYPE_OLM, NULL); + self->curve_key = g_strdup (sender_identity_key); + self->out_gp_session = g_steal_pointer (&session); + self->session_id = (char *)g_steal_pointer (&session_id); + self->session_key = (char *)g_steal_pointer (&session_key); + self->created_time = time (NULL) * 1000; + self->type = SESSION_MEGOLM_V1_OUT; + + return self; +} + +CmSessionType +cm_olm_get_session_type (CmOlm *self) +{ + g_return_val_if_fail (CM_IS_OLM (self), 0); + + return self->type; +} + +size_t +cm_olm_get_message_index (CmOlm *self) +{ + g_return_val_if_fail (CM_IS_OLM (self), 0); + g_return_val_if_fail (self->out_gp_session, 0); + + return olm_outbound_group_session_message_index (self->out_gp_session); +} + +gint64 +cm_olm_get_created_time (CmOlm *self) +{ + g_return_val_if_fail (CM_IS_OLM (self), 0); + + return self->created_time; +} + +/** + * cm_olm_set_state: + * @self: A #CmOlm + * @state: A #CmOlmState + * + * Set the usability state of @self. + */ +void +cm_olm_set_state (CmOlm *self, + CmOlmState state) +{ + g_return_if_fail (CM_IS_OLM (self)); + + if (state == self->state) + return; + + g_return_if_fail (self->state == OLM_STATE_USABLE); + self->state = state; +} + +CmOlmState +cm_olm_get_state (CmOlm *self) +{ + g_return_val_if_fail (CM_IS_OLM (self), OLM_STATE_NOT_SET); + + return self->state; +} + +void +cm_olm_update_validity (CmOlm *self, + guint count, + gint64 duration) +{ + g_return_if_fail (CM_IS_OLM (self)); + g_return_if_fail (count); + g_return_if_fail (duration > 0); + + if (cm_olm_get_message_index (self) >= count || + cm_olm_get_created_time (self) + duration <= time (NULL) * 1000) + cm_olm_set_state (self, OLM_STATE_ROTATED); +} + +void +cm_olm_set_sender_details (CmOlm *self, + const char *room_id, + GRefString *sender_id) +{ + g_return_if_fail (CM_IS_OLM (self)); + g_return_if_fail (sender_id && *sender_id == '@'); + g_return_if_fail (!self->sender_id); + + self->room_id = g_strdup (room_id); + self->sender_id = g_ref_string_acquire (sender_id); +} + +void +cm_olm_set_account_details (CmOlm *self, + GRefString *account_user_id, + const char *account_device_id) +{ + g_return_if_fail (CM_IS_OLM (self)); + g_return_if_fail (account_user_id && *account_user_id == '@'); + g_return_if_fail (account_device_id && *account_device_id); + g_return_if_fail (!self->account_user_id); + g_return_if_fail (!self->account_device_id); + + self->account_user_id = g_ref_string_acquire (account_user_id); + self->account_device_id = g_strdup (account_device_id); +} + +void +cm_olm_set_db (CmOlm *self, + gpointer cm_db) +{ + g_return_if_fail (CM_IS_OLM (self)); + g_return_if_fail (CM_IS_DB (cm_db)); + g_return_if_fail (!self->cm_db); + + self->cm_db = g_object_ref (cm_db); +} + +void +cm_olm_set_key (CmOlm *self, + const char *key) +{ + g_return_if_fail (CM_IS_OLM (self)); + g_return_if_fail (key && *key); + g_return_if_fail (!self->pickle_key); + + self->pickle_key = g_strdup (key); +} + +gboolean +cm_olm_save (CmOlm *self) +{ + char *pickle; + + g_return_val_if_fail (CM_IS_OLM (self), FALSE); + g_return_val_if_fail (self->cm_db, FALSE); + g_return_val_if_fail (self->pickle_key, FALSE); + g_return_val_if_fail (self->account_user_id, FALSE); + g_return_val_if_fail (self->account_device_id, FALSE); + + pickle = cm_olm_get_olm_session_pickle (self); + g_return_val_if_fail (pickle && *pickle, FALSE); + + return cm_db_add_session (self->cm_db, self, pickle); +} + +char * +cm_olm_encrypt (CmOlm *self, + const char *plain_text) +{ + g_autofree char *encrypted = NULL; + size_t len; + + g_return_val_if_fail (CM_IS_OLM (self), NULL); + g_assert (self->olm_session || self->out_gp_session); + + if (!plain_text) + return NULL; + + if (self->olm_session) + { + cm_gcry_t random = NULL; + size_t rand_len; + + rand_len = olm_encrypt_random_length (self->olm_session); + if (rand_len) + random = gcry_random_bytes (rand_len, GCRY_STRONG_RANDOM); + + len = olm_encrypt_message_length (self->olm_session, strlen (plain_text)); + encrypted = g_malloc (len + 1); + len = olm_encrypt (self->olm_session, plain_text, strlen (plain_text), + random, rand_len, encrypted, len); + gcry_free (random); + } + else if (self->out_gp_session) + { + len = olm_group_encrypt_message_length (self->out_gp_session, strlen (plain_text)); + encrypted = g_malloc (len + 1); + len = olm_group_encrypt (self->out_gp_session, + (gpointer)plain_text, strlen (plain_text), + (gpointer)encrypted, len); + } + else + g_return_val_if_reached (NULL); + + if (len == olm_error ()) + { + const char *error = NULL; + + if (self->olm_session) + error = olm_session_last_error (self->olm_session); + else if (self->out_gp_session) + error = olm_outbound_group_session_last_error (self->out_gp_session); + + if (error) + g_warning ("Error encrypting: %s", error); + + return NULL; + } + encrypted[len] = '\0'; + + return g_steal_pointer (&encrypted); +} + +static char * +session_decrypt (CmOlm *self, + size_t type, + const char *ciphertext) +{ + g_autofree char *plaintext = NULL; + char *copy; + size_t len; + + g_assert (CM_IS_OLM (self)); + g_assert (self->olm_session); + + copy = g_strdup (ciphertext); + len = olm_decrypt_max_plaintext_length (self->olm_session, + type, copy, strlen (copy)); + g_free (copy); + + if (len == olm_error ()) + { + g_warning ("Error getting max length: %s", + olm_session_last_error (self->olm_session)); + + return NULL; + } + + copy = g_strdup (ciphertext); + plaintext = g_malloc (len + 1); + len = olm_decrypt (self->olm_session, type, copy, + strlen (copy), plaintext, len); + g_free (copy); + + if (len == olm_error ()) + { + g_warning ("Error decrypting: %s", + olm_session_last_error (self->olm_session)); + + return NULL; + } + + plaintext[len] = '\0'; + + return g_steal_pointer (&plaintext); +} + +static char * +group_session_decrypt (CmOlm *self, + const char *ciphertext) +{ + g_autofree char *plaintext = NULL; + char *copy; + size_t len; + + g_assert (CM_IS_OLM (self)); + g_assert (self->in_gp_session); + + copy = g_strdup (ciphertext); + len = olm_group_decrypt_max_plaintext_length (self->in_gp_session, + (gpointer)copy, strlen (copy)); + g_free (copy); + + plaintext = g_malloc (len + 1); + copy = g_strdup (ciphertext); + len = olm_group_decrypt (self->in_gp_session, (gpointer)copy, strlen (copy), + (gpointer)plaintext, len, NULL); + g_free (copy); + + if (len == olm_error ()) + { + g_warning ("Error decrypting: %s", + olm_inbound_group_session_last_error (self->in_gp_session)); + return NULL; + } + + plaintext[len] = '\0'; + + return g_steal_pointer (&plaintext); +} + +char * +cm_olm_decrypt (CmOlm *self, + size_t type, + const char *message) +{ + g_assert (CM_IS_OLM (self)); + g_return_val_if_fail (message, NULL); + + if (self->olm_session) + return session_decrypt (self, type, message); + + if (self->in_gp_session) + return group_session_decrypt (self, message); + + return NULL; +} + +size_t +cm_olm_get_message_type (CmOlm *self) +{ + g_assert (CM_IS_OLM (self)); + g_assert (self->olm_session); + + return olm_encrypt_message_type (self->olm_session); +} + +const char * +cm_olm_get_session_id (CmOlm *self) +{ + size_t len; + + g_return_val_if_fail (CM_IS_OLM (self), NULL); + + if (!self->session_id) + { + void *session_id = NULL; + + if (self->olm_session) + { + len = olm_session_id_length (self->olm_session); + session_id = g_malloc (len + 1); + olm_session_id (self->olm_session, session_id, len); + } + else if (self->out_gp_session) + { + len = olm_outbound_group_session_id_length (self->out_gp_session); + session_id = g_malloc (len + 1); + olm_outbound_group_session_id (self->out_gp_session, session_id, len); + } + else if (self->in_gp_session) + { + len = olm_inbound_group_session_id_length (self->in_gp_session); + session_id = g_malloc (len + 1); + olm_inbound_group_session_id (self->in_gp_session, session_id, len); + } + + if (session_id) + ((char *)session_id)[len] = '\0'; + self->session_id = session_id; + } + + return self->session_id; +} + +/** + * cm_olm_get_session_key: + * @self: A #CmOlm of type %SESSION_MEGOLM_V1_OUT + * + * Get the session for the next message to be sent. + * The session key shall change after a message sent + * + * The session key can be used to decrypt future + * messages, but not the past ones. + * + * Returns: The session key string + */ +const char * +cm_olm_get_session_key (CmOlm *self) +{ + size_t len; + + g_return_val_if_fail (CM_IS_OLM (self), NULL); + + /* Each message is sent with a different ratchet key. So regenerate the + * so that ratchet key that will be used for the next message shall be + * returned. We want let the other party see only the messages sent + * after this session key, not the past ones. + */ + cm_utils_free_buffer ((char *)self->current_session_key); + self->current_session_key = NULL; + + if (self->out_gp_session) + { + len = olm_outbound_group_session_key_length (self->out_gp_session); + self->current_session_key = g_malloc (len + 1); + olm_outbound_group_session_key (self->out_gp_session, + self->current_session_key, len); + self->current_session_key[len] = '\0'; + } + + return (char *)self->current_session_key; +} + +const char * +cm_olm_get_room_id (CmOlm *self) +{ + g_return_val_if_fail (CM_IS_OLM (self), NULL); + + return self->room_id; +} + +const char * +cm_olm_get_sender_key (CmOlm *self) +{ + g_return_val_if_fail (CM_IS_OLM (self), NULL); + + return self->curve_key; +} + +GRefString * +cm_olm_get_account_id (CmOlm *self) +{ + g_return_val_if_fail (CM_IS_OLM (self), NULL); + + return self->account_user_id; +} + +const char * +cm_olm_get_account_device (CmOlm *self) +{ + g_return_val_if_fail (CM_IS_OLM (self), NULL); + + return self->account_device_id; +} + +gpointer +cm_olm_match_olm_session (const char *body, + gsize body_len, + size_t message_type, + const char *pickle, + const char *pickle_key, + const char *sender_identify_key, + CmSessionType session_type, + char **out_decrypted) +{ + g_autoptr(CmOlm) self = NULL; + g_autofree char *pickle_copy = NULL; + + g_assert (out_decrypted); + + pickle_copy = g_strdup (pickle); + self = cm_olm_new_from_pickle (pickle_copy, pickle_key, sender_identify_key, session_type); + + if (!self) + return NULL; + + /* If it's a pre key message, check if the session matches */ + if (message_type == OLM_MESSAGE_TYPE_PRE_KEY) + { + g_autofree char *body_copy = NULL; + size_t match = 0; + + body_copy = g_malloc (body_len + 1); + memcpy (body_copy, body, body_len + 1); + match = olm_matches_inbound_session (self->olm_session, body_copy, body_len); + /* If it doesn't match, don't consider using it for decryption */ + if (match != 1) + return NULL; + } + + /* Try decrypting with the given session */ + *out_decrypted = cm_olm_decrypt (self, message_type, body); + + if (*out_decrypted) + return g_steal_pointer (&self); + + return NULL; +} diff --git a/subprojects/libcmatrix/src/cm-room-private.h b/subprojects/libcmatrix/src/cm-room-private.h new file mode 100644 index 0000000000000000000000000000000000000000..551f03c6a5d882e5bcc163f5ffdc65150b01c92d --- /dev/null +++ b/subprojects/libcmatrix/src/cm-room-private.h @@ -0,0 +1,99 @@ +/* -*- mode: c; c-basic-offset: 2; indent-tabs-mode: nil; -*- */ +/* + * Copyright (C) 2022 Purism SPC + * + * SPDX-License-Identifier: LGPL-2.1-or-later + */ + +#pragma once + +#if !defined(_CMATRIX_TAKEN) && !defined(CMATRIX_COMPILATION) +# error "Only <cmatrix.h> can be included directly." +#endif + +#include <json-glib/json-glib.h> + +#include <glib-object.h> +#include <gio/gio.h> + +#include "cm-room.h" + +G_BEGIN_DECLS + +CmRoom *cm_room_new (const char *room_id); +CmRoom *cm_room_new_from_json (const char *room_id, + JsonObject *root, + CmEvent *last_event); +char *cm_room_get_json (CmRoom *self); +const char *cm_room_get_replacement_room (CmRoom *self); +CmClient *cm_room_get_client (CmRoom *self); +void cm_room_set_client (CmRoom *self, + CmClient *client); +gboolean cm_room_has_state_sync (CmRoom *self); +GPtrArray *cm_room_set_data (CmRoom *self, + JsonObject *object); +JsonObject *cm_room_decrypt (CmRoom *self, + JsonObject *root); +void cm_room_add_events (CmRoom *self, + GPtrArray *events, + gboolean append); +void cm_room_user_changed (CmRoom *self, + GPtrArray *changed_users); +const char *cm_room_get_prev_batch (CmRoom *self); +void cm_room_set_prev_batch (CmRoom *self, + const char *prev_batch); +void cm_room_set_name (CmRoom *self, + const char *name); +void cm_room_set_generated_name (CmRoom *self, + const char *name); +gint64 cm_room_get_encryption_rotation_time (CmRoom *self); +CmStatus cm_room_get_status (CmRoom *self); +void cm_room_set_status (CmRoom *self, + CmStatus status); +guint cm_room_get_encryption_msg_count (CmRoom *self); +gboolean cm_room_is_direct (CmRoom *self); +void cm_room_set_is_direct (CmRoom *self, + gboolean is_direct); +void cm_room_get_name_async (CmRoom *self, + GCancellable *cancellable, + GAsyncReadyCallback callback, + gpointer user_data); +char *cm_room_get_name_finish (CmRoom *self, + GAsyncResult *result, + GError **error); +void cm_room_load_async (CmRoom *self, + GCancellable *cancellable, + GAsyncReadyCallback callback, + gpointer user_data); +gboolean cm_room_load_finish (CmRoom *self, + GAsyncResult *result, + GError **error); +void cm_room_save (CmRoom *self); +void cm_room_load_joined_members_async (CmRoom *self, + GCancellable *cancellable, + GAsyncReadyCallback callback, + gpointer user_data); +gboolean cm_room_load_joined_members_finish (CmRoom *self, + GAsyncResult *result, + GError **error); +void cm_room_is_encrypted_async (CmRoom *self, + GCancellable *cancellable, + GAsyncReadyCallback callback, + gpointer user_data); +gboolean cm_room_is_encrypted_finish (CmRoom *self, + GAsyncResult *result, + GError **error); +void cm_room_load_prev_batch_async (CmRoom *self, + GCancellable *cancellable, + GAsyncReadyCallback callback, + gpointer user_data); +GPtrArray *cm_room_load_prev_batch_finish (CmRoom *self, + GAsyncResult *result, + GError **error); +CmUser *cm_room_find_user (CmRoom *self, + GRefString *matrix_id, + gboolean add_if_missing); +void cm_room_update_user (CmRoom *self, + CmEvent *event); + +G_END_DECLS diff --git a/subprojects/libcmatrix/src/cm-room.c b/subprojects/libcmatrix/src/cm-room.c new file mode 100644 index 0000000000000000000000000000000000000000..337974f98f018a7d4fdb365ff20e6c8517875236 --- /dev/null +++ b/subprojects/libcmatrix/src/cm-room.c @@ -0,0 +1,2588 @@ +/* -*- mode: c; c-basic-offset: 2; indent-tabs-mode: nil; -*- */ +/* + * Copyright (C) 2022 Purism SPC + * + * SPDX-License-Identifier: LGPL-2.1-or-later + */ + +#define G_LOG_DOMAIN "cm-room" + +#ifdef HAVE_CONFIG_H +# include "config.h" +#endif + +#include "cm-input-stream-private.h" +#include "cm-client-private.h" +#include "cm-utils-private.h" +#include "cm-net-private.h" +#include "cm-enc-private.h" +#include "cm-common.h" +#include "events/cm-event-private.h" +#include "events/cm-room-event-list-private.h" +#include "events/cm-room-message-event-private.h" +#include "users/cm-room-member-private.h" +#include "users/cm-room-member.h" +#include "users/cm-user.h" +#include "users/cm-user-private.h" +#include "users/cm-user-list-private.h" +#include "cm-room-private.h" +#include "cm-room.h" + +#define KEY_TIMEOUT 10000 /* milliseconds */ +#define TYPING_TIMEOUT 4 /* seconds */ + +struct _CmRoom +{ + GObject parent_instance; + + CmRoomEventList *room_event; + GListStore *joined_members; + GHashTable *joined_members_table; + GListStore *invited_members; + GHashTable *invited_members_table; + + /* key: GRefString (user_id), value: #GPtrArray of #CmDevice */ + /* Shall store only devices that are added */ + /* Reset if any device/user is removed/left */ + GHashTable *changed_devices; + /* array of #CmUser */ + GPtrArray *changed_users; + /* array of #CmUserKey */ + GPtrArray *one_time_keys; + GCancellable *enc_cancellable; + JsonObject *local_json; + CmClient *client; + char *name; + char *generated_name; + char *past_name; + char *room_id; + char *replacement_room; + char *encryption; + char *prev_batch; + + GQueue *message_queue; + guint retry_timeout_id; + gint unread_count; + + CmStatus room_status; + gboolean has_prev_batch; + gboolean is_direct; + + /* Use g_get_monotonic_time(), we only need the interval */ + gint64 typing_set_time; + /* set doesn't mean the user has typing state set, + * also compare with typing_set_time and TYPING_TIMEOUT */ + gboolean typing; + + gboolean loading_initial_sync; + gboolean loading_past_events; + gboolean db_save_pending; + gboolean is_sending_message; + gboolean name_loaded; + gboolean name_loading; + gboolean joined_members_loading; + gboolean joined_members_loaded; + gboolean querying_keys; + gboolean claiming_keys; + gboolean keys_claimed; + gboolean uploading_keys; + gboolean initial_sync_done; + + gboolean is_accepting_invite; + gboolean is_rejecting_invite; + gboolean invite_accept_success; + gboolean invite_reject_success; +}; + +G_DEFINE_TYPE (CmRoom, cm_room, G_TYPE_OBJECT) + +enum { + PROP_0, + PROP_ENCRYPTED, + PROP_NAME, + N_PROPS +}; + +static GParamSpec *properties[N_PROPS]; + +/* static gboolean room_resend_message (gpointer user_data); */ +static void room_send_message_from_queue (CmRoom *self); + +static CmUser * +room_find_user (CmRoom *self, + GRefString *matrix_id, + gboolean add_if_missing) +{ + CmUserList *user_list; + GListModel *model; + CmUser *user = NULL; + + g_assert (CM_IS_ROOM (self)); + g_assert (matrix_id && *matrix_id == '@'); + g_return_val_if_fail (self->client, NULL); + + user_list = cm_client_get_user_list (self->client); + user = cm_user_list_find_user (user_list, matrix_id, add_if_missing); + model = G_LIST_MODEL (self->joined_members); + + if (user && + !cm_utils_get_item_position (model, user, NULL)) + { + if (cm_room_is_encrypted (self)) + g_ptr_array_add (self->changed_users, g_object_ref (user)); + + g_list_store_append (self->joined_members, user); + g_hash_table_insert (self->joined_members_table, + g_ref_string_acquire (matrix_id), + g_object_ref (user)); + } + + return user; +} + +static char * +cm_room_generate_name (CmRoom *self) +{ + GListModel *model; + const char *name_a = NULL, *name_b = NULL; + guint n_items, count; + + g_assert (CM_IS_ROOM (self)); + + model = G_LIST_MODEL (self->joined_members); + count = n_items = g_list_model_get_n_items (model); + + if (n_items == 1) + { + g_autoptr(CmUser) user = NULL; + + user = g_list_model_get_item (model, 0); + + /* Don't add self to create room name */ + if (cm_user_get_id (user) == cm_client_get_user_id (self->client)) + count = n_items = 0; + } + + if (!n_items) + { + model = G_LIST_MODEL (self->invited_members); + count = n_items = g_list_model_get_n_items (model); + } + for (guint i = 0; i < MIN (3, n_items); i++) { + g_autoptr(CmUser) user = NULL; + + user = g_list_model_get_item (model, i); + + /* Don't add self to create room name */ + if (cm_user_get_id (user) == cm_client_get_user_id (self->client)) + { + count--; + continue; + } + + if (!name_a) { + name_a = cm_user_get_display_name (user); + + if (!name_a || !*name_a) + name_a = cm_user_get_id (user); + } else { + name_b = cm_user_get_display_name (user); + + if (!name_b || !*name_b) + name_b = cm_user_get_id (user); + } + } + + /* fixme: Depend on gettext and make these strings translatable */ + if (count == 0) + return g_strdup ("Empty room"); + + if (count == 1) + return g_strdup (name_a); + + if (count == 2) + return g_strdup_printf ("%s and %s", name_a ?: "", name_b ?: ""); + + return g_strdup_printf ("%s and %u other(s)", name_a ?: "", count - 1); +} + +static void +room_add_event_to_db (CmRoom *self, + CmEvent *event) +{ + g_autoptr(GPtrArray) events = NULL; + + g_assert (CM_IS_ROOM (self)); + g_assert (CM_IS_EVENT (event)); + + events = g_ptr_array_new_with_free_func (g_object_unref); + g_ptr_array_add (events, g_object_ref (event)); + cm_db_add_room_events (cm_client_get_db (self->client), + self, events, FALSE); +} + +static void +room_load_device_keys_cb (GObject *object, + GAsyncResult *result, + gpointer user_data) +{ + g_autoptr(CmRoom) self = user_data; + g_autoptr(GPtrArray) users = NULL; + g_autoptr(GError) error = NULL; + + users = cm_user_list_load_devices_finish (CM_USER_LIST (object), result, &error); + self->querying_keys = FALSE; + + g_debug ("(%p) Load user devices %s", self, CM_LOG_SUCCESS (!error)); + + if (error) + { + g_debug ("(%p) Load user devices error: %s", self, error->message); + } + else + { + room_send_message_from_queue (self); + } +} + +static void +room_claim_keys_cb (GObject *object, + GAsyncResult *result, + gpointer user_data) +{ + g_autoptr(CmRoom) self = user_data; + g_autoptr(GError) error = NULL; + GPtrArray *keys; + + g_clear_pointer (&self->one_time_keys, g_ptr_array_unref); + keys = cm_user_list_claim_keys_finish (CM_USER_LIST (object), result, &error); + self->one_time_keys = keys; + self->claiming_keys = FALSE; + + g_debug ("(%p) Claim keys %s", self, CM_LOG_SUCCESS (!error)); + + if (error) + { + g_debug ("(%p) claim keys error: %s", self, error->message); + } + else + { + self->keys_claimed = TRUE; + room_send_message_from_queue (self); + } +} + +static void +room_upload_keys_cb (GObject *object, + GAsyncResult *result, + gpointer user_data) +{ + g_autoptr(CmRoom) self = user_data; + g_autoptr(GError) error = NULL; + + cm_user_list_upload_keys_finish (CM_USER_LIST (object), result, &error); + self->uploading_keys = FALSE; + g_debug ("(%p) Upload keys %s", self, CM_LOG_SUCCESS (!error)); + + if (error) + { + g_debug ("(%p) Upload keys error: %s", self, error->message); + } + else + { + g_ptr_array_set_size (self->one_time_keys, 0); + room_send_message_from_queue (self); + } +} + +static void +ensure_encryption_keys (CmRoom *self) +{ + CmUserList *user_list; + + g_return_if_fail (cm_room_is_encrypted (self)); + + if (self->enc_cancellable && + g_cancellable_is_cancelled (self->enc_cancellable)) + g_clear_object (&self->enc_cancellable); + + if (!self->enc_cancellable) + self->enc_cancellable = g_cancellable_new (); + + if (self->joined_members_loading || self->querying_keys || + self->claiming_keys || self->uploading_keys) + return; + + user_list = cm_client_get_user_list (self->client); + + if (!self->joined_members_loaded) + cm_room_load_joined_members_async (self, self->enc_cancellable, NULL, NULL); + else if (self->changed_users->len) + { + self->querying_keys = TRUE; + self->keys_claimed = FALSE; + g_debug ("(%p) Load user devices", self); + cm_user_list_load_devices_async (user_list, self->changed_users, + room_load_device_keys_cb, + g_object_ref (self)); + } + else if (!self->keys_claimed || + (self->changed_devices && g_hash_table_size (self->changed_devices))) + { + g_autoptr(GHashTable) users = NULL; + GHashTable *table; + + self->claiming_keys = TRUE; + + table = g_hash_table_new_full (g_direct_hash, + g_direct_equal, + (GDestroyNotify)g_ref_string_release, + (GDestroyNotify)g_ptr_array_unref); + + if (g_hash_table_size (self->changed_devices)) + { + g_debug ("(%p) Has %u changed users for claiming keys", + self, g_hash_table_size (self->changed_devices)); + users = g_steal_pointer (&self->changed_devices); + self->changed_devices = table; + } + else + { + GListModel *members; + + members = G_LIST_MODEL (self->joined_members); + users = table; + + g_debug ("(%p) Has %u room users for claiming keys", + self, g_list_model_get_n_items (members)); + for (guint i = 0; i < g_list_model_get_n_items (members); i++) + { + g_autoptr(CmUser) user = NULL; + GListModel *device_list; + GRefString *user_id; + GPtrArray *devices; + + user = g_list_model_get_item (members, i); + devices = g_ptr_array_new_full (32, g_object_unref); + device_list = cm_user_get_devices (user); + + for (guint j = 0; j < g_list_model_get_n_items (device_list); j++) + g_ptr_array_add (devices, g_list_model_get_item (device_list, j)); + + user_id = g_ref_string_acquire (cm_user_get_id (user)); + g_hash_table_insert (users, user_id, devices); + } + } + + g_debug ("(%p) Claim keys for %u users", self, g_hash_table_size (users)); + cm_user_list_claim_keys_async (user_list, self, users, + room_claim_keys_cb, + g_object_ref (self)); + } + else + { + if (!self->one_time_keys || !self->one_time_keys->len) + { + g_warning ("(%p) no keys uploaded, and no keys left to upload", self); + return; + } + + self->uploading_keys = TRUE; + g_debug ("(%p) Upload keys", self); + cm_user_list_upload_keys_async (user_list, self, + self->one_time_keys, + room_upload_keys_cb, + g_object_ref (self)); + } +} + +static void +cm_room_get_property (GObject *object, + guint prop_id, + GValue *value, + GParamSpec *pspec) +{ + CmRoom *self = (CmRoom *)object; + + switch (prop_id) + { + case PROP_ENCRYPTED: + g_value_set_boolean (value, cm_room_is_encrypted (self)); + break; + + case PROP_NAME: + g_value_set_string (value, cm_room_get_name (self)); + break; + + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + break; + } +} + +static void +cm_room_finalize (GObject *object) +{ + CmRoom *self = (CmRoom *)object; + + if (self->enc_cancellable) + g_cancellable_cancel (self->enc_cancellable); + g_clear_object (&self->enc_cancellable); + + g_hash_table_unref (self->joined_members_table); + g_clear_object (&self->joined_members); + + g_hash_table_unref (self->invited_members_table); + g_clear_object (&self->invited_members); + + g_clear_pointer (&self->one_time_keys, g_ptr_array_unref); + + g_clear_object (&self->client); + g_free (self->room_id); + + g_free (self->name); + g_free (self->generated_name); + + g_queue_free_full (self->message_queue, g_object_unref); + + G_OBJECT_CLASS (cm_room_parent_class)->finalize (object); +} + +static void +cm_room_class_init (CmRoomClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + + object_class->get_property = cm_room_get_property; + object_class->finalize = cm_room_finalize; + + properties[PROP_ENCRYPTED] = + g_param_spec_string ("encrypted", + "encrypted", + "Whether room is encrypted", + NULL, + G_PARAM_READABLE | G_PARAM_STATIC_STRINGS); + + properties[PROP_NAME] = + g_param_spec_string ("name", + "Name", + "The room name", + NULL, + G_PARAM_READABLE | G_PARAM_STATIC_STRINGS); + + g_object_class_install_properties (object_class, N_PROPS, properties); +} + +static void +cm_room_init (CmRoom *self) +{ + self->room_event = cm_room_event_list_new (self); + self->one_time_keys = g_ptr_array_new_full (32, g_free); + self->changed_users = g_ptr_array_new_full (32, g_object_unref); + self->joined_members = g_list_store_new (CM_TYPE_USER); + self->changed_devices = g_hash_table_new_full (g_direct_hash, + g_direct_equal, + (GDestroyNotify)g_ref_string_release, + (GDestroyNotify)g_ptr_array_unref); + self->joined_members_table = g_hash_table_new_full (g_direct_hash, + g_direct_equal, + (GDestroyNotify)g_ref_string_release, + g_object_unref); + self->invited_members = g_list_store_new (CM_TYPE_USER); + self->invited_members_table = g_hash_table_new_full (g_direct_hash, + g_direct_equal, + (GDestroyNotify)g_ref_string_release, + g_object_unref); + self->message_queue = g_queue_new (); +} + +CmRoom * +cm_room_new (const char *room_id) +{ + CmRoom *self; + + g_return_val_if_fail (room_id && *room_id, NULL); + + self = g_object_new (CM_TYPE_ROOM, NULL); + self->room_id = g_strdup (room_id); + + return self; +} + +/* + * cm_room_new_from_json: + * @room_id: room id string + * @root: (nullable): A JsonObject with "local" object + * last_event: (nullable) (transfer full): A #CmEvent + * + * Create a new #CmRoom. @last_event is the + * last sync event in the room. + */ +CmRoom * +cm_room_new_from_json (const char *room_id, + JsonObject *root, + CmEvent *last_event) +{ + g_autoptr(GString) str = NULL; + CmRoom *self; + + self = cm_room_new (room_id); + + str = g_string_new (NULL); + cm_utils_anonymize (str, room_id); + CM_TRACE ("(%p) new room '%s' from json", self, str->str); + + if (root) + { + JsonObject *local, *child; + + self->local_json = root; + self->initial_sync_done = TRUE; + local = cm_utils_json_object_get_object (root, "local"); + self->name = g_strdup (cm_utils_json_object_get_string (local, "alias")); + self->generated_name = cm_utils_json_object_dup_string (local, "generated_alias"); + self->past_name = cm_utils_json_object_dup_string (local, "past_alias"); + cm_room_set_is_direct (self, cm_utils_json_object_get_bool (local, "direct")); + self->encryption = cm_utils_json_object_dup_string (local, "encryption"); + child = cm_utils_json_object_get_object (local, "unread_notifications"); + self->unread_count = cm_utils_json_object_get_int (child, "highlight_count"); + + cm_room_event_list_set_local_json (self->room_event, root, last_event); + + if (last_event) + g_debug ("(%p) Added 1 event from db", self); + } + + return self; +} + +static JsonObject * +room_generate_json (CmRoom *self) +{ + JsonObject *json, *child; + + g_assert (CM_IS_ROOM (self)); + + json = cm_room_event_list_get_local_json (self->room_event); + + child = cm_utils_json_object_get_object (json, "local"); + + json_object_set_string_member (child, "generated_alias", self->generated_name); + if (self->past_name) + json_object_set_string_member (child, "past_alias", self->past_name); + json_object_set_string_member (child, "alias", cm_room_get_name (self)); + /* Alias set before the current one, may be used if current one is NULL (eg: was x) */ + json_object_set_string_member (child, "last_alias", cm_room_get_name (self)); + json_object_set_boolean_member (child, "direct", cm_room_is_direct (self)); + json_object_set_int_member (child, "encryption", cm_room_is_encrypted (self)); + + return json; +} + +/* + * cm_room_get_json: + * + * Get the json which is to be stored + * in db as such + */ +char * +cm_room_get_json (CmRoom *self) +{ + JsonObject *json; + + g_return_val_if_fail (CM_IS_ROOM (self), NULL); + + json = room_generate_json (self); + + return cm_utils_json_object_to_string (json, FALSE); +} + +/* + * cm_room_get_replacement_room: + * @self: A #CmRoom + * + * Get the id of the room this room has been + * replaced with, which means that this room + * has been obsolete and should no longer + * be used for conversations. + * + * Returns: (nullable): The replacement room + * if this room has tombstone + */ +const char * +cm_room_get_replacement_room (CmRoom *self) +{ + CmEvent *event; + + g_return_val_if_fail (CM_IS_ROOM (self), FALSE); + + event = cm_room_event_list_get_event (self->room_event, CM_M_ROOM_TOMBSTONE); + if (!event) + return NULL; + + return cm_room_event_get_replacement_room_id ((gpointer)event); +} + +static void +room_user_changed (CmRoom *self, + CmUser *user, + GPtrArray *added, + GPtrArray *removed, + CmUserList *user_list) +{ + GRefString *user_id; + + g_assert (CM_IS_ROOM (self)); + g_assert (CM_IS_USER (user)); + g_assert (CM_IS_USER_LIST (user_list)); + + /* User changes has to be tracked only if the room is encrypted */ + if (!cm_room_is_encrypted (self)) + return; + + user_id = cm_user_get_id (user); + + if (!g_hash_table_contains (self->joined_members_table, user_id)) + return; + + CM_TRACE ("(%p) user changed, added: %u, removed: %u", self, + added ? added->len : 0, removed ? removed->len : 0); + + /* If any device got removed, create a new key and invalidate old */ + if (removed && removed->len) + { + guint n_items; + + cm_enc_rm_room_group_key (cm_client_get_enc (self->client), self); + + self->keys_claimed = FALSE; + g_ptr_array_set_size (self->changed_users, 0); + g_hash_table_remove_all (self->changed_devices); + n_items = g_list_model_get_n_items (G_LIST_MODEL (self->joined_members)); + + for (guint i = 0; i < n_items; i++) + { + CmUser *member; + + member = g_list_model_get_item (G_LIST_MODEL (self->joined_members), i); + g_ptr_array_add (self->changed_users, member); + } + + return; + } + + if (added && added->len) + { + GPtrArray *devices; + + devices = g_hash_table_lookup (self->changed_devices, user); + + if (devices) + { + g_ptr_array_extend (devices, added, (gpointer)g_object_ref, NULL); + } + else + { + g_ref_string_acquire (user_id); + devices = g_ptr_array_copy (added, (gpointer)g_object_ref, NULL); + g_hash_table_insert (self->changed_devices, user_id, devices); + } + } +} + +void +cm_room_set_client (CmRoom *self, + CmClient *client) +{ + CmUserList *user_list; + + g_return_if_fail (CM_IS_CLIENT (client)); + g_return_if_fail (!self->client); + + self->client = g_object_ref (client); + user_list = cm_client_get_user_list (client); + + g_signal_connect_object (user_list, "user-changed", + G_CALLBACK (room_user_changed), + self, G_CONNECT_SWAPPED); + cm_room_event_list_set_client (self->room_event, client); +} + +CmClient * +cm_room_get_client (CmRoom *self) +{ + g_return_val_if_fail (CM_IS_ROOM (self), NULL); + + return self->client; +} + +gboolean +cm_room_has_state_sync (CmRoom *self) +{ + g_return_val_if_fail (CM_IS_ROOM (self), FALSE); + + return self->initial_sync_done; +} + +/** + * cm_room_get_id: + * @self: A #CmRoom + * + * Get the matrix room id. + * + * Returns: The room id + */ +const char * +cm_room_get_id (CmRoom *self) +{ + g_return_val_if_fail (CM_IS_ROOM (self), NULL); + + return self->room_id; +} + +gboolean +cm_room_self_has_power_for_event (CmRoom *self, + CmEventType type) +{ + CmEvent *event; + + g_return_val_if_fail (CM_IS_ROOM (self), FALSE); + + event = cm_room_event_list_get_event (self->room_event, CM_M_ROOM_POWER_LEVELS); + if (!event) + return FALSE; + + return cm_room_event_user_has_power (CM_ROOM_EVENT (event), + cm_client_get_user_id (self->client), + type); +} + +/** + * cm_room_get_name: + * @self: A #CmRoom + * + * Get the matrix room name. Can be %NULL + * if the room name is not set, or the name + * is not yet loaded. + * + * Returns: (nullable): The room name + */ +const char * +cm_room_get_name (CmRoom *self) +{ + g_return_val_if_fail (CM_IS_ROOM (self), NULL); + + if (!self->generated_name && !self->name) + { + g_autofree char *name = NULL; + + name = cm_room_generate_name (self); + cm_room_set_generated_name (self, name); + cm_room_save (self); + } + + if (self->name) + return self->name; + + return self->generated_name; +} + +/** + * cm_room_get_past_name: + * @self: A #CmRoom + * + * Get the matrix room name set before the current one. + * The past name is set only if the room is a direct + * room and current room name is empty (in the case + * where cm_room_get_name() shall return "Empty room") + * + * Returns: (nullable): The past room name + */ +const char * +cm_room_get_past_name (CmRoom *self) +{ + g_return_val_if_fail (CM_IS_ROOM (self), NULL); + + if (!self->name && + g_strcmp0 (self->generated_name, "Empty room") == 0) + return self->past_name; + + return NULL; +} + +/** + * cm_room_is_encrypted: + * @self: A #CmRoom + * + * Get if the matrix room @self is encrypted + * or not. + * + * Returns: %TRUE if @self is encrypted. + * %FALSE otherwise. + */ +gboolean +cm_room_is_encrypted (CmRoom *self) +{ + CmEvent *event; + + g_return_val_if_fail (CM_IS_ROOM (self), TRUE); + + if (self->encryption) + return TRUE; + + event = cm_room_event_list_get_event (self->room_event, CM_M_ROOM_ENCRYPTION); + + return !!event; +} + +GListModel * +cm_room_get_joined_members (CmRoom *self) +{ + g_return_val_if_fail (CM_IS_ROOM (self), NULL); + + return G_LIST_MODEL (self->joined_members); +} + +GListModel * +cm_room_get_events_list (CmRoom *self) +{ + g_return_val_if_fail (CM_IS_ROOM (self), NULL); + + return cm_room_event_list_get_events (self->room_event); +} + +gint64 +cm_room_get_unread_notification_counts (CmRoom *self) +{ + g_return_val_if_fail (CM_IS_ROOM (self), 0); + + return self->unread_count; +} + +CmStatus +cm_room_get_status (CmRoom *self) +{ + g_return_val_if_fail (CM_IS_ROOM (self), CM_STATUS_UNKNOWN); + + return self->room_status; +} + +void +cm_room_set_status (CmRoom *self, + CmStatus status) +{ + g_return_if_fail (CM_IS_ROOM (self)); + g_return_if_fail (status == CM_STATUS_INVITE || + status == CM_STATUS_JOIN || + status == CM_STATUS_LEAVE); + + if (self->room_status == status) + return; + + self->room_status = status; + + if (self->client) + { + self->db_save_pending = TRUE; + cm_room_save (self); + } +} + +JsonObject * +cm_room_decrypt (CmRoom *self, + JsonObject *root) +{ + char *plain_text = NULL; + JsonObject *content; + CmEnc *enc; + + g_return_val_if_fail (CM_IS_ROOM (self), NULL); + + enc = cm_client_get_enc (self->client); + + if (!enc || !root) + return NULL; + + content = cm_utils_json_object_get_object (root, "content"); + plain_text = cm_enc_handle_join_room_encrypted (enc, self, content); + + return cm_utils_string_to_json_object (plain_text); +} + +void +cm_room_add_events (CmRoom *self, + GPtrArray *events, + gboolean append) +{ + g_return_if_fail (CM_IS_ROOM (self)); + + cm_room_event_list_add_events (self->room_event, events, append); +} + +GPtrArray * +cm_room_set_data (CmRoom *self, + JsonObject *object) +{ + g_autoptr(GPtrArray) events = NULL; + JsonObject *child, *local; + JsonArray *array; + guint length = 0; + + g_return_val_if_fail (CM_IS_ROOM (self), NULL); + g_return_val_if_fail (object, NULL); + + child = cm_utils_json_object_get_object (object, "unread_notifications"); + + if (child) + { + local = cm_room_event_list_get_local_json (self->room_event); + json_object_set_object_member (local, "unread_notifications", json_object_ref (child)); + self->unread_count = cm_utils_json_object_get_int (child, "notification_count"); + } + + events = g_ptr_array_new_full (100, g_object_unref); + child = cm_utils_json_object_get_object (object, "state"); + cm_room_event_list_parse_events (self->room_event, child, NULL, FALSE); + + child = cm_utils_json_object_get_object (object, "invite_state"); + cm_room_event_list_parse_events (self->room_event, child, NULL, FALSE); + + child = cm_utils_json_object_get_object (object, "timeline"); + cm_room_event_list_parse_events (self->room_event, child, events, FALSE); + CM_TRACE ("(%p) New timeline events count: %u", self, events->len); + + if (cm_utils_json_object_get_bool (child, "limited")) + { + const char *prev; + + prev = cm_utils_json_object_get_string (child, "prev_batch"); + cm_room_set_prev_batch (self, prev); + } + self->db_save_pending = TRUE; + + length = 0; + array = cm_utils_json_object_get_array (object, "left"); + + if (array) + length = json_array_get_length (array); + + if (length) + { + CM_TRACE ("(%p) %u users left", self, length); + cm_enc_rm_room_group_key (cm_client_get_enc (self->client), self); + } + + for (guint i = 0; i < length; i++) + { + g_autoptr(GRefString) user_id = NULL; + CmRoomMember *member; + const char *member_id; + + member_id = json_array_get_string_element (array, i); + user_id = g_ref_string_new_intern (member_id); + member = g_hash_table_lookup (self->joined_members_table, user_id); + + if (member) + { + g_hash_table_remove (self->joined_members_table, user_id); + cm_utils_remove_list_item (self->joined_members, member); + self->keys_claimed = FALSE; + } + } + + self->initial_sync_done = TRUE; + cm_room_save (self); + + return g_steal_pointer (&events); +} + +/* + * cm_room_user_changed: + * @self: A #CmRoom + * @changed_users: An array of #CmUser + * + * Inform that the user devices for @changed_users has + * has changed. The function simply returns if any of + * the user in @changed_users is not in the room + * + * This is useful for encrypted rooms where the user + * devices has to be updated before sending anything + * encrypted. + */ +void +cm_room_user_changed (CmRoom *self, + GPtrArray *changed_users) +{ + GRefString *user_id; + CmUser *user; + + g_return_if_fail (CM_IS_ROOM (self)); + g_return_if_fail (changed_users); + + /* We need to track the user changes only if room is encrypted */ + if (!cm_room_is_encrypted (self)) + return; + + for (guint i = 0; i < changed_users->len; i++) + { + user = changed_users->pdata[i]; + user_id = cm_user_get_id (user); + + if (g_hash_table_contains (self->joined_members_table, user_id) && + !g_ptr_array_find (self->changed_users, user, NULL)) + g_ptr_array_add (self->changed_users, g_object_ref (user)); + } + + g_debug ("(%p) Room user(s) changed, count: %u", self, self->changed_users->len); +} + +const char * +cm_room_get_prev_batch (CmRoom *self) +{ + g_return_val_if_fail (CM_IS_ROOM (self), NULL); + + return self->prev_batch; +} + +void +cm_room_set_prev_batch (CmRoom *self, + const char *prev_batch) +{ + g_return_if_fail (CM_IS_ROOM (self)); + + g_free (self->prev_batch); + self->prev_batch = g_strdup (prev_batch); +} + +void +cm_room_set_name (CmRoom *self, + const char *name) +{ + JsonObject *json, *child; + + g_return_if_fail (CM_IS_ROOM (self)); + + if (g_strcmp0 (name, self->name) == 0) + return; + + g_free (self->name); + self->name = g_strdup (name); + + json = room_generate_json (self); + child = cm_utils_json_object_get_object (json, "local"); + + if (name) + json_object_set_string_member (child, "alias", name); + else + json_object_remove_member (child, "alias"); + + self->db_save_pending = TRUE; + + g_object_notify_by_pspec (G_OBJECT (self), properties[PROP_NAME]); +} + +void +cm_room_set_generated_name (CmRoom *self, + const char *name) +{ + JsonObject *local, *child; + + g_return_if_fail (CM_IS_ROOM (self)); + + if (g_strcmp0 (name, self->generated_name) == 0) + return; + + g_free (self->generated_name); + self->generated_name = g_strdup (name); + + local = cm_room_event_list_get_local_json (self->room_event); + child = cm_utils_json_object_get_object (local, "local"); + json_object_set_string_member (child, "generated_alias", name); + + self->db_save_pending = TRUE; + + g_object_notify_by_pspec (G_OBJECT (self), properties[PROP_NAME]); +} + +/* cm_room_get_encryption_rotation_time: + * + * Return the time in milliseconds after which the key + * should be rotated after the first key use. + * + * The key should be regenerated regardless of whether + * the maximum message count for the rotation has reached + * or not. + */ +gint64 +cm_room_get_encryption_rotation_time (CmRoom *self) +{ + CmEvent *event; + + g_return_val_if_fail (CM_IS_ROOM (self), 60 * 60 * 24 * 7); + + event = cm_room_event_list_get_event (self->room_event, CM_M_ROOM_ENCRYPTION); + if (!event) + return 60 * 60 * 24 * 7; + + return cm_room_event_get_rotation_time ((gpointer)event); +} + +/* cm_room_get_encryption_msg_count: + * + * Return the number of messages after which the encryption + * key for sending messages in the room should be rotated. + */ +guint +cm_room_get_encryption_msg_count (CmRoom *self) +{ + CmEvent *event; + + g_return_val_if_fail (CM_IS_ROOM (self), 100); + + event = cm_room_event_list_get_event (self->room_event, CM_M_ROOM_ENCRYPTION); + if (!event) + return 100; + + return cm_room_event_get_rotation_count ((gpointer)event); +} + +gboolean +cm_room_is_direct (CmRoom *self) +{ + g_return_val_if_fail (CM_IS_ROOM (self), FALSE); + + return self->is_direct; +} + +void +cm_room_set_is_direct (CmRoom *self, + gboolean is_direct) +{ + g_return_if_fail (CM_IS_ROOM (self)); + + self->is_direct = !!is_direct; +} + +static void +send_cb (GObject *obj, + GAsyncResult *result, + gpointer user_data) +{ + CmRoom *self; + GTask *message_task = user_data; + g_autoptr(JsonObject) object = NULL; + GError *error = NULL; + const char *event_id = NULL; + CmEvent *event; + + g_assert (G_IS_TASK (message_task)); + + self = g_task_get_source_object (message_task); + event = g_task_get_task_data (message_task); + object = g_task_propagate_pointer (G_TASK (result), &error); + + event_id = cm_utils_json_object_get_string (object, "event_id"); + g_debug ("(%p) Send message %s. txn-id: '%s'", self, + CM_LOG_SUCCESS (!error), cm_event_get_txn_id (event)); + + self->is_sending_message = FALSE; + + if (error) + { + cm_event_set_state (event, CM_EVENT_STATE_SENDING_FAILED); + g_debug ("(%p) Send message error: %s", self, error->message); + g_task_return_error (message_task, error); + } + else + { + cm_event_set_state (event, CM_EVENT_STATE_SENT); + room_add_event_to_db (self, event); + + /* Set event after saving to db so that event id is not stored in db + * and we replace id less events when we sync, so that the event is + * placed in the right order. + */ + cm_event_set_id (event, event_id); + + g_task_return_pointer (message_task, g_strdup (event_id), g_free); + } +} + +static void +room_send_file_cb (GObject *object, + GAsyncResult *result, + gpointer user_data) +{ + CmRoom *self; + g_autoptr(GTask) task = user_data; + g_autoptr(GError) error = NULL; + g_autofree char *uri = NULL; + CmInputStream *stream; + char *mxc_uri = NULL; + GTask *message_task; + GFile *message_file; + CmRoomMessageEvent *message; + + g_assert (G_IS_TASK (task)); + + self = g_task_get_source_object (task); + message_task = g_task_get_task_data (task); + + g_assert (CM_IS_ROOM (self)); + g_assert (G_TASK (message_task)); + + message = g_task_get_task_data (message_task); + g_assert (CM_IS_ROOM_MESSAGE_EVENT (message)); + + mxc_uri = cm_net_put_file_finish (CM_NET (object), result, &error); + + g_debug ("(%p) Upload file %s. txn-id: '%s'", self, + CM_LOG_SUCCESS (!error && mxc_uri), + cm_event_get_txn_id (CM_EVENT (message))); + + if (!mxc_uri) + { + self->is_sending_message = FALSE; + + g_task_return_new_error (message_task, G_IO_ERROR, G_IO_ERROR_FAILED, + "Failed to upload file: %s", error->message ?: ""); + room_send_message_from_queue (self); + + return; + } + + stream = g_object_get_data (G_OBJECT (result), "stream"); + g_assert (stream); + g_object_ref (stream); + + message_file = cm_room_message_event_get_file (message); + g_assert (G_IS_FILE (message_file)); + + g_object_set_data_full (G_OBJECT (message_file), "uri", mxc_uri, g_free); + g_object_set_data_full (G_OBJECT (message_file), "stream", stream, g_object_unref); + g_object_set_data_full (G_OBJECT (stream), "uri", g_strdup (mxc_uri), g_free); + + uri = cm_event_get_api_url (CM_EVENT (message), self); + + cm_net_send_json_async (cm_client_get_net (self->client), 0, + cm_event_generate_json (CM_EVENT (message), self), + uri, SOUP_METHOD_PUT, NULL, g_task_get_cancellable (message_task), + send_cb, message_task); +} + +static void +room_send_message_from_queue (CmRoom *self) +{ + CmRoomMessageEvent *message; + GTask *message_task; + g_autofree char *uri = NULL; + + g_assert (CM_IS_ROOM (self)); + + message_task = g_queue_peek_head (self->message_queue); + + if (!message_task) + return; + + if (self->is_sending_message || self->retry_timeout_id) + return; + + if (cm_room_is_encrypted (self) && + (!cm_enc_has_room_group_key (cm_client_get_enc (self->client), self) || + self->changed_users->len || !self->keys_claimed || + (self->one_time_keys && self->one_time_keys->len))) + { + ensure_encryption_keys (self); + return; + } + + self->is_sending_message = TRUE; + message_task = g_queue_pop_head (self->message_queue); + message = g_task_get_task_data (message_task); + g_assert (CM_IS_ROOM_MESSAGE_EVENT (message)); + + if (cm_room_message_event_get_msg_type (message) == CM_CONTENT_TYPE_FILE) + { + GFileProgressCallback progress_cb; + gpointer progress_user_data; + GTask *task; + + progress_cb = g_object_get_data (G_OBJECT (message_task), "progress-cb"); + progress_user_data = g_object_get_data (G_OBJECT (message_task), "progress-cb-data"); + + task = g_task_new (self, NULL, NULL, NULL); + g_task_set_task_data (task, g_object_ref (message_task), g_object_unref); + g_debug ("(%p) Upload file, txn-id: '%s'", + self, cm_event_get_txn_id (CM_EVENT (message))); + cm_net_put_file_async (cm_client_get_net (self->client), + cm_room_message_event_get_file (message), + cm_room_is_encrypted (self), + progress_cb, progress_user_data, + g_task_get_cancellable (message_task), + room_send_file_cb, task); + return; + } + + uri = cm_event_get_api_url (CM_EVENT (message), self); + + g_debug ("(%p) Send message, txn-id: '%s'", + self, cm_event_get_txn_id (CM_EVENT (message))); + cm_event_set_state (CM_EVENT (message), CM_EVENT_STATE_SENDING); + cm_net_send_json_async (cm_client_get_net (self->client), 0, + cm_event_generate_json (CM_EVENT (message), self), + uri, SOUP_METHOD_PUT, NULL, g_task_get_cancellable (message_task), + send_cb, message_task); +} + +/* todo */ +#if 0 +static gboolean +room_resend_message (gpointer user_data) +{ + CmRoom *self = user_data; + + g_assert (CM_IS_ROOM (self)); + + self->retry_timeout_id = 0; + room_send_message_from_queue (self); + + return G_SOURCE_REMOVE; +} +#endif + +static void +room_accept_invite_cb (GObject *object, + GAsyncResult *result, + gpointer user_data) +{ + CmRoom *self; + g_autoptr(GTask) task = user_data; + GError *error = NULL; + gboolean success; + + self = g_task_get_source_object (task); + success = cm_client_join_room_by_id_finish (self->client, result, &error); + + self->invite_accept_success = success; + self->is_accepting_invite = FALSE; + + if (error) + g_task_return_error (task, error); + else + g_task_return_boolean (task, success); +} + +void +cm_room_accept_invite_async (CmRoom *self, + GCancellable *cancellable, + GAsyncReadyCallback callback, + gpointer user_data) +{ + g_autoptr(GTask) task = NULL; + + g_return_if_fail (CM_IS_ROOM (self)); + g_return_if_fail (!cancellable || G_IS_CANCELLABLE (cancellable)); + g_return_if_fail (!self->is_accepting_invite); + + task = g_task_new (self, cancellable, callback, user_data); + + g_debug ("(%p) Accept room invite", self); + + if (self->room_status != CM_STATUS_INVITE) + { + g_debug ("(%p) Accept room invite error, room is not invite", self); + g_task_return_new_error (task, CM_ERROR, CM_ERROR_INVALID_ROOM_STATE, + "Room is not in invite state"); + return; + } + + if (self->invite_accept_success) + { + g_debug ("(%p) Accept room invite already succeeded", self); + g_task_return_boolean (task, TRUE); + return; + } + + if (self->invite_accept_success) + { + g_debug ("(%p) Accept room error, user has already accepted invite", self); + g_task_return_new_error (task, CM_ERROR, CM_ERROR_INVALID_ROOM_STATE, + "User has already accepted invite"); + return; + } + + if (self->is_accepting_invite) + { + g_debug ("(%p) Accept room, already in progress", self); + g_task_return_new_error (task, G_IO_ERROR, G_IO_ERROR_PENDING, + "Accept room invite in progress"); + return; + } + + self->is_accepting_invite = TRUE; + + cm_client_join_room_by_id_async (self->client, self->room_id, + cancellable, + room_accept_invite_cb, + g_steal_pointer (&task)); +} + +gboolean +cm_room_accept_invite_finish (CmRoom *self, + GAsyncResult *result, + GError **error) +{ + g_return_val_if_fail (CM_IS_ROOM (self), FALSE); + g_return_val_if_fail (G_IS_TASK (result), FALSE); + g_return_val_if_fail (!error || !*error, FALSE); + + return g_task_propagate_boolean (G_TASK (result), error); +} + +static void +room_reject_invite_cb (GObject *object, + GAsyncResult *result, + gpointer user_data) +{ + CmRoom *self; + g_autoptr(GTask) task = user_data; + GError *error = NULL; + gboolean success; + + self = g_task_get_source_object (task); + success = cm_room_leave_finish (self, result, &error); + + self->invite_reject_success = success; + self->is_rejecting_invite = FALSE; + + if (error) + g_task_return_error (task, error); + else + g_task_return_boolean (task, success); +} + +void +cm_room_reject_invite_async (CmRoom *self, + GCancellable *cancellable, + GAsyncReadyCallback callback, + gpointer user_data) +{ + g_autoptr(GTask) task = NULL; + + g_return_if_fail (CM_IS_ROOM (self)); + g_return_if_fail (!cancellable || G_IS_CANCELLABLE (cancellable)); + g_return_if_fail (!self->is_accepting_invite); + + task = g_task_new (self, cancellable, callback, user_data); + + g_debug ("(%p) Reject room invite", self); + + if (self->room_status != CM_STATUS_INVITE) + { + g_debug ("(%p) Reject room invite error, room is not invite", self); + g_task_return_new_error (task, CM_ERROR, CM_ERROR_INVALID_ROOM_STATE, + "Room is not in invite state"); + return; + } + + if (self->invite_reject_success) + { + g_debug ("(%p) Reject room invite already succeeded", self); + g_task_return_boolean (task, TRUE); + return; + } + + if (self->invite_accept_success) + { + g_debug ("(%p) Reject room error, user has already accepted invite", self); + g_task_return_new_error (task, CM_ERROR, CM_ERROR_INVALID_ROOM_STATE, + "User has already accepted invite"); + return; + } + + if (self->is_rejecting_invite) + { + g_debug ("(%p) Reject room, already in progress", self); + g_task_return_new_error (task, G_IO_ERROR, G_IO_ERROR_PENDING, + "Reject room invite in progress"); + return; + } + + self->is_rejecting_invite = TRUE; + + cm_room_leave_async (self, cancellable, + room_reject_invite_cb, + g_steal_pointer (&task)); +} + +gboolean +cm_room_reject_invite_finish (CmRoom *self, + GAsyncResult *result, + GError **error) +{ + g_return_val_if_fail (CM_IS_ROOM (self), FALSE); + g_return_val_if_fail (G_IS_TASK (result), FALSE); + g_return_val_if_fail (!error || !*error, FALSE); + + return g_task_propagate_boolean (G_TASK (result), error); +} + +/** + * cm_room_send_text_async: + * @self: A #CmRoom + * @text: The text message to send + * @cancellable: (nullable): A #Gcancellable + * @callback: A #GasyncReadyCallback + * @user_data: The user data for @callback. + * + * Send @text as a text message to the room + * @self. If the room is encrypted, the text + * shall be encrypted and sent. + * + * Returns: The event id string used for the event. + * This can be used to track the event when received + * via the /sync callback or so. + */ +const char * +cm_room_send_text_async (CmRoom *self, + const char *text, + GCancellable *cancellable, + GAsyncReadyCallback callback, + gpointer user_data) +{ + CmRoomMessageEvent *message; + CmUser *user; + GTask *task; + + g_return_val_if_fail (CM_IS_ROOM (self), NULL); + + task = g_task_new (self, cancellable, callback, user_data); + message = cm_room_message_event_new (CM_CONTENT_TYPE_TEXT); + cm_event_set_state (CM_EVENT (message), CM_EVENT_STATE_WAITING); + cm_room_message_event_set_body (message, text); + cm_event_create_txn_id (CM_EVENT (message), + cm_client_pop_event_id (self->client)); + g_task_set_task_data (task, message, g_object_unref); + + user = room_find_user (self, cm_client_get_user_id (self->client), TRUE); + cm_event_set_sender (CM_EVENT (message), user); + + g_debug ("(%p) Queue send text message, txn-id: '%s'", + self, cm_event_get_txn_id (CM_EVENT (message))); + cm_room_event_list_append_event (self->room_event, CM_EVENT (message)); + room_add_event_to_db (self, CM_EVENT (message)); + + g_queue_push_tail (self->message_queue, task); + + room_send_message_from_queue (self); + + return cm_event_get_txn_id (CM_EVENT (message)); +} + +/** + * cm_room_send_text_finish: + * @self: A #CmRoom + * @result: A #GAsyncResult + * @error: (nullable): A #GError + * + * Finish the call to cm_room_send_text_async(). + * + * Returns: The event id string used for the event + * on success. %NULL on error. This is the same + * as the one you get from cm_room_send_text_async(). + * Free with g_free(). + */ +char * +cm_room_send_text_finish (CmRoom *self, + GAsyncResult *result, + GError **error) +{ + g_return_val_if_fail (CM_IS_ROOM (self), FALSE); + g_return_val_if_fail (G_IS_TASK (result), FALSE); + g_return_val_if_fail (!error || !*error, FALSE); + + return g_task_propagate_pointer (G_TASK (result), error); +} + +const char * +cm_room_send_file_async (CmRoom *self, + GFile *file, + const char *body, + GFileProgressCallback progress_callback, + gpointer progress_user_data, + GCancellable *cancellable, + GAsyncReadyCallback callback, + gpointer user_data) +{ + CmRoomMessageEvent *message; + CmUser *user; + GTask *task; + + g_return_val_if_fail (CM_IS_ROOM (self), NULL); + g_return_val_if_fail (G_IS_FILE (file), NULL); + + task = g_task_new (self, cancellable, callback, user_data); + g_object_set_data (G_OBJECT (task), "progress-cb", progress_callback); + g_object_set_data (G_OBJECT (task), "progress-cb-data", progress_user_data); + + message = cm_room_message_event_new (CM_CONTENT_TYPE_FILE); + cm_event_set_state (CM_EVENT (message), CM_EVENT_STATE_WAITING); + cm_room_message_event_set_file (message, body, file); + cm_event_create_txn_id (CM_EVENT (message), + cm_client_pop_event_id (self->client)); + g_task_set_task_data (task, message, g_object_unref); + + user = room_find_user (self, cm_client_get_user_id (self->client), TRUE); + cm_event_set_sender (CM_EVENT (message), user); + + g_debug ("(%p) Queue send file message, txn-id: '%s'", + self, cm_event_get_txn_id (CM_EVENT (message))); + cm_room_event_list_append_event (self->room_event, CM_EVENT (message)); + + g_queue_push_tail (self->message_queue, task); + + room_send_message_from_queue (self); + + return cm_event_get_txn_id (CM_EVENT (message)); +} + +char * +cm_room_send_file_finish (CmRoom *self, + GAsyncResult *result, + GError **error) +{ + g_return_val_if_fail (CM_IS_ROOM (self), FALSE); + g_return_val_if_fail (G_IS_TASK (result), FALSE); + g_return_val_if_fail (!error || !*error, FALSE); + + return g_task_propagate_pointer (G_TASK (result), error); +} + +static void +send_typing_cb (GObject *obj, + GAsyncResult *result, + gpointer user_data) +{ + CmRoom *self; + g_autoptr(GTask) task = user_data; + g_autoptr(JsonObject) object = NULL; + g_autoptr(GError) error = NULL; + + g_assert (G_IS_TASK (task)); + self = g_task_get_source_object (task); + + object = g_task_propagate_pointer (G_TASK (result), &error); + + CM_TRACE ("(%p) Set typing to '%s' %s", self, + CM_LOG_BOOL (self->typing), CM_LOG_SUCCESS (!error)); + + if (error) + { + self->typing = GPOINTER_TO_INT (g_object_get_data (G_OBJECT (task), "was-typing")); + self->typing_set_time = GPOINTER_TO_SIZE (g_object_get_data (G_OBJECT (task), "was-typing-time")); + g_debug ("(%p) Set typing error: %s", self, error->message); + } +} + +/** + * cm_room_set_typing_notice_async: + * @self: A #CmRoom + * @typing: set/unset typing + * @cancellable: (nullable): A #Gcancellable + * @callback: A #GasyncReadyCallback + * @user_data: The user data for @callback. + * + * Set/Unset if the self user is typing or not. + * The typing set is timeout after 4 seconds. + */ +void +cm_room_set_typing_notice_async (CmRoom *self, + gboolean typing, + GCancellable *cancellable, + GAsyncReadyCallback callback, + gpointer user_data) +{ + g_autofree char *uri = NULL; + JsonObject *object; + GTask *task; + + g_return_if_fail (CM_IS_ROOM (self)); + + task = g_task_new (self, cancellable, callback, user_data); + g_object_set_data (G_OBJECT (task), "was-typing", GINT_TO_POINTER (self->typing)); + g_object_set_data (G_OBJECT (task), "was-typing-time", GSIZE_TO_POINTER (self->typing_set_time)); + + if (typing == self->typing && + g_get_monotonic_time () - self->typing_set_time < TYPING_TIMEOUT * G_USEC_PER_SEC) + { + g_task_return_boolean (task, TRUE); + return; + } + + CM_TRACE ("(%p) Set typing to '%s'", self, CM_LOG_BOOL (typing)); + self->typing_set_time = g_get_monotonic_time (); + self->typing = !!typing; + + /* https://matrix.org/docs/spec/client_server/r0.6.1#put-matrix-client-r0-rooms-roomid-typing-userid */ + object = json_object_new (); + json_object_set_boolean_member (object, "typing", !!typing); + if (typing) + json_object_set_int_member (object, "timeout", TYPING_TIMEOUT); + + uri = g_strconcat ("/_matrix/client/r0/rooms/", self->room_id, + "/typing/", cm_client_get_user_id (self->client), NULL); + + cm_net_send_json_async (cm_client_get_net (self->client), 0, object, + uri, SOUP_METHOD_PUT, + NULL, cancellable, send_typing_cb, task); +} + +/** + * cm_room_set_typing_notice_finish: + * @self: A #CmRoom + * @result: A #GAsyncResult + * @error: (nullable): A #GError + * + * Finish the call to cm_room_set_typing_notice_async(). + * + * Returns: %TRUE if typing notice was successfully + * set, %FALSE otherwise. + */ +gboolean +cm_room_set_typing_notice_finish (CmRoom *self, + GAsyncResult *result, + GError **error) +{ + g_return_val_if_fail (CM_IS_ROOM (self), FALSE); + g_return_val_if_fail (G_IS_TASK (result), FALSE); + g_return_val_if_fail (!error || !*error, FALSE); + + return g_task_propagate_boolean (G_TASK (result), error); +} + +static void +room_set_encryption_cb (GObject *obj, + GAsyncResult *result, + gpointer user_data) +{ + CmRoom *self; + g_autoptr(GTask) task = user_data; + g_autoptr(JsonObject) object = NULL; + GError *error = NULL; + + self = g_task_get_source_object (task); + object = g_task_propagate_pointer (G_TASK (result), &error); + g_debug ("(%p) Enable encryption %s", self, CM_LOG_SUCCESS (!error)); + + if (error) + { + g_warning ("(%p) Enable encryption failed, error: %s", self, error->message); + g_task_return_error (task, error); + } + else + { + const char *event; + + event = cm_utils_json_object_get_string (object, "event_id"); + self->encryption = g_strdup ("encrypted"); + g_object_notify_by_pspec (G_OBJECT (self), properties[PROP_ENCRYPTED]); + self->db_save_pending = TRUE; + cm_room_save (self); + g_task_return_boolean (task, !!event); + } +} + +/** + * cm_room_enable_encryption_async: + * @self: A #CmRoom + * @cancellable: (nullable): A #GCancellable + * @callback: A #GAsyncReadyCallback + * @user_data: user data passed to @callback + * + * Enable encryption for @self. You can't disable + * encryption once enabled. Also, it's a noop + * if @self has already enabled encryption. The + * @callback shall run in any case. + * + * To get the result, finish the call with + * cm_room_enable_encryption_finish(). + */ +void +cm_room_enable_encryption_async (CmRoom *self, + GCancellable *cancellable, + GAsyncReadyCallback callback, + gpointer user_data) +{ + g_autofree char *uri = NULL; + JsonObject *object; + GTask *task; + + g_return_if_fail (CM_IS_ROOM (self)); + + task = g_task_new (self, cancellable, callback, user_data); + g_debug ("(%p) Enable encryption", self); + + if (cm_room_is_encrypted (self)) + { + g_debug ("(%p) Enable encryption. Already encrypted, ignored", self); + g_task_return_boolean (task, TRUE); + return; + } + + object = json_object_new (); + json_object_set_string_member (object, "algorithm", ALGORITHM_MEGOLM); + uri = g_strconcat ("/_matrix/client/r0/rooms/", self->room_id, "/state/m.room.encryption", NULL); + cm_net_send_json_async (cm_client_get_net (self->client), 2, object, uri, SOUP_METHOD_PUT, + NULL, cancellable, room_set_encryption_cb, task); +} + +/** + * cm_room_enable_encryption_finish: + * @self: A #CmRoom + * @result: A #GAsyncResult + * @error: (nullable): A #GError + * + * Finish the call to cm_room_enable_encryption_async(). + * + * Returns: %TRUE if encryption was enabled, + * %FALSE otherwise. + */ +gboolean +cm_room_enable_encryption_finish (CmRoom *self, + GAsyncResult *result, + GError **error) +{ + g_return_val_if_fail (CM_IS_ROOM (self), FALSE); + g_return_val_if_fail (G_IS_TASK (result), FALSE); + g_return_val_if_fail (!error || !*error, FALSE); + + return g_task_propagate_boolean (G_TASK (result), error); +} + +static void +room_leave_cb (GObject *obj, + GAsyncResult *result, + gpointer user_data) +{ + CmRoom *self; + g_autoptr(GTask) task = user_data; + g_autoptr(JsonObject) object = NULL; + GError *error = NULL; + + g_assert (G_IS_TASK (task)); + + self = g_task_get_source_object (task); + g_assert (CM_IS_ROOM (self)); + + object = g_task_propagate_pointer (G_TASK (result), &error); + + g_debug ("(%p) Leave room %s", self, CM_LOG_SUCCESS (!error)); + + if (error && !g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CANCELLED)) + g_debug ("(%p) Leave room error: %s", self, error->message); + + if (error) + g_task_return_error (task, error); + else + g_task_return_boolean (task, TRUE); +} + +void +cm_room_leave_async (CmRoom *self, + GCancellable *cancellable, + GAsyncReadyCallback callback, + gpointer user_data) +{ + GTask *task; + g_autofree char *uri = NULL; + + g_return_if_fail (CM_IS_ROOM (self)); + + g_debug ("(%p) leave room", self); + task = g_task_new (self, cancellable, callback, user_data); + uri = g_strdup_printf ("/_matrix/client/r0/rooms/%s/leave", self->room_id); + cm_net_send_json_async (cm_client_get_net (self->client), 1, NULL, + uri, SOUP_METHOD_POST, + NULL, cancellable, room_leave_cb, task); +} + +gboolean +cm_room_leave_finish (CmRoom *self, + GAsyncResult *result, + GError **error) +{ + g_return_val_if_fail (CM_IS_ROOM (self), FALSE); + g_return_val_if_fail (G_IS_TASK (result), FALSE); + g_return_val_if_fail (!error || !*error, FALSE); + + return g_task_propagate_boolean (G_TASK (result), error); +} + +static void +room_set_read_marker_cb (GObject *obj, + GAsyncResult *result, + gpointer user_data) +{ + CmRoom *self; + g_autoptr(GTask) task = user_data; + g_autoptr(JsonObject) object = NULL; + GError *error = NULL; + + self = g_task_get_source_object (task); + + object = g_task_propagate_pointer (G_TASK (result), &error); + CM_TRACE ("(%p) Set read marker %s", self, CM_LOG_SUCCESS (!error)); + + if (error) + g_task_return_error (task, error); + else + g_task_return_boolean (task, TRUE); +} + +void +cm_room_set_read_marker_async (CmRoom *self, + CmEvent *fully_read_event, + CmEvent *read_receipt_event, + GAsyncReadyCallback callback, + gpointer user_data) +{ + const char *fully_read_id, *read_receipt_id; + g_autofree char *uri = NULL; + JsonObject *root; + GTask *task; + + g_return_if_fail (CM_IS_ROOM (self)); + g_return_if_fail (CM_IS_EVENT (fully_read_event)); + g_return_if_fail (CM_IS_EVENT (read_receipt_event)); + + fully_read_id = cm_event_get_id (fully_read_event); + read_receipt_id = cm_event_get_id (read_receipt_event); + + root = json_object_new (); + json_object_set_string_member (root, "m.fully_read", fully_read_id); + json_object_set_string_member (root, "m.read", read_receipt_id); + + task = g_task_new (self, NULL, callback, user_data); + + CM_TRACE ("(%p) Set read marker", self); + uri = g_strdup_printf ("/_matrix/client/r0/rooms/%s/read_markers", self->room_id); + cm_net_send_json_async (cm_client_get_net (self->client), 0, root, + uri, SOUP_METHOD_POST, + NULL, NULL, room_set_read_marker_cb, task); +} + +gboolean +cm_room_set_read_marker_finish (CmRoom *self, + GAsyncResult *result, + GError **error) +{ + g_return_val_if_fail (CM_IS_ROOM (self), FALSE); + g_return_val_if_fail (G_IS_TASK (result), FALSE); + g_return_val_if_fail (!error || !*error, FALSE); + + return g_task_propagate_boolean (G_TASK (result), error); +} + +static void +room_load_prev_batch_cb (GObject *obj, + GAsyncResult *result, + gpointer user_data) +{ + CmRoom *self; + g_autoptr(GTask) task = user_data; + g_autoptr(JsonObject) object = NULL; + GPtrArray *events = NULL; + GError *error = NULL; + const char *end; + + self = g_task_get_source_object (task); + g_assert (CM_IS_ROOM (self)); + + object = g_task_propagate_pointer (G_TASK (result), &error); + g_debug ("(%p) Load prev batch %s", self, CM_LOG_SUCCESS (!error)); + + if (error) + { + g_debug ("(%p) Load prev batch error: %s", self, error->message); + g_task_return_error (task, error); + return; + } + + end = cm_utils_json_object_get_string (object, "end"); + + /* If start and end are same, we have reached the start of room history */ + if (g_strcmp0 (cm_utils_json_object_get_string (object, "end"), + cm_utils_json_object_get_string (object, "start")) == 0) + end = NULL; + + cm_room_set_prev_batch (self, end); + self->db_save_pending = TRUE; + cm_room_save (self); + + events = g_ptr_array_new_full (64, g_object_unref); + cm_room_event_list_parse_events (self->room_event, object, events, TRUE); + cm_db_add_room_events (cm_client_get_db (self->client), self, events, TRUE); + g_debug ("(%p) Load prev batch events: %u", self, events->len); + + g_task_return_pointer (task, events, (GDestroyNotify)g_ptr_array_unref); +} + +void +cm_room_load_prev_batch_async (CmRoom *self, + GCancellable *cancellable, + GAsyncReadyCallback callback, + gpointer user_data) +{ + g_autofree char *uri = NULL; + const char *prev_batch; + GHashTable *query; + GTask *task; + + g_return_if_fail (CM_IS_ROOM (self)); + + task = g_task_new (self, cancellable, callback, user_data); + prev_batch = cm_room_get_prev_batch (self); + g_debug ("(%p) Load prev batch", self); + + if (!prev_batch) + { + g_debug ("(%p) Load prev batch error: missing prev_batch", self); + g_task_return_pointer (task, NULL, NULL); + return; + } + + /* Create a query to get past 30 messages */ + query = g_hash_table_new_full (g_str_hash, g_str_equal, free, + (GDestroyNotify)cm_utils_free_buffer); + g_hash_table_insert (query, g_strdup ("from"), g_strdup (prev_batch)); + g_hash_table_insert (query, g_strdup ("dir"), g_strdup ("b")); + g_hash_table_insert (query, g_strdup ("limit"), g_strdup ("30")); + /* if (upto_batch) */ + /* g_hash_table_insert (query, g_strdup ("to"), g_strdup (upto_batch)); */ + + /* https://matrix.org/docs/spec/client_server/r0.6.1#get-matrix-client-r0-rooms-roomid-messages */ + uri = g_strconcat ("/_matrix/client/r0/rooms/", self->room_id, "/messages", NULL); + cm_net_send_json_async (cm_client_get_net (self->client), 0, NULL, + uri, SOUP_METHOD_GET, + query, cancellable, room_load_prev_batch_cb, task); +} + +GPtrArray * +cm_room_load_prev_batch_finish (CmRoom *self, + GAsyncResult *result, + GError **error) +{ + g_return_val_if_fail (CM_IS_ROOM (self), NULL); + g_return_val_if_fail (G_IS_TASK (result), NULL); + g_return_val_if_fail (!error || !*error, NULL); + + return g_task_propagate_pointer (G_TASK (result), error); +} + +static void +room_prev_batch_cb (GObject *object, + GAsyncResult *result, + gpointer user_data) +{ + CmRoom *self; + g_autoptr(GTask) task = user_data; + GPtrArray *events = NULL; + GError *error = NULL; + + g_assert (G_IS_TASK (task)); + + self = g_task_get_source_object (task); + g_assert (CM_IS_ROOM (self)); + + events = cm_room_load_prev_batch_finish (self, result, &error); + self->loading_past_events = FALSE; + g_debug ("(%p) Load prev batch %s, events: %d", self, + CM_LOG_SUCCESS (!error), events ? events->len : 0); + + if (error) + g_task_return_error (task, error); + else + g_task_return_boolean (task, !!events); +} + +static void +room_get_past_db_events_cb (GObject *object, + GAsyncResult *result, + gpointer user_data) +{ + CmRoom *self; + g_autoptr(GTask) task = user_data; + g_autoptr(GPtrArray) events = NULL; + g_autoptr(GError) error = NULL; + + g_assert (G_IS_TASK (task)); + + self = g_task_get_source_object (task); + g_assert (CM_IS_ROOM (self)); + + events = cm_db_get_past_events_finish (CM_DB (object), result, &error); + self->loading_past_events = FALSE; + g_debug ("(%p) Load db events %s, count: %d", self, + CM_LOG_SUCCESS (!error), events ? events->len : 0); + + if (events && events->len) + { + g_debug ("(%p) Loaded %u db events", self, events->len); + + cm_room_add_events (self, events, FALSE); + g_task_return_boolean (task, TRUE); + } + else if (self->prev_batch) + { + g_debug ("(%p) Load prev batch", self); + self->loading_past_events = TRUE; + cm_room_load_prev_batch_async (self, NULL, room_prev_batch_cb, + g_steal_pointer (&task)); + } + else + { + g_task_return_boolean (task, FALSE); + } +} + +static void +room_load_sync_cb (GObject *object, + GAsyncResult *result, + gpointer user_data) +{ + CmRoom *self; + g_autoptr(GTask) task = user_data; + GError *error = NULL; + GAsyncReadyCallback callback; + gpointer cb_user_data; + + g_assert (G_IS_TASK (task)); + + self = g_task_get_source_object (task); + g_assert (CM_IS_ROOM (self)); + + cm_room_load_finish (self, result, &error); + g_debug ("(%p) Initial sync before past events %s", + self, CM_LOG_SUCCESS (!error)); + + if (error) + { + g_task_return_error (task, error); + return; + } + + callback = g_object_get_data (G_OBJECT (task), "callback"); + cb_user_data = g_object_get_data (G_OBJECT (task), "cb-user-data"); + cm_room_load_past_events_async (self, callback, cb_user_data); +} + +void +cm_room_load_past_events_async (CmRoom *self, + GAsyncReadyCallback callback, + gpointer user_data) +{ + g_autoptr(CmEvent) from = NULL; + g_autoptr(GTask) task = NULL; + GListModel *events; + + g_return_if_fail (CM_IS_ROOM (self)); + g_return_if_fail (!from || CM_IS_ROOM_EVENT (from)); + + task = g_task_new (self, NULL, callback, user_data); + g_object_set_data (G_OBJECT (task), "callback", callback); + g_object_set_data (G_OBJECT (task), "cb-user-data", user_data); + g_debug ("(%p) Load db events", self); + + if (self->loading_initial_sync || self->loading_past_events) + { + g_debug ("(%p) Load db events, loading already in progress", self); + g_task_return_new_error (task, G_IO_ERROR, G_IO_ERROR_PENDING, + "Past events are being already loaded"); + return; + } + + if (!self->initial_sync_done) + { + g_debug ("(%p) Initial sync before loading past events", self); + cm_room_load_async (self, NULL, + room_load_sync_cb, + g_steal_pointer (&task)); + return; + } + + self->loading_past_events = TRUE; + + events = cm_room_event_list_get_events (self->room_event); + from = g_list_model_get_item (events, 0); + cm_db_get_past_events_async (cm_client_get_db (self->client), + self, from, + room_get_past_db_events_cb, + g_steal_pointer (&task)); +} + +gboolean +cm_room_load_past_events_finish (CmRoom *self, + GAsyncResult *result, + GError **error) +{ + g_return_val_if_fail (CM_IS_ROOM (self), FALSE); + g_return_val_if_fail (G_IS_TASK (result), FALSE); + g_return_val_if_fail (!error || !*error, FALSE); + + return g_task_propagate_boolean (G_TASK (result), error); +} + +static void +get_joined_members_cb (GObject *obj, + GAsyncResult *result, + gpointer user_data) +{ + CmRoom *self; + g_autoptr(GTask) task = user_data; + g_autoptr(JsonObject) object = NULL; + GError *error = NULL; + + g_assert (G_IS_TASK (task)); + + self = g_task_get_source_object (task); + object = g_task_propagate_pointer (G_TASK (result), &error); + g_debug ("(%p) Room load joined members %s", self, CM_LOG_SUCCESS (!error)); + + if (error) + { + self->joined_members_loading = FALSE; + if (!g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CANCELLED)) + g_debug ("Error getting room members: %s", error->message); + g_task_return_error (task, error); + } + else + { + g_autoptr(GList) members = NULL; + JsonObject *joined; + + joined = cm_utils_json_object_get_object (object, "joined"); + members = json_object_get_members (joined); + + for (GList *member = members; member; member = member->next) + { + g_autoptr(GRefString) user_id = NULL; + CmUser *user; + JsonObject *data; + + user_id = g_ref_string_new_intern (member->data); + user = g_hash_table_lookup (self->joined_members_table, user_id); + + if (!user) + room_find_user (self, user_id, TRUE); + + data = json_object_get_object_member (joined, member->data); + cm_user_set_json_data (user, data); + } + + /* We have to keep track of user changes only if the room is encrypted */ + if (cm_room_is_encrypted (self)) + { + CmDb *db; + + db = cm_client_get_db (self->client); + cm_db_mark_user_device_change (db, self->client, self->changed_users, TRUE, TRUE); + } + + g_debug ("(%p) Load joined members, count; %u", self, + g_list_model_get_n_items (G_LIST_MODEL (self->joined_members))); + self->joined_members_loaded = TRUE; + self->joined_members_loading = FALSE; + g_task_return_pointer (task, self->joined_members, NULL); + + /* TODO: handle failures */ + room_send_message_from_queue (self); + } +} + +void +cm_room_load_joined_members_async (CmRoom *self, + GCancellable *cancellable, + GAsyncReadyCallback callback, + gpointer user_data) +{ + g_autofree char *uri = NULL; + GTask *task; + + g_return_if_fail (CM_IS_ROOM (self)); + + task = g_task_new (self, cancellable, callback, user_data); + g_debug ("(%p) Load joined members", self); + + if (self->joined_members_loaded) + { + g_debug ("(%p) Load joined members, members already loaded", self); + g_task_return_boolean (task, TRUE); + return; + } + + if (self->joined_members_loading) + { + g_debug ("(%p) Load joined members, members already being loaded", self); + g_task_return_new_error (task, G_IO_ERROR, G_IO_ERROR_FAILED, + "Members list are already loading"); + return; + } + + self->joined_members_loading = TRUE; + + uri = g_strconcat ("/_matrix/client/r0/rooms/", self->room_id, "/joined_members", NULL); + cm_net_send_json_async (cm_client_get_net (self->client), -1, NULL, uri, SOUP_METHOD_GET, + NULL, cancellable, get_joined_members_cb, task); +} + +gboolean +cm_room_load_joined_members_finish (CmRoom *self, + GAsyncResult *result, + GError **error) +{ + g_return_val_if_fail (CM_IS_ROOM (self), FALSE); + g_return_val_if_fail (G_IS_TASK (result), FALSE); + g_return_val_if_fail (!error || !*error, FALSE); + + return g_task_propagate_boolean (G_TASK (result), error); +} + +static void +get_room_state_cb (GObject *object, + GAsyncResult *result, + gpointer user_data) +{ + g_autoptr(GTask) task = user_data; + g_autoptr(CmRoom) self = user_data; + g_autoptr(JsonObject) root = NULL; + GError *error = NULL; + JsonArray *array; + + g_assert (G_IS_TASK (task)); + + self = g_task_get_source_object (task); + g_assert (CM_IS_ROOM (self)); + + array = g_task_propagate_pointer (G_TASK (result), &error); + g_debug ("(%p) Load room initial sync %s", self, CM_LOG_SUCCESS (!error)); + self->loading_initial_sync = FALSE; + + if (error) + { + g_task_return_error (task, error); + return; + } + + root = json_object_new (); + json_object_set_array_member (root, "events", array); + cm_room_event_list_parse_events (self->room_event, root, NULL, FALSE); + self->initial_sync_done = TRUE; + + self->db_save_pending = TRUE; + cm_room_save (self); + g_task_return_boolean (task, TRUE); +} + +void +cm_room_load_async (CmRoom *self, + GCancellable *cancellable, + GAsyncReadyCallback callback, + gpointer user_data) +{ + g_autoptr(GTask) task = NULL; + g_autofree char *uri = NULL; + + task = g_task_new (self, cancellable, callback, user_data); + g_debug ("(%p) Load room initial sync", self); + + if (self->initial_sync_done) + { + g_debug ("(%p) Load room initial sync already done", self); + g_task_return_boolean (task, TRUE); + return; + } + + if (self->loading_initial_sync) + { + g_debug ("(%p) Load room initial sync already being loaded", self); + g_task_return_new_error (task, G_IO_ERROR, G_IO_ERROR_PENDING, + "room initial sync is already in progress"); + return; + } + + self->loading_initial_sync = TRUE; + uri = g_strconcat ("/_matrix/client/r0/rooms/", self->room_id, "/state", NULL); + cm_net_send_json_async (cm_client_get_net (self->client), 0, NULL, + uri, SOUP_METHOD_GET, + NULL, NULL, get_room_state_cb, + g_steal_pointer (&task)); +} + +gboolean +cm_room_load_finish (CmRoom *self, + GAsyncResult *result, + GError **error) +{ + g_return_val_if_fail (CM_IS_ROOM (self), FALSE); + g_return_val_if_fail (G_IS_TASK (result), FALSE); + g_return_val_if_fail (!error || !*error, FALSE); + + return g_task_propagate_boolean (G_TASK (result), error); +} + +static void +save_room_cb (GObject *object, + GAsyncResult *result, + gpointer user_data) +{ + g_autoptr(CmRoom) self = user_data; + g_autoptr(GError) error = NULL; + + if (!cm_db_save_room_finish (CM_DB (object), result, &error)) + { + cm_room_event_list_set_save_pending (self->room_event, TRUE); + self->db_save_pending = TRUE; + g_warning ("(%p) Saving room details error: %s", self, error->message); + } +} + +void +cm_room_save (CmRoom *self) +{ + g_return_if_fail (CM_IS_ROOM (self)); + + if (!self->db_save_pending) + return; + + self->db_save_pending = FALSE; + cm_room_event_list_set_save_pending (self->room_event, FALSE); + cm_db_save_room_async (cm_client_get_db (self->client), self->client, self, + save_room_cb, + g_object_ref (self)); +} + +CmUser * +cm_room_find_user (CmRoom *self, + GRefString *matrix_id, + gboolean add_if_missing) +{ + return room_find_user (self, matrix_id, add_if_missing); +} + +void +cm_room_update_user (CmRoom *self, + CmEvent *event) +{ + g_autoptr(JsonObject) child = NULL; + CmUserList *user_list; + CmUser *member = NULL; + GRefString *user_id; + CmStatus member_status; + + g_return_if_fail (CM_IS_ROOM (self)); + g_return_if_fail (CM_IS_EVENT (event)); + g_return_if_fail (cm_event_get_m_type (event) == CM_M_ROOM_MEMBER); + + member_status = cm_room_event_get_status (CM_ROOM_EVENT (event)); + + if (member_status == CM_STATUS_JOIN && + cm_event_get_sender_id (event) == + cm_client_get_user_id (self->client)) + return; + + child = cm_event_get_json (event); + g_return_if_fail (child); + + user_id = cm_room_event_get_room_member_id (CM_ROOM_EVENT (event)); + user_list = cm_client_get_user_list (self->client); + member = cm_user_list_find_user (user_list, user_id, TRUE); + cm_user_set_json_data (member, child); + + g_debug ("(%p) Updating user %p, status: %d", self, member, member_status); + + if (member_status == CM_STATUS_JOIN) + { + CmRoomMember *invite; + + invite = g_hash_table_lookup (self->invited_members_table, user_id); + if (invite) + { + g_hash_table_remove (self->invited_members_table, user_id); + cm_utils_remove_list_item (self->invited_members, invite); + } + + if (g_hash_table_contains (self->joined_members_table, user_id)) + { + CmRoomMember *cm_member; + + cm_member = g_hash_table_lookup (self->joined_members_table, user_id); + cm_user_set_json_data (CM_USER (cm_member), child); + + g_free (self->past_name); + self->past_name = g_steal_pointer (&self->generated_name); + g_object_notify_by_pspec (G_OBJECT (self), properties[PROP_NAME]); + return; + } + + g_list_store_append (self->joined_members, member); + g_hash_table_insert (self->joined_members_table, + g_ref_string_acquire (user_id), + g_object_ref (member)); + + /* Clear the name so that it will be regenerated when name is requested */ + g_free (self->past_name); + self->past_name = g_steal_pointer (&self->generated_name); + g_object_notify_by_pspec (G_OBJECT (self), properties[PROP_NAME]); + } + else if (member_status == CM_STATUS_INVITE) + { + if (g_hash_table_contains (self->invited_members_table, user_id)) + { + CmRoomMember *cm_member; + + cm_member = g_hash_table_lookup (self->invited_members_table, user_id); + cm_user_set_json_data (CM_USER (cm_member), child); + + /* Clear the name so that it will be regenerated when name is requested */ + g_free (self->past_name); + self->past_name = g_steal_pointer (&self->generated_name); + g_object_notify_by_pspec (G_OBJECT (self), properties[PROP_NAME]); + return; + } + + g_list_store_append (self->invited_members, member); + g_hash_table_insert (self->invited_members_table, + g_ref_string_acquire (user_id), + g_object_ref (member)); + self->db_save_pending = TRUE; + + /* Clear the name so that it will be regenerated when name is requested */ + g_free (self->past_name); + self->past_name = g_steal_pointer (&self->generated_name); + g_object_notify_by_pspec (G_OBJECT (self), properties[PROP_NAME]); + } + else if (member_status == CM_STATUS_LEAVE) + { + CmRoomMember *join; + + /* Generate a name if it doesn't exist so that we can + * use it as the past name if the new name is empty + */ + if (!self->name && !self->generated_name) + self->generated_name = cm_room_generate_name (self); + + join = g_hash_table_lookup (self->joined_members_table, user_id); + + if (join) + { + g_hash_table_remove (self->joined_members_table, user_id); + cm_utils_remove_list_item (self->joined_members, join); + } + + /* Clear the name so that it will be regenerated when name is requested */ + g_free (self->past_name); + self->past_name = g_steal_pointer (&self->generated_name); + g_object_notify_by_pspec (G_OBJECT (self), properties[PROP_NAME]); + self->db_save_pending = TRUE; + } +} diff --git a/subprojects/libcmatrix/src/cm-room.h b/subprojects/libcmatrix/src/cm-room.h new file mode 100644 index 0000000000000000000000000000000000000000..11caf397263cfe29daba9b41809cf6971efa84c7 --- /dev/null +++ b/subprojects/libcmatrix/src/cm-room.h @@ -0,0 +1,106 @@ +/* -*- mode: c; c-basic-offset: 2; indent-tabs-mode: nil; -*- */ +/* + * Copyright (C) 2022 Purism SPC + * + * SPDX-License-Identifier: LGPL-2.1-or-later + */ + +#pragma once + +#if !defined(_CMATRIX_TAKEN) && !defined(CMATRIX_COMPILATION) +# error "Only <cmatrix.h> can be included directly." +#endif + +#include <glib-object.h> +#include <gio/gio.h> + +G_BEGIN_DECLS + +#include "cm-client.h" +#include "cm-enums.h" +#include "events/cm-event.h" + +#define CM_TYPE_ROOM (cm_room_get_type ()) + +G_DECLARE_FINAL_TYPE (CmRoom, cm_room, CM, ROOM, GObject) + +const char *cm_room_get_id (CmRoom *self); +gboolean cm_room_self_has_power_for_event (CmRoom *self, + CmEventType type); +const char *cm_room_get_name (CmRoom *self); +const char *cm_room_get_past_name (CmRoom *self); +gboolean cm_room_is_encrypted (CmRoom *self); +GListModel *cm_room_get_joined_members (CmRoom *self); +GListModel *cm_room_get_events_list (CmRoom *self); +gint64 cm_room_get_unread_notification_counts (CmRoom *self); +void cm_room_accept_invite_async (CmRoom *self, + GCancellable *cancellable, + GAsyncReadyCallback callback, + gpointer user_data); +gboolean cm_room_accept_invite_finish (CmRoom *self, + GAsyncResult *result, + GError **error); +void cm_room_reject_invite_async (CmRoom *self, + GCancellable *cancellable, + GAsyncReadyCallback callback, + gpointer user_data); +gboolean cm_room_reject_invite_finish (CmRoom *self, + GAsyncResult *result, + GError **error); +const char *cm_room_send_text_async (CmRoom *self, + const char *text, + GCancellable *cancellable, + GAsyncReadyCallback callback, + gpointer user_data); +char *cm_room_send_text_finish (CmRoom *self, + GAsyncResult *result, + GError **error); +const char *cm_room_send_file_async (CmRoom *self, + GFile *file, + const char *body, + GFileProgressCallback progress_callback, + gpointer progress_user_data, + GCancellable *cancellable, + GAsyncReadyCallback callback, + gpointer user_data); +char *cm_room_send_file_finish (CmRoom *self, + GAsyncResult *result, + GError **error); +void cm_room_set_typing_notice_async (CmRoom *self, + gboolean typing, + GCancellable *cancellable, + GAsyncReadyCallback callback, + gpointer user_data); +gboolean cm_room_set_typing_notice_finish (CmRoom *self, + GAsyncResult *result, + GError **error); +void cm_room_enable_encryption_async (CmRoom *self, + GCancellable *cancellable, + GAsyncReadyCallback callback, + gpointer user_data); +gboolean cm_room_enable_encryption_finish (CmRoom *self, + GAsyncResult *result, + GError **error); +void cm_room_leave_async (CmRoom *self, + GCancellable *cancellable, + GAsyncReadyCallback callback, + gpointer user_data); +gboolean cm_room_leave_finish (CmRoom *self, + GAsyncResult *result, + GError **error); +void cm_room_set_read_marker_async (CmRoom *self, + CmEvent *fully_read_event, + CmEvent *read_receipt_event, + GAsyncReadyCallback callback, + gpointer user_data); +gboolean cm_room_set_read_marker_finish (CmRoom *self, + GAsyncResult *result, + GError **error); +void cm_room_load_past_events_async (CmRoom *self, + GAsyncReadyCallback callback, + gpointer user_data); +gboolean cm_room_load_past_events_finish (CmRoom *self, + GAsyncResult *result, + GError **error); +G_END_DECLS + diff --git a/subprojects/libcmatrix/src/cm-secret-store-private.h b/subprojects/libcmatrix/src/cm-secret-store-private.h new file mode 100644 index 0000000000000000000000000000000000000000..b731104665443b6195315c25660cb6d1aa252e76 --- /dev/null +++ b/subprojects/libcmatrix/src/cm-secret-store-private.h @@ -0,0 +1,46 @@ +/* -*- mode: c; c-basic-offset: 2; indent-tabs-mode: nil; -*- */ +/* cm-secret-store.h + * + * Copyright 2022 Purism SPC + * + * Author(s): + * Mohammed Sadiq <sadiq@sadiqpk.org> + * + * SPDX-License-Identifier: LGPL-2.1-or-later + */ + +#pragma once + +#include <glib.h> + +#include "cm-client.h" + +#define CM_USERNAME_ATTRIBUTE "username" +#define CM_SERVER_ATTRIBUTE "server" +#define CM_PROTOCOL_ATTRIBUTE "protocol" + +G_BEGIN_DECLS + +void cm_secret_store_save_async (CmClient *client, + char *access_token, + char *pickle_key, + GCancellable *cancellable, + GAsyncReadyCallback callback, + gpointer user_data); +gboolean cm_secret_store_save_finish (GAsyncResult *result, + GError **error); + +void cm_secret_store_load_async (GCancellable *cancellable, + GAsyncReadyCallback callback, + gpointer user_data); +GPtrArray *cm_secret_store_load_finish (GAsyncResult *result, + GError **error); + +void cm_secret_store_delete_async (CmClient *client, + GCancellable *cancellable, + GAsyncReadyCallback callback, + gpointer user_data); +gboolean cm_secret_store_delete_finish (GAsyncResult *result, + GError **error); + +G_END_DECLS diff --git a/subprojects/libcmatrix/src/cm-secret-store.c b/subprojects/libcmatrix/src/cm-secret-store.c new file mode 100644 index 0000000000000000000000000000000000000000..ef164bf4bdac15c0c9f5db39f8f6081c69e6b39f --- /dev/null +++ b/subprojects/libcmatrix/src/cm-secret-store.c @@ -0,0 +1,240 @@ +/* -*- mode: c; c-basic-offset: 2; indent-tabs-mode: nil; -*- */ +/* cm-secret-store.c + * + * Copyright 2022 Purism SPC + * + * Author(s): + * Mohammed Sadiq <sadiq@sadiqpk.org> + * + * SPDX-License-Identifier: LGPL-2.0-or-later + */ + +#define G_LOG_DOMAIN "cm-secret-store" + +#include <libsecret/secret.h> +#include <glib/gi18n.h> + +#include "cm-matrix-private.h" +#include "cm-utils-private.h" +#include "cm-secret-store-private.h" + +#define PROTOCOL_MATRIX_STR "matrix" + +static const SecretSchema * +secret_store_get_schema (void) +{ + static SecretSchema *schema; + static char *secret_id; + + if (schema) + return schema; + + if (!secret_id) + secret_id = g_strconcat (cm_matrix_get_app_id (), ".CMatrix", NULL); + + /** SECRET_SCHEMA_DONT_MATCH_NAME is used as a workaround for a bug in gnome-keyring + * which prevents cold keyrings from being searched (and hence does not prompt for unlocking) + * see https://gitlab.gnome.org/GNOME/gnome-keyring/-/issues/89 and + * https://gitlab.gnome.org/GNOME/libsecret/-/issues/7 for more information + */ + schema = secret_schema_new (secret_id, SECRET_SCHEMA_DONT_MATCH_NAME, + CM_USERNAME_ATTRIBUTE, SECRET_SCHEMA_ATTRIBUTE_STRING, + CM_SERVER_ATTRIBUTE, SECRET_SCHEMA_ATTRIBUTE_STRING, + CM_PROTOCOL_ATTRIBUTE, SECRET_SCHEMA_ATTRIBUTE_STRING, + NULL); + return schema; +} + +void +cm_secret_store_save_async (CmClient *client, + char *access_token, + char *pickle_key, + GCancellable *cancellable, + GAsyncReadyCallback callback, + gpointer user_data) +{ + const SecretSchema *schema; + g_autofree char *label = NULL; + const char *server, *old_pass, *username, *device_id; + char *password = NULL, *token = NULL, *key = NULL; + CmAccount *account; + char *credentials; + + g_return_if_fail (CM_IS_CLIENT (client)); + g_return_if_fail (!cancellable || G_IS_CANCELLABLE (cancellable)); + + old_pass = cm_client_get_password (client); + + if (old_pass && *old_pass) + password = g_strescape (old_pass, NULL); + if (access_token && *access_token) + token = g_strescape (access_token, NULL); + if (pickle_key && *pickle_key) + key = g_strescape (pickle_key, NULL); + + account = cm_client_get_account (client); + device_id = cm_client_get_device_id (client); + username = cm_account_get_login_id (account); + + if (!device_id) + device_id = ""; + + /* We don't use json APIs here so that we can manage memory better (and securely free them) */ + /* TODO: Use a non-pageable memory */ + /* XXX: We use a dumb string search, so don't change the order or spacing of the format string */ + credentials = g_strdup_printf ("{\"username\": \"%s\", \"password\": \"%s\"," + "\"access-token\": \"%s\", " + "\"pickle-key\": \"%s\", \"device-id\": \"%s\", \"enabled\": \"%s\"}", + cm_client_get_user_id (client) ?: "", + password ? password : "", token ? token : "", + key ? key : "", device_id, + cm_client_get_enabled (client) ? "true" : "false"); + schema = secret_store_get_schema (); + server = cm_client_get_homeserver (client); + + if (!server) + { + g_task_report_new_error (NULL, callback, user_data, NULL, + G_IO_ERROR, G_IO_ERROR_FAILED, + "Homeserver required to store to db"); + return; + } + + /* todo: translate the string */ + label = g_strdup_printf ("%s Matrix password for \"%s\"", + cm_matrix_get_app_id (), username); + + secret_password_store (schema, NULL, label, credentials, + cancellable, callback, user_data, + CM_USERNAME_ATTRIBUTE, username, + CM_SERVER_ATTRIBUTE, server, + CM_PROTOCOL_ATTRIBUTE, PROTOCOL_MATRIX_STR, + NULL); + + cm_utils_free_buffer (access_token); + cm_utils_free_buffer (credentials); + cm_utils_free_buffer (pickle_key); + cm_utils_free_buffer (password); + cm_utils_free_buffer (token); + cm_utils_free_buffer (key); +} + +gboolean +cm_secret_store_save_finish (GAsyncResult *result, + GError **error) +{ + g_return_val_if_fail (G_IS_ASYNC_RESULT (result), FALSE); + + return secret_password_store_finish (result, error); +} + +static void +secret_load_cb (GObject *object, + GAsyncResult *result, + gpointer user_data) +{ + g_autoptr(GTask) task = user_data; + GPtrArray *accounts = NULL; + GError *error = NULL; + GList *secrets; + + g_assert_true (G_IS_TASK (task)); + + secrets = secret_password_search_finish (result, &error); + + if (error) { + g_task_return_error (task, error); + return; + } + + for (GList *item = secrets; item; item = item->next) { + g_autofree char *label = NULL; + g_autofree char *expected = NULL; + + label = secret_retrievable_get_label (item->data); + expected = g_strconcat (cm_matrix_get_app_id (), " Matrix password", NULL); + + if (!label || !expected) + continue; + + if (!g_str_has_prefix (label, expected)) + continue; + + if (!accounts) + accounts = g_ptr_array_new_full (5, g_object_unref); + + if (item->data) + g_ptr_array_add (accounts, g_object_ref (item->data)); + } + + if (secrets) + g_list_free_full (secrets, g_object_unref); + + g_task_return_pointer (task, accounts, (GDestroyNotify)g_ptr_array_unref); +} + +void +cm_secret_store_load_async (GCancellable *cancellable, + GAsyncReadyCallback callback, + gpointer user_data) +{ + const SecretSchema *schema; + GTask *task; + + g_return_if_fail (!cancellable || G_IS_CANCELLABLE (cancellable)); + + schema = secret_store_get_schema (); + task = g_task_new (NULL, cancellable, callback, user_data); + + /** With using SECRET_SCHEMA_DONT_MATCH_NAME we need some other attribute + * (apart from the schema name itself) to use for the lookup. + * The protocol attribute seems like a reasonable choice. + */ + secret_password_search (schema, + SECRET_SEARCH_ALL | SECRET_SEARCH_UNLOCK | SECRET_SEARCH_LOAD_SECRETS, + cancellable, secret_load_cb, task, + CM_PROTOCOL_ATTRIBUTE, PROTOCOL_MATRIX_STR, + NULL); +} + +GPtrArray * +cm_secret_store_load_finish (GAsyncResult *result, + GError **error) +{ + g_return_val_if_fail (G_IS_ASYNC_RESULT (result), NULL); + + return g_task_propagate_pointer (G_TASK (result), error); +} + +void +cm_secret_store_delete_async (CmClient *client, + GCancellable *cancellable, + GAsyncReadyCallback callback, + gpointer user_data) +{ + const SecretSchema *schema; + const char *server, *username; + CmAccount *account; + + g_return_if_fail (!cancellable || G_IS_CANCELLABLE (cancellable)); + + account = cm_client_get_account (client); + username = cm_account_get_login_id (account); + + schema = secret_store_get_schema (); + server = cm_client_get_homeserver (client); + secret_password_clear (schema, cancellable, callback, user_data, + CM_USERNAME_ATTRIBUTE, username, + CM_SERVER_ATTRIBUTE, server, + CM_PROTOCOL_ATTRIBUTE, PROTOCOL_MATRIX_STR, + NULL); +} + +gboolean +cm_secret_store_delete_finish (GAsyncResult *result, + GError **error) +{ + g_return_val_if_fail (G_IS_ASYNC_RESULT (result), FALSE); + + return secret_password_clear_finish (result, error); +} diff --git a/subprojects/libcmatrix/src/cm-types.h b/subprojects/libcmatrix/src/cm-types.h new file mode 100644 index 0000000000000000000000000000000000000000..d0c1fd75366de08da2a38533aa110b23be737220 --- /dev/null +++ b/subprojects/libcmatrix/src/cm-types.h @@ -0,0 +1,38 @@ +/* -*- mode: c; c-basic-offset: 2; indent-tabs-mode: nil; -*- */ +/* cm-types.h + * + * Copyright 2022 Purism SPC + * + * Author(s): + * Mohammed Sadiq <sadiq@sadiqpk.org> + * + * SPDX-License-Identifier: LGPL-2.1-or-later + */ + +#pragma once + +#if !defined(_CMATRIX_TAKEN) && !defined(CMATRIX_COMPILATION) +# error "Only <cmatrix.h> can be included directly." +#endif + +#include <glib.h> + +G_BEGIN_DECLS + +typedef struct _CmUser CmUser; +typedef struct _CmAccount CmAccount; +typedef struct _CmRoomMember CmRoomMember; +typedef struct _CmClient CmClient; +typedef struct _CmRoom CmRoom; +typedef struct _CmEvent CmEvent; +typedef struct _CmRoomEvent CmRoomEvent; +typedef struct _CmRoomMessageEvent CmRoomMessageEvent; + +/* Private types */ +#ifdef CMATRIX_COMPILATION +typedef struct _CmDb CmDb; +typedef struct _CmOlm CmOlm; +typedef struct _CmUserList CmUserList; +#endif /* CMATRIX_COMPILATION */ + +G_END_DECLS diff --git a/subprojects/libcmatrix/src/cm-utils-private.h b/subprojects/libcmatrix/src/cm-utils-private.h new file mode 100644 index 0000000000000000000000000000000000000000..9ac43ea3a146a4aceff36801b03f52bb68989ae1 --- /dev/null +++ b/subprojects/libcmatrix/src/cm-utils-private.h @@ -0,0 +1,97 @@ +/* -*- mode: c; c-basic-offset: 2; indent-tabs-mode: nil; -*- */ +/* + * Copyright (C) 2022 Purism SPC + * + * SPDX-License-Identifier: LGPL-2.1-or-later + */ + +#ifdef HAVE_CONFIG_H +# include "config.h" +#endif + +#include <glib-object.h> +#include <json-glib/json-glib.h> +#include <libsoup/soup.h> + +#include "cm-enums.h" + +/* Hack to check format specifier arguments match */ +static inline void check_format (const char *fmt, ...) G_GNUC_PRINTF (1, 2); +static inline void check_format (const char *fmt, ...) {} + +#define CM_TRACE(fmt, ...) do { \ + check_format (fmt, ##__VA_ARGS__); \ + g_log_structured (G_LOG_DOMAIN, \ + (1 << G_LOG_LEVEL_USER_SHIFT), \ + "CODE_FILE", __FILE__, \ + "CODE_LINE", G_STRINGIFY (__LINE__), \ + "CODE_FUNC", G_STRFUNC, \ + "MESSAGE", fmt, ##__VA_ARGS__); \ +} while (0) +#define CM_LOG_SUCCESS(_value) cm_utils_log_bool_str (_value, TRUE) +#define CM_LOG_BOOL(_value) cm_utils_log_bool_str (_value, FALSE) + +const char *cm_utils_log_bool_str (gboolean value, + gboolean use_success); +const char *cm_utils_anonymize (GString *str, + const char *value); +GError *cm_utils_json_node_get_error (JsonNode *node); +gboolean cm_utils_get_item_position (GListModel *list, + gpointer item, + guint *position); +gboolean cm_utils_remove_list_item (GListStore *store, + gpointer item); +const char *cm_utils_get_event_type_str (CmEventType type); +char *cm_utils_json_object_to_string (JsonObject *object, + gboolean prettify); +GString *cm_utils_json_get_canonical (JsonObject *object, + GString *out); +JsonObject *cm_utils_string_to_json_object (const char *json_str); +gboolean cm_utils_json_object_has_member (JsonObject *object, + const char *member); +gint64 cm_utils_json_object_get_int (JsonObject *object, + const char *member); +gboolean cm_utils_json_object_get_bool (JsonObject *object, + const char *member); +const char *cm_utils_json_object_get_string (JsonObject *object, + const char *member); +char *cm_utils_json_object_dup_string (JsonObject *object, + const char *member); +JsonObject *cm_utils_json_object_get_object (JsonObject *object, + const char *member); +JsonArray *cm_utils_json_object_get_array (JsonObject *object, + const char *member); +void cm_utils_clear (char *buffer, + size_t length); +void cm_utils_free_buffer (char *buffer); +const char *cm_utils_get_url_from_user_id (const char *user_id); +gboolean cm_utils_user_name_valid (const char *matrix_user_id); +gboolean cm_utils_user_name_is_email (const char *user_id); +gboolean cm_utils_mobile_is_valid (const char *mobile_num); + +gboolean cm_utils_home_server_valid (const char *homeserver); +void cm_utils_read_uri_async (const char *uri, + guint timeout, + GCancellable *cancellable, + GAsyncReadyCallback callback, + gpointer user_data); +gpointer cm_utils_read_uri_finish (GAsyncResult *result, + GError **error); +void cm_utils_get_homeserver_async (const char *username, + guint timeout, + GCancellable *cancellable, + GAsyncReadyCallback callback, + gpointer user_data); +char *cm_utils_get_homeserver_finish (GAsyncResult *result, + GError **error); +void cm_utils_verify_homeserver_async (const char *server, + guint timeout, + GCancellable *cancellable, + GAsyncReadyCallback callback, + gpointer user_data); +gboolean cm_utils_verify_homeserver_finish (GAsyncResult *result, + GError **error); +char *cm_utils_get_path_for_m_type (const char *base_path, + CmEventType type, + gboolean thumbnail, + const char *file_name); diff --git a/subprojects/libcmatrix/src/cm-utils.c b/subprojects/libcmatrix/src/cm-utils.c new file mode 100644 index 0000000000000000000000000000000000000000..b27e26a5f878e1d6e612a074d355a0070487d211 --- /dev/null +++ b/subprojects/libcmatrix/src/cm-utils.c @@ -0,0 +1,1312 @@ +/* -*- mode: c; c-basic-offset: 2; indent-tabs-mode: nil; -*- */ +/* + * Copyright (C) 2022 Purism SPC + * + * SPDX-License-Identifier: LGPL-2.1-or-later + */ + +#define G_LOG_DOMAIN "cm-utils" +#define BUFFER_SIZE 256 + +#ifdef HAVE_CONFIG_H +# include "config.h" +#endif + +#define __STDC_WANT_LIB_EXT1__ 1 +#include <stdio.h> +#include <string.h> +#include <libsoup/soup.h> +#include <json-glib/json-glib.h> + +#include "cm-common.h" +#include "cm-enums.h" +#include "cm-utils-private.h" + +static const char *error_codes[] = { + "", /* Index 0 is reserved for no error */ + "M_FORBIDDEN", + "M_UNKNOWN_TOKEN", + "M_MISSING_TOKEN", + "M_BAD_JSON", + "M_NOT_JSON", + "M_NOT_FOUND", + "M_LIMIT_EXCEEDED", + "M_UNKNOWN", + "M_UNRECOGNIZED", + "M_UNAUTHORIZED", + "M_USER_DEACTIVATED", + "M_USER_IN_USE", + "M_INVALID_USERNAME", + "M_ROOM_IN_USE", + "M_INVALID_ROOM_STATE", + "M_THREEPID_IN_USE", + "M_THREEPID_NOT_FOUND", + "M_THREEPID_AUTH_FAILED", + "M_THREEPID_DENIED", + "M_SERVER_NOT_TRUSTED", + "M_UNSUPPORTED_ROOM_VERSION", + "M_INCOMPATIBLE_ROOM_VERSION", + "M_BAD_STATE", + "M_GUEST_ACCESS_FORBIDDEN", + "M_CAPTCHA_NEEDED", + "M_CAPTCHA_INVALID", + "M_MISSING_PARAM", + "M_INVALID_PARAM", + "M_TOO_LARGE", + "M_EXCLUSIVE", + "M_RESOURCE_LIMIT_EXCEEDED", + "M_CANNOT_LEAVE_SERVER_NOTICE_ROOM", +}; + +const char * +cm_utils_log_bool_str (gboolean value, + gboolean use_success) +{ + if (!g_log_writer_supports_color (fileno (stdout)) || + g_log_writer_is_journald (fileno (stderr))) + { + if (value) + return use_success ? "success" : "true"; + else + return use_success ? "fail" : "false"; + } + + if (value) + { + if (use_success) + return "\033[1;32m" "success" "\033[0m"; + else + return "\033[1;32m" "true" "\033[0m"; + } + else + { + if (use_success) + return "\033[1;31m" "fail" "\033[0m"; + else + return "\033[1;31m" "false" "\033[0m"; + } +} + +const char * +cm_utils_anonymize (GString *str, + const char *value) +{ + gunichar c, next_c, prev_c; + + g_assert (str); + + if (!value || !*value) + return str->str; + + if (str->len && str->str[str->len - 1] != ' ') + g_string_append_c (str, ' '); + + if (!g_utf8_validate (value, -1, NULL)) + { + g_string_append (str, "******"); + return str->str; + } + + if (*value == '!' || *value == '@' || *value == '+') + { + c = g_utf8_get_char (value); + value = g_utf8_next_char (value); + g_string_append_unichar (str, c); + } + + if (!*value) + return str->str; + + c = g_utf8_get_char (value); + value = g_utf8_next_char (value); + g_string_append_unichar (str, c); + + if (!*value) + return str->str; + + c = g_utf8_get_char (value); + value = g_utf8_next_char (value); + g_string_append_unichar (str, c); + + while (*value) + { + prev_c = c; + c = g_utf8_get_char (value); + + value = g_utf8_next_char (value); + next_c = g_utf8_get_char (value); + + if (!g_unichar_isalnum (c)) + g_string_append_unichar (str, c); + else if (!g_unichar_isalnum (prev_c) || !g_unichar_isalnum (next_c)) + g_string_append_unichar (str, c); + else + g_string_append_c (str, '#'); + } + + return str->str; +} + +GError * +cm_utils_json_node_get_error (JsonNode *node) +{ + JsonObject *object = NULL; + const char *error, *err_code; + + if (!node || (!JSON_NODE_HOLDS_OBJECT (node) && !JSON_NODE_HOLDS_ARRAY (node))) + return g_error_new (CM_ERROR, CM_ERROR_NOT_JSON, + "Not JSON Object"); + + /* Returned by /_matrix/client/r0/rooms/{roomId}/state */ + if (JSON_NODE_HOLDS_ARRAY (node)) + return NULL; + + object = json_node_get_object (node); + err_code = cm_utils_json_object_get_string (object, "errcode"); + + if (!err_code) + return NULL; + + error = cm_utils_json_object_get_string (object, "error"); + + if (!error) + error = "Unknown Error"; + + if (!g_str_has_prefix (err_code, "M_")) + return g_error_new (CM_ERROR, CM_ERROR_UNKNOWN, + "Invalid Error code"); + + for (guint i = 0; i < G_N_ELEMENTS (error_codes); i++) + if (g_str_equal (error_codes[i], err_code)) + return g_error_new (CM_ERROR, i, "%s", error); + + return g_error_new (CM_ERROR, CM_ERROR_UNKNOWN, "Unknown Error"); +} + +gboolean +cm_utils_get_item_position (GListModel *list, + gpointer item, + guint *position) +{ + guint n_items; + + g_return_val_if_fail (G_IS_LIST_MODEL (list), FALSE); + g_return_val_if_fail (item != NULL, FALSE); + + n_items = g_list_model_get_n_items (list); + + for (guint i = 0; i < n_items; i++) + { + g_autoptr(GObject) object = NULL; + + object = g_list_model_get_item (list, i); + + if (object == item) + { + if (position) + *position = i; + + return TRUE; + } + } + + return FALSE; +} + +gboolean +cm_utils_remove_list_item (GListStore *store, + gpointer item) +{ + GListModel *model; + guint n_items; + + g_return_val_if_fail (G_IS_LIST_STORE (store), FALSE); + + if (!item) + return FALSE; + + model = G_LIST_MODEL (store); + n_items = g_list_model_get_n_items (model); + + for (guint i = 0; i < n_items; i++) + { + g_autoptr(GObject) object = NULL; + + object = g_list_model_get_item (model, i); + + if (object == item) + { + g_list_store_remove (store, i); + + return TRUE; + } + } + + return FALSE; +} + +const char * +cm_utils_get_event_type_str (CmEventType type) +{ + switch (type) + { + case CM_M_CALL_ANSWER: + return "m.call.answer"; + + case CM_M_CALL_ASSERTED_IDENTITY: + return "m.call.asserted_identity"; + + case CM_M_CALL_ASSERTED_IDENTITY_PREFIX: + return "org.matrix.call.asserted_identity"; + + case CM_M_CALL_CANDIDATES: + return "m.call.candidates"; + + case CM_M_CALL_HANGUP: + return "m.call.hangup"; + + case CM_M_CALL_INVITE: + return "m.call.invite"; + + case CM_M_CALL_NEGOTIATE: + return "m.call.negotiate"; + + case CM_M_CALL_REJECT: + return "m.call.reject"; + + case CM_M_CALL_REPLACES: + return "m.call.replaces"; + + case CM_M_CALL_SELECT_ANSWER: + return "m.call.select_answer"; + + case CM_M_DIRECT: + return "m.direct"; + + case CM_M_DUMMY: + return "m.dummy"; + + case CM_M_FORWARDED_ROOM_KEY: + return "m.forwarded_room_key"; + + case CM_M_FULLY_READ: + return "m.fully_read"; + + case CM_M_IGNORED_USER_LIST: + return "m.ignored_user_list"; + + case CM_M_KEY_VERIFICATION_ACCEPT: + return "m.key.verification_accept"; + + case CM_M_KEY_VERIFICATION_CANCEL: + return "m.key.verification.cancel"; + + case CM_M_KEY_VERIFICATION_DONE: + return "m.key.verification.done"; + + case CM_M_KEY_VERIFICATION_KEY: + return "m.key.verification.key"; + + case CM_M_KEY_VERIFICATION_MAC: + return "m.key.verification.mac"; + + case CM_M_KEY_VERIFICATION_READY: + return "m.key.verification.ready"; + + case CM_M_KEY_VERIFICATION_REQUEST: + return "m.key.verification.request"; + + case CM_M_KEY_VERIFICATION_START: + return "m.key.verification.start"; + + case CM_M_PRESENCE: + return "m.presence"; + + case CM_M_PUSH_RULES: + return "m.push_rules"; + + case CM_M_REACTION: + return "m.reaction"; + + case CM_M_RECEIPT: + return "m.receipt"; + + case CM_M_ROOM_ALIASES: + return "m.room.aliases"; + + case CM_M_ROOM_AVATAR: + return "m.room.avatar"; + + case CM_M_ROOM_BOT_OPTIONS: + return "m.room.bot.options"; + + case CM_M_ROOM_CANONICAL_ALIAS: + return "m.room.canonical_alias"; + + case CM_M_ROOM_CREATE: + return "m.room.create"; + + case CM_M_ROOM_ENCRYPTED: + return "m.room.encrypted"; + + case CM_M_ROOM_ENCRYPTION: + return "m.room.encryption"; + + case CM_M_ROOM_GUEST_ACCESS: + return "m.room.guest_access"; + + case CM_M_ROOM_HISTORY_VISIBILITY: + return "m.room.history_visibility"; + + case CM_M_ROOM_JOIN_RULES: + return "m.room.join_rules"; + + case CM_M_ROOM_KEY: + return "m.room_key"; + + case CM_M_ROOM_KEY_REQUEST: + return "m.room_key.request"; + + case CM_M_ROOM_MEMBER: + return "m.room.member"; + + case CM_M_ROOM_MESSAGE: + return "m.room.message"; + + case CM_M_ROOM_MESSAGE_FEEDBACK: + return "m.room.message.feedback"; + + case CM_M_ROOM_NAME: + return "m.room.name"; + + case CM_M_ROOM_PINNED_EVENTS: + return "m.room.pinned_events"; + + case CM_M_ROOM_PLUMBING: + return "m.room.plumbing"; + + case CM_M_ROOM_POWER_LEVELS: + return "m.room.power_levels"; + + case CM_M_ROOM_REDACTION: + return "m.room.redaction"; + + case CM_M_ROOM_RELATED_GROUPS: + return "m.room.related_groups"; + + case CM_M_ROOM_SERVER_ACL: + return "m.room.server_acl"; + + case CM_M_ROOM_THIRD_PARTY_INVITE: + return "m.room.third_party_invite"; + + case CM_M_ROOM_TOMBSTONE: + return "m.room.tombstone"; + + case CM_M_ROOM_TOPIC: + return "m.room.topic"; + + case CM_M_SECRET_REQUEST: + return "m.secret.request"; + + case CM_M_SECRET_SEND: + return "m.secret.send"; + + case CM_M_SECRET_STORAGE_DEFAULT_KEY: + return "m.secret_storage.default_key"; + + case CM_M_SPACE_CHILD: + return "m.space.child"; + + case CM_M_SPACE_PARENT: + return "m.space.parent"; + + case CM_M_STICKER: + return "m.sticker"; + + case CM_M_TAG: + return "m.tag"; + + case CM_M_TYPING: + return "m.typing"; + + case CM_M_UNKNOWN: + case CM_M_USER_STATUS: + case CM_M_ROOM_INVITE: + case CM_M_ROOM_BAN: + case CM_M_ROOM_KICK: + default: + g_return_val_if_reached (NULL); + } + + return NULL; +} + +void +cm_utils_clear (char *buffer, + size_t length) +{ + if (!buffer || length == 0) + return; + + /* Note: we are not comparing with -1 */ + if (length == (size_t)-1) + length = strlen (buffer); + +#ifdef __STDC_LIB_EXT1__ + memset_s (buffer, length, 0xAD, length); +#elif HAVE_EXPLICIT_BZERO + explicit_bzero (buffer, length); +#else + { + volatile char *end = buffer + length; + + /* Set something nonzero, so it'll likely crash on reuse */ + while (buffer != end) + *(buffer++) = 0xAD; + } +#endif +} + +void +cm_utils_free_buffer (char *buffer) +{ + cm_utils_clear (buffer, -1); + g_free (buffer); +} + +const char * +cm_utils_get_url_from_user_id (const char *user_id) +{ + if (!cm_utils_user_name_valid (user_id)) + return NULL; + + /* Return the string after ‘:’ */ + return strchr (user_id, ':') + 1; +} + +/* https://spec.matrix.org/v1.2/appendices/#user-identifiers */ +/* domain part is validated separately, so the regex is not complete */ +#define MATRIX_USER_ID_RE "^@[A-Z0-9.=_-]+:[A-Z0-9.-]+$" + +gboolean +cm_utils_user_name_valid (const char *matrix_user_id) +{ + const char *url_start; + + if (!matrix_user_id || !*matrix_user_id) + return FALSE; + + /* GRegex is deprecated, but we can change this anytime: + * https://gitlab.gnome.org/GNOME/glib/-/merge_requests/1451 + * https://gitlab.gnome.org/GNOME/glib/-/merge_requests/2529 + */ + if (!g_regex_match_simple (MATRIX_USER_ID_RE, matrix_user_id, G_REGEX_CASELESS, 0)) + return FALSE; + + url_start = strchr (matrix_user_id, ':') + 1; + + if (!cm_utils_home_server_valid (url_start)) + return FALSE; + + if (G_UNLIKELY (strlen (matrix_user_id) > 255)) + return FALSE; + + return TRUE; +} +#undef MATRIX_USER_ID_RE + +#define EMAIL_RE "^[[:alnum:]._%+-]+@[A-Z0-9.-]+\\.[A-Z]{2,}$" + +gboolean +cm_utils_user_name_is_email (const char *user_id) +{ + if (!user_id || !*user_id) + return FALSE; + + if (g_regex_match_simple (EMAIL_RE, user_id, G_REGEX_CASELESS, 0)) + return TRUE; + + return FALSE; +} +#undef EMAIL_RE + +/* Rough estimate for an E.164 number */ +#define MOBILE_RE "^\\+[0-9]{10,15}$" + +gboolean +cm_utils_mobile_is_valid (const char *mobile_num) +{ + if (!mobile_num || !*mobile_num) + return FALSE; + + if (g_regex_match_simple (MOBILE_RE, mobile_num, 0, 0)) + return TRUE; + + return FALSE; +} +#undef MOBILE_RE + +gboolean +cm_utils_home_server_valid (const char *homeserver) +{ + gboolean valid = FALSE; + + if (homeserver && !*homeserver) + homeserver = NULL; + + if (homeserver) + { + g_autofree char *server = NULL; + g_autoptr(GUri) uri = NULL; + const char *scheme = NULL; + const char *path = NULL; + const char *host = NULL; + + if (!strstr (homeserver, "//")) + server = g_strconcat ("https://", homeserver, NULL); + + uri = g_uri_parse (server ?: homeserver, G_URI_FLAGS_NONE, NULL); + + if (uri) + { + scheme = g_uri_get_scheme (uri); + path = g_uri_get_path (uri); + host = g_uri_get_host (uri); + } + + valid = scheme && *scheme; + valid = valid && (g_str_equal (scheme, "http") || g_str_equal (scheme, "https")); + valid = valid && host && *host; + valid = valid && !g_str_has_suffix (host, "."); + valid = valid && (!path || !*path || g_str_equal (path, "/")); + } + + return valid; +} + +char * +cm_utils_json_object_to_string (JsonObject *object, + gboolean prettify) +{ + g_autoptr(JsonNode) node = NULL; + + g_return_val_if_fail (object, NULL); + + node = json_node_new (JSON_NODE_OBJECT); + json_node_init_object (node, object); + + return json_to_string (node, !!prettify); +} + +static void utils_json_canonical_array (JsonArray *array, + GString *out); +static void +utils_handle_node (JsonNode *node, + GString *out) +{ + GType type; + + g_assert (node); + g_assert (out); + + type = json_node_get_value_type (node); + + if (type == JSON_TYPE_OBJECT) + cm_utils_json_get_canonical (json_node_get_object (node), out); + else if (type == JSON_TYPE_ARRAY) + utils_json_canonical_array (json_node_get_array (node), out); + else if (type == G_TYPE_INVALID) + g_string_append (out, "null"); + else if (type == G_TYPE_STRING) + g_string_append_printf (out, "\"%s\"", json_node_get_string (node)); + else if (type == G_TYPE_INT64) + g_string_append_printf (out, "%" G_GINT64_FORMAT, json_node_get_int (node)); + else if (type == G_TYPE_DOUBLE) + g_string_append_printf (out, "%f", json_node_get_double (node)); + else if (type == G_TYPE_BOOLEAN) + g_string_append (out, json_node_get_boolean (node) ? "true" : "false"); + else + g_return_if_reached (); +} + +static void +utils_json_canonical_array (JsonArray *array, + GString *out) +{ + g_autoptr(GList) elements = NULL; + + g_assert (array); + g_assert (out); + + g_string_append_c (out, '['); + elements = json_array_get_elements (array); + + /* The order of array members shouldn’t be changed */ + for (GList *item = elements; item; item = item->next) +{ + utils_handle_node (item->data, out); + + if (item->next) + g_string_append_c (out, ','); + } + + g_string_append_c (out, ']'); +} + +GString * +cm_utils_json_get_canonical (JsonObject *object, + GString *out) +{ + JsonNode *signatures, *non_signed; + + g_autoptr(GList) members = NULL; + + g_return_val_if_fail (object, NULL); + + if (!out) + out = g_string_sized_new (BUFFER_SIZE); + + signatures = json_object_dup_member (object, "signatures"); + non_signed = json_object_dup_member (object, "unsigned"); + + /* Remove the non signed members before verification */ + json_object_remove_member (object, "signatures"); + json_object_remove_member (object, "unsigned"); + + g_string_append_c (out, '{'); + + members = json_object_get_members (object); + members = g_list_sort (members, (GCompareFunc)g_strcmp0); + + for (GList *item = members; item; item = item->next) +{ + JsonNode *node; + + g_string_append_printf (out, "\"%s\":", (char *)item->data); + + node = json_object_get_member (object, item->data); + utils_handle_node (node, out); + + if (item->next) + g_string_append_c (out, ','); + } + + g_string_append_c (out, '}'); + + /* Revert the changes we made to the JSON object */ + if (signatures) + json_object_set_member (object, "signatures", signatures); + if (non_signed) + json_object_set_member (object, "unsigned", non_signed); + + return out; +} + +JsonObject * +cm_utils_string_to_json_object (const char *json_str) +{ + g_autoptr(JsonParser) parser = NULL; + JsonNode *node; + + if (!json_str || !*json_str) + return NULL; + + parser = json_parser_new (); + if (!json_parser_load_from_data (parser, json_str, -1, NULL)) + return NULL; + + node = json_parser_get_root (parser); + + if (!JSON_NODE_HOLDS_OBJECT (node)) + return NULL; + + return json_node_dup_object (node); +} + +gboolean +cm_utils_json_object_has_member (JsonObject *object, + const char *member) +{ + JsonNode *node; + + if (!object || !member || !*member) + return 0; + + node = json_object_get_member (object, member); + + return !!node; +} + +gint64 +cm_utils_json_object_get_int (JsonObject *object, + const char *member) +{ + JsonNode *node; + + if (!object || !member || !*member) + return 0; + + node = json_object_get_member (object, member); + + if (node && JSON_NODE_HOLDS_VALUE (node)) + return json_node_get_int (node); + + return 0; +} + +gboolean +cm_utils_json_object_get_bool (JsonObject *object, + const char *member) +{ + JsonNode *node; + + if (!object || !member || !*member) + return FALSE; + + node = json_object_get_member (object, member); + + if (node && JSON_NODE_HOLDS_VALUE (node)) + return json_node_get_boolean (node); + + return FALSE; +} + +const char * +cm_utils_json_object_get_string (JsonObject *object, + const char *member) +{ + JsonNode *node; + + if (!object || !member || !*member) + return NULL; + + node = json_object_get_member (object, member); + + if (node && JSON_NODE_HOLDS_VALUE (node)) + return json_node_get_string (node); + + return NULL; +} + +char * +cm_utils_json_object_dup_string (JsonObject *object, + const char *member) +{ + const char *str; + + str = cm_utils_json_object_get_string (object, member); + + return g_strdup (str); +} + +JsonObject * +cm_utils_json_object_get_object (JsonObject *object, + const char *member) +{ + JsonNode *node; + + if (!object || !member || !*member) + return NULL; + + node = json_object_get_member (object, member); + + if (node && JSON_NODE_HOLDS_OBJECT (node)) + return json_node_get_object (node); + + return NULL; +} + +JsonArray * +cm_utils_json_object_get_array (JsonObject *object, + const char *member) +{ + JsonNode *node; + + if (!object || !member || !*member) + return NULL; + + node = json_object_get_member (object, member); + + if (node && JSON_NODE_HOLDS_ARRAY (node)) + return json_node_get_array (node); + + return NULL; +} + +static void +load_from_stream_cb (JsonParser *parser, + GAsyncResult *result, + gpointer user_data) +{ + g_autoptr(GTask) task = user_data; + GError *error = NULL; + gboolean timeout; + + g_assert (JSON_IS_PARSER (parser)); + g_assert (G_IS_TASK (task)); + + timeout = GPOINTER_TO_INT (g_task_get_task_data (task)); + + /* Task return is handled somewhere else */ + if (timeout) + return; + + if (json_parser_load_from_stream_finish (parser, result, &error)) + g_task_return_pointer (task, json_parser_steal_root (parser), + (GDestroyNotify)json_node_unref); + else + g_task_return_error (task, error); +} + +static gboolean +cancel_read_uri (gpointer user_data) +{ + g_autoptr(GTask) task = user_data; + + g_assert (G_IS_TASK (task)); + + g_object_set_data (G_OBJECT (task), "timeout-id", 0); + + /* XXX: Not thread safe? */ + if (g_task_get_completed (task) || g_task_had_error (task)) + return G_SOURCE_REMOVE; + + g_task_set_task_data (task, GINT_TO_POINTER (TRUE), NULL); + g_task_return_new_error (task, G_IO_ERROR, G_IO_ERROR_TIMED_OUT, + "Request timeout"); + g_cancellable_cancel (g_task_get_cancellable (task)); + + return G_SOURCE_REMOVE; +} + +static void +uri_file_read_cb (GObject *object, + GAsyncResult *result, + gpointer user_data) +{ + SoupSession *session = (SoupSession *)object; + g_autoptr(GTask) task = user_data; + g_autoptr(GInputStream) stream = NULL; + g_autoptr(JsonParser) parser = NULL; + GCancellable *cancellable; + SoupMessage *message; + GError *error = NULL; + gboolean has_timeout; + GTlsCertificateFlags err_flags; + + g_assert (G_IS_TASK (task)); + g_assert (SOUP_IS_SESSION (session)); + + stream = soup_session_send_finish (session, result, &error); + message = g_object_get_data (G_OBJECT (task), "message"); + has_timeout = GPOINTER_TO_INT (g_task_get_task_data (task)); + + /* Task return is handled somewhere else */ + if (has_timeout) + return; + + if (error) +{ + g_task_return_error (task, error); + return; + } + +#if SOUP_MAJOR_VERSION == 2 + soup_message_get_https_status (message, NULL, &err_flags); +#else + err_flags = soup_message_get_tls_peer_certificate_errors (message); +#endif + + if (message && err_flags) +{ + guint timeout_id, timeout; + + timeout = GPOINTER_TO_UINT (g_object_get_data (G_OBJECT (task), "timeout")); + timeout_id = GPOINTER_TO_UINT (g_object_get_data (G_OBJECT (task), "timeout-id")); + g_clear_handle_id (&timeout_id, g_source_remove); + g_object_unref (task); + + /* fixme: handle SSL errors */ + /* if (cm_utils_handle_ssl_error (message)) */ + /* { */ + /* g_task_return_new_error (task, G_IO_ERROR, G_IO_ERROR_CANCELLED, */ + /* "Cancelled"); */ + /* return; */ + /* } */ + + timeout_id = g_timeout_add_seconds (timeout, cancel_read_uri, g_object_ref (task)); + g_object_set_data (G_OBJECT (task), "timeout-id", GUINT_TO_POINTER (timeout_id)); + } + + cancellable = g_task_get_cancellable (task); + parser = json_parser_new (); + json_parser_load_from_stream_async (parser, stream, cancellable, + (GAsyncReadyCallback)load_from_stream_cb, + g_steal_pointer (&task)); +} + +static void +message_network_event_cb (SoupMessage *msg, + GSocketClientEvent event, + GIOStream *connection, + gpointer user_data) +{ + GSocketAddress *address; + + /* We shall have a non %NULL @connection by %G_SOCKET_CLIENT_CONNECTING event */ + if (event != G_SOCKET_CLIENT_CONNECTING) + return; + + /* @connection is a #GSocketConnection */ + address = g_socket_connection_get_remote_address (G_SOCKET_CONNECTION (connection), NULL); + g_object_set_data_full (user_data, "address", address, g_object_unref); +} + +#if SOUP_MAJOR_VERSION == 3 +static gboolean +accept_certificate_callback (SoupMessage *msg, + GTlsCertificate *certificate, + GTlsCertificateFlags tls_errors, + gpointer user_data) +{ + /* Returning TRUE trusts it anyway. */ + return TRUE; +} +#endif + +void +cm_utils_read_uri_async (const char *uri, + guint timeout, + GCancellable *cancellable, + GAsyncReadyCallback callback, + gpointer user_data) +{ + g_autoptr(SoupSession) session = NULL; + g_autoptr(SoupMessage) message = NULL; + g_autoptr(GCancellable) cancel = NULL; + g_autoptr(GTask) task = NULL; + guint timeout_id; + + g_return_if_fail (uri && *uri); + g_return_if_fail (!cancellable || G_IS_CANCELLABLE (cancellable)); + + if (cancellable) + cancel = g_object_ref (cancellable); + else + cancel = g_cancellable_new (); + + task = g_task_new (NULL, cancel, callback, user_data); + /* if this changes to TRUE, we consider it has been timeout */ + g_task_set_task_data (task, GINT_TO_POINTER (FALSE), NULL); + g_task_set_source_tag (task, cm_utils_read_uri_async); + + timeout = CLAMP (timeout, 5, 60); + timeout_id = g_timeout_add_seconds (timeout, cancel_read_uri, g_object_ref (task)); + g_object_set_data (G_OBJECT (task), "timeout", GUINT_TO_POINTER (timeout)); + g_object_set_data (G_OBJECT (task), "timeout-id", GUINT_TO_POINTER (timeout_id)); + + message = soup_message_new (SOUP_METHOD_GET, uri); + if (!message) +{ + g_task_return_new_error (task, G_IO_ERROR, G_IO_ERROR_INVALID_FILENAME, + "%s is not a valid uri", uri); + return; + } + + soup_message_set_flags (message, SOUP_MESSAGE_NO_REDIRECT); + g_object_set_data_full (G_OBJECT (task), "message", g_object_ref (message), g_object_unref); + + g_signal_connect_object (message, "network-event", + G_CALLBACK (message_network_event_cb), task, + G_CONNECT_AFTER); + session = soup_session_new (); +#if SOUP_MAJOR_VERSION == 2 + g_object_set (G_OBJECT (session), SOUP_SESSION_SSL_STRICT, FALSE, NULL); + + soup_session_send_async (session, message, cancel, + uri_file_read_cb, + g_steal_pointer (&task)); +#else + /* Accept invalid certificates */ + g_signal_connect (message, "accept-certificate", G_CALLBACK (accept_certificate_callback), NULL); + + soup_session_send_async (session, message, 0, cancel, + uri_file_read_cb, + g_steal_pointer (&task)); +#endif +} + +gpointer +cm_utils_read_uri_finish (GAsyncResult *result, + GError **error) +{ + GTask *task = (GTask *)result; + + g_return_val_if_fail (G_IS_TASK (result), NULL); + g_return_val_if_fail (g_task_get_source_tag (task) == cm_utils_read_uri_async, NULL); + + return g_task_propagate_pointer (G_TASK (result), error); +} + +static void +get_homeserver_cb (GObject *obj, + GAsyncResult *result, + gpointer user_data) +{ + g_autoptr(GTask) task = user_data; + g_autoptr(JsonNode) root = NULL; + JsonObject *object = NULL; + const char *homeserver = NULL; + GError *error = NULL; + + g_assert (G_IS_TASK (task)); + + root = cm_utils_read_uri_finish (result, &error); + + if (!root) +{ + g_task_return_error (task, error); + return; + } + + g_object_set_data_full (G_OBJECT (task), "address", + g_object_steal_data (G_OBJECT (result), "address"), + g_object_unref); + + if (JSON_NODE_HOLDS_OBJECT (root)) + object = json_node_get_object (root); + + if (object) + object = cm_utils_json_object_get_object (object, "m.homeserver"); + + if (object) + homeserver = cm_utils_json_object_get_string (object, "base_url"); + + if (!homeserver) + g_task_return_new_error (task, G_IO_ERROR, G_IO_ERROR_INVALID_DATA, + "Got invalid response from server"); + else + g_task_return_pointer (task, g_strdup (homeserver), g_free); +} + +/** + * cm_utils_get_homeserver_async: + * @username: A complete matrix username + * @timeout: timeout in seconds + * @cancellable: (nullable): A #GCancellable + * @callback: The callback to run + * @user_data: (nullable): The data passed to @callback + * + * Get homeserver from the given @username. @userename + * should be in complete form (eg: @user:example.org) + * + * @timeout is clamped between 5 and 60 seconds. + * + * This is a network operation and shall connect to the + * network to fetch homeserver details. + * + * See https://matrix.org/docs/spec/client_server/r0.6.1#server-discovery + */ +void +cm_utils_get_homeserver_async (const char *username, + guint timeout, + GCancellable *cancellable, + GAsyncReadyCallback callback, + gpointer user_data) +{ + g_autoptr(GTask) task = NULL; + g_autofree char *uri = NULL; + const char *url; + + g_return_if_fail (username); + g_return_if_fail (!cancellable || G_IS_CANCELLABLE (cancellable)); + g_return_if_fail (callback); + + task = g_task_new (NULL, cancellable, callback, user_data); + g_task_set_source_tag (task, cm_utils_get_homeserver_async); + + if (!cm_utils_user_name_valid (username)) +{ + g_task_return_new_error (task, G_IO_ERROR, G_IO_ERROR_INVALID_FILENAME, + "Username '%s' is not a complete matrix id", username); + return; + } + + url = cm_utils_get_url_from_user_id (username); + uri = g_strconcat ("https://", url, "/.well-known/matrix/client", NULL); + + cm_utils_read_uri_async (uri, timeout, cancellable, + get_homeserver_cb, g_steal_pointer (&task)); +} + +/** + * cm_utils_get_homeserver_finish: + * @result: A #GAsyncResult + * @error: (optional): A #GError + * + * Finish call to cm_utils_get_homeserver_async(). + * + * Returns: (nullable) : The homeserver string or %NULL + * on error. Free with g_free(). + */ +char * +cm_utils_get_homeserver_finish (GAsyncResult *result, + GError **error) +{ + GTask *task = (GTask *)result; + + g_return_val_if_fail (G_IS_TASK (result), NULL); + g_return_val_if_fail (g_task_get_source_tag (task) == cm_utils_get_homeserver_async, NULL); + + return g_task_propagate_pointer (G_TASK (result), error); +} + +static void +api_get_version_cb (GObject *obj, + GAsyncResult *result, + gpointer user_data) +{ + g_autoptr(GTask) task = user_data; + g_autoptr(JsonNode) root = NULL; + JsonObject *object = NULL; + JsonArray *array = NULL; + GError *error = NULL; + const char *server; + gboolean valid; + + g_assert (G_IS_TASK (task)); + + server = g_task_get_task_data (task); + root = cm_utils_read_uri_finish (result, &error); + + if (!error && root) + error = cm_utils_json_node_get_error (root); + + if (!root || + g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CANCELLED) || + g_error_matches (error, G_IO_ERROR, G_IO_ERROR_TIMED_OUT)) +{ + if (error) + g_task_return_error (task, error); + else + g_task_return_new_error (task, G_IO_ERROR, G_IO_ERROR_FAILED, + "Failed to get version for server '%s'", server); + return; + } + + g_object_set_data_full (G_OBJECT (task), "address", + g_object_steal_data (G_OBJECT (result), "address"), + g_object_unref); + + object = json_node_get_object (root); + array = cm_utils_json_object_get_array (object, "versions"); + valid = FALSE; + + if (array) +{ + g_autoptr(GString) versions = NULL; + guint length; + + versions = g_string_new (""); + length = json_array_get_length (array); + + for (guint i = 0; i < length; i++) +{ + const char *version; + + version = json_array_get_string_element (array, i); + g_string_append_printf (versions, " %s", version); + + /* We have tested only with r0.6.x and r0.5.0 */ + if (g_str_has_prefix (version, "r0.5.") || + g_str_has_prefix (version, "r0.6.") || + g_str_has_prefix (version, "v1.")) + valid = TRUE; + } + + g_debug ("'%s' has versions:%s, valid: %d", + server, versions->str, valid); + } + + g_task_return_boolean (task, valid); +} + +void +cm_utils_verify_homeserver_async (const char *server, + guint timeout, + GCancellable *cancellable, + GAsyncReadyCallback callback, + gpointer user_data) +{ + g_autoptr(GTask) task = NULL; + g_autofree char *uri = NULL; + + g_return_if_fail (!cancellable || G_IS_CANCELLABLE (cancellable)); + g_return_if_fail (callback); + + task = g_task_new (NULL, cancellable, callback, user_data); + g_task_set_task_data (task, g_strdup (server), g_free); + g_task_set_source_tag (task, cm_utils_verify_homeserver_async); + + if (!server || !*server || + !g_str_has_prefix (server, "http")) +{ + g_task_return_new_error (task, G_IO_ERROR, + G_IO_ERROR_INVALID_DATA, + "URI '%s' is invalid", server); + return; + } + + uri = g_strconcat (server, "/_matrix/client/versions", NULL); + cm_utils_read_uri_async (uri, timeout, cancellable, + api_get_version_cb, + g_steal_pointer (&task)); +} + +gboolean +cm_utils_verify_homeserver_finish (GAsyncResult *result, + GError **error) +{ + g_return_val_if_fail (G_IS_TASK (result), FALSE); + + return g_task_propagate_boolean (G_TASK (result), error); +} + +/* + * The @base_path should contain the base path up to 'cmatrix' + * directory + */ +char * +cm_utils_get_path_for_m_type (const char *base_path, + CmEventType type, + gboolean thumbnail, + const char *file_name) +{ + const char *thumbnail_path = NULL; + g_autofree char *path = NULL; + + g_return_val_if_fail (base_path && *base_path, NULL); + + if (thumbnail) + thumbnail_path = "thumbnails"; + + if (type == CM_M_ROOM_MESSAGE) + path = g_build_filename (base_path, "files", thumbnail_path, NULL); + + if (type == CM_M_ROOM_AVATAR) + path = g_build_filename (base_path, "avatars", "rooms", thumbnail_path, NULL); + + if (type == CM_M_ROOM_MEMBER) + path = g_build_filename (base_path, "avatars", "users", thumbnail_path, NULL); + + if (path) + { + if (file_name && *file_name) + return g_build_filename (path, file_name, NULL); + else + return g_steal_pointer (&path); + } + + g_return_val_if_reached (NULL); +} diff --git a/subprojects/libcmatrix/src/cmatrix.h b/subprojects/libcmatrix/src/cmatrix.h new file mode 100644 index 0000000000000000000000000000000000000000..b6b5a92cb5dcd30fb8d1872bc659e56cd67a174e --- /dev/null +++ b/subprojects/libcmatrix/src/cmatrix.h @@ -0,0 +1,37 @@ +/* + * Copyright (C) 2022 Purism SPC + * + * SPDX-License-Identifier: LGPL-2.1-or-later + */ + +#pragma once + +#include <glib-object.h> + +G_BEGIN_DECLS + +#if !GLIB_CHECK_VERSION(2, 66, 0) +# error "libcmatrix requires glib-2.0 >= 2.66.0" +#endif + +#ifndef CMATRIX_USE_EXPERIMENTAL_API +# error "libcmatrix API is experimental, define CMATRIX_USE_EXPERIMENTAL_API to use " +#endif + +#define _CMATRIX_TAKEN + +#include "users/cm-user.h" +#include "users/cm-account.h" +#include "cm-enums.h" +#include "cm-common.h" +#include "cm-client.h" +#include "cm-matrix.h" +#include "cm-room.h" +#include "events/cm-room-message-event.h" +/* these are not yet public */ +/* #include "cm-room-member.h" */ +/* #include "cm-device.h" */ + +#undef _CMATRIX_TAKEN + +G_END_DECLS diff --git a/subprojects/libcmatrix/src/events/cm-event-private.h b/subprojects/libcmatrix/src/events/cm-event-private.h new file mode 100644 index 0000000000000000000000000000000000000000..5a767145dfa6e26b748cc90772a545219921b56f --- /dev/null +++ b/subprojects/libcmatrix/src/events/cm-event-private.h @@ -0,0 +1,71 @@ +/* -*- mode: c; c-basic-offset: 2; indent-tabs-mode: nil; -*- */ +/* + * Copyright (C) 2022 Purism SPC + * + * SPDX-License-Identifier: LGPL-2.1-or-later + */ + +#pragma once + +#if !defined(_CMATRIX_TAKEN) && !defined(CMATRIX_COMPILATION) +# error "Only <cmatrix.h> can be included directly." +#endif + +#include <json-glib/json-glib.h> + +#include "cm-types.h" +#include "cm-event.h" + +G_BEGIN_DECLS + +/* The order if the enum SHOULD NEVER be changed, + * as it's used in db + */ +typedef enum { + CM_RELATION_NONE, + CM_RELATION_UNKNOWN, + CM_RELATION_ANNOTATION, + CM_RELATION_REPLACE, + CM_RELATION_REFERENCE, + CM_RELATION_THREAD +} CmRelationType; + +CmEvent *cm_event_new (CmEventType type); +CmEvent *cm_event_new_from_json (JsonObject *root, + JsonObject *encrypted); +void cm_event_set_state (CmEvent *self, + CmEventState state); +const char *cm_event_get_transaction_id (CmEvent *self); +const char *cm_event_get_verification_key (CmEvent *self); +const char *cm_event_get_txn_id (CmEvent *self); +void cm_event_create_txn_id (CmEvent *self, + guint id); +const char *cm_event_get_state_key (CmEvent *self); +void cm_event_set_id (CmEvent *self, + const char *id); +const char *cm_event_get_replaces_id (CmEvent *self); +const char *cm_event_get_reply_to_id (CmEvent *self); +void cm_event_set_m_type (CmEvent *self, + CmEventType type); +void cm_event_set_json (CmEvent *self, + JsonObject *root, + JsonObject *encrypted); +GRefString *cm_event_get_sender_id (CmEvent *self); +void cm_event_set_sender (CmEvent *self, + CmUser *sender); +const char *cm_event_get_sender_device_id (CmEvent *self); +gboolean cm_event_has_encrypted_content (CmEvent *self); +gboolean cm_event_is_decrypted (CmEvent *self); + +char *cm_event_get_json_str (CmEvent *self, + gboolean prettify); +JsonObject *cm_event_get_json (CmEvent *self); +JsonObject *cm_event_get_encrypted_json (CmEvent *self); +JsonObject *cm_event_generate_json (CmEvent *self, + gpointer room); +char *cm_event_get_api_url (CmEvent *self, + gpointer room); +const char *cm_event_get_original_json (CmEvent *self); +gboolean cm_event_is_verified (CmEvent *self); + +G_END_DECLS diff --git a/subprojects/libcmatrix/src/events/cm-event.c b/subprojects/libcmatrix/src/events/cm-event.c new file mode 100644 index 0000000000000000000000000000000000000000..5d0d3d942a8e3a2add419c4140d8af809fb59cbd --- /dev/null +++ b/subprojects/libcmatrix/src/events/cm-event.c @@ -0,0 +1,671 @@ +/* -*- mode: c; c-basic-offset: 2; indent-tabs-mode: nil; -*- */ +/* + * Copyright (C) 2022 Purism SPC + * + * SPDX-License-Identifier: LGPL-2.1-or-later + */ + +#define G_LOG_DOMAIN "cm-event" + +#ifdef HAVE_CONFIG_H +# include "config.h" +#endif + +#include "cm-room-private.h" +#include "users/cm-account.h" +#include "cm-utils-private.h" +#include "cm-event-private.h" + +typedef struct +{ + CmUser *sender; + GRefString *sender_id; + char *sender_device_id; + char *event_id; + char *replaces_event_id; + char *reply_to_event_id; + /* Transaction id generated/recived for every event */ + char *txn_id; + + /* Transaction id received in events (like key verification) */ + char *transaction_id; + char *verification_key; + + char *state_key; + JsonObject *json; + /* The JSON source if the event was encrypted */ + JsonObject *encrypted_json; + gint64 time_stamp; + CmEventType event_type; + CmEventState event_state; +} CmEventPrivate; + +G_DEFINE_TYPE_WITH_PRIVATE (CmEvent, cm_event, G_TYPE_OBJECT) + +enum { + UPDATED, + N_SIGNALS +}; + +static guint signals[N_SIGNALS]; + +#define event_type_string(_event_type) (cm_utils_get_event_type_str(_event_type)) + +static char * +create_txn_id (guint id) +{ + return g_strdup_printf ("cm%"G_GINT64_FORMAT".%d", + g_get_real_time () / G_TIME_SPAN_MILLISECOND, id); +} + +static void +event_parse_relations (CmEvent *self, + JsonObject *root) +{ + CmEventPrivate *priv = cm_event_get_instance_private (self); + JsonObject *child; + const char *type, *value; + + g_assert (CM_IS_EVENT (self)); + + child = cm_utils_json_object_get_object (root, "content"); + child = cm_utils_json_object_get_object (child, "m.relates_to"); + + type = cm_utils_json_object_get_string (child, "rel_type"); + value = cm_utils_json_object_get_string (child, "event_id"); + + if (g_strcmp0 (type, "m.replace") == 0) + priv->replaces_event_id = g_strdup (value); + + if (!priv->replaces_event_id) + { + child = cm_utils_json_object_get_object (root, "unsigned"); + priv->replaces_event_id = g_strdup (cm_utils_json_object_get_string (child, "replaces_state")); + } + + if (!priv->replaces_event_id) + { + child = cm_utils_json_object_get_object (root, "unsigned"); + child = cm_utils_json_object_get_object (child, "m.relations"); + child = cm_utils_json_object_get_object (child, "m.replace"); + priv->replaces_event_id = g_strdup (cm_utils_json_object_get_string (child, "event_id")); + } + + if (!priv->replaces_event_id) + { + type = cm_utils_json_object_get_string (root, "type"); + + if (g_strcmp0 (event_type_string (CM_M_ROOM_REDACTION), type) == 0) + priv->replaces_event_id = cm_utils_json_object_dup_string (root, "redacts"); + } +} + +static gpointer +cm_event_real_generate_json (CmEvent *self, + gpointer room) +{ + /* todo */ + g_assert_not_reached (); + + return NULL; +} + +static char * +cm_event_real_get_api_url (CmEvent *self, + gpointer room) +{ + /* todo */ + g_assert_not_reached (); + + return NULL; +} + +static void +cm_event_finalize (GObject *object) +{ + CmEvent *self = (CmEvent *)object; + CmEventPrivate *priv = cm_event_get_instance_private (self); + + g_clear_object (&priv->sender); + g_clear_pointer (&priv->sender_id, g_ref_string_release); + g_free (priv->sender_device_id); + g_free (priv->event_id); + g_free (priv->replaces_event_id); + g_free (priv->reply_to_event_id); + g_free (priv->txn_id); + g_free (priv->transaction_id); + g_free (priv->verification_key); + g_free (priv->state_key); + g_clear_pointer (&priv->encrypted_json, json_object_unref); + g_clear_pointer (&priv->json, json_object_unref); + + G_OBJECT_CLASS (cm_event_parent_class)->finalize (object); +} + +static void +cm_event_class_init (CmEventClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + CmEventClass *event_class = CM_EVENT_CLASS (klass); + + object_class->finalize = cm_event_finalize; + + event_class->generate_json = cm_event_real_generate_json; + event_class->get_api_url = cm_event_real_get_api_url; + + signals [UPDATED] = + g_signal_new ("updated", + G_TYPE_FROM_CLASS (klass), + G_SIGNAL_RUN_LAST, + 0, NULL, NULL, NULL, + G_TYPE_NONE, 0); +} + +static void +cm_event_init (CmEvent *self) +{ +} + +CmEvent * +cm_event_new (CmEventType type) +{ + CmEventPrivate *priv; + CmEvent *self; + + g_return_val_if_fail (type == CM_M_UNKNOWN || + (type >= CM_M_KEY_VERIFICATION_ACCEPT && + type <= CM_M_KEY_VERIFICATION_START), NULL); + + self = g_object_new (CM_TYPE_EVENT, NULL); + priv = cm_event_get_instance_private (self); + + priv->event_type = type; + + return self; +} + +CmEvent * +cm_event_new_from_json (JsonObject *root, + JsonObject *encrypted) +{ + CmEventPrivate *priv; + JsonObject *child; + CmEvent *self; + CmEventType type; + + g_return_val_if_fail (root || encrypted, NULL); + + self = g_object_new (CM_TYPE_EVENT, NULL); + priv = cm_event_get_instance_private (self); + cm_event_set_json (self, root, encrypted); + + type = cm_event_get_m_type (self); + + if (type >= CM_M_KEY_VERIFICATION_ACCEPT && + type <= CM_M_KEY_VERIFICATION_START) + { + child = cm_utils_json_object_get_object (root, "content"); + priv->transaction_id = cm_utils_json_object_dup_string (child, "transaction_id"); + + if (type == CM_M_KEY_VERIFICATION_KEY) + priv->verification_key = cm_utils_json_object_dup_string (child, "key"); + + if (type == CM_M_KEY_VERIFICATION_REQUEST || + type == CM_M_KEY_VERIFICATION_START) + { + priv->time_stamp = cm_utils_json_object_get_int (child, "timestamp"); + priv->sender_device_id = cm_utils_json_object_dup_string (child, "from_device"); + } + } + + return self; +} + +const char * +cm_event_get_id (CmEvent *self) +{ + CmEventPrivate *priv = cm_event_get_instance_private (self); + + g_return_val_if_fail (CM_IS_EVENT (self), NULL); + + return priv->event_id; +} + +void +cm_event_set_id (CmEvent *self, + const char *id) +{ + CmEventPrivate *priv = cm_event_get_instance_private (self); + + g_return_if_fail (CM_IS_EVENT (self)); + g_return_if_fail (!priv->event_id); + + priv->event_id = g_strdup (id); +} + +const char * +cm_event_get_replaces_id (CmEvent *self) +{ + CmEventPrivate *priv = cm_event_get_instance_private (self); + + g_return_val_if_fail (CM_IS_EVENT (self), NULL); + + return priv->replaces_event_id; +} + +const char * +cm_event_get_reply_to_id (CmEvent *self) +{ + CmEventPrivate *priv = cm_event_get_instance_private (self); + + g_return_val_if_fail (CM_IS_EVENT (self), NULL); + + return priv->reply_to_event_id; +} + +const char * +cm_event_get_transaction_id (CmEvent *self) +{ + CmEventPrivate *priv = cm_event_get_instance_private (self); + + g_return_val_if_fail (CM_IS_EVENT (self), NULL); + + return priv->transaction_id; +} + +const char * +cm_event_get_verification_key (CmEvent *self) +{ + CmEventPrivate *priv = cm_event_get_instance_private (self); + + g_return_val_if_fail (CM_IS_EVENT (self), NULL); + + return priv->verification_key; +} + +const char * +cm_event_get_txn_id (CmEvent *self) +{ + CmEventPrivate *priv = cm_event_get_instance_private (self); + + g_return_val_if_fail (CM_IS_EVENT (self), NULL); + + return priv->txn_id; +} + +void +cm_event_create_txn_id (CmEvent *self, + guint id) +{ + CmEventPrivate *priv = cm_event_get_instance_private (self); + + g_return_if_fail (CM_IS_EVENT (self)); + g_return_if_fail (!priv->event_id); + + priv->txn_id = create_txn_id (id); +} + +const char * +cm_event_get_state_key (CmEvent *self) +{ + CmEventPrivate *priv = cm_event_get_instance_private (self); + + g_return_val_if_fail (CM_IS_EVENT (self), NULL); + + if (priv->state_key && *priv->state_key) + return priv->state_key; + + return NULL; +} + +CmEventType +cm_event_get_m_type (CmEvent *self) +{ + CmEventPrivate *priv = cm_event_get_instance_private (self); + + g_return_val_if_fail (CM_IS_EVENT (self), CM_M_UNKNOWN); + + return priv->event_type; +} + +void +cm_event_set_m_type (CmEvent *self, + CmEventType type) +{ + CmEventPrivate *priv = cm_event_get_instance_private (self); + + g_return_if_fail (CM_IS_EVENT (self)); + g_return_if_fail (!priv->event_type); + g_return_if_fail (type != CM_M_UNKNOWN); + + priv->event_type = type; +} + +CmEventState +cm_event_get_state (CmEvent *self) +{ + CmEventPrivate *priv = cm_event_get_instance_private (self); + + g_return_val_if_fail (CM_IS_EVENT (self), CM_EVENT_STATE_UNKNOWN); + + if (priv->event_state == CM_EVENT_STATE_UNKNOWN && + CM_IS_ACCOUNT (priv->sender)) + return CM_EVENT_STATE_SENT; + + return priv->event_state; +} + +void +cm_event_set_state (CmEvent *self, + CmEventState state) +{ + CmEventPrivate *priv = cm_event_get_instance_private (self); + + if (priv->event_state == state) + return; + + priv->event_state = state; + g_signal_emit (self, signals[UPDATED], 0); +} + +void +cm_event_set_json (CmEvent *self, + JsonObject *root, + JsonObject *encrypted) +{ + CmEventPrivate *priv = cm_event_get_instance_private (self); + JsonObject *child; + const char *type; + + g_return_if_fail (CM_IS_EVENT (self)); + + if (!root && !encrypted) + return; + + type = cm_utils_json_object_get_string (root, "type"); + + if (!type) + type = cm_utils_json_object_get_string (encrypted, "type"); + + /* todo: Handle content less encrypted events (eg: redactions) */ + if (g_strcmp0 (type, event_type_string (CM_M_ROOM_ENCRYPTED)) == 0) + { + /* We got something encrypted */ + if (!encrypted) + encrypted = g_steal_pointer (&root); + priv->event_type = CM_M_ROOM_ENCRYPTED; + } + + if (encrypted) + priv->encrypted_json = json_object_ref (encrypted); + + priv->event_id = g_strdup (cm_utils_json_object_get_string (encrypted ?: root, "event_id")); + priv->time_stamp = cm_utils_json_object_get_int (encrypted ?: root, "origin_server_ts"); + if (cm_utils_json_object_get_string (encrypted ?: root, "sender")) + priv->sender_id = g_ref_string_new_intern (cm_utils_json_object_get_string (encrypted ?: root, "sender")); + + child = cm_utils_json_object_get_object (encrypted ?: root, "unsigned"); + if (cm_utils_json_object_has_member (child, "transaction_id")) + { + g_free (priv->txn_id); + priv->txn_id = cm_utils_json_object_dup_string (child, "transaction_id"); + } + + if (encrypted) + event_parse_relations (self, encrypted); + + if (!root) + return; + + event_parse_relations (self, root); + priv->state_key = g_strdup (cm_utils_json_object_get_string (root, "state_key")); + priv->json = json_object_ref (root); + + type = cm_utils_json_object_get_string (root, "type"); + + if (g_strcmp0 (type, event_type_string (CM_M_ROOM_MESSAGE)) == 0) + priv->event_type = CM_M_ROOM_MESSAGE; + else if (g_strcmp0 (type, event_type_string (CM_M_ROOM_MEMBER)) == 0) + priv->event_type = CM_M_ROOM_MEMBER; + else if (g_strcmp0 (type, event_type_string (CM_M_REACTION)) == 0) + priv->event_type = CM_M_REACTION; + else if (g_strcmp0 (type, event_type_string (CM_M_ROOM_REDACTION)) == 0) + priv->event_type = CM_M_ROOM_REDACTION; + else if (g_strcmp0 (type, event_type_string (CM_M_ROOM_TOPIC)) == 0) + priv->event_type = CM_M_ROOM_TOPIC; + else if (g_strcmp0 (type, event_type_string (CM_M_ROOM_AVATAR)) == 0) + priv->event_type = CM_M_ROOM_AVATAR; + else if (g_strcmp0 (type, event_type_string (CM_M_CALL_INVITE)) == 0) + priv->event_type = CM_M_CALL_INVITE; + else if (g_strcmp0 (type, event_type_string (CM_M_CALL_CANDIDATES)) == 0) + priv->event_type = CM_M_CALL_CANDIDATES; + else if (g_strcmp0 (type, event_type_string (CM_M_CALL_ANSWER)) == 0) + priv->event_type = CM_M_CALL_ANSWER; + else if (g_strcmp0 (type, event_type_string (CM_M_CALL_SELECT_ANSWER)) == 0) + priv->event_type = CM_M_CALL_SELECT_ANSWER; + else if (g_strcmp0 (type, event_type_string (CM_M_CALL_HANGUP)) == 0) + priv->event_type = CM_M_CALL_HANGUP; + else if (g_strcmp0 (type, event_type_string (CM_M_ROOM_CANONICAL_ALIAS)) == 0) + priv->event_type = CM_M_ROOM_CANONICAL_ALIAS; + else if (g_strcmp0 (type, event_type_string (CM_M_ROOM_NAME)) == 0) + priv->event_type = CM_M_ROOM_NAME; + else if (g_strcmp0 (type, event_type_string (CM_M_ROOM_CREATE)) == 0) + priv->event_type = CM_M_ROOM_CREATE; + else if (g_strcmp0 (type, event_type_string (CM_M_ROOM_POWER_LEVELS)) == 0) + priv->event_type = CM_M_ROOM_POWER_LEVELS; + else if (g_strcmp0 (type, event_type_string (CM_M_ROOM_GUEST_ACCESS)) == 0) + priv->event_type = CM_M_ROOM_GUEST_ACCESS; + else if (g_strcmp0 (type, event_type_string (CM_M_ROOM_HISTORY_VISIBILITY)) == 0) + priv->event_type = CM_M_ROOM_HISTORY_VISIBILITY; + else if (g_strcmp0 (type, event_type_string (CM_M_ROOM_JOIN_RULES)) == 0) + priv->event_type = CM_M_ROOM_JOIN_RULES; + else if (g_strcmp0 (type, event_type_string (CM_M_ROOM_SERVER_ACL)) == 0) + priv->event_type = CM_M_ROOM_SERVER_ACL; + else if (g_strcmp0 (type, event_type_string (CM_M_ROOM_ENCRYPTION)) == 0) + priv->event_type = CM_M_ROOM_ENCRYPTION; + else if (g_strcmp0 (type, event_type_string (CM_M_ROOM_THIRD_PARTY_INVITE)) == 0) + priv->event_type = CM_M_ROOM_THIRD_PARTY_INVITE; + else if (g_strcmp0 (type, event_type_string (CM_M_ROOM_RELATED_GROUPS)) == 0) + priv->event_type = CM_M_ROOM_RELATED_GROUPS; + else if (g_strcmp0 (type, event_type_string (CM_M_ROOM_TOMBSTONE)) == 0) + priv->event_type = CM_M_ROOM_TOMBSTONE; + else if (g_strcmp0 (type, event_type_string (CM_M_ROOM_PINNED_EVENTS)) == 0) + priv->event_type = CM_M_ROOM_PINNED_EVENTS; + else if (g_strcmp0 (type, event_type_string (CM_M_ROOM_PLUMBING)) == 0) + priv->event_type = CM_M_ROOM_PLUMBING; + else if (g_strcmp0 (type, event_type_string (CM_M_ROOM_BOT_OPTIONS)) == 0) + priv->event_type = CM_M_ROOM_BOT_OPTIONS; + else if (g_strcmp0 (type, event_type_string (CM_M_KEY_VERIFICATION_ACCEPT)) == 0) + priv->event_type = CM_M_KEY_VERIFICATION_ACCEPT; + else if (g_strcmp0 (type, event_type_string (CM_M_KEY_VERIFICATION_CANCEL)) == 0) + priv->event_type = CM_M_KEY_VERIFICATION_CANCEL; + else if (g_strcmp0 (type, event_type_string (CM_M_KEY_VERIFICATION_DONE)) == 0) + priv->event_type = CM_M_KEY_VERIFICATION_DONE; + else if (g_strcmp0 (type, event_type_string (CM_M_KEY_VERIFICATION_KEY)) == 0) + priv->event_type = CM_M_KEY_VERIFICATION_KEY; + else if (g_strcmp0 (type, event_type_string (CM_M_KEY_VERIFICATION_MAC)) == 0) + priv->event_type = CM_M_KEY_VERIFICATION_MAC; + else if (g_strcmp0 (type, event_type_string (CM_M_KEY_VERIFICATION_READY)) == 0) + priv->event_type = CM_M_KEY_VERIFICATION_READY; + else if (g_strcmp0 (type, event_type_string (CM_M_KEY_VERIFICATION_REQUEST)) == 0) + priv->event_type = CM_M_KEY_VERIFICATION_REQUEST; + else if (g_strcmp0 (type, event_type_string (CM_M_KEY_VERIFICATION_START)) == 0) + priv->event_type = CM_M_KEY_VERIFICATION_START; + else + CM_TRACE ("unhandled event type: %s", type); +} + +GRefString * +cm_event_get_sender_id (CmEvent *self) +{ + CmEventPrivate *priv = cm_event_get_instance_private (self); + + g_return_val_if_fail (CM_IS_EVENT (self), NULL); + + if (priv->sender) + return cm_user_get_id (priv->sender); + + return priv->sender_id; +} + +CmUser * +cm_event_get_sender (CmEvent *self) +{ + CmEventPrivate *priv = cm_event_get_instance_private (self); + + g_return_val_if_fail (CM_IS_EVENT (self), NULL); + + return priv->sender; +} + +void +cm_event_set_sender (CmEvent *self, + CmUser *sender) +{ + CmEventPrivate *priv = cm_event_get_instance_private (self); + + g_return_if_fail (CM_IS_EVENT (self)); + g_return_if_fail (!priv->sender); + + if (priv->sender_id && + priv->sender_id != cm_user_get_id (sender)) + g_critical ("user name '%s' and '%s' doesn't match", + priv->sender_id, cm_user_get_id (sender)); + + priv->sender = g_object_ref (sender); +} + +const char * +cm_event_get_sender_device_id (CmEvent *self) +{ + CmEventPrivate *priv = cm_event_get_instance_private (self); + + g_return_val_if_fail (CM_IS_EVENT (self), NULL); + + return priv->sender_device_id; +} + +gboolean +cm_event_is_encrypted (CmEvent *self) +{ + CmEventPrivate *priv = cm_event_get_instance_private (self); + + g_return_val_if_fail (CM_IS_EVENT (self), FALSE); + + return !!priv->encrypted_json; +} + +gboolean +cm_event_has_encrypted_content (CmEvent *self) +{ + CmEventPrivate *priv = cm_event_get_instance_private (self); + JsonObject *child; + + g_return_val_if_fail (CM_IS_EVENT (self), FALSE); + + if (!priv->encrypted_json) + return FALSE; + + child = cm_utils_json_object_get_object (priv->encrypted_json, "content"); + + return cm_utils_json_object_has_member (child, "ciphertext"); +} + +gboolean +cm_event_is_decrypted (CmEvent *self) +{ + CmEventPrivate *priv = cm_event_get_instance_private (self); + + g_return_val_if_fail (CM_IS_EVENT (self), FALSE); + + return !!priv->json; +} + +/** + * cm_event_get_time_stamp: + * @self: A #CmEvent + * + * Get the event time stamp in milliseconds + * + * Returns: The milliseconds since Unix epoc + */ +gint64 +cm_event_get_time_stamp (CmEvent *self) +{ + CmEventPrivate *priv = cm_event_get_instance_private (self); + + g_return_val_if_fail (CM_IS_EVENT (self), FALSE); + + if (!priv->time_stamp) + return time (NULL) * 1000; + + return priv->time_stamp; +} + +char * +cm_event_get_json_str (CmEvent *self, + gboolean prettify) +{ + CmEventPrivate *priv = cm_event_get_instance_private (self); + + g_return_val_if_fail (CM_IS_EVENT (self), NULL); + + if (priv->json) + return cm_utils_json_object_to_string (priv->json, prettify); + + return NULL; +} + +/* + * cm_event_get_json: + * + * Can return %NULL, eg: when the event is encrypted, + * and was not able to decrypt. + * + * Returns: (transfer full) (nullable) + */ +JsonObject * +cm_event_get_json (CmEvent *self) +{ + CmEventPrivate *priv = cm_event_get_instance_private (self); + + g_return_val_if_fail (CM_IS_EVENT (self), NULL); + + if (priv->json) + return json_object_ref (priv->json); + + return NULL; +} + +JsonObject * +cm_event_get_encrypted_json (CmEvent *self) +{ + CmEventPrivate *priv = cm_event_get_instance_private (self); + + g_return_val_if_fail (CM_IS_EVENT (self), NULL); + + if (priv->encrypted_json) + return json_object_ref (priv->encrypted_json); + + return NULL; +} + +JsonObject * +cm_event_generate_json (CmEvent *self, + gpointer room) +{ + g_return_val_if_fail (CM_IS_EVENT (self), NULL); + g_return_val_if_fail (!room || CM_IS_ROOM (room), NULL); + + return CM_EVENT_GET_CLASS (self)->generate_json (self, room); +} + +char * +cm_event_get_api_url (CmEvent *self, + gpointer room) +{ + g_return_val_if_fail (CM_IS_EVENT (self), NULL); + g_return_val_if_fail (!room || CM_IS_ROOM (room), NULL); + + return CM_EVENT_GET_CLASS (self)->get_api_url (self, room); +} diff --git a/subprojects/libcmatrix/src/events/cm-event.h b/subprojects/libcmatrix/src/events/cm-event.h new file mode 100644 index 0000000000000000000000000000000000000000..296c44e11b1b933e1c8f40fa94390d02801f8d24 --- /dev/null +++ b/subprojects/libcmatrix/src/events/cm-event.h @@ -0,0 +1,49 @@ +/* -*- mode: c; c-basic-offset: 2; indent-tabs-mode: nil; -*- */ +/* cm-event.h + * + * Copyright 2022 Purism SPC + * + * Author(s): + * Mohammed Sadiq <sadiq@sadiqpk.org> + * + * SPDX-License-Identifier: LGPL-2.1-or-later + */ + +#pragma once + +#if !defined(_CMATRIX_TAKEN) && !defined(CMATRIX_COMPILATION) +# error "Only <cmatrix.h> can be included directly." +#endif + +#include <glib-object.h> +#include <gio/gio.h> + +#include "cm-types.h" +#include "cm-enums.h" + +G_BEGIN_DECLS + +#define CM_TYPE_EVENT (cm_event_get_type ()) + +G_DECLARE_DERIVABLE_TYPE (CmEvent, cm_event, CM, EVENT, GObject) + +struct _CmEventClass +{ + GObjectClass parent_class; + + /*< private >*/ + gpointer (*generate_json) (CmEvent *self, + gpointer room); + char *(*get_api_url) (CmEvent *self, + gpointer room); + gpointer reserved[8]; +}; + +const char *cm_event_get_id (CmEvent *self); +CmUser *cm_event_get_sender (CmEvent *self); +CmEventType cm_event_get_m_type (CmEvent *self); +CmEventState cm_event_get_state (CmEvent *self); +gint64 cm_event_get_time_stamp (CmEvent *self); +gboolean cm_event_is_encrypted (CmEvent *self); + +G_END_DECLS diff --git a/subprojects/libcmatrix/src/events/cm-room-event-list-private.h b/subprojects/libcmatrix/src/events/cm-room-event-list-private.h new file mode 100644 index 0000000000000000000000000000000000000000..215fde7fc4cbb01f48ad0091e4aa596521686b98 --- /dev/null +++ b/subprojects/libcmatrix/src/events/cm-room-event-list-private.h @@ -0,0 +1,48 @@ +/* -*- mode: c; c-basic-offset: 2; indent-tabs-mode: nil; -*- */ +/* cm-client.c + * + * Copyright 2022 Purism SPC + * + * Author(s): + * Mohammed Sadiq <sadiq@sadiqpk.org> + * + * SPDX-License-Identifier: LGPL-2.1-or-later + */ + +#pragma once + +#include <json-glib/json-glib.h> +#include <gio/gio.h> + +#include "cm-types.h" + +G_BEGIN_DECLS + +#define CM_TYPE_ROOM_EVENT_LIST (cm_room_event_list_get_type ()) + +G_DECLARE_FINAL_TYPE (CmRoomEventList, cm_room_event_list, CM, ROOM_EVENT_LIST, GObject) + +CmRoomEventList *cm_room_event_list_new (CmRoom *room); +void cm_room_event_list_set_client (CmRoomEventList *self, + CmClient *client); +CmEvent *cm_room_event_list_get_event (CmRoomEventList *self, + CmEventType type); +GListModel *cm_room_event_list_get_events (CmRoomEventList *self); +void cm_room_event_list_set_save_pending (CmRoomEventList *self, + gboolean save_pending); +gboolean cm_room_event_list_save_pending (CmRoomEventList *self); +JsonObject *cm_room_event_list_get_local_json (CmRoomEventList *self); +void cm_room_event_list_append_event (CmRoomEventList *self, + CmEvent *event); +void cm_room_event_list_add_events (CmRoomEventList *self, + GPtrArray *events, + gboolean append); +void cm_room_event_list_set_local_json (CmRoomEventList *self, + JsonObject *root, + CmEvent *last_event); +void cm_room_event_list_parse_events (CmRoomEventList *self, + JsonObject *root, + GPtrArray *events, + gboolean past); + +G_END_DECLS diff --git a/subprojects/libcmatrix/src/events/cm-room-event-list.c b/subprojects/libcmatrix/src/events/cm-room-event-list.c new file mode 100644 index 0000000000000000000000000000000000000000..74c09497702343b7f179671477385b6456ec8a06 --- /dev/null +++ b/subprojects/libcmatrix/src/events/cm-room-event-list.c @@ -0,0 +1,565 @@ +/* -*- mode: c; c-basic-offset: 2; indent-tabs-mode: nil; -*- */ +/* cm-client.c + * + * Copyright 2022 Purism SPC + * + * Author(s): + * Mohammed Sadiq <sadiq@sadiqpk.org> + * + * SPDX-License-Identifier: LGPL-2.1-or-later + */ + +#define G_LOG_DOMAIN "cm-room-event-list" + +#ifdef HAVE_CONFIG_H +# include "config.h" +#endif + +#include "cm-utils-private.h" +#include "cm-event-private.h" +#include "cm-client-private.h" +#include "users/cm-user-private.h" +#include "users/cm-user-list-private.h" +#include "users/cm-room-member-private.h" +#include "cm-enc-private.h" +#include "cm-room-event-private.h" +#include "cm-room-message-event-private.h" +#include "cm-room-private.h" +#include "cm-room-event-list-private.h" + +struct _CmRoomEventList +{ + GObject parent_instance; + + CmRoom *room; + CmClient *client; + + GListStore *events_list; + CmEvent *room_create_event; + CmEvent *room_name_event; + CmEvent *canonical_alias_event; + CmEvent *room_topic_event; + CmEvent *power_level_event; + CmEvent *encryption_event; + CmEvent *guest_access_event; + CmEvent *join_rules_event; + CmEvent *history_visibility_event; + CmEvent *tombstone_event; + + JsonObject *local_json; + + gboolean save_pending: 1; +}; + +G_DEFINE_TYPE (CmRoomEventList, cm_room_event_list, G_TYPE_OBJECT) + +#define event_m_type_str(_type) (cm_utils_get_event_type_str (_type)) +#define set_json_from_event(_event, _json) do { \ + CmEventType _type; \ + \ + if (!_event) \ + break; \ + \ + _type = cm_event_get_m_type(_event); \ + json_object_set_object_member (_json, \ + event_m_type_str (_type), \ + cm_event_get_json (_event)); \ +} while (0) + +#define set_event_from_json(_room, _event, _json, _m_type) do { \ + JsonObject *_child; \ + const char *_type; \ + \ + _type = cm_utils_get_event_type_str (_m_type); \ + _child = cm_utils_json_object_get_object (_json, _type); \ + if (!_child) \ + break; \ + \ + _event = (gpointer)cm_room_event_new_from_json (_room, \ + _child, \ + NULL); \ +} while (0) + +#define set_local_json_event(_json, _event) do { \ + JsonObject *_local; \ + CmEventType _type; \ + \ + if (!cm_utils_json_object_get_object (_json, "local")) \ + break; \ + \ + _local = cm_utils_json_object_get_object (_json, "local"); \ + _type = cm_event_get_m_type(_event); \ + json_object_set_object_member (_local, \ + event_m_type_str (_type), \ + cm_event_get_json (event)); \ +} while (0) + +static void +remove_event_with_txn_id (CmRoomEventList *self, + CmEvent *event) +{ + GListModel *events; + guint n_items; + + g_assert (CM_IS_ROOM_EVENT_LIST (self)); + g_assert (CM_IS_EVENT (event)); + + if (!cm_event_get_txn_id (event)) + return; + + events = G_LIST_MODEL (self->events_list); + n_items = g_list_model_get_n_items (events); + + /* i and n_items are unsigned */ + for (guint i = n_items - 1; i + 1 > 0; i--) + { + g_autoptr(CmEvent) item = NULL; + + item = g_list_model_get_item (events, i); + + if (!cm_event_get_txn_id (item)) + continue; + + if (g_strcmp0 (cm_event_get_txn_id (event), + cm_event_get_txn_id (item)) == 0) + { + cm_utils_remove_list_item (self->events_list, item); + break; + } + } +} + +static void +room_event_list_generate_json (CmRoomEventList *self) +{ + JsonObject *json, *child; + + g_assert (CM_IS_ROOM_EVENT_LIST (self)); + g_assert (!self->local_json); + + json = json_object_new (); + child = json_object_new (); + self->local_json = json; + + json_object_set_object_member (json, "local", child); + + json_object_set_string_member (child, "alias", cm_room_get_name (self->room)); + /* Alias set before the current one, may be used if current one is NULL (eg: was x) */ + json_object_set_string_member (child, "last_alias", cm_room_get_name (self->room)); + json_object_set_boolean_member (child, "direct", cm_room_is_direct (self->room)); + json_object_set_int_member (child, "encryption", cm_room_is_encrypted (self->room)); + + set_json_from_event (self->room_create_event, child); + set_json_from_event (self->room_name_event, child); + set_json_from_event (self->canonical_alias_event, child); + set_json_from_event (self->room_topic_event, child); + set_json_from_event (self->encryption_event, child); + set_json_from_event (self->power_level_event, child); + set_json_from_event (self->guest_access_event, child); + set_json_from_event (self->join_rules_event, child); + set_json_from_event (self->history_visibility_event, child); + set_json_from_event (self->tombstone_event, child); + + /* todo */ + /* Set only if there is only one member in the room */ + /* json_object_set_string_member (child, "m.room.member", "bad"); */ + /* json_object_set_object_member (child, "summary", "bad"); */ + /* json_object_set_object_member (child, "unread_notifications", "bad"); */ +} + +static void +cm_room_event_list_finalize (GObject *object) +{ + CmRoomEventList *self = (CmRoomEventList *)object; + + g_clear_object (&self->events_list); + g_clear_object (&self->room_create_event); + g_clear_object (&self->room_name_event); + g_clear_object (&self->canonical_alias_event); + g_clear_object (&self->room_topic_event); + g_clear_object (&self->power_level_event); + g_clear_object (&self->encryption_event); + g_clear_object (&self->guest_access_event); + g_clear_object (&self->join_rules_event); + g_clear_object (&self->history_visibility_event); + g_clear_object (&self->tombstone_event); + g_clear_pointer (&self->local_json, json_object_unref); + + G_OBJECT_CLASS (cm_room_event_list_parent_class)->finalize (object); +} + +static void +cm_room_event_list_class_init (CmRoomEventListClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + + object_class->finalize = cm_room_event_list_finalize; +} + +static void +cm_room_event_list_init (CmRoomEventList *self) +{ + self->events_list = g_list_store_new (CM_TYPE_EVENT); +} + +CmRoomEventList * +cm_room_event_list_new (CmRoom *room) +{ + CmRoomEventList *self; + + self = g_object_new (CM_TYPE_ROOM_EVENT_LIST, NULL); + g_set_weak_pointer (&self->room, room); + + g_debug ("(%p) New event list for room %p", self, room); + + return self; +} + +void +cm_room_event_list_set_client (CmRoomEventList *self, + CmClient *client) +{ + guint n_items; + + g_return_if_fail (CM_IS_ROOM_EVENT_LIST (self)); + g_return_if_fail (CM_IS_CLIENT (client)); + g_return_if_fail (!self->client); + + g_set_weak_pointer (&self->client, client); + + n_items = g_list_model_get_n_items (G_LIST_MODEL (self->events_list)); + + for (guint i = 0; i < n_items; i++) + { + g_autoptr(CmEvent) event = NULL; + CmUser *user; + + event = g_list_model_get_item (G_LIST_MODEL (self->events_list), i); + user = cm_event_get_sender (event); + + if (!user) + { + user = cm_room_find_user (self->room, cm_event_get_sender_id (event), TRUE); + cm_event_set_sender (event, user); + } + + cm_user_set_client (user, client); + } +} + +CmEvent * +cm_room_event_list_get_event (CmRoomEventList *self, + CmEventType type) +{ + g_return_val_if_fail (CM_IS_ROOM_EVENT_LIST (self), NULL); + + if (type == CM_M_ROOM_CREATE) + return self->room_create_event; + + if (type == CM_M_ROOM_NAME) + return self->tombstone_event; + + if (type == CM_M_ROOM_CANONICAL_ALIAS) + return self->canonical_alias_event; + + if (type == CM_M_ROOM_POWER_LEVELS) + return self->power_level_event; + + if (type == CM_M_ROOM_ENCRYPTION) + return self->encryption_event; + + if (type == CM_M_ROOM_GUEST_ACCESS) + return self->guest_access_event; + + if (type == CM_M_ROOM_JOIN_RULES) + return self->join_rules_event; + + if (type == CM_M_ROOM_HISTORY_VISIBILITY) + return self->history_visibility_event; + + if (type == CM_M_ROOM_TOMBSTONE) + return self->tombstone_event; + + return NULL; +} + +GListModel * +cm_room_event_list_get_events (CmRoomEventList *self) +{ + g_return_val_if_fail (self, NULL); + g_return_val_if_fail (self->room, NULL); + + return G_LIST_MODEL (self->events_list); +} + +void +cm_room_event_list_set_save_pending (CmRoomEventList *self, + gboolean save_pending) +{ + g_return_if_fail (CM_IS_ROOM_EVENT_LIST (self)); + + self->save_pending = !!save_pending; +} + +gboolean +cm_room_event_list_save_pending (CmRoomEventList *self) +{ + g_return_val_if_fail (CM_IS_ROOM_EVENT_LIST (self), FALSE); + + return self->save_pending; +} + +JsonObject * +cm_room_event_list_get_local_json (CmRoomEventList *self) +{ + g_return_val_if_fail (CM_IS_ROOM_EVENT_LIST (self), NULL); + g_return_val_if_fail (self->room, NULL); + + if (!self->local_json) + room_event_list_generate_json (self); + + return self->local_json; +} + +void +cm_room_event_list_append_event (CmRoomEventList *self, + CmEvent *event) +{ + g_assert (CM_IS_ROOM_EVENT_LIST (self)); + g_assert (CM_IS_ROOM (self->room)); + + g_list_store_append (self->events_list, event); +} + +void +cm_room_event_list_add_events (CmRoomEventList *self, + GPtrArray *events, + gboolean append) +{ + CmClient *client; + guint position = 0; + + g_assert (CM_IS_ROOM_EVENT_LIST (self)); + g_assert (CM_IS_ROOM (self->room)); + + if (!events || !events->len) + return; + + client = cm_room_get_client (self->room); + + for (guint i = 0; i < events->len; i++) + { + CmEvent *event = events->pdata[i]; + CmUser *user; + + if (cm_event_get_sender (event) || !client) + continue; + + user = cm_room_find_user (self->room, cm_event_get_sender_id (event), TRUE); + cm_event_set_sender (event, user); + } + + if (append) + { + position = g_list_model_get_n_items (G_LIST_MODEL (self->events_list)); + g_list_store_splice (self->events_list, + position, 0, events->pdata, events->len); + } + else + { + g_autoptr(GPtrArray) reversed = NULL; + + reversed = g_ptr_array_sized_new (events->len); + + for (guint i = 0; i < events->len; i++) + g_ptr_array_insert (reversed, 0, events->pdata[i]); + g_list_store_splice (self->events_list, + 0, 0, reversed->pdata, reversed->len); + } +} + +void +cm_room_event_list_set_local_json (CmRoomEventList *self, + JsonObject *root, + CmEvent *last_event) +{ + GListModel *model; + JsonObject *local; + CmRoom *room; + + g_return_if_fail (CM_IS_ROOM_EVENT_LIST (self)); + g_return_if_fail (!self->local_json); + + model = G_LIST_MODEL (self->events_list); + /* Local json should be set only before the events are loaded */ + g_return_if_fail (g_list_model_get_n_items (model) == 0); + + if (last_event) + g_list_store_append (self->events_list, last_event); + + if (!root) + return; + + room = self->room; + self->local_json = json_object_ref (root); + local = cm_utils_json_object_get_object (root, "local"); + + set_event_from_json (room, self->room_name_event, local, CM_M_ROOM_NAME); + set_event_from_json (room, self->room_topic_event, local, CM_M_ROOM_TOPIC); + set_event_from_json (room, self->room_create_event, local, CM_M_ROOM_CREATE); + set_event_from_json (room, self->tombstone_event, local, CM_M_ROOM_TOMBSTONE); + set_event_from_json (room, self->encryption_event, local, CM_M_ROOM_ENCRYPTION); + set_event_from_json (room, self->join_rules_event, local, CM_M_ROOM_JOIN_RULES); + set_event_from_json (room, self->power_level_event, local, CM_M_ROOM_POWER_LEVELS); + set_event_from_json (room, self->guest_access_event, local, CM_M_ROOM_GUEST_ACCESS); + set_event_from_json (room, self->canonical_alias_event, local, CM_M_ROOM_CANONICAL_ALIAS); + set_event_from_json (room, self->history_visibility_event, local, CM_M_ROOM_HISTORY_VISIBILITY); +} + +static JsonObject * +event_list_decrypt (CmRoomEventList *self, + JsonObject *root) +{ + char *plain_text = NULL; + JsonObject *content; + CmClient *client; + CmEnc *enc; + + g_assert (CM_IS_ROOM_EVENT_LIST (self)); + + client = cm_room_get_client (self->room); + enc = cm_client_get_enc (client); + + if (!enc || !root) + return NULL; + + content = cm_utils_json_object_get_object (root, "content"); + plain_text = cm_enc_handle_join_room_encrypted (enc, self->room, content); + + return cm_utils_string_to_json_object (plain_text); +} + +void +cm_room_event_list_parse_events (CmRoomEventList *self, + JsonObject *root, + GPtrArray *events, + gboolean past) +{ + JsonObject *child; + JsonArray *array; + guint length = 0; + + g_return_if_fail (CM_IS_ROOM_EVENT_LIST (self)); + g_return_if_fail (self->room); + + if (!root) + return; + + /* If @events is NULL, they are considered to be state + * events and thus it shouldn't be past events. + */ + if (!events) + g_return_if_fail (!past); + + g_debug ("(%p) Parsing events %p, state event: %s, past events: %s", + self->room, root, CM_LOG_BOOL (!events), CM_LOG_BOOL (past)); + + array = cm_utils_json_object_get_array (root, "events"); + + if (!array) + array = cm_utils_json_object_get_array (root, "chunk"); + + if (array) + length = json_array_get_length (array); + + for (guint i = 0; i < length; i++) + { + g_autoptr(CmEvent) event = NULL; + JsonObject *decrypted = NULL; + CmUser *user; + const char *value; + CmEventType type; + gboolean encrypted = FALSE; + + child = json_array_get_object_element (array, i); + + if (g_strcmp0 (cm_utils_json_object_get_string (child, "type"), + "m.room.encrypted") == 0) + { + decrypted = event_list_decrypt (self, child); + encrypted = TRUE; + } + + event = (gpointer)cm_room_event_new_from_json (self->room, encrypted ? decrypted : child, + encrypted ? child : NULL); + if (!event) + { + g_debug ("no event created from json"); + continue; + } + + value = cm_event_get_sender_id (event); + user = cm_room_find_user (self->room, cm_event_get_sender_id (event), TRUE); + cm_event_set_sender (event, user); + + if (events) + { + if (CM_IS_ROOM_MESSAGE_EVENT (event) && + cm_event_get_txn_id (event)) + remove_event_with_txn_id (self, event); + + g_ptr_array_add (events, g_object_ref (event)); + } + + /* past events shouldn't alter room state, as they may be obsolete */ + if (past) + continue; + + type = cm_event_get_m_type (event); + + if (type == CM_M_ROOM_NAME) + { + g_set_object (&self->room_name_event, event); + value = cm_room_event_get_room_name (CM_ROOM_EVENT (event)); + cm_room_set_name (self->room, value); + } + else if (type == CM_M_ROOM_MEMBER) + cm_room_update_user (self->room, event); + else if (type == CM_M_ROOM_POWER_LEVELS) + g_set_object (&self->power_level_event, event); + else if (type == CM_M_ROOM_ENCRYPTION) + g_set_object (&self->encryption_event, event); + else if (type == CM_M_ROOM_CANONICAL_ALIAS) + g_set_object (&self->canonical_alias_event, event); + else if (type == CM_M_ROOM_TOPIC) + g_set_object (&self->room_topic_event, event); + else if (type == CM_M_ROOM_GUEST_ACCESS) + g_set_object (&self->guest_access_event, event); + else if (type == CM_M_ROOM_JOIN_RULES) + g_set_object (&self->join_rules_event, event); + else if (type == CM_M_ROOM_HISTORY_VISIBILITY) + g_set_object (&self->history_visibility_event, event); + else if (type == CM_M_ROOM_TOMBSTONE) + g_set_object (&self->tombstone_event, event); + + if (type == CM_M_ROOM_NAME || + type == CM_M_ROOM_POWER_LEVELS || + type == CM_M_ROOM_ENCRYPTION || + type == CM_M_ROOM_CANONICAL_ALIAS || + type == CM_M_ROOM_TOPIC || + type == CM_M_ROOM_GUEST_ACCESS || + type == CM_M_ROOM_JOIN_RULES || + type == CM_M_ROOM_HISTORY_VISIBILITY || + type == CM_M_ROOM_TOMBSTONE) + { + set_local_json_event (self->local_json, event); + self->save_pending = TRUE; + } + + if (type == CM_M_ROOM_ENCRYPTION) + g_object_notify (G_OBJECT (self->room), "encrypted"); + } + + if (events && events->len) + cm_room_event_list_add_events (self, events, !past); +} diff --git a/subprojects/libcmatrix/src/events/cm-room-event-private.h b/subprojects/libcmatrix/src/events/cm-room-event-private.h new file mode 100644 index 0000000000000000000000000000000000000000..e7aa1c990827f398f829a7e18ab240e5d3d0b2b3 --- /dev/null +++ b/subprojects/libcmatrix/src/events/cm-room-event-private.h @@ -0,0 +1,43 @@ +/* -*- mode: c; c-basic-offset: 2; indent-tabs-mode: nil; -*- */ +/* + * Copyright (C) 2022 Purism SPC + * + * SPDX-License-Identifier: LGPL-2.1-or-later + */ + +#pragma once + +#if !defined(_CMATRIX_TAKEN) && !defined(CMATRIX_COMPILATION) +# error "Only <cmatrix.h> can be included directly." +#endif + +#include <json-glib/json-glib.h> + +#include "cm-types.h" +#include "cm-room-event.h" + +G_BEGIN_DECLS + +CmRoomEvent *cm_room_event_new_from_json (gpointer room, + JsonObject *root, + JsonObject *encrypted); +const char *cm_room_event_get_room_name (CmRoomEvent *self); +const char *cm_room_event_get_encryption (CmRoomEvent *self); +JsonObject *cm_room_event_get_room_member_json (CmRoomEvent *self, + const char **user_id); +void cm_room_event_set_room_member (CmRoomEvent *self, + CmUser *user); +GRefString *cm_room_event_get_room_member_id (CmRoomEvent *self); +gboolean cm_room_event_user_has_power (CmRoomEvent *self, + const char *user_id, + CmEventType event); + +GPtrArray *cm_room_event_get_admin_ids (CmRoomEvent *self); +void cm_room_event_set_admin_users (CmRoomEvent *self, + GPtrArray *users); +CmStatus cm_room_event_get_status (CmRoomEvent *self); +const char *cm_room_event_get_replacement_room_id (CmRoomEvent *self); +guint cm_room_event_get_rotation_count (CmRoomEvent *self); +gint64 cm_room_event_get_rotation_time (CmRoomEvent *self); + +G_END_DECLS diff --git a/subprojects/libcmatrix/src/events/cm-room-event.c b/subprojects/libcmatrix/src/events/cm-room-event.c new file mode 100644 index 0000000000000000000000000000000000000000..a9b0f96495ff46492f791a89c1d090408d17f1af --- /dev/null +++ b/subprojects/libcmatrix/src/events/cm-room-event.c @@ -0,0 +1,476 @@ +/* -*- mode: c; c-basic-offset: 2; indent-tabs-mode: nil; -*- */ +/* + * Copyright (C) 2022 Purism SPC + * + * SPDX-License-Identifier: LGPL-2.1-or-later + */ + +#define G_LOG_DOMAIN "cm-room-event" + +#ifdef HAVE_CONFIG_H +# include "config.h" +#endif + +#include "cm-room.h" +#include "cm-room-private.h" +#include "cm-utils-private.h" +#include "cm-event-private.h" +#include "cm-room-message-event-private.h" +#include "cm-room-event-private.h" +#include "cm-room-event.h" + +typedef struct +{ + CmRoom *room; + char *room_name; + char *encryption; + GRefString *member_id; + char *replacement_room_id; + GPtrArray *users; + JsonObject *json; + CmStatus member_status; + + guint enc_rotation_count; + guint enc_rotation_time; +} CmRoomEventPrivate; + +G_DEFINE_TYPE_WITH_PRIVATE (CmRoomEvent, cm_room_event, CM_TYPE_EVENT) + +#define ret_val_if_fail(_event, _val1, _val2, _ret) do { \ + CmEventType type, val; \ + \ + val = _val1 ?: _val2; \ + type = cm_event_get_m_type (CM_EVENT (_event)); \ + g_return_val_if_fail (type == _val1 || type == val, _ret); \ + } while (0) + +#define ret_if_fail(_event, _expected) do { \ + CmEventType type; \ + \ + type = cm_event_get_m_type (CM_EVENT (_event)); \ + g_return_if_fail (type == _expected); \ + } while (0) + +static void +cm_room_event_finalize (GObject *object) +{ + CmRoomEvent *self = (CmRoomEvent *)object; + CmRoomEventPrivate *priv = cm_room_event_get_instance_private (self); + + g_clear_object (&priv->room); + g_free (priv->room_name); + g_free (priv->encryption); + g_clear_pointer (&priv->member_id, g_ref_string_release); + g_free (priv->replacement_room_id); + g_clear_pointer (&priv->users, g_ptr_array_unref); + g_clear_pointer (&priv->json, json_object_unref); + + G_OBJECT_CLASS (cm_room_event_parent_class)->finalize (object); +} + +static void +cm_room_event_class_init (CmRoomEventClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + + object_class->finalize = cm_room_event_finalize; +} + +static void +cm_room_event_init (CmRoomEvent *self) +{ +} + +CmRoomEvent * +cm_room_event_new_from_json (gpointer room, + JsonObject *root, + JsonObject *encrypted) +{ + CmRoomEventPrivate *priv; + CmRoomEvent *self = NULL; + JsonObject *child; + const char *value, *event_type; + CmEventType type; + + g_return_val_if_fail (CM_IS_ROOM (room), NULL); + g_return_val_if_fail (root || encrypted, NULL); + + event_type = cm_utils_json_object_get_string (root, "type"); + + /* currently, only room messages are encrypted */ + if (encrypted && root) + self = cm_room_message_event_new_from_json (root); + + if (!self) + { + if (g_strcmp0 (event_type, "m.room.message") == 0) + self = cm_room_message_event_new_from_json (root); + else + self = g_object_new (CM_TYPE_ROOM_EVENT, NULL); + } + + priv = cm_room_event_get_instance_private (self); + priv->room = g_object_ref (room); + cm_event_set_json (CM_EVENT (self), root, encrypted); + + if (!root) + return self; + + priv->json = json_object_ref (root); + + if (CM_IS_ROOM_MESSAGE_EVENT (self)) + return self; + + type = cm_event_get_m_type (CM_EVENT (self)); + child = cm_utils_json_object_get_object (root, "content"); + + if (type == CM_M_ROOM_NAME) + { + value = cm_utils_json_object_get_string (child, "name"); + g_free (priv->room_name); + priv->room_name = g_strdup (value); + } + else if (type == CM_M_ROOM_ENCRYPTION) + { + value = cm_utils_json_object_get_string (child, "algorithm"); + priv->encryption = g_strdup (value); + priv->enc_rotation_count = cm_utils_json_object_get_int (child, "rotation_period_msgs"); + priv->enc_rotation_time = cm_utils_json_object_get_int (child, "rotation_period_ms"); + + /* Set recommended defaults if not set */ + if (!priv->enc_rotation_count) + priv->enc_rotation_count = 100; + + if (!priv->enc_rotation_time) + priv->enc_rotation_time = 60 * 60 * 24 * 7; /* One week */ + } + else if (type == CM_M_ROOM_MEMBER) + { + const char *membership; + + membership = cm_utils_json_object_get_string (child, "membership"); + priv->member_status = CM_STATUS_UNKNOWN; + + if (g_strcmp0 (membership, "join") == 0) + priv->member_status = CM_STATUS_JOIN; + else if (g_strcmp0 (membership, "invite") == 0) + priv->member_status = CM_STATUS_INVITE; + else if (g_strcmp0 (membership, "leave") == 0) + priv->member_status = CM_STATUS_LEAVE; + else if (g_strcmp0 (membership, "ban") == 0) + priv->member_status = CM_STATUS_BAN; + else if (g_strcmp0 (membership, "knock") == 0) + priv->member_status = CM_STATUS_KNOCK; + + if (priv->member_status == CM_STATUS_INVITE) + priv->member_id = g_ref_string_new_intern (cm_event_get_state_key (CM_EVENT (self))); + else + priv->member_id = g_ref_string_new_intern (cm_event_get_sender_id (CM_EVENT (self))); + } + else if (type == CM_M_ROOM_TOMBSTONE) + { + value = cm_utils_json_object_get_string (child, "replacement_room"); + priv->replacement_room_id = g_strdup (value); + } + + return self; +} + +CmRoom * +cm_room_event_get_room (CmRoomEvent *self) +{ + CmRoomEventPrivate *priv = cm_room_event_get_instance_private (self); + + g_return_val_if_fail (CM_IS_ROOM_EVENT (self), NULL); + + return priv->room; +} + +const char * +cm_room_event_get_room_name (CmRoomEvent *self) +{ + CmRoomEventPrivate *priv = cm_room_event_get_instance_private (self); + + g_return_val_if_fail (CM_IS_ROOM_EVENT (self), NULL); + ret_val_if_fail (self, CM_M_ROOM_NAME, 0, NULL); + + return priv->room_name; +} + +const char * +cm_room_event_get_encryption (CmRoomEvent *self) +{ + CmRoomEventPrivate *priv = cm_room_event_get_instance_private (self); + + g_return_val_if_fail (CM_IS_ROOM_EVENT (self), NULL); + ret_val_if_fail (self, CM_M_ROOM_ENCRYPTION, 0, NULL); + + return priv->encryption; +} + +JsonObject * +cm_room_event_get_room_member_json (CmRoomEvent *self, + const char **user_id) +{ + CmRoomEventPrivate *priv = cm_room_event_get_instance_private (self); + JsonObject *child; + + g_return_val_if_fail (CM_IS_ROOM_EVENT (self), NULL); + ret_val_if_fail (self, CM_M_ROOM_MEMBER, 0, NULL); + + child = cm_utils_json_object_get_object (priv->json, "content"); + + if (user_id) + { + if (g_strcmp0 (cm_utils_json_object_get_string (child, "membership"), "join") == 0) + *user_id = cm_utils_json_object_get_string (priv->json, "sender"); + else + *user_id = cm_utils_json_object_get_string (priv->json, "state_key"); + + if (G_UNLIKELY (!*user_id || !**user_id)) + *user_id = cm_utils_json_object_get_string (priv->json, "sender"); + } + + return child; +} + +void +cm_room_event_set_room_member (CmRoomEvent *self, + CmUser *user) +{ + CmRoomEventPrivate *priv = cm_room_event_get_instance_private (self); + const char *user_id; + + g_return_if_fail (CM_IS_ROOM_EVENT (self)); + g_return_if_fail (CM_IS_USER (user)); + g_return_if_fail (!priv->users); + ret_if_fail (self, CM_M_ROOM_MEMBER); + + cm_room_event_get_room_member_json (self, &user_id); + g_return_if_fail (g_strcmp0 (cm_user_get_id (user), user_id) == 0); + + priv->users = g_ptr_array_new_full (1, g_object_unref); + g_ptr_array_add (priv->users, g_object_ref (user)); +} + +CmUser * +cm_room_event_get_room_member (CmRoomEvent *self) +{ + CmRoomEventPrivate *priv = cm_room_event_get_instance_private (self); + + g_return_val_if_fail (CM_IS_ROOM_EVENT (self), NULL); + ret_val_if_fail (self, CM_M_ROOM_MEMBER, 0, NULL); + g_return_val_if_fail (priv->users, NULL); + + return priv->users->pdata[0]; +} + +GRefString * +cm_room_event_get_room_member_id (CmRoomEvent *self) +{ + CmRoomEventPrivate *priv = cm_room_event_get_instance_private (self); + + g_return_val_if_fail (CM_IS_ROOM_EVENT (self), NULL); + ret_val_if_fail (self, CM_M_ROOM_MEMBER, 0, NULL); + + return priv->member_id; +} + +gboolean +cm_room_event_user_has_power (CmRoomEvent *self, + const char *user_id, + CmEventType event) +{ + CmRoomEventPrivate *priv = cm_room_event_get_instance_private (self); + JsonObject *child, *content; + int user_power = 0; + + g_return_val_if_fail (CM_IS_ROOM_EVENT (self), FALSE); + g_return_val_if_fail (user_id && *user_id == '@', FALSE); + g_return_val_if_fail (priv->json, FALSE); + ret_val_if_fail (self, CM_M_ROOM_POWER_LEVELS, 0, FALSE); + + if (!priv->json) + return FALSE; + + content = cm_utils_json_object_get_object (priv->json, "content"); + child = cm_utils_json_object_get_object (content, "users"); + user_power = cm_utils_json_object_get_int (child, user_id); + + if (!user_power) + user_power = cm_utils_json_object_get_int (content, "users_default"); + + child = cm_utils_json_object_get_object (content, "events"); + + if (event == CM_M_ROOM_NAME) + return user_power >= cm_utils_json_object_get_int (child, "m.room.name"); + + if (event == CM_M_ROOM_POWER_LEVELS) + return user_power >= cm_utils_json_object_get_int (child, "m.room.power_levels"); + + if (event == CM_M_ROOM_HISTORY_VISIBILITY) + return user_power >= cm_utils_json_object_get_int (child, "m.room.history_visibility"); + + if (event == CM_M_ROOM_CANONICAL_ALIAS) + return user_power >= cm_utils_json_object_get_int (child, "m.room.canonical_alias"); + + if (event == CM_M_ROOM_AVATAR) + return user_power >= cm_utils_json_object_get_int (child, "m.room.avatar"); + + if (event == CM_M_ROOM_TOMBSTONE) + return user_power >= cm_utils_json_object_get_int (child, "m.room.tombstone"); + + if (event == CM_M_ROOM_SERVER_ACL) + return user_power >= cm_utils_json_object_get_int (child, "m.room.server_acl"); + + if (event == CM_M_ROOM_ENCRYPTION) + return user_power >= cm_utils_json_object_get_int (child, "m.room.encryption"); + + if (event == CM_M_ROOM_INVITE) + { + if (!cm_utils_json_object_has_member (content, "invite")) + return user_power >= 50; + + return user_power >= cm_utils_json_object_get_int (content, "invite"); + } + + if (event == CM_M_ROOM_BAN) + { + if (!cm_utils_json_object_has_member (content, "ban")) + return user_power >= 50; + + return user_power >= cm_utils_json_object_get_int (content, "ban"); + } + + if (event == CM_M_ROOM_KICK) + { + if (!cm_utils_json_object_has_member (content, "kick")) + return user_power >= 50; + + return user_power >= cm_utils_json_object_get_int (content, "kick"); + } + + return user_power >= cm_utils_json_object_get_int (content, "events_default"); +} + +/* + * cm_room_event_get_power_level_admins: + * @self: A #CmRoomEvent + * + * Get the list of users that have power greater than + * the default room power level, as reported by the + * server. + * + * Returns: (transfer full): An GPtrArray of strings + */ +GPtrArray * +cm_room_event_get_admin_ids (CmRoomEvent *self) +{ + CmRoomEventPrivate *priv = cm_room_event_get_instance_private (self); + g_autoptr(GList) users = NULL; + GPtrArray *admin_ids; + JsonObject *child; + + g_return_val_if_fail (CM_IS_ROOM_EVENT (self), NULL); + g_return_val_if_fail (priv->json, NULL); + ret_val_if_fail (self, CM_M_ROOM_POWER_LEVELS, 0, NULL); + + child = cm_utils_json_object_get_object (priv->json, "content"); + child = cm_utils_json_object_get_object (child, "users"); + if (child) + users = json_object_get_members (child); + + admin_ids = g_ptr_array_new_full (16, g_free); + + for (GList *node = users; node && node->data; node = node->next) + g_ptr_array_add (admin_ids, g_strdup (node->data)); + + return admin_ids; +} + +/* + * cm_room_event_set_admin_users: + * @self: A #CmRoomEvent + * @users: (transfer full): An array of #CmUser + * + * Set the list of admin users, the list of user id + * should match cm_room_event_get_admin_ids() + * + * each member in @users should have a ref added. + */ +void +cm_room_event_set_admin_users (CmRoomEvent *self, + GPtrArray *users) +{ + CmRoomEventPrivate *priv = cm_room_event_get_instance_private (self); + JsonObject *child; + + g_return_if_fail (CM_IS_ROOM_EVENT (self)); + g_return_if_fail (users); + g_return_if_fail (priv->json); + g_return_if_fail (!priv->users); + ret_if_fail (self, CM_M_ROOM_POWER_LEVELS); + + child = cm_utils_json_object_get_object (priv->json, "content"); + child = cm_utils_json_object_get_object (child, "users"); + g_return_if_fail (child); + g_return_if_fail (json_object_get_size (child) == users->len); + + for (guint i = 0; i < users->len; i++) + { + CmUser *user = users->pdata[i]; + + /* Never supposed to happen */ + if (!json_object_has_member (child, cm_user_get_id (user))) + { + g_critical ("User '%s' not in list", cm_user_get_id (user)); + return; + } + } + + priv->users = users; +} + +CmStatus +cm_room_event_get_status (CmRoomEvent *self) +{ + CmRoomEventPrivate *priv = cm_room_event_get_instance_private (self); + + g_return_val_if_fail (CM_IS_ROOM_EVENT (self), CM_STATUS_UNKNOWN); + ret_val_if_fail (self, CM_M_ROOM_MEMBER, 0, CM_STATUS_UNKNOWN); + + return priv->member_status; +} + +const char * +cm_room_event_get_replacement_room_id (CmRoomEvent *self) +{ + CmRoomEventPrivate *priv = cm_room_event_get_instance_private (self); + + g_return_val_if_fail (CM_IS_ROOM_EVENT (self), NULL); + ret_val_if_fail (self, CM_M_ROOM_TOMBSTONE, 0, NULL); + + return priv->replacement_room_id; +} + +guint +cm_room_event_get_rotation_count (CmRoomEvent *self) +{ + CmRoomEventPrivate *priv = cm_room_event_get_instance_private (self); + + g_return_val_if_fail (CM_IS_ROOM_EVENT (self), 100); + ret_val_if_fail (self, CM_M_ROOM_ENCRYPTION, 0, 100); + + return priv->enc_rotation_count; +} + +gint64 +cm_room_event_get_rotation_time (CmRoomEvent *self) +{ + CmRoomEventPrivate *priv = cm_room_event_get_instance_private (self); + + g_return_val_if_fail (CM_IS_ROOM_EVENT (self), 100); + ret_val_if_fail (self, CM_M_ROOM_ENCRYPTION, 0, 100); + + return priv->enc_rotation_time; +} diff --git a/subprojects/libcmatrix/src/events/cm-room-event.h b/subprojects/libcmatrix/src/events/cm-room-event.h new file mode 100644 index 0000000000000000000000000000000000000000..2e77ea35a44092e0c7aca4810436944965c2ccdd --- /dev/null +++ b/subprojects/libcmatrix/src/events/cm-room-event.h @@ -0,0 +1,38 @@ +/* -*- mode: c; c-basic-offset: 2; indent-tabs-mode: nil; -*- */ +/* cm-room-event.h + * + * Copyright 2022 Purism SPC + * + * Author(s): + * Mohammed Sadiq <sadiq@sadiqpk.org> + * + * SPDX-License-Identifier: LGPL-2.1-or-later + */ + +#pragma once + +#if !defined(_CMATRIX_TAKEN) && !defined(CMATRIX_COMPILATION) +# error "Only <cmatrix.h> can be included directly." +#endif + +#include "cm-event.h" +#include "cm-types.h" + +G_BEGIN_DECLS + +#define CM_TYPE_ROOM_EVENT (cm_room_event_get_type ()) + +G_DECLARE_DERIVABLE_TYPE (CmRoomEvent, cm_room_event, CM, ROOM_EVENT, CmEvent) + +struct _CmRoomEventClass +{ + CmEventClass parent_class; + + /*< private >*/ + gpointer reserved[8]; +}; + +CmRoom *cm_room_event_get_room (CmRoomEvent *self); +CmUser *cm_room_event_get_room_member (CmRoomEvent *self); + +G_END_DECLS diff --git a/subprojects/libcmatrix/src/events/cm-room-message-event-private.h b/subprojects/libcmatrix/src/events/cm-room-message-event-private.h new file mode 100644 index 0000000000000000000000000000000000000000..0f36b31e0a7cba5acd93988a80ecb4c2bda8227a --- /dev/null +++ b/subprojects/libcmatrix/src/events/cm-room-message-event-private.h @@ -0,0 +1,33 @@ +/* -*- mode: c; c-basic-offset: 2; indent-tabs-mode: nil; -*- */ +/* + * Copyright (C) 2022 Purism SPC + * + * SPDX-License-Identifier: LGPL-2.1-or-later + */ + +#pragma once + +#if !defined(_CMATRIX_TAKEN) && !defined(CMATRIX_COMPILATION) +# error "Only <cmatrix.h> can be included directly." +#endif + +#include <json-glib/json-glib.h> +#include <glib-object.h> +#include <gio/gio.h> + +#include "cm-room-event-private.h" +#include "cm-room-message-event.h" + +G_BEGIN_DECLS + +CmRoomMessageEvent *cm_room_message_event_new (CmContentType type); +CmRoomEvent *cm_room_message_event_new_from_json (JsonObject *root); +void cm_room_message_event_set_body (CmRoomMessageEvent *self, + const char *text); +void cm_room_message_event_set_file (CmRoomMessageEvent *self, + const char *body, + GFile *file); +GFile *cm_room_message_event_get_file (CmRoomMessageEvent *self); + + +G_END_DECLS diff --git a/subprojects/libcmatrix/src/events/cm-room-message-event.c b/subprojects/libcmatrix/src/events/cm-room-message-event.c new file mode 100644 index 0000000000000000000000000000000000000000..485affea2df755cfa3fb7c79a0a75e8fdfc23cbc --- /dev/null +++ b/subprojects/libcmatrix/src/events/cm-room-message-event.c @@ -0,0 +1,450 @@ +/* -*- mode: c; c-basic-offset: 2; indent-tabs-mode: nil; -*- */ +/* + * Copyright (C) 2022 Purism SPC + * + * Author(s): + * Mohammed Sadiq <sadiq@sadiqpk.org> + * + * SPDX-License-Identifier: LGPL-2.1-or-later + */ + +#define G_LOG_DOMAIN "cm-room-message-event" + +#ifdef HAVE_CONFIG_H +# include "config.h" +#endif + +#include "cm-utils-private.h" +#include "cm-common.h" +#include "cm-matrix-private.h" +#include "cm-enums.h" +#include "cm-client-private.h" +#include "cm-room-private.h" +#include "cm-input-stream-private.h" +#include "cm-event-private.h" +#include "cm-room-message-event-private.h" + +struct _CmRoomMessageEvent +{ + CmRoomEvent parent_instance; + + JsonObject *json; + CmContentType type; + + char *body; + GFile *file; + GInputStream *file_istream; + char *mxc_uri; + + gboolean downloading_file; +}; + +G_DEFINE_TYPE (CmRoomMessageEvent, cm_room_message_event, CM_TYPE_ROOM_EVENT) + +static JsonObject * +room_message_generate_json (CmRoomMessageEvent *self, + CmRoom *room) +{ + g_autofree char *uri = NULL; + const char *body, *room_id; + CmClient *client; + JsonObject *root; + GFile *file; + + g_assert (CM_IS_ROOM_MESSAGE_EVENT (self)); + g_assert (CM_IS_ROOM (room)); + + body = cm_room_message_event_get_body (self); + file = cm_room_message_event_get_file (self); + client = cm_room_get_client (room); + room_id = cm_room_get_id (room); + + root = json_object_new (); + if (file) + { + g_autofree char *name = NULL; + + name = g_file_get_basename (file); + json_object_set_string_member (root, "msgtype", "m.file"); + json_object_set_string_member (root, "body", name); + json_object_set_string_member (root, "filename", name); + if (!cm_room_is_encrypted (room)) + { + const char *mxc_uri; + + mxc_uri = g_object_get_data (G_OBJECT (file), "uri"); + if (mxc_uri) + json_object_set_string_member (root, "url", mxc_uri); + else + g_warn_if_reached (); + } + } + else + { + json_object_set_string_member (root, "msgtype", "m.text"); + json_object_set_string_member (root, "body", body); + } + + if (cm_room_is_encrypted (room)) + { + g_autofree char *text = NULL; + JsonObject *object; + + object = json_object_new (); + json_object_set_string_member (object, "type", "m.room.message"); + json_object_set_string_member (object, "room_id", room_id); + json_object_set_object_member (object, "content", root); + + if (file) + { + JsonObject *file_json; + CmInputStream *stream; + + stream = g_object_get_data (G_OBJECT (file), "stream"); + file_json = cm_input_stream_get_file_json (stream); + json_object_set_object_member (root, "file", file_json); + } + + text = cm_utils_json_object_to_string (object, FALSE); + json_object_unref (object); + object = cm_enc_encrypt_for_chat (cm_client_get_enc (client), + room, text); + return object; + } + else + { + return root; + } +} + +static gpointer +cm_room_message_event_generate_json (CmEvent *event, + gpointer room) +{ + CmRoomMessageEvent *self = (CmRoomMessageEvent *)event; + + g_assert (CM_IS_ROOM_MESSAGE_EVENT (self)); + g_return_val_if_fail (CM_IS_ROOM (room), NULL); + + return room_message_generate_json (self, room); +} + +static char * +cm_room_message_event_get_api_url (CmEvent *event, + gpointer room) +{ + CmRoomMessageEvent *self = (CmRoomMessageEvent *)event; + char *uri; + + g_assert (CM_IS_ROOM_MESSAGE_EVENT (self)); + g_return_val_if_fail (CM_IS_ROOM (room), NULL); + g_return_val_if_fail (cm_event_get_txn_id (event), NULL); + + if (cm_room_is_encrypted (room)) + uri = g_strdup_printf ("/_matrix/client/r0/rooms/%s/send/m.room.encrypted/%s", + cm_room_get_id (room), + cm_event_get_txn_id (event)); + else + uri = g_strdup_printf ("/_matrix/client/r0/rooms/%s/send/m.room.message/%s", + cm_room_get_id (room), + cm_event_get_txn_id (event)); + + return uri; +} + +static void +cm_room_message_event_finalize (GObject *object) +{ + CmRoomMessageEvent *self = (CmRoomMessageEvent *)object; + + g_clear_pointer (&self->json, json_object_unref); + g_free (self->body); + g_free (self->mxc_uri); + + G_OBJECT_CLASS (cm_room_message_event_parent_class)->finalize (object); +} + +static void +cm_room_message_event_class_init (CmRoomMessageEventClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + CmEventClass *event_class = CM_EVENT_CLASS (klass); + + object_class->finalize = cm_room_message_event_finalize; + + event_class->generate_json = cm_room_message_event_generate_json; + event_class->get_api_url = cm_room_message_event_get_api_url; +} + +static void +cm_room_message_event_init (CmRoomMessageEvent *self) +{ +} + +CmRoomMessageEvent * +cm_room_message_event_new (CmContentType type) +{ + CmRoomMessageEvent *self; + + self = g_object_new (CM_TYPE_ROOM_MESSAGE_EVENT, NULL); + self->type = type; + + return self; +} + +CmRoomEvent * +cm_room_message_event_new_from_json (JsonObject *root) +{ + CmRoomMessageEvent *self; + const char *type, *body; + JsonObject *child; + + g_return_val_if_fail (root, NULL); + + type = cm_utils_json_object_get_string (root, "type"); + + if (g_strcmp0 (type, "m.room.message") != 0) + g_return_val_if_reached (NULL); + + self = g_object_new (CM_TYPE_ROOM_MESSAGE_EVENT, NULL); + self->json = json_object_ref (root); + + child = cm_utils_json_object_get_object (root, "content"); + type = cm_utils_json_object_get_string (child, "msgtype"); + body = cm_utils_json_object_get_string (child, "body"); + self->body = g_strdup (body); + self->mxc_uri = g_strdup (cm_utils_json_object_get_string (child, "url")); + + if (g_strcmp0 (type, "m.text") == 0) + self->type = CM_CONTENT_TYPE_TEXT; + else if (g_strcmp0 (type, "m.file") == 0) + self->type = CM_CONTENT_TYPE_FILE; + else if (g_strcmp0 (type, "m.image") == 0) + self->type = CM_CONTENT_TYPE_IMAGE; + else if (g_strcmp0 (type, "m.audio") == 0) + self->type = CM_CONTENT_TYPE_AUDIO; + else if (g_strcmp0 (type, "m.location") == 0) + self->type = CM_CONTENT_TYPE_LOCATION; + else if (g_strcmp0 (type, "m.emote") == 0) + self->type = CM_CONTENT_TYPE_EMOTE; + else if (g_strcmp0 (type, "m.notice") == 0) + self->type = CM_CONTENT_TYPE_NOTICE; + + return CM_ROOM_EVENT (self); +} + +CmContentType +cm_room_message_event_get_msg_type (CmRoomMessageEvent *self) +{ + g_return_val_if_fail (CM_IS_ROOM_MESSAGE_EVENT (self), CM_CONTENT_TYPE_UNKNOWN); + + return self->type; +} + +void +cm_room_message_event_set_body (CmRoomMessageEvent *self, + const char *text) +{ + g_return_if_fail (CM_IS_ROOM_MESSAGE_EVENT (self)); + g_return_if_fail (self->type == CM_CONTENT_TYPE_TEXT); + + g_free (self->body); + self->body = g_strdup (text); +} + +const char * +cm_room_message_event_get_body (CmRoomMessageEvent *self) +{ + g_return_val_if_fail (CM_IS_ROOM_MESSAGE_EVENT (self), NULL); + + return self->body; +} + +void +cm_room_message_event_set_file (CmRoomMessageEvent *self, + const char *body, + GFile *file) +{ + g_return_if_fail (CM_IS_ROOM_MESSAGE_EVENT (self)); + g_return_if_fail (self->type == CM_CONTENT_TYPE_FILE); + g_return_if_fail (!self->file); + + g_set_object (&self->file, file); + g_free (self->body); + + if (body && *body) + self->body = g_strdup (body); + else if (file) + self->body = g_file_get_basename (file); +} + +GFile * +cm_room_message_event_get_file (CmRoomMessageEvent *self) +{ + g_return_val_if_fail (CM_IS_ROOM_MESSAGE_EVENT (self), NULL); + + return self->file; +} + +static void +message_file_stream_cb (GObject *obj, + GAsyncResult *result, + gpointer user_data) +{ + CmRoomMessageEvent *self; + g_autoptr(GTask) task = user_data; + GError *error = NULL; + + g_assert (G_IS_TASK (task)); + + self = g_task_get_source_object (task); + g_assert (CM_IS_ROOM_MESSAGE_EVENT (self)); + + g_output_stream_splice_finish (G_OUTPUT_STREAM (obj), result, &error); + self->downloading_file = FALSE; + + if (error) { + g_task_return_error (task, error); + } else { + g_autofree char *file_name = NULL; + GFile *out_file; + + out_file = g_object_steal_data (user_data, "out-file"); + self->file_istream = (GInputStream *)g_file_read (out_file, NULL, NULL); + g_object_set_data_full (G_OBJECT (self->file_istream), "out-file", + out_file, g_object_unref); + g_task_return_pointer (task, g_object_ref (self->file_istream), g_object_unref); + } +} + +static void +message_event_get_file_cb (GObject *object, + GAsyncResult *result, + gpointer user_data) +{ + CmRoomMessageEvent *self; + g_autoptr(GTask) task = user_data; + g_autoptr(GInputStream) istream = NULL; + GOutputStream *out_stream = NULL; + GFile *out_file = NULL; + GError *error = NULL; + + g_assert (G_IS_TASK (task)); + + self = g_task_get_source_object (task); + g_assert (CM_IS_ROOM_MESSAGE_EVENT (self)); + + istream = cm_client_get_file_finish (CM_CLIENT (object), result, &error); + self->downloading_file = FALSE; + + if (error) + { + g_task_return_error (task, error); + + return; + } + else + { + g_autofree char *file_path = NULL; + g_autofree char *file_name = NULL; + const char *path; + + path = cm_matrix_get_data_dir (); + file_name = g_path_get_basename (self->mxc_uri); + file_path = cm_utils_get_path_for_m_type (path, CM_M_ROOM_MESSAGE, FALSE, file_name); + out_file = g_file_new_build_filename (file_path, NULL); + out_stream = (GOutputStream *)g_file_create (out_file, G_FILE_CREATE_NONE, NULL, &error); + g_object_set_data_full (G_OBJECT (task), "out-file", out_file, g_object_unref); + } + + if (g_error_matches (error, G_IO_ERROR, G_IO_ERROR_EXISTS)) + { + GFileIOStream *io_stream; + + g_clear_error (&error); + io_stream = g_file_open_readwrite (out_file, NULL, &error); + if (io_stream) + { + g_object_set_data_full (G_OBJECT (task), "io-stream", io_stream, g_object_unref); + out_stream = g_io_stream_get_output_stream (G_IO_STREAM (io_stream)); + } + } + + if (out_stream) + { + self->downloading_file = TRUE; + g_output_stream_splice_async (G_OUTPUT_STREAM (out_stream), istream, + G_OUTPUT_STREAM_SPLICE_CLOSE_SOURCE | + G_OUTPUT_STREAM_SPLICE_CLOSE_TARGET, + 0, NULL, + message_file_stream_cb, + g_steal_pointer (&task)); + } + else + { + if (error) + g_task_return_error (task, error); + else + g_task_return_boolean (task, FALSE); + } +} + +void +cm_room_message_event_get_file_async (CmRoomMessageEvent *self, + GCancellable *cancellable, + GFileProgressCallback progress_callback, + gpointer progress_user_data, + GAsyncReadyCallback callback, + gpointer user_data) +{ + CmRoom *room; + GTask *task; + + g_return_if_fail (CM_IS_ROOM_MESSAGE_EVENT (self)); + g_return_if_fail (!cancellable || G_IS_CANCELLABLE (cancellable)); + g_return_if_fail (self->type == CM_CONTENT_TYPE_FILE || + self->type == CM_CONTENT_TYPE_AUDIO || + self->type == CM_CONTENT_TYPE_IMAGE); + + task = g_task_new (self, cancellable, callback, user_data); + g_object_set_data (G_OBJECT (task), "progress-cb", progress_callback); + g_object_set_data (G_OBJECT (task), "progress-cb-data", progress_user_data); + + if (self->downloading_file) + { + g_task_return_new_error (task, G_IO_ERROR, G_IO_ERROR_PENDING, + "File already being downloaded"); + return; + } + + if (self->file && !self->mxc_uri && !self->file_istream) + self->file_istream = (gpointer)g_file_read (self->file, NULL, NULL); + + if (self->file_istream) + { + g_task_return_pointer (task, g_object_ref (self->file_istream), g_object_unref); + return; + } + + g_return_if_fail (self->mxc_uri); + + self->downloading_file = TRUE; + room = cm_room_event_get_room (CM_ROOM_EVENT (self)); + cm_client_get_file_async (cm_room_get_client (room), + self->mxc_uri, + cancellable, + NULL, NULL, + message_event_get_file_cb, task); +} + +GInputStream * +cm_room_message_event_get_file_finish (CmRoomMessageEvent *self, + GAsyncResult *result, + GError **error) +{ + g_return_val_if_fail (CM_IS_ROOM_MESSAGE_EVENT (self), FALSE); + g_return_val_if_fail (G_IS_TASK (result), FALSE); + g_return_val_if_fail (!error || !*error, FALSE); + + return g_task_propagate_pointer (G_TASK (result), error); +} diff --git a/subprojects/libcmatrix/src/events/cm-room-message-event.h b/subprojects/libcmatrix/src/events/cm-room-message-event.h new file mode 100644 index 0000000000000000000000000000000000000000..50f640d700c5ba4824bcb0632c221c3886137d6f --- /dev/null +++ b/subprojects/libcmatrix/src/events/cm-room-message-event.h @@ -0,0 +1,43 @@ +/* -*- mode: c; c-basic-offset: 2; indent-tabs-mode: nil; -*- */ +/* cm-room-message-event.h + * + * Copyright 2022 Purism SPC + * + * Author(s): + * Mohammed Sadiq <sadiq@sadiqpk.org> + * + * SPDX-License-Identifier: LGPL-2.1-or-later + */ + +#pragma once + +#if !defined(_CMATRIX_TAKEN) && !defined(CMATRIX_COMPILATION) +# error "Only <cmatrix.h> can be included directly." +#endif + +#include <glib-object.h> +#include <gio/gio.h> + +#include "cm-room-event.h" +#include "cm-enums.h" + +G_BEGIN_DECLS + +#define CM_TYPE_ROOM_MESSAGE_EVENT (cm_room_message_event_get_type ()) + +G_DECLARE_FINAL_TYPE (CmRoomMessageEvent, cm_room_message_event, CM, ROOM_MESSAGE_EVENT, CmRoomEvent) + +CmContentType cm_room_message_event_get_msg_type (CmRoomMessageEvent *self); +const char *cm_room_message_event_get_body (CmRoomMessageEvent *self); +void cm_room_message_event_get_file_async (CmRoomMessageEvent *self, + GCancellable *cancellable, + GFileProgressCallback progress_callback, + gpointer progress_user_data, + GAsyncReadyCallback callback, + gpointer user_data); +GInputStream *cm_room_message_event_get_file_finish (CmRoomMessageEvent *self, + GAsyncResult *result, + GError **error); + + +G_END_DECLS diff --git a/subprojects/libcmatrix/src/meson.build b/subprojects/libcmatrix/src/meson.build new file mode 100644 index 0000000000000000000000000000000000000000..eadc6f18b7cc81ea22bfc68f76bdd5b6dd81cf0c --- /dev/null +++ b/subprojects/libcmatrix/src/meson.build @@ -0,0 +1,71 @@ +libcmatrix_sources = [ + 'cm-client.c', + 'cm-common.c', + 'cm-db.c', + 'cm-device.c', + 'cm-enc.c', + 'cm-olm.c', + 'cm-olm-sas.c', + 'cm-matrix.c', + 'cm-net.c', + 'cm-room.c', + 'cm-secret-store.c', + 'cm-input-stream.c', + 'cm-utils.c', + 'events/cm-event.c', + 'events/cm-room-event.c', + 'events/cm-room-message-event.c', + 'events/cm-room-event-list.c', + 'users/cm-user.c', + 'users/cm-account.c', + 'users/cm-room-member.c', + 'users/cm-user-list.c', +] + +libcmatrix_headers = [ + 'cmatrix.h', + 'cm-client.h', + 'cm-common.h', + 'cm-device.h', + 'cm-enums.h', + 'cm-matrix.h', + 'cm-room.h', + 'users/cm-user.h', + 'users/cm-account.h', + 'users/cm-room-member.h', +] + +# Currently we do only have static library and should be +# used only as a subproject +cmatrix_lib = static_library('libcmatrix', + [libcmatrix_headers, libcmatrix_sources], + include_directories: [ root_inc, src_inc ], + dependencies: cmatrix_deps +) + +libcmatrix_dep = declare_dependency( + include_directories: include_directories('.'), + dependencies: cmatrix_deps, + link_with: cmatrix_lib +) + +if get_option('gtk_doc') + libcmatrix_gir_extra_args = [ + '-L@0@'.format(meson.current_build_dir()), + '--quiet', + ] + + libcmatrix_gir = gnome.generate_gir(cmatrix_lib, + sources: libcmatrix_sources + libcmatrix_headers, + nsversion: '0', + namespace: 'CM', + export_packages: 'cmatrix-0', + symbol_prefix: 'cm', + identifier_prefix: 'Cm', + link_with: cmatrix_lib, + includes: ['Gio-2.0', 'Soup-2.4'], + install: false, + extra_args: libcmatrix_gir_extra_args, + ) + +endif diff --git a/subprojects/libcmatrix/src/users/cm-account.c b/subprojects/libcmatrix/src/users/cm-account.c new file mode 100644 index 0000000000000000000000000000000000000000..364fec6008334d3fe0cd6e6676ddedcb1d3476d7 --- /dev/null +++ b/subprojects/libcmatrix/src/users/cm-account.c @@ -0,0 +1,480 @@ +/* -*- mode: c; c-basic-offset: 2; indent-tabs-mode: nil; -*- */ +/* cm-account.c + * + * Copyright 2022 Purism SPC + * + * Author(s): + * Mohammed Sadiq <sadiq@sadiqpk.org> + * + * SPDX-License-Identifier: LGPL-2.1-or-later + */ + +#define G_LOG_DOMAIN "cm-account" + +#ifdef HAVE_CONFIG_H +# include "config.h" +#endif + +#include <libsoup/soup.h> + +#include "cm-client-private.h" +#include "cm-net-private.h" +#include "cm-utils-private.h" +#include "cm-user-private.h" +#include "cm-user.h" +#include "cm-account.h" + +/** + * SECTION: cm-account + * @title: CmAccount + * @short_description: A high-level API to manage the client owner’s account. + * @include: "cm-account.h" + */ + +struct _CmAccount +{ + CmUser parent_instance; + + /* @login_username can be email/[incomplete] matrix-id/phone-number etc */ + char *login_id; +}; + +G_DEFINE_TYPE (CmAccount, cm_account, CM_TYPE_USER) + +static void +cm_account_finalize (GObject *object) +{ + CmAccount *self = (CmAccount *)object; + + g_free (self->login_id); + + G_OBJECT_CLASS (cm_account_parent_class)->finalize (object); +} + +static void +cm_account_class_init (CmAccountClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + + object_class->finalize = cm_account_finalize; +} + +static void +cm_account_init (CmAccount *self) +{ +} + +/** + * cm_account_set_login_id: + * @self: A #CmAccount + * @login_id: A login ID for the client. + * + * Set login ID for the account. The login can be a + * fully qualified matrix ID, or an email address + * which shall be used to log in to the server + * in to the server. + * + * Returns: %TRUE if @login_id was successfully set, + * %FALSE otherwise. + */ +gboolean +cm_account_set_login_id (CmAccount *self, + const char *login_id) +{ + g_autoptr(GString) str = NULL; + CmClient *client; + + g_return_val_if_fail (CM_IS_ACCOUNT (self), FALSE); + + client = cm_user_get_client (CM_USER (self)); + g_return_val_if_fail (CM_IS_CLIENT (client), FALSE); + g_return_val_if_fail (!cm_client_get_logged_in (client), FALSE); + g_return_val_if_fail (!cm_client_get_logged_in (client), FALSE); + g_return_val_if_fail (!cm_client_is_sync (client), FALSE); + + if (login_id && g_strcmp0 (login_id, self->login_id) == 0) + return TRUE; + + str = g_string_new (NULL); + + if (cm_utils_user_name_valid (login_id) || + cm_utils_user_name_is_email (login_id)) + { + g_free (self->login_id); + self->login_id = g_strdup (login_id); + g_debug ("(%p) New login id set: '%s'", client, + cm_utils_anonymize (str, login_id)); + + return TRUE; + } + + g_debug ("(%p) New login id failed to set: '%s'", client, + cm_utils_anonymize (str, login_id)); + + return FALSE; +} + +/** + * cm_account_get_login_id: + * @self: A #CmAccount + * + * Get the login ID set for the account + * + * Returns: (nullable): The login ID of the account + */ +const char * +cm_account_get_login_id (CmAccount *self) +{ + g_return_val_if_fail (CM_IS_ACCOUNT (self), FALSE); + + return self->login_id; +} + +static void +account_set_display_name_cb (GObject *obj, + GAsyncResult *result, + gpointer user_data) +{ + CmAccount *self; + g_autoptr(GTask) task = user_data; + g_autoptr(JsonObject) object = NULL; + GError *error = NULL; + + g_assert (G_IS_TASK (task)); + + self = g_task_get_source_object (task); + g_assert (CM_IS_ACCOUNT (self)); + + object = g_task_propagate_pointer (G_TASK (result), &error); + + if (error) + g_task_return_error (task, error); + else + g_task_return_boolean (task, TRUE); +} + +/** + * cm_account_set_name_async: + * @self: A #CmAccount + * @name: (nullable): The display name for the account user + * @cancellable: A #GCancellable + * @callback: A callback to run when finished + * @user_data: The user data for @callback + * + * Set the user's name of @self to @name. If + * @name is %NULL, the currently set name is + * unset + * + * Finish the call with cm_account_set_name_finish(). + */ +void +cm_account_set_display_name_async (CmAccount *self, + const char *name, + GCancellable *cancellable, + GAsyncReadyCallback callback, + gpointer user_data) +{ + g_autofree char *uri = NULL; + JsonObject *root = NULL; + CmClient *client; + GTask *task; + + g_return_if_fail (CM_IS_ACCOUNT (self)); + g_return_if_fail (!cancellable || G_IS_CANCELLABLE (cancellable)); + + if (name && *name) + { + root = json_object_new (); + json_object_set_string_member (root, "displayname", name); + } + + task = g_task_new (self, cancellable, callback, user_data); + g_task_set_task_data (task, g_strdup (name), g_free); + + client = cm_user_get_client (CM_USER (self)); + uri = g_strdup_printf ("/_matrix/client/r0/profile/%s/displayname", + cm_client_get_user_id (client)); + cm_net_send_json_async (cm_client_get_net (client), 1, + root, uri, SOUP_METHOD_PUT, + NULL, cancellable, account_set_display_name_cb, task); +} + +/** + * cm_account_set_name_finish: + * @self: A #CmAccount + * @result: A #GAsyncResult + * @error: A #GError + * + * Finish the call started with cm_account_set_name_async(). + * + * Returns: %TRUE if setting name was a success. + * %FALSE otherwise with @error set. + */ +gboolean +cm_account_set_display_name_finish (CmAccount *self, + GAsyncResult *result, + GError **error) +{ + g_return_val_if_fail (CM_IS_ACCOUNT (self), FALSE); + g_return_val_if_fail (G_IS_TASK (result), FALSE); + g_return_val_if_fail (!error || !*error, FALSE); + + return g_task_propagate_boolean (G_TASK (result), error); +} + +static void +account_set_user_avatar_cb (GObject *obj, + GAsyncResult *result, + gpointer user_data) +{ + CmAccount *self; + g_autoptr(GTask) task = user_data; + g_autoptr(JsonObject) object = NULL; + GError *error = NULL; + + g_assert (G_IS_TASK (task)); + + self = g_task_get_source_object (task); + g_assert (CM_IS_ACCOUNT (self)); + + object = g_task_propagate_pointer (G_TASK (result), &error); + + if (error) + g_task_return_error (task, error); + else + g_task_return_boolean (task, TRUE); +} + +void +cm_account_set_user_avatar_async (CmAccount *self, + GFile *image_file, + GCancellable *cancellable, + GAsyncReadyCallback callback, + gpointer user_data) +{ + g_autoptr(GTask) task = NULL; + CmClient *client; + CmNet *cm_net; + + g_return_if_fail (CM_IS_ACCOUNT (self)); + g_return_if_fail (!cancellable || G_IS_CANCELLABLE (cancellable)); + + task = g_task_new (self, cancellable, callback, user_data); + client = cm_user_get_client (CM_USER (self)); + cm_net = cm_client_get_net (client); + + if (!image_file) + { + g_autofree char *uri = NULL; + const char *data; + + data = "{\"avatar_url\":\"\"}"; + uri = g_strdup_printf ("/_matrix/client/r0/profile/%s/avatar_url", + cm_client_get_user_id (client)); + cm_net_send_data_async (cm_net, 2, g_strdup (data), strlen (data), + uri, SOUP_METHOD_PUT, NULL, cancellable, + account_set_user_avatar_cb, g_steal_pointer (&task)); + } + else + { + g_task_return_new_error (task, G_IO_ERROR, G_IO_ERROR_NOT_SUPPORTED, + "Setting new user avatar not implemented"); + } +} + +gboolean +cm_account_set_user_avatar_finish (CmAccount *self, + GAsyncResult *result, + GError **error) +{ + g_return_val_if_fail (CM_IS_ACCOUNT (self), FALSE); + g_return_val_if_fail (G_IS_TASK (result), FALSE); + g_return_val_if_fail (!error || !*error, FALSE); + + return g_task_propagate_boolean (G_TASK (result), error); +} + +static void +account_get_3pid_cb (GObject *obj, + GAsyncResult *result, + gpointer user_data) +{ + CmAccount *self; + g_autoptr(GTask) task = user_data; + GPtrArray *emails, *phones; + GError *error = NULL; + g_autoptr(JsonObject) object = NULL; + JsonArray *array; + guint length; + + g_assert (G_IS_TASK (task)); + + self = g_task_get_source_object (task); + g_assert (CM_IS_ACCOUNT (self)); + + object = g_task_propagate_pointer (G_TASK (result), &error); + array = cm_utils_json_object_get_array (object, "threepids"); + + g_debug ("Getting 3pid, success: %d, user: %s", !error, + cm_client_get_user_id (cm_user_get_client (CM_USER (self)))); + + if (!array) + { + if (error) + g_task_return_error (task, error); + else + g_task_return_boolean (task, FALSE); + + return; + } + + emails = g_ptr_array_new_full (1, g_free); + phones = g_ptr_array_new_full (1, g_free); + + length = json_array_get_length (array); + + for (guint i = 0; i < length; i++) + { + const char *type, *value; + + object = json_array_get_object_element (array, i); + value = cm_utils_json_object_get_string (object, "address"); + type = cm_utils_json_object_get_string (object, "medium"); + + if (g_strcmp0 (type, "email") == 0) + g_ptr_array_add (emails, g_strdup (value)); + else if (g_strcmp0 (type, "msisdn") == 0) + g_ptr_array_add (phones, g_strdup (value)); + } + + g_object_set_data_full (G_OBJECT (task), "email", emails, + (GDestroyNotify)g_ptr_array_unref); + g_object_set_data_full (G_OBJECT (task), "phone", phones, + (GDestroyNotify)g_ptr_array_unref); + + g_task_return_boolean (task, TRUE); +} + +void +cm_account_get_3pids_async (CmAccount *self, + GCancellable *cancellable, + GAsyncReadyCallback callback, + gpointer user_data) +{ + g_autoptr(GTask) task = NULL; + const char *user_id; + CmClient *client; + + g_return_if_fail (CM_IS_ACCOUNT (self)); + g_return_if_fail (!cancellable || G_IS_CANCELLABLE (cancellable)); + + task = g_task_new (self, cancellable, callback, user_data); + client = cm_user_get_client (CM_USER (self)); + user_id = cm_client_get_user_id (client); + + if (!user_id) + { + g_task_return_new_error (task, G_IO_ERROR, G_IO_ERROR_INVALID_ARGUMENT, + "user hasn't logged in yet"); + return; + } + + g_debug ("Getting 3pid of user '%s'", user_id); + + cm_net_send_json_async (cm_client_get_net (client), 1, NULL, + "/_matrix/client/r0/account/3pid", SOUP_METHOD_GET, + NULL, cancellable, account_get_3pid_cb, + g_steal_pointer (&task)); +} + +gboolean +cm_account_get_3pids_finish (CmAccount *self, + GPtrArray **emails, + GPtrArray **phones, + GAsyncResult *result, + GError **error) +{ + g_return_val_if_fail (CM_IS_ACCOUNT (self), FALSE); + g_return_val_if_fail (G_IS_TASK (result), FALSE); + g_return_val_if_fail (!error || !*error, FALSE); + + if (emails) + *emails = g_object_steal_data (G_OBJECT (result), "email"); + if (phones) + *phones = g_object_steal_data (G_OBJECT (result), "phone"); + + return g_task_propagate_boolean (G_TASK (result), error); +} + +static void +account_delete_3pid_cb (GObject *obj, + GAsyncResult *result, + gpointer user_data) +{ + CmAccount *self; + g_autoptr(GTask) task = user_data; + GError *error = NULL; + g_autoptr(JsonObject) object = NULL; + + g_assert (G_IS_TASK (task)); + + self = g_task_get_source_object (task); + g_assert (CM_IS_ACCOUNT (self)); + + object = g_task_propagate_pointer (G_TASK (result), &error); + + if (error) + { + g_task_return_error (task, error); + return; + } + + g_task_return_boolean (task, TRUE); +} + +void +cm_account_delete_3pid_async (CmAccount *self, + const char *value, + const char *type, + GCancellable *cancellable, + GAsyncReadyCallback callback, + gpointer user_data) +{ + CmClient *client; + JsonObject *root; + GTask *task; + + g_return_if_fail (CM_IS_ACCOUNT (self)); + g_return_if_fail (value && *value); + g_return_if_fail (g_strcmp0 (type, "email") == 0 || g_strcmp0 (type, "msisdn") == 0); + g_return_if_fail (!cancellable || G_IS_CANCELLABLE (cancellable)); + + root = json_object_new (); + json_object_set_string_member (root, "address", value); + if (g_strcmp0 (type, "msisdn") == 0) + json_object_set_string_member (root, "medium", "msisdn"); + else + json_object_set_string_member (root, "medium", "email"); + + task = g_task_new (self, cancellable, callback, user_data); + g_object_set_data (G_OBJECT (task), "type", GINT_TO_POINTER (type)); + g_object_set_data_full (G_OBJECT (task), "value", + g_strdup (value), g_free); + + client = cm_user_get_client (CM_USER (self)); + cm_net_send_json_async (cm_client_get_net (client), 2, root, + "/_matrix/client/r0/account/3pid/delete", SOUP_METHOD_POST, + NULL, cancellable, account_delete_3pid_cb, task); +} + +gboolean +cm_account_delete_3pid_finish (CmAccount *self, + GAsyncResult *result, + GError **error) +{ + g_return_val_if_fail (CM_IS_ACCOUNT (self), FALSE); + g_return_val_if_fail (G_IS_TASK (result), FALSE); + g_return_val_if_fail (!error || !*error, FALSE); + + return g_task_propagate_boolean (G_TASK (result), error); +} diff --git a/subprojects/libcmatrix/src/users/cm-account.h b/subprojects/libcmatrix/src/users/cm-account.h new file mode 100644 index 0000000000000000000000000000000000000000..45719d4718414b10b5f661a56c9518c99fb7c019 --- /dev/null +++ b/subprojects/libcmatrix/src/users/cm-account.h @@ -0,0 +1,67 @@ +/* -*- mode: c; c-basic-offset: 2; indent-tabs-mode: nil; -*- */ +/* cm-account.h + * + * Copyright 2022 Purism SPC + * + * Author(s): + * Mohammed Sadiq <sadiq@sadiqpk.org> + * + * SPDX-License-Identifier: LGPL-2.1-or-later + */ + +#pragma once + +#if !defined(_CMATRIX_TAKEN) && !defined(CMATRIX_COMPILATION) +# error "Only <cmatrix.h> can be included directly." +#endif + +#include <glib-object.h> +#include <gio/gio.h> + +#include "cm-user.h" + +G_BEGIN_DECLS + +#define CM_TYPE_ACCOUNT (cm_account_get_type ()) + +G_DECLARE_FINAL_TYPE (CmAccount, cm_account, CM, ACCOUNT, CmUser) + +gboolean cm_account_set_login_id (CmAccount *self, + const char *login_id); +const char *cm_account_get_login_id (CmAccount *self); +void cm_account_set_display_name_async (CmAccount *self, + const char *name, + GCancellable *cancellable, + GAsyncReadyCallback callback, + gpointer user_data); +gboolean cm_account_set_display_name_finish (CmAccount *self, + GAsyncResult *result, + GError **error); +void cm_account_set_user_avatar_async (CmAccount *self, + GFile *image_file, + GCancellable *cancellable, + GAsyncReadyCallback callback, + gpointer user_data); +gboolean cm_account_set_user_avatar_finish (CmAccount *self, + GAsyncResult *result, + GError **error); +void cm_account_get_3pids_async (CmAccount *self, + GCancellable *cancellable, + GAsyncReadyCallback callback, + gpointer user_data); +gboolean cm_account_get_3pids_finish (CmAccount *self, + GPtrArray **emails, + GPtrArray **phones, + GAsyncResult *result, + GError **error); +void cm_account_delete_3pid_async (CmAccount *self, + const char *value, + const char *type, + GCancellable *cancellable, + GAsyncReadyCallback callback, + gpointer user_data); +gboolean cm_account_delete_3pid_finish (CmAccount *self, + GAsyncResult *result, + GError **error); + +G_END_DECLS diff --git a/subprojects/libcmatrix/src/users/cm-room-member-private.h b/subprojects/libcmatrix/src/users/cm-room-member-private.h new file mode 100644 index 0000000000000000000000000000000000000000..fa6b0820ff22eec4d76c3965ebcd1d1f1fae4214 --- /dev/null +++ b/subprojects/libcmatrix/src/users/cm-room-member-private.h @@ -0,0 +1,28 @@ +/* -*- mode: c; c-basic-offset: 2; indent-tabs-mode: nil; -*- */ +/* cm-room-member-private.h + * + * Copyright 2022 Purism SPC + * + * Author(s): + * Mohammed Sadiq <sadiq@sadiqpk.org> + * + * SPDX-License-Identifier: LGPL-2.1-or-later + */ + +#pragma once + +#if !defined(_CMATRIX_TAKEN) && !defined(CMATRIX_COMPILATION) +# error "Only <cmatrix.h> can be included directly." +#endif + +#include <glib-object.h> +#include <gio/gio.h> +#include <json-glib/json-glib.h> + +#include "cm-room-member.h" + +G_BEGIN_DECLS + +CmRoomMember *cm_room_member_new (GRefString *user_id); + +G_END_DECLS diff --git a/subprojects/libcmatrix/src/users/cm-room-member.c b/subprojects/libcmatrix/src/users/cm-room-member.c new file mode 100644 index 0000000000000000000000000000000000000000..d313f7eb6d54f5a336ed18786a78ee5c90d3c559 --- /dev/null +++ b/subprojects/libcmatrix/src/users/cm-room-member.c @@ -0,0 +1,58 @@ +/* -*- mode: c; c-basic-offset: 2; indent-tabs-mode: nil; -*- */ +/* cm-room-member.c + * + * Copyright 2022 Purism SPC + * + * Author(s): + * Mohammed Sadiq <sadiq@sadiqpk.org> + * + * SPDX-License-Identifier: LGPL-2.1-or-later + */ + +#define G_LOG_DOMAIN "cm-room-member" + +#ifdef HAVE_CONFIG_H +# include "config.h" +#endif + +#include "cm-client.h" +#include "cm-client-private.h" +#include "cm-utils-private.h" +#include "cm-device.h" +#include "cm-device-private.h" +#include "cm-room.h" +#include "cm-room-private.h" +#include "cm-enc-private.h" +#include "cm-user-private.h" +#include "cm-room-member-private.h" +#include "cm-room-member.h" + +struct _CmRoomMember +{ + CmUser parent_instance; +}; + +G_DEFINE_TYPE (CmRoomMember, cm_room_member, CM_TYPE_USER) + +static void +cm_room_member_class_init (CmRoomMemberClass *klass) +{ +} + +static void +cm_room_member_init (CmRoomMember *self) +{ +} + +CmRoomMember * +cm_room_member_new (GRefString *user_id) +{ + CmRoomMember *self; + + g_return_val_if_fail (user_id && *user_id == '@', NULL); + + self = g_object_new (CM_TYPE_ROOM_MEMBER, NULL); + cm_user_set_user_id (CM_USER (self), user_id); + + return self; +} diff --git a/subprojects/libcmatrix/src/users/cm-room-member.h b/subprojects/libcmatrix/src/users/cm-room-member.h new file mode 100644 index 0000000000000000000000000000000000000000..53d9a9a8c9b89aeec3e5fa7db65127eec51a4fe2 --- /dev/null +++ b/subprojects/libcmatrix/src/users/cm-room-member.h @@ -0,0 +1,34 @@ +/* -*- mode: c; c-basic-offset: 2; indent-tabs-mode: nil; -*- */ +/* cm-room-member.h + * + * Copyright 2022 Purism SPC + * + * Author(s): + * Mohammed Sadiq <sadiq@sadiqpk.org> + * + * SPDX-License-Identifier: LGPL-2.1-or-later + */ + +#pragma once + +#if !defined(_CMATRIX_TAKEN) && !defined(CMATRIX_COMPILATION) +# error "Only <cmatrix.h> can be included directly." +#endif + +#include <glib-object.h> +#include <gio/gio.h> + +G_BEGIN_DECLS + +#include "cm-user.h" +#include "cm-client.h" +#include "cm-enums.h" + +#define CM_TYPE_ROOM_MEMBER (cm_room_member_get_type ()) + +G_DECLARE_FINAL_TYPE (CmRoomMember, cm_room_member, CM, ROOM_MEMBER, CmUser) + +const char *cm_room_member_get_name (CmRoomMember *self); + +G_END_DECLS + diff --git a/subprojects/libcmatrix/src/users/cm-user-list-private.h b/subprojects/libcmatrix/src/users/cm-user-list-private.h new file mode 100644 index 0000000000000000000000000000000000000000..d968355fb278b925b3ffb5e0ff1d1efa6eb24a73 --- /dev/null +++ b/subprojects/libcmatrix/src/users/cm-user-list-private.h @@ -0,0 +1,69 @@ +/* -*- mode: c; c-basic-offset: 2; indent-tabs-mode: nil; -*- */ +/* cm-user-list-private.h + * + * Copyright 2022 Purism SPC + * + * Author(s): + * Mohammed Sadiq <sadiq@sadiqpk.org> + * + * SPDX-License-Identifier: LGPL-2.1-or-later + */ + +#pragma once + +#include <json-glib/json-glib.h> +#include <glib-object.h> + +#include "cm-types.h" + +G_BEGIN_DECLS + +typedef struct _CmUserKey CmUserKey; + +struct _CmUserKey { + CmUser *user; + GPtrArray *devices; + GPtrArray *keys; +}; + +#define CM_TYPE_USER_LIST (cm_user_list_get_type ()) + +G_DECLARE_FINAL_TYPE (CmUserList, cm_user_list, CM, USER_LIST, GObject) + +void cm_user_key_free (gpointer data); +G_DEFINE_AUTOPTR_CLEANUP_FUNC (CmUserKey, cm_user_key_free) + +CmUserList *cm_user_list_new (CmClient *self); +void cm_user_list_device_changed (CmUserList *self, + JsonObject *root, + GPtrArray *changed); +void cm_user_list_set_account (CmUserList *self, + CmAccount *account); +CmUser *cm_user_list_find_user (CmUserList *self, + GRefString *user_id, + gboolean create_if_missing); +void cm_user_list_load_devices_async (CmUserList *self, + GPtrArray *users, + GAsyncReadyCallback callback, + gpointer user_data); +GPtrArray *cm_user_list_load_devices_finish (CmUserList *self, + GAsyncResult *result, + GError **error); +void cm_user_list_claim_keys_async (CmUserList *self, + CmRoom *room, + GHashTable *users, + GAsyncReadyCallback callback, + gpointer user_data); +GPtrArray *cm_user_list_claim_keys_finish (CmUserList *self, + GAsyncResult *result, + GError **error); +void cm_user_list_upload_keys_async (CmUserList *self, + CmRoom *room, + GPtrArray *one_time_keys, + GAsyncReadyCallback callback, + gpointer user_data); +gboolean cm_user_list_upload_keys_finish (CmUserList *self, + GAsyncResult *result, + GError **error); + +G_END_DECLS diff --git a/subprojects/libcmatrix/src/users/cm-user-list.c b/subprojects/libcmatrix/src/users/cm-user-list.c new file mode 100644 index 0000000000000000000000000000000000000000..4172d9d4e851f549d035e401f563cae6adff0ac6 --- /dev/null +++ b/subprojects/libcmatrix/src/users/cm-user-list.c @@ -0,0 +1,821 @@ +/* -*- mode: c; c-basic-offset: 2; indent-tabs-mode: nil; -*- */ +/* cm-user-list.c + * + * Copyright 2022 Purism SPC + * + * Author(s): + * Mohammed Sadiq <sadiq@sadiqpk.org> + * + * SPDX-License-Identifier: LGPL-2.1-or-later + */ + +#define G_LOG_DOMAIN "cm-user-list" + +#ifdef HAVE_CONFIG_H +# include "config.h" +#endif + +#include <libsoup/soup.h> +#include <json-glib/json-glib.h> + +#include "events/cm-event-private.h" +#include "cm-user-private.h" +#include "cm-net-private.h" +#include "cm-room-member-private.h" +#include "cm-utils-private.h" +#include "cm-common.h" +#include "cm-device-private.h" +#include "cm-client-private.h" +#include "cm-user-list-private.h" + +/** + * SECTION: cm-user-list + * @title: CmUserList + * @short_description: Track all users that belongs to the account + * @include: "cm-user-list.h" + * + * #CmUserList tracks all users associated with the account, instead + * of tracking them per room individually. Please note that only + * relevant users (eg: Users that shares an encrypted room, or users + * that have sent an event recently) are stored to avoid eating too + * much memory + */ + +/* + * Device key request (ie, loading all devices of user(s) from server): + * https://matrix.org/docs/spec/client_server/r0.6.1#tracking-the-device-list-for-a-user + * - We keep track of all changed users in `changed_users` hash table + * - changed_users may not contain users that don't share any + * encrypted room. + * - Only one request shall run at a time so as to avoid races + * - On a request to load user devices, if no requested user is in the + * changed_users table, return early. + * - On a request, Remove the users from `changed_users`, and keep the + * items in `current_request`. + * - We remove the items early so that if the user devices change + * again midst the request, `changed_users` shall have them again. + * - Add back to changed_users if the request fails. + * - On success, check if any of the requested user is in `changed_users` + * - If so return CM_ERROR_USER_DEVICE_CHANGED, + * - else return %TRUE + */ + +#define KEY_TIMEOUT 10000 /* milliseconds */ + +struct _CmUserList +{ + GObject parent_instance; + + CmClient *client; + GHashTable *users_table; + GHashTable *changed_users; + + /* We make only one request a time */ + GQueue *device_request_queue; + GTask *current_request; + + gboolean is_requesting_device; +}; + +G_DEFINE_TYPE (CmUserList, cm_user_list, G_TYPE_OBJECT) + +enum { + USER_CHANGED, + N_SIGNALS +}; + +static guint signals[N_SIGNALS]; + +static void request_device_keys_from_queue (CmUserList *self); + +static void +device_keys_query_cb (GObject *obj, + GAsyncResult *result, + gpointer user_data) +{ + CmUserList *self; + g_autoptr(GTask) task = user_data; + g_autoptr(JsonObject) object = NULL; + GPtrArray *users = NULL; + GError *error = NULL; + CmUser *user = NULL; + + self = g_task_get_source_object (task); + users = g_task_get_task_data (task); + object = g_task_propagate_pointer (G_TASK (result), &error); + + g_assert (CM_IS_USER_LIST (self)); + g_assert (users); + + g_debug ("(%p) Load user devices %s", users, CM_LOG_SUCCESS (!error)); + + if (error) + { + /* Re-add the users to changed_users */ + for (guint i = 0; i < users->len; i++) + { + GRefString *user_id; + + user = users->pdata[i]; + user_id = cm_user_get_id (user); + g_hash_table_insert (self->changed_users, user_id, g_object_ref (user)); + } + + g_debug ("(%p) Load user devices error: %s", users, error->message); + } + else + { + g_autoptr(GList) members = NULL; + JsonObject *keys; + + keys = cm_utils_json_object_get_object (object, "device_keys"); + if (object) + members = json_object_get_members (keys); + + g_debug ("(%p) Load user devices, to load: %u, loaded: %u", users, users->len, + keys ? json_object_get_size (keys) : 0); + for (GList *member = members; member; member = member->next) + { + g_autoptr(GRefString) user_id = NULL; + g_autoptr(GPtrArray) removed = NULL; + g_autoptr(GPtrArray) added = NULL; + JsonObject *key; + gboolean check_again; + + user_id = g_ref_string_new_intern (member->data); + user = g_hash_table_lookup (self->users_table, user_id); + + if (!user) + { + g_debug ("(%p) Load user devices, '%s' not in users list", + users, user_id); + if (user) + g_ptr_array_remove (users, user); + continue; + } + + added = g_ptr_array_new_full (32, g_object_unref); + removed = g_ptr_array_new_full (32, g_object_unref); + check_again = g_hash_table_contains (self->changed_users, user_id); + key = cm_utils_json_object_get_object (keys, member->data); + cm_user_set_devices (user, key, !check_again, added, removed); + cm_db_update_user_devices (cm_client_get_db (self->client), self->client, + user, added, removed, FALSE); + g_signal_emit (self, signals[USER_CHANGED], 0, user, added, removed); + g_ptr_array_remove (users, user); + + g_debug ("(%p) Load user devices, user: %s, devices, added: %u, removed: %u", + users, user_id, added->len, removed->len); + } + } + + if (!error && users->len) + g_debug ("(%p) Load user devices, %u users changed again", + users, users->len); + + g_clear_object (&self->current_request); + self->is_requesting_device = FALSE; + request_device_keys_from_queue (self); + + if (error) + g_task_return_error (task, error); + else + g_task_return_pointer (task, + g_ptr_array_ref (users), + (GDestroyNotify)g_ptr_array_unref); +} + +static void +remove_unlisted_users (CmUserList *self, + GPtrArray *users) +{ + GPtrArray *old_users = NULL; + guint len; + + g_assert (CM_IS_USER_LIST (self)); + g_assert (users); + + len = users->len; + + if (self->current_request) + old_users = g_task_get_task_data (self->current_request); + + /* If the users list contain users not in the changed users table, or in + * current request, remove them as we shall have already loaded them */ + for (guint i = 0; i < users->len;) + { + GListModel *devices; + CmUser *user = users->pdata[i]; + GRefString *user_id; + + user_id = cm_user_get_id (user); + devices = cm_user_get_devices (user); + + /* Don't remove if the user is in changed users or in the current request */ + if (g_hash_table_contains (self->changed_users, user_id) || + g_list_model_get_n_items (devices) == 0 || + users == old_users || + (old_users && g_ptr_array_find (old_users, user_id, NULL))) + i++; + else + g_ptr_array_remove_index (users, i); + } + + if (len != users->len) + g_debug ("(%p) Request to load device keys removed %u users from %u", + users, len - users->len, len); +} + +static void +request_device_keys_from_queue (CmUserList *self) +{ + GCancellable *cancellable; + GPtrArray *users; + JsonObject *object, *child; + GTask *task; + + g_assert (CM_IS_USER_LIST (self)); + + if (!g_queue_peek_head (self->device_request_queue) || + self->is_requesting_device) + return; + + self->is_requesting_device = TRUE; + task = g_queue_pop_head (self->device_request_queue); + users = g_task_get_task_data (task); + cancellable = g_task_get_cancellable (task); + + g_assert (users); + g_assert (!self->current_request); + g_set_object (&self->current_request, task); + + for (guint i = 0; i < users->len; i++) + g_hash_table_remove (self->changed_users, cm_user_get_id (users->pdata[i])); + + remove_unlisted_users (self, users); + + /* If no users left to request, return */ + if (users->len == 0) + { + g_debug ("(%p) Load user devices %s", users, CM_LOG_SUCCESS (TRUE)); + g_task_return_boolean (task, TRUE); + self->is_requesting_device = FALSE; + /* Repeat */ + request_device_keys_from_queue (self); + return; + } + + object = json_object_new (); + child = json_object_new (); + json_object_set_int_member (object, "timeout", KEY_TIMEOUT); + json_object_set_object_member (object, "device_keys", child); + + for (guint i = 0; i < users->len; i++) + json_object_set_array_member (child, + cm_user_get_id (users->pdata[i]), + json_array_new ()); + + g_debug ("(%p) Load user devices, users count: %u", users, users->len); + cm_net_send_json_async (cm_client_get_net (self->client), 0, object, + "/_matrix/client/r0/keys/query", SOUP_METHOD_POST, + NULL, cancellable, device_keys_query_cb, task); +} + +static void +cm_user_list_finalize (GObject *object) +{ + CmUserList *self = (CmUserList *)object; + + g_clear_object (&self->client); + g_hash_table_unref (self->users_table); + g_clear_object (&self->current_request); + g_hash_table_unref (self->changed_users); + + G_OBJECT_CLASS (cm_user_list_parent_class)->finalize (object); +} + +static void +cm_user_list_class_init (CmUserListClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + + object_class->finalize = cm_user_list_finalize; + + /** + * CmUserList::user-changed: + * @self: a #CmUserList + * @added_devices: The number of devices newly added + * @removed_devices: The number of existing devices removed + * + * user-changed signal is emitted when the user's + * device list changes. + */ + signals [USER_CHANGED] = + g_signal_new ("user-changed", + G_TYPE_FROM_CLASS (klass), + G_SIGNAL_RUN_LAST, + 0, NULL, NULL, NULL, + G_TYPE_NONE, 3, + CM_TYPE_USER, + G_TYPE_PTR_ARRAY, + G_TYPE_PTR_ARRAY); +} + +static void +cm_user_list_init (CmUserList *self) +{ + self->device_request_queue = g_queue_new (); + self->users_table = g_hash_table_new_full (g_direct_hash, + g_direct_equal, + (GDestroyNotify)g_ref_string_release, + g_object_unref); + self->changed_users = g_hash_table_new_full (g_direct_hash, + g_direct_equal, + (GDestroyNotify)g_ref_string_release, + g_object_unref); +} + +void +cm_user_key_free (gpointer data) +{ + CmUserKey *key = data; + + if (!data) + return; + + g_clear_object (&key->user); + g_clear_pointer (&key->devices, g_ptr_array_unref); + g_clear_pointer (&key->keys, g_ptr_array_unref); + g_free (key); +} + +CmUserList * +cm_user_list_new (CmClient *client) +{ + CmUserList *self; + + g_return_val_if_fail (CM_IS_CLIENT (client), NULL); + + self = g_object_new (CM_TYPE_USER_LIST, NULL); + self->client = g_object_ref (client); + + g_debug ("(%p) New user list with client %p created", self, client); + + return self; +} + +/** + * cm_user_list_device_changed: + * @self: A #CmUserList + * @root: A #JsonObject + * @changed: (out): A #GPtrArray + * + * @changed should be created with g_object_unref() + * as free function. @changed shall be filled with + * the #CmUser that got changed. + */ +void +cm_user_list_device_changed (CmUserList *self, + JsonObject *root, + GPtrArray *changed) +{ + JsonArray *users; + CmUser *user; + guint length = 0; + + g_return_if_fail (CM_IS_USER_LIST (self)); + g_return_if_fail (root); + /* The user list should be empty, we shall fill it in */ + g_return_if_fail (changed && changed->len == 0); + + users = cm_utils_json_object_get_array (root, "changed"); + if (users) + length = json_array_get_length (users); + + for (guint i = 0; i < length; i++) + { + GRefString *matrix_id; + const char *user_id; + + user_id = json_array_get_string_element (users, i); + CM_TRACE ("(%p) User '%s' device changed", self->client, user_id); + matrix_id = g_ref_string_new_intern (user_id); + user = cm_user_list_find_user (self, matrix_id, TRUE); + g_ptr_array_add (changed, g_object_ref (user)); + g_hash_table_insert (self->changed_users, matrix_id, + g_object_ref (user)); + } +} + +/** + * cm_user_list_find_user: + * @self: A #CmUserList + * @user_id: A fully qualified matrix id + * @create_if_missing: Create if missing + * + * Find the #CmUser for the given @user_id. If + * @create_if_missing is %FALSE and the user is not + * already in the cache, %NULL shall be returned. + * + * Returns: (nullable) (transfer none): A #CmUser + */ +CmUser * +cm_user_list_find_user (CmUserList *self, + GRefString *user_id, + gboolean create_if_missing) +{ + CmUser *user; + + g_return_val_if_fail (CM_IS_USER_LIST (self), NULL); + g_return_val_if_fail (user_id && *user_id == '@', NULL); + + user = g_hash_table_lookup (self->users_table, user_id); + + if (user || !create_if_missing) + return user; + + user = (CmUser *)cm_room_member_new (user_id); + cm_user_set_client (user, self->client); + g_hash_table_insert (self->users_table, + g_ref_string_acquire (user_id), user); + + return user; +} + +/* + * cm_user_list_set_account: + * @self: A #CmUserList + * @account: A #CmAccount + * + * Set the @account of @self after the matrix user id + * of @account is set. This should be set before adding + * any user to the list. + */ +void +cm_user_list_set_account (CmUserList *self, + CmAccount *account) +{ + GRefString *user_id; + + g_return_if_fail (CM_IS_USER_LIST (self)); + g_return_if_fail (CM_IS_ACCOUNT (account)); + + user_id = cm_user_get_id (CM_USER (account)); + g_return_if_fail (user_id); + + if (g_hash_table_contains (self->users_table, user_id)) + return; + + /* @account should be the first user added to the table */ + g_return_if_fail (g_hash_table_size (self->users_table) == 0); + + g_hash_table_insert (self->users_table, + g_ref_string_acquire (user_id), account); +} + +/** + * cm_user_list_load_devices_async: + * @self: A #CmUserList + * @users: A #GPtrArray of #CmUsers + * @callback: A #GAsyncReadyCallback + * @user_data: user data for @callback + * + * Load all devices for the given users. Each #CmUser + * in @users should have a ref added. + */ +void +cm_user_list_load_devices_async (CmUserList *self, + GPtrArray *users, + GAsyncReadyCallback callback, + gpointer user_data) +{ + g_autoptr(GTask) task = NULL; + + g_return_if_fail (CM_IS_USER_LIST (self)); + g_return_if_fail (users && users->len > 0); + + g_debug ("(%p) Queue Load %p user devices, users count: %u", + self->client, users, users->len); + + task = g_task_new (self, NULL, callback, user_data); + g_task_set_task_data (task, g_ptr_array_ref (users), + (GDestroyNotify)g_ptr_array_unref); + remove_unlisted_users (self, users); + + /* If no users left to request, return */ + if (users->len == 0) + { + g_debug ("(%p) Load %p user devices %s", self->client, users, CM_LOG_SUCCESS (TRUE)); + g_task_return_boolean (task, TRUE); + } + else + { + g_queue_push_tail (self->device_request_queue, g_steal_pointer (&task)); + } + + request_device_keys_from_queue (self); +} + +/** + * cm_user_list_load_devices_finish: + * @self: A #CmUserList + * @result: A #GAsyncResult + * @error: A #GError + * + * Returns: (transfer full): An array of #CmUser whose + * device keys was not updated. + */ +GPtrArray * +cm_user_list_load_devices_finish (CmUserList *self, + GAsyncResult *result, + GError **error) +{ + g_return_val_if_fail (CM_IS_USER_LIST (self), NULL); + g_return_val_if_fail (G_IS_TASK (result), NULL); + g_return_val_if_fail (!error || !*error, NULL); + + return g_task_propagate_pointer (G_TASK (result), error); +} + +static void +claim_keys_cb (GObject *obj, + GAsyncResult *result, + gpointer user_data) +{ + CmUserList *self; + g_autoptr(GTask) task = user_data; + g_autoptr(JsonObject) root = NULL; + JsonObject *object = NULL; + GPtrArray *users = NULL; + GError *error = NULL; + CmRoom *room; + + self = g_task_get_source_object (task); + room = g_object_get_data (user_data, "cm-room"); + users = g_task_get_task_data (task); + g_assert (CM_IS_USER_LIST (self)); + g_assert (users); + + root = g_task_propagate_pointer (G_TASK (result), &error); + g_debug ("(%p) Claim %p user keys %s", room, users, CM_LOG_SUCCESS (!error)); + + if (error) + { + g_debug ("(%p) Claim %p user keys, error: %s", room, users, error->message); + g_task_return_error (task, error); + } + else + { + g_autoptr(GPtrArray) one_time_keys = NULL; + g_autoptr(GList) members = NULL; + const char *room_id; + + object = cm_utils_json_object_get_object (root, "one_time_keys"); + one_time_keys = g_ptr_array_new_full (32, cm_user_key_free); + room_id = cm_room_get_id (room); + + if (object) + members = json_object_get_members (object); + + for (GList *member = members; member; member = member->next) + { + g_autoptr(GRefString) user_id = NULL; + CmUser *user; + JsonObject *keys; + + user_id = g_ref_string_new_intern (member->data); + user = g_hash_table_lookup (self->users_table, user_id); + + keys = cm_utils_json_object_get_object (object, member->data); + cm_user_add_one_time_keys (user, room_id, keys, one_time_keys); + } + + g_debug ("(%p) Claim %p user keys success, keys: %u", + room, users, one_time_keys->len); + + g_task_return_pointer (task, + g_steal_pointer (&one_time_keys), + (GDestroyNotify)g_ptr_array_unref); + } +} + +/** + * cm_user_list_claim_keys_async: + * @self: A #CmUserList + * @room: A #CmRoom + * @users: A #GHashTable of users + * @callback: A callback to run when finished + * @user_data: The user data for @callback + * + * Claim one time keys for the devices of given + * users in @users. + * + * The key in @users should be a user_id #GRefString. + * The value of @users should a #GPtrArray of #CmDevices + * for which the one time keys should be claimed. + * + * The method shall return %CM_ERROR_USER_DEVICE_CHANGED + * error if any of the user in @users is in the list of + * changed users in @self. + */ +void +cm_user_list_claim_keys_async (CmUserList *self, + CmRoom *room, + GHashTable *users, + GAsyncReadyCallback callback, + gpointer user_data) +{ + g_autoptr(GTask) task = NULL; + g_autoptr(GList) keys = NULL; + JsonObject *root, *child; + guint changed_count = 0; + + g_return_if_fail (CM_IS_USER_LIST (self)); + g_return_if_fail (CM_IS_ROOM (room)); + g_return_if_fail (users); + + task = g_task_new (self, NULL, callback, user_data); + g_task_set_task_data (task, g_hash_table_ref (users), + (GDestroyNotify)g_hash_table_unref); + g_object_set_data_full (G_OBJECT (task), + "cm-room", g_object_ref (room), + g_object_unref); + + g_debug ("(%p) Claim %p user keys, users: %u", + room, users, g_hash_table_size (users)); + + keys = g_hash_table_get_keys (users); + + /* Check if any user's device got changed ... */ + for (GList *node = keys; node; node = node->next) + { + GRefString *user_id = node->data; + + if (g_hash_table_contains (self->changed_users, user_id)) + changed_count++; + } + + /* ... if so, return an error as the caller should update user devices. */ + if (changed_count) + { + g_debug ("(%p) Claim %p user keys error, %u users pending update", + room, users, changed_count); + g_task_return_new_error (task, CM_ERROR, CM_ERROR_USER_DEVICE_CHANGED, + "%u users have their devices changed", changed_count); + return; + } + + /* https://matrix.org/docs/spec/client_server/r0.6.1#post-matrix-client-r0-keys-claim */ + root = json_object_new (); + json_object_set_int_member (root, "timeout", KEY_TIMEOUT); + child = json_object_new (); + + for (GList *node = keys; node; node = node->next) + { + GRefString *user_id = node->data; + GPtrArray *devices; + JsonObject *object; + + devices = g_hash_table_lookup (users, user_id); + object = json_object_new (); + + for (guint i = 0; i < devices->len; i++) + { + const char *device_id; + + device_id = cm_device_get_id (devices->pdata[i]); + json_object_set_string_member (object, device_id, "signed_curve25519"); + } + + if (object) + json_object_set_object_member (child, user_id, object); + } + + json_object_set_object_member (root, "one_time_keys", child); + + cm_net_send_json_async (cm_client_get_net (self->client), 0, root, + "/_matrix/client/r0/keys/claim", SOUP_METHOD_POST, + NULL, NULL, claim_keys_cb, + g_steal_pointer (&task)); +} + +/** + * cm_user_list_claim_keys_finish: + * @self: A #CmUserList + * @result: A #GAsyncResult + * @error: A #GError + * + * Get the claimed one time keys + * + * Returns: (transfer full): A #GPtrArray of #CmUserKey + */ +GPtrArray * +cm_user_list_claim_keys_finish (CmUserList *self, + GAsyncResult *result, + GError **error) +{ + g_return_val_if_fail (CM_IS_USER_LIST (self), FALSE); + g_return_val_if_fail (G_IS_TASK (result), FALSE); + g_return_val_if_fail (!error || !*error, FALSE); + + return g_task_propagate_pointer (G_TASK (result), error); +} + +static void +upload_group_keys_cb (GObject *obj, + GAsyncResult *result, + gpointer user_data) +{ + CmUserList *self; + g_autoptr(GTask) task = user_data; + g_autoptr(JsonObject) object = NULL; + GError *error = NULL; + CmRoom *room = NULL; + + self = g_task_get_source_object (task); + room = g_task_get_task_data (task); + g_assert (CM_IS_USER_LIST (self)); + g_assert (CM_IS_ROOM (room)); + + object = g_task_propagate_pointer (G_TASK (result), &error); + g_debug ("(%p) Upload group keys %s", room, CM_LOG_SUCCESS (!error)); + + if (error) + { + g_debug ("(%p) Upload group keys error: %s", room, error->message); + g_task_return_error (task, error); + } + else + { + gpointer session; + + session = g_object_get_data (G_OBJECT (task), "session"); + cm_enc_set_room_group_key (cm_client_get_enc (self->client), + room, session); + g_task_return_boolean (task, TRUE); + } +} + +/** + * cm_user_list_upload_keys_async: + * @self: A #CmUserList + * @room: A #CmRoom + * @one_time_keys: A #GptrArray for #CmUserKey + * @callback: A callback to run when finished + * @user_data: The user data for @callback + * + * Upload the given @one_time_keys to server. + */ +void +cm_user_list_upload_keys_async (CmUserList *self, + CmRoom *room, + GPtrArray *one_time_keys, + GAsyncReadyCallback callback, + gpointer user_data) +{ + g_autoptr(CmEvent) event = NULL; + g_autoptr(GTask) task = NULL; + g_autofree char *uri = NULL; + JsonObject *root, *object; + gpointer olm_session = NULL; + + g_return_if_fail (CM_IS_USER_LIST (self)); + g_return_if_fail (CM_IS_ROOM (room)); + g_return_if_fail (one_time_keys && one_time_keys->len); + + g_debug ("(%p) Upload group keys, keys count: %u", + room, one_time_keys->len); + + task = g_task_new (self, NULL, callback, user_data); + g_task_set_task_data (task, g_object_ref (room), g_object_unref); + + root = json_object_new (); + object = cm_enc_create_out_group_keys (cm_client_get_enc (self->client), + room, one_time_keys, &olm_session); + g_object_set_data_full (G_OBJECT (task), "session", olm_session, g_object_unref); + json_object_set_object_member (root, "messages", object); + + /* Create an event only to create event id */ + event = cm_event_new (CM_M_UNKNOWN); + cm_event_create_txn_id (event, cm_client_pop_event_id (self->client)); + + uri = g_strdup_printf ("/_matrix/client/r0/sendToDevice/m.room.encrypted/%s", + cm_event_get_txn_id (event)); + cm_net_send_json_async (cm_client_get_net (self->client), + 0, root, uri, SOUP_METHOD_PUT, + NULL, NULL, + upload_group_keys_cb, + g_steal_pointer (&task)); +} + +gboolean +cm_user_list_upload_keys_finish (CmUserList *self, + GAsyncResult *result, + GError **error) +{ + g_return_val_if_fail (CM_IS_USER_LIST (self), FALSE); + g_return_val_if_fail (G_IS_TASK (result), FALSE); + g_return_val_if_fail (!error || !*error, FALSE); + + return g_task_propagate_boolean (G_TASK (result), error); +} diff --git a/subprojects/libcmatrix/src/users/cm-user-private.h b/subprojects/libcmatrix/src/users/cm-user-private.h new file mode 100644 index 0000000000000000000000000000000000000000..9d688a86524bf8b72ed69d0ffd079ef712e27da7 --- /dev/null +++ b/subprojects/libcmatrix/src/users/cm-user-private.h @@ -0,0 +1,52 @@ +/* -*- mode: c; c-basic-offset: 2; indent-tabs-mode: nil; -*- */ +/* cm-user-private.h + * + * Copyright 2022 Purism SPC + * + * Author(s): + * Mohammed Sadiq <sadiq@sadiqpk.org> + * + * SPDX-License-Identifier: LGPL-2.1-or-later + */ + +#pragma once + +#if !defined(_CMATRIX_TAKEN) && !defined(CMATRIX_COMPILATION) +# error "Only <cmatrix.h> can be included directly." +#endif + +#include <json-glib/json-glib.h> +#include <glib-object.h> +#include <gio/gio.h> + +#include "cm-client.h" +#include "cm-device.h" +#include "cm-user.h" + +G_BEGIN_DECLS + +JsonObject *cm_user_generate_json (CmUser *self); +void cm_user_set_json_data (CmUser *self, + JsonObject *root); +void cm_user_set_client (CmUser *self, + CmClient *client); +CmClient *cm_user_get_client (CmUser *self); +void cm_user_set_user_id (CmUser *self, + GRefString *user_id); +void cm_user_set_details (CmUser *self, + const char *display_name, + const char *avatar_url); +GListModel *cm_user_get_devices (CmUser *self); +CmDevice *cm_user_find_device (CmUser *self, + const char *device_id); +void cm_user_set_devices (CmUser *self, + JsonObject *root, + gboolean update_state, + GPtrArray *added, + GPtrArray *removed); +void cm_user_add_one_time_keys (CmUser *self, + const char *room_id, + JsonObject *root, + GPtrArray *out_keys); + +G_END_DECLS diff --git a/subprojects/libcmatrix/src/users/cm-user.c b/subprojects/libcmatrix/src/users/cm-user.c new file mode 100644 index 0000000000000000000000000000000000000000..63a7548f65ca0794c199e68214e060743dcdc941 --- /dev/null +++ b/subprojects/libcmatrix/src/users/cm-user.c @@ -0,0 +1,622 @@ +/* -*- mode: c; c-basic-offset: 2; indent-tabs-mode: nil; -*- */ +/* cm-user.c + * + * Copyright 2022 Purism SPC + * + * Author(s): + * Mohammed Sadiq <sadiq@sadiqpk.org> + * + * SPDX-License-Identifier: LGPL-2.1-or-later + */ + +#define G_LOG_DOMAIN "cm-user" + +#ifdef HAVE_CONFIG_H +# include "config.h" +#endif + +#include "cm-utils-private.h" +#include "cm-client-private.h" +#include "cm-device.h" +#include "cm-device-private.h" +#include "cm-matrix-private.h" +#include "cm-user-list-private.h" +#include "cm-user-private.h" +#include "cm-user.h" + +typedef struct +{ + CmClient *cm_client; + + GRefString *user_id; + char *display_name; + char *avatar_url; + char *avatar_file_path; + + GFile *avatar_file; + JsonObject *generated_json; + + GListStore *devices; + GHashTable *devices_table; + + /* Set when we know about some change, but not sure what it is */ + gboolean device_changed; + + gboolean info_loaded; +} CmUserPrivate; + +G_DEFINE_TYPE_WITH_PRIVATE (CmUser, cm_user, G_TYPE_OBJECT) + +enum { + CHANGED, + N_SIGNALS +}; + +static guint signals[N_SIGNALS]; + +static void +cm_user_finalize (GObject *object) +{ + CmUser *self = (CmUser *)object; + CmUserPrivate *priv = cm_user_get_instance_private (self); + + g_clear_pointer (&priv->user_id, g_ref_string_release); + g_free (priv->display_name); + g_free (priv->avatar_url); + g_free (priv->avatar_file_path); + + g_list_store_remove_all (priv->devices); + g_clear_object (&priv->devices); + g_clear_pointer (&priv->devices_table, g_hash_table_unref); + + g_clear_object (&priv->avatar_file); + g_clear_pointer (&priv->generated_json, json_object_unref); + + G_OBJECT_CLASS (cm_user_parent_class)->finalize (object); +} + +static void +cm_user_class_init (CmUserClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + + object_class->finalize = cm_user_finalize; + + /** + * CmUser::changed: + * @self: a #CmUser + * + * changed signal is emitted when name or avatar + * of the user changes. + */ + signals [CHANGED] = + g_signal_new ("changed", + G_TYPE_FROM_CLASS (klass), + G_SIGNAL_RUN_LAST, + 0, NULL, NULL, NULL, + G_TYPE_NONE, 0); +} + +static void +cm_user_init (CmUser *self) +{ + CmUserPrivate *priv = cm_user_get_instance_private (self); + + priv->devices = g_list_store_new (CM_TYPE_DEVICE); + priv->devices_table = g_hash_table_new_full (g_str_hash, g_str_equal, + g_free, g_object_unref); +} + +JsonObject * +cm_user_generate_json (CmUser *self) +{ + CmUserPrivate *priv = cm_user_get_instance_private (self); + + g_return_val_if_fail (CM_IS_USER (self), NULL); + + if (!priv->generated_json && + (priv->display_name || priv->avatar_url || priv->avatar_file)) + { + g_autofree char *avatar_path = NULL; + JsonObject *local, *child; + GFile *parent; + + parent = g_file_new_for_path (cm_matrix_get_data_dir ()); + local = json_object_new (); + json_object_set_object_member (local, "local", json_object_new ()); + priv->generated_json = local; + + if (priv->avatar_file) + avatar_path = g_file_get_relative_path (parent, priv->avatar_file); + + child = json_object_get_object_member (local, "local"); + if (priv->display_name) + json_object_set_string_member (child, "display_name", priv->display_name); + if (priv->avatar_url) + json_object_set_string_member (child, "avatar_url", priv->avatar_url); + if (avatar_path) + json_object_set_string_member (child, "avatar_path", avatar_path); + } + + return priv->generated_json; +} + +void +cm_user_set_json_data (CmUser *self, + JsonObject *root) +{ + const char *name, *avatar_url; + JsonObject *child; + + g_return_if_fail (CM_IS_USER (self)); + + if (!root) + return; + + child = cm_utils_json_object_get_object (root, "content"); + + if (!child) + child = root; + + name = cm_utils_json_object_get_string (child, "display_name"); + if (!name) + name = cm_utils_json_object_get_string (child, "displayname"); + + avatar_url = cm_utils_json_object_get_string (child, "avatar_url"); + cm_user_set_details (self, name, avatar_url); +} + +void +cm_user_set_client (CmUser *self, + CmClient *client) +{ + CmUserPrivate *priv = cm_user_get_instance_private (self); + + g_return_if_fail (CM_IS_USER (self)); + g_return_if_fail (CM_IS_CLIENT (client)); + + if (!priv->cm_client) + priv->cm_client = g_object_ref (client); +} + +CmClient * +cm_user_get_client (CmUser *self) +{ + CmUserPrivate *priv = cm_user_get_instance_private (self); + + g_return_val_if_fail (CM_IS_USER (self), NULL); + + return priv->cm_client; +} + +void +cm_user_set_user_id (CmUser *self, + GRefString *user_id) +{ + CmUserPrivate *priv = cm_user_get_instance_private (self); + + g_return_if_fail (CM_IS_USER (self)); + + if (priv->user_id == user_id) + return; + + /* Allow setting user id only once, and never allow changing */ + g_return_if_fail (!priv->user_id); + g_return_if_fail (user_id && *user_id == '@'); + + priv->user_id = g_ref_string_acquire (user_id); +} + +void +cm_user_set_details (CmUser *self, + const char *display_name, + const char *avatar_url) +{ + CmUserPrivate *priv = cm_user_get_instance_private (self); + gboolean changed = FALSE; + + g_return_if_fail (CM_IS_USER (self)); + + if (g_strcmp0 (display_name, priv->display_name) != 0) + { + g_free (priv->display_name); + priv->display_name = g_strdup (display_name); + changed = TRUE; + } + + if (g_strcmp0 (avatar_url, priv->avatar_url) != 0) + { + g_free (priv->avatar_url); + priv->avatar_url = g_strdup (avatar_url); + changed = TRUE; + } + + /* If we are not already loading info, mark as info has loaded */ + if (changed) + g_signal_emit (self, signals[CHANGED], 0); +} + +GRefString * +cm_user_get_id (CmUser *self) +{ + CmUserPrivate *priv = cm_user_get_instance_private (self); + + g_return_val_if_fail (CM_IS_USER (self), NULL); + + return priv->user_id; +} + +const char * +cm_user_get_display_name (CmUser *self) +{ + CmUserPrivate *priv = cm_user_get_instance_private (self); + + g_return_val_if_fail (CM_IS_USER (self), NULL); + + return priv->display_name; +} + +const char * +cm_user_get_avatar_url (CmUser *self) +{ + CmUserPrivate *priv = cm_user_get_instance_private (self); + + g_return_val_if_fail (CM_IS_USER (self), NULL); + + return priv->avatar_url; +} + +static void +user_get_user_info_cb (GObject *obj, + GAsyncResult *result, + gpointer user_data); + +static void +user_get_avatar_cb (GObject *object, + GAsyncResult *result, + gpointer user_data) +{ + CmUser *self; + g_autoptr(GTask) task = user_data; + GInputStream *stream; + GError *error = NULL; + + g_assert (G_IS_TASK (task)); + + self = g_task_get_source_object (task); + + stream = cm_client_get_file_finish (CM_CLIENT (object), result, &error); + g_debug ("(%p) Get avatar %s", self, CM_LOG_SUCCESS (!error)); + + if (error) + g_task_return_error (task, error); + else + g_task_return_pointer (task, stream, g_object_unref); +} + +void +cm_user_get_avatar_async (CmUser *self, + GCancellable *cancellable, + GAsyncReadyCallback callback, + gpointer user_data) +{ + CmUserPrivate *priv = cm_user_get_instance_private (self); + GTask *task; + + task = g_task_new (self, cancellable, callback, user_data); + g_task_set_source_tag (task, cm_user_get_avatar_async); + + g_debug ("(%p) Get avatar", self); + + if ((!priv->display_name && !priv->avatar_url) || !priv->info_loaded) + cm_user_load_info_async (self, cancellable, + user_get_avatar_cb, task); + else if (priv->avatar_url) + cm_client_get_file_async (priv->cm_client, priv->avatar_url, cancellable, + NULL, NULL, + user_get_avatar_cb, g_steal_pointer (&task)); + else + g_task_return_pointer (task, NULL, NULL); +} + +GInputStream * +cm_user_get_avatar_finish (CmUser *self, + GAsyncResult *result, + GError **error) +{ + g_return_val_if_fail (CM_IS_USER (self), NULL); + g_return_val_if_fail (G_IS_TASK (result), NULL); + g_return_val_if_fail (!error || !*error, NULL); + + return g_task_propagate_pointer (G_TASK (result), error); +} + +static void +user_get_user_info_cb (GObject *obj, + GAsyncResult *result, + gpointer user_data) +{ + CmUser *self; + CmUserPrivate *priv; + g_autoptr(GTask) task = user_data; + const char *name, *avatar_url; + GError *error = NULL; + g_autoptr(JsonObject) object = NULL; + + g_assert (G_IS_TASK (task)); + + self = g_task_get_source_object (task); + priv = cm_user_get_instance_private (self); + g_assert (CM_IS_USER (self)); + + object = g_task_propagate_pointer (G_TASK (result), &error); + g_debug ("(%p) Load info %s", self, CM_LOG_SUCCESS (!error)); + + if (error) + { + g_task_return_error (task, error); + return; + } + + name = cm_utils_json_object_get_string (object, "displayname"); + avatar_url = cm_utils_json_object_get_string (object, "avatar_url"); + + g_free (priv->display_name); + g_free (priv->avatar_url); + + priv->display_name = g_strdup (name); + priv->avatar_url = g_strdup (avatar_url); + priv->info_loaded = TRUE; + + if (g_task_get_source_tag (task) == cm_user_get_avatar_async) + { + GCancellable *cancellable; + + cancellable = g_task_get_cancellable (task); + + if (priv->avatar_url) + cm_client_get_file_async (priv->cm_client, priv->avatar_url, cancellable, + NULL, NULL, + user_get_avatar_cb, g_steal_pointer (&task)); + else + g_task_return_pointer (task, NULL, NULL); + } + else + { + g_task_return_boolean (task, TRUE); + } +} + +void +cm_user_load_info_async (CmUser *self, + GCancellable *cancellable, + GAsyncReadyCallback callback, + gpointer user_data) +{ + CmUserPrivate *priv = cm_user_get_instance_private (self); + g_autofree char *uri = NULL; + GTask *task; + + g_return_if_fail (CM_IS_USER (self)); + g_return_if_fail (!cancellable || G_IS_CANCELLABLE (cancellable)); + + task = g_task_new (self, cancellable, callback, user_data); + g_debug ("(%p) Load info", self); + + uri = g_strdup_printf ("/_matrix/client/r0/profile/%s", priv->user_id); + cm_net_send_json_async (cm_client_get_net (priv->cm_client), + 1, NULL, uri, SOUP_METHOD_GET, + NULL, cancellable, user_get_user_info_cb, task); +} + +gboolean +cm_user_load_info_finish (CmUser *self, + GAsyncResult *result, + GError **error) +{ + g_return_val_if_fail (CM_IS_USER (self), FALSE); + g_return_val_if_fail (G_IS_TASK (result), FALSE); + g_return_val_if_fail (!error || !*error, FALSE); + + return g_task_propagate_boolean (G_TASK (result), error); +} + +GListModel * +cm_user_get_devices (CmUser *self) +{ + CmUserPrivate *priv = cm_user_get_instance_private (self); + + g_return_val_if_fail (CM_IS_USER (self), NULL); + + return G_LIST_MODEL (priv->devices); +} + +CmDevice * +cm_user_find_device (CmUser *self, + const char *device_id) +{ + GListModel *devices; + guint n_items; + + g_return_val_if_fail (CM_IS_USER (self), NULL); + g_return_val_if_fail (device_id && *device_id, NULL); + + devices = cm_user_get_devices (self); + n_items = g_list_model_get_n_items (devices); + + for (guint i = 0; i < n_items; i++) + { + g_autoptr(CmDevice) device = NULL; + const char *id; + + device = g_list_model_get_item (devices, i); + id = cm_device_get_id (device); + + if (g_strcmp0 (id, device_id) == 0) + return device; + } + + return NULL; +} + +/* + * cm_user_set_devices: + * @self: A #CmUser + * @root: A #JsonObject + * @update_state: Whether to update state + * @added: (out): The number of new devices added + * @removed: (out): The number of existing devices removed + * + * Set devices for @self removing all + * non existing devices in @root + * + * If @update_state is %FALSE, the device changed info + * shall not be updated, and so cm_user_get_device_changed() + * shall return the old values. + * + */ +void +cm_user_set_devices (CmUser *self, + JsonObject *root, + gboolean update_state, + GPtrArray *added, + GPtrArray *removed) +{ + CmUserPrivate *priv = cm_user_get_instance_private (self); + g_autoptr(GHashTable) devices_table = NULL; + g_autoptr(GList) members = NULL; + GHashTable *old_devices; + JsonObject *child; + + g_return_if_fail (CM_IS_USER (self)); + g_return_if_fail (root); + + /* Create a table of devices and add the items here */ + devices_table = g_hash_table_new_full (g_str_hash, g_str_equal, + g_free, g_object_unref); + members = json_object_get_members (root); + + for (GList *member = members; member; member = member->next) + { + g_autoptr(CmDevice) device = NULL; + g_autofree char *device_name = NULL; + const char *device_id, *user; + + child = cm_utils_json_object_get_object (root, member->data); + device_id = cm_utils_json_object_get_string (child, "device_id"); + user = cm_utils_json_object_get_string (child, "user_id"); + + if (!device_id || !*device_id) + continue; + + if (priv->devices_table && + (device = g_hash_table_lookup (priv->devices_table, device_id))) + { + /* If the device is already in the old table, remove it there + * so that it's present only in the new devices_table. + */ + /* device variable is autofree, so the ref is unref later automatically */ + g_object_ref (device); + g_hash_table_remove (priv->devices_table, device_id); + g_hash_table_insert (devices_table, g_strdup (device_id), g_object_ref (device)); + continue; + } + + if (g_strcmp0 (user, cm_user_get_id (self)) != 0) + { + g_warning ("‘%s’ and ‘%s’ are not the same users", + user, cm_user_get_id (self)); + continue; + } + + if (g_strcmp0 (member->data, device_id) != 0) + { + g_warning ("‘%s’ and ‘%s’ are not the same device", (char *)member->data, device_id); + continue; + } + + device = cm_device_new (self, priv->cm_client, child); + g_hash_table_insert (devices_table, g_strdup (device_id), g_object_ref (device)); + g_list_store_append (priv->devices, device); + if (added) + g_ptr_array_add (added, g_object_ref (device)); + } + + old_devices = priv->devices_table; + priv->devices_table = g_steal_pointer (&devices_table); + /* Assign so as to autofree */ + devices_table = old_devices; + + if (old_devices) + { + g_autoptr(GList) devices = NULL; + + /* The old table now contains the devices that are not used by the user anymore */ + devices = g_hash_table_get_values (old_devices); + + for (GList *device = devices; device && device->data; device = device->next) + { + if (removed) + g_ptr_array_add (removed, g_object_ref (device->data)); + cm_utils_remove_list_item (priv->devices, device->data); + } + } +} + +void +cm_user_add_one_time_keys (CmUser *self, + const char *room_id, + JsonObject *root, + GPtrArray *out_keys) +{ + CmUserPrivate *priv = cm_user_get_instance_private (self); + g_autoptr(CmUserKey) key = NULL; + JsonObject *object, *child; + guint n_items; + + g_return_if_fail (CM_IS_USER (self)); + g_return_if_fail (root); + g_return_if_fail (out_keys); + + n_items = g_list_model_get_n_items (G_LIST_MODEL (priv->devices)); + + key = g_new (CmUserKey, 1); + key->user = g_object_ref (self); + key->devices = g_ptr_array_new_full (n_items, g_object_unref); + key->keys = g_ptr_array_new_full (n_items, g_free); + + for (guint i = 0; i < n_items; i++) + { + g_autoptr(CmDevice) device = NULL; + g_autoptr(GList) members = NULL; + const char *device_id; + + device = g_list_model_get_item (G_LIST_MODEL (priv->devices), i); + device_id = cm_device_get_id (device); + child = cm_utils_json_object_get_object (root, device_id); + + if (!child) + { + g_debug ("device '%s' doesn't have any keys", device_id); + continue; + } + + members = json_object_get_members (child); + + for (GList *node = members; node; node = node->next) + { + object = cm_utils_json_object_get_object (child, node->data); + + if (cm_enc_verify (cm_client_get_enc (priv->cm_client), object, + cm_user_get_id (self), device_id, + cm_device_get_ed_key (device))) + { + g_ptr_array_add (key->devices, g_object_ref (device)); + g_ptr_array_add (key->keys, cm_utils_json_object_dup_string (object, "key")); + } + } + } + + if (key->devices->len) + g_ptr_array_add (out_keys, g_steal_pointer (&key)); +} diff --git a/subprojects/libcmatrix/src/users/cm-user.h b/subprojects/libcmatrix/src/users/cm-user.h new file mode 100644 index 0000000000000000000000000000000000000000..b7f161780184288bb5332c041048a8d7995b7c8e --- /dev/null +++ b/subprojects/libcmatrix/src/users/cm-user.h @@ -0,0 +1,53 @@ +/* -*- mode: c; c-basic-offset: 2; indent-tabs-mode: nil; -*- */ +/* cm-user.h + * + * Copyright 2022 Purism SPC + * + * Author(s): + * Mohammed Sadiq <sadiq@sadiqpk.org> + * + * SPDX-License-Identifier: LGPL-2.1-or-later + */ + +#pragma once + +#if !defined(_CMATRIX_TAKEN) && !defined(CMATRIX_COMPILATION) +# error "Only <cmatrix.h> can be included directly." +#endif + +#include <glib-object.h> +#include <gio/gio.h> + +G_BEGIN_DECLS + +#define CM_TYPE_USER (cm_user_get_type ()) + +G_DECLARE_DERIVABLE_TYPE (CmUser, cm_user, CM, USER, GObject) + +struct _CmUserClass +{ + GObjectClass parent_class; + + /*< private >*/ + gpointer reserved[8]; +}; + +GRefString *cm_user_get_id (CmUser *self); +const char *cm_user_get_display_name (CmUser *self); +const char *cm_user_get_avatar_url (CmUser *self); +void cm_user_get_avatar_async (CmUser *self, + GCancellable *cancellable, + GAsyncReadyCallback callback, + gpointer user_data); +GInputStream *cm_user_get_avatar_finish (CmUser *self, + GAsyncResult *result, + GError **error); +void cm_user_load_info_async (CmUser *self, + GCancellable *cancellable, + GAsyncReadyCallback callback, + gpointer user_data); +gboolean cm_user_load_info_finish (CmUser *self, + GAsyncResult *result, + GError **error); + +G_END_DECLS diff --git a/subprojects/libcmatrix/tests/client.c b/subprojects/libcmatrix/tests/client.c new file mode 100644 index 0000000000000000000000000000000000000000..8c9e313c6afd52b000f0f1e70073d7e62b4f8aa1 --- /dev/null +++ b/subprojects/libcmatrix/tests/client.c @@ -0,0 +1,91 @@ +/* -*- mode: c; c-basic-offset: 2; indent-tabs-mode: nil; -*- */ +/* utils.c + * + * Copyright 2022 Purism SPC + * + * Author(s): + * Mohammed Sadiq <sadiq@sadiqpk.org> + * + * SPDX-License-Identifier: LGPL-2.1-or-later + */ + +#undef NDEBUG +#undef G_DISABLE_ASSERT +#undef G_DISABLE_CHECKS +#undef G_DISABLE_CAST_CHECKS +#undef G_LOG_DOMAIN + +#include "cm-client.h" + +static void +test_cm_client_new (void) +{ + CmClient *client; + CmAccount *account; + + client = cm_client_new (); + /* Mark client to not save changes to db */ + g_object_set_data (G_OBJECT (client), "no-save", GINT_TO_POINTER (TRUE)); + g_assert (CM_IS_CLIENT (client)); + + g_assert_null (cm_client_get_user_id (client)); + cm_client_set_user_id (client, "@invalid:bad:"); + g_assert_null (cm_client_get_user_id (client)); + cm_client_set_user_id (client, "@user:example.com"); + g_assert_cmpstr (cm_client_get_user_id (client), ==, "@user:example.com"); + + g_assert_false (cm_client_get_enabled (client)); + + account = cm_client_get_account (client); + g_assert_null (cm_account_get_login_id (account)); + cm_account_set_login_id (account, "user@@invalid"); + g_assert_null (cm_account_get_login_id (account)); + cm_account_set_login_id (account, "user@example.com"); + g_assert_cmpstr (cm_account_get_login_id (account), ==, "user@example.com"); + + g_assert_null (cm_client_get_homeserver (client)); + cm_client_set_homeserver (client, "http://localhost:8008/"); + g_assert_cmpstr (cm_client_get_homeserver (client), ==, "http://localhost:8008"); + cm_client_set_homeserver (client, "http://sub.domain.example.com/"); + g_assert_cmpstr (cm_client_get_homeserver (client), ==, "http://sub.domain.example.com"); + + g_assert_null (cm_client_get_password (client)); + cm_client_set_password (client, "hunter2"); + g_assert_cmpstr (cm_client_get_password (client), ==, "hunter2"); + + g_assert_null (cm_client_get_access_token (client)); + cm_client_set_access_token (client, "ec-8b67-37f0683"); + g_assert_cmpstr (cm_client_get_access_token (client), ==, "ec-8b67-37f0683"); + + g_assert_null (cm_client_get_device_id (client)); + cm_client_set_device_id (client, "DEADBEAF"); + g_assert_cmpstr (cm_client_get_device_id (client), ==, "DEADBEAF"); + + g_assert_null (cm_client_get_device_name (client)); + cm_client_set_device_name (client, "Chatty"); + g_assert_cmpstr (cm_client_get_device_name (client), ==, "Chatty"); + + g_assert_null (cm_client_get_pickle_key (client)); + cm_client_set_pickle_key (client, "passw@rd"); + /* We don't have set encryption, so password shall be NULL */ + g_assert_null (cm_client_get_pickle_key (client)); + + g_assert_false (cm_client_is_sync (client)); + g_assert_false (cm_client_get_logging_in (client)); + g_assert_false (cm_client_get_logged_in (client)); + + /* todo */ + /* g_assert_finalize_object (client); */ + g_object_unref (client); +} + +int +main (int argc, + char *argv[]) +{ + g_test_init (&argc, &argv, NULL); + + g_test_add_func ("/cm-client/new", test_cm_client_new); + + return g_test_run (); +} diff --git a/subprojects/libcmatrix/tests/cm-db.c b/subprojects/libcmatrix/tests/cm-db.c new file mode 100644 index 0000000000000000000000000000000000000000..5fb363a423c9bf8cd5d2d6293853f1cf305a10c9 --- /dev/null +++ b/subprojects/libcmatrix/tests/cm-db.c @@ -0,0 +1,500 @@ +/* -*- mode: c; c-basic-offset: 2; indent-tabs-mode: nil; -*- */ +/* cm-db.c + * + * Copyright 2022 Purism SPC + * + * Author(s): + * Mohammed Sadiq <sadiq@sadiqpk.org> + * + * SPDX-License-Identifier: LGPL-2.1-or-later + */ + +#undef NDEBUG +#undef G_DISABLE_ASSERT +#undef G_DISABLE_CHECKS +#undef G_DISABLE_CAST_CHECKS +#undef G_LOG_DOMAIN + +#include <glib/gstdio.h> +#include <sqlite3.h> + +#include "cm-matrix.h" +#include "cm-db-private.h" +#include "cm-client.h" + +typedef struct _Data +{ + const char *username; + const char *device_id; +} Data; + +static void +finish_bool_cb (GObject *object, + GAsyncResult *result, + gpointer user_data) +{ + g_autoptr(GError) error = NULL; + GTask *task = user_data; + GObject *obj; + gboolean status; + + g_assert_true (G_IS_TASK (task)); + + status = g_task_propagate_boolean (G_TASK (result), &error); + g_assert_no_error (error); + + obj = G_OBJECT (result); + g_object_set_data (user_data, "enabled", g_object_get_data (obj, "enabled")); + g_object_set_data_full (user_data, "pickle", g_object_steal_data (obj, "pickle"), g_free); + g_object_set_data_full (user_data, "device", g_object_steal_data (obj, "device"), g_free); + g_object_set_data_full (user_data, "username", g_object_steal_data (obj, "username"), g_free); + g_task_return_boolean (task, status); +} + +static gboolean +client_matches_user_details (gconstpointer client, + gconstpointer data) +{ + Data *details = (gpointer)data; + + g_assert_true (CM_IS_CLIENT ((gpointer)client)); + g_assert_true (details); + g_assert_nonnull (details->username); + g_assert_nonnull (details->device_id); + + return g_strcmp0 (details->username, cm_client_get_user_id ((gpointer)client)) == 0 && + g_strcmp0 (details->device_id, cm_client_get_device_id ((gpointer)client)) == 0; +} + +static void +add_matrix_account (CmDb *db, + GPtrArray *client_array, + const char *username, + const char *pickle, + const char *device_id, + gboolean enabled) +{ + CmClient *client; + GObject *object; + GTask *task; + GError *error = NULL; + gboolean success; + g_autofree Data *data = NULL; + guint i; + + g_assert_true (CM_IS_DB (db)); + g_assert_nonnull (client_array); + g_assert_nonnull (username); + + data = g_new0 (Data, 1); + data->device_id = device_id; + data->username = username; + + if (g_ptr_array_find_with_equal_func (client_array, data, + client_matches_user_details, &i)) { + client = client_array->pdata[i]; + } else { + client = cm_client_new (); + /* Mark client to not save changes to db */ + g_object_set_data (G_OBJECT (client), "no-save", GINT_TO_POINTER (TRUE)); + cm_client_set_user_id (client, username); + cm_client_set_device_id (client, device_id); + g_ptr_array_add (client_array, client); + } + + g_assert_true (CM_IS_CLIENT (client)); + object = G_OBJECT (client); + + g_object_set_data (object, "enabled", GINT_TO_POINTER (enabled)); + g_object_set_data_full (object, "pickle", g_strdup (pickle), g_free); + g_object_set_data_full (object, "device", g_strdup (device_id), g_free); + + task = g_task_new (NULL, NULL, NULL, NULL); + cm_db_save_client_async (db, client, g_strdup (pickle), + finish_bool_cb, task); + + while (!g_task_get_completed (task)) + g_main_context_iteration (NULL, TRUE); + + success = g_task_propagate_boolean (task, &error); + g_assert_no_error (error); + g_assert_true (success); + g_clear_object (&task); + + g_assert_true (g_ptr_array_find (client_array, client, &i)); + client = client_array->pdata[i]; + task = g_task_new (NULL, NULL, NULL, NULL); + cm_db_load_client_async (db, client, device_id, finish_bool_cb, task); + + while (!g_task_get_completed (task)) + g_main_context_iteration (NULL, TRUE); + + success = g_task_propagate_boolean (task, &error); + g_assert_no_error (error); + g_assert_true (success); + g_assert_cmpstr (g_object_get_data (G_OBJECT (task), "username"), ==, + cm_client_get_user_id (client)); + g_assert_cmpstr (g_object_get_data (G_OBJECT (task), "pickle"), ==, + g_object_get_data (object, "pickle")); + g_assert_cmpstr (g_object_get_data (G_OBJECT (task), "device"), ==, + g_object_get_data (object, "device")); + g_clear_object (&task); +} + +static void +test_cm_db_account (void) +{ + GTask *task; + CmDb *db; + gboolean status; + GPtrArray *account_array; + + g_remove (g_test_get_filename (G_TEST_BUILT, "test-matrix.db", NULL)); + + db = cm_db_new (); + task = g_task_new (NULL, NULL, NULL, NULL); + cm_db_open_async (db, g_strdup (g_test_get_dir (G_TEST_BUILT)), + "test-matrix.db", finish_bool_cb, task); + + while (!g_task_get_completed (task)) + g_main_context_iteration (NULL, TRUE); + + status = g_task_propagate_boolean (task, NULL); + g_assert_finalize_object (task); + g_assert_true (status); + + account_array = g_ptr_array_new (); + g_ptr_array_set_free_func (account_array, (GDestroyNotify)g_object_unref); + + add_matrix_account (db, account_array, "@alice:example.org", + NULL, "AABBCCDD", TRUE); + add_matrix_account (db, account_array, "@alice:example.org", + NULL, "CCDDEE", FALSE); + add_matrix_account (db, account_array, "@alice:example.com", + NULL, "XXAABBDD", FALSE); + add_matrix_account (db, account_array, "@alice:example.com", + "Some Pickle", "XXAABBDD", TRUE); + + add_matrix_account (db, account_array, "@bob:example.org", + NULL, "XXAABBDD", FALSE); + + g_ptr_array_unref (account_array); +} + +static void +test_cm_db_new (void) +{ + const char *file_name; + CmDb *db; + GTask *task; + gboolean status; + + file_name = g_test_get_filename (G_TEST_BUILT, "test-matrix.db", NULL); + g_remove (file_name); + g_assert_false (g_file_test (file_name, G_FILE_TEST_EXISTS)); + + db = cm_db_new (); + task = g_task_new (NULL, NULL, NULL, NULL); + cm_db_open_async (db, g_strdup (g_test_get_dir (G_TEST_BUILT)), + "test-matrix.db", finish_bool_cb, task); + + while (!g_task_get_completed (task)) + g_main_context_iteration (NULL, TRUE); + + status = g_task_propagate_boolean (task, NULL); + g_assert_true (g_file_test (file_name, G_FILE_TEST_IS_REGULAR)); + g_assert_true (cm_db_is_open (db)); + g_assert_true (status); + g_clear_object (&task); + + task = g_task_new (NULL, NULL, NULL, NULL); + cm_db_close_async (db, finish_bool_cb, task); + + while (!g_task_get_completed (task)) + g_main_context_iteration (NULL, TRUE); + + status = g_task_propagate_boolean (task, NULL); + g_assert_true (status); + g_assert_false (cm_db_is_open (db)); + g_clear_object (&db); + g_clear_object (&task); + + g_remove (file_name); + g_assert_false (g_file_test (file_name, G_FILE_TEST_EXISTS)); +} + +static void +matrix_export_sql_file (const char *sql_path, + const char *file_name, + sqlite3 **db) +{ + g_autofree char *export_file = NULL; + g_autofree char *input_file = NULL; + g_autofree char *content = NULL; + g_autoptr(GError) err = NULL; + char *error = NULL; + int status; + + g_assert (sql_path && *sql_path); + g_assert (file_name && *file_name); + g_assert (db); + + input_file = g_build_filename (sql_path, file_name, NULL); + export_file = g_test_build_filename (G_TEST_BUILT, file_name, NULL); + strcpy (export_file + strlen (export_file) - strlen ("sql"), "db"); + g_remove (export_file); + status = sqlite3_open (export_file, db); + g_assert_cmpint (status, ==, SQLITE_OK); + g_file_get_contents (input_file, &content, NULL, &err); + g_assert_no_error (err); + + status = sqlite3_exec (*db, content, NULL, NULL, &error); + if (error) + g_warning ("%s error: %s", G_STRLOC, error); + g_assert_cmpint (status, ==, SQLITE_OK); +} + +static int +db_get_int (sqlite3 *db, + const char *statement) +{ + sqlite3_stmt *stmt; + int value, status; + + g_assert (db); + + status = sqlite3_prepare_v2 (db, statement, -1, &stmt, NULL); + g_assert_cmpint (status, ==, SQLITE_OK); + + status = sqlite3_step (stmt); + g_assert_cmpint (status, ==, SQLITE_ROW); + + value = sqlite3_column_int (stmt, 0); + + sqlite3_finalize (stmt); + + return value; +} + +static void +compare_table (sqlite3 *db, + const char *sql, + int expected_count, + int count) +{ + sqlite3_stmt *stmt; + int status; + + status = sqlite3_prepare_v2 (db, sql, -1, &stmt, NULL); + if (status != SQLITE_OK) + g_warning ("sql error: %s", sqlite3_errmsg (db)); + + g_assert_cmpint (status, ==, SQLITE_OK); + + if (expected_count != count) + g_warning ("%d %d sql: %s", expected_count, count, sql); + status = sqlite3_step (stmt); + g_assert_cmpint (status, ==, SQLITE_ROW); + g_assert_cmpint (expected_count, ==, count); + + count = sqlite3_column_int (stmt, 0); + if (expected_count != count) + g_warning ("%d %d sql: %s", expected_count, count, sql); + + g_assert_cmpint (expected_count, ==, count); + + sqlite3_finalize (stmt); +} + +static void +compare_matrix_db (sqlite3 *db) +{ + /* Pragma version should match */ + g_assert_cmpint (db_get_int (db, "PRAGMA main.user_version;"), ==, + db_get_int (db, "PRAGMA test.user_version;")); + + /* Each db should have the same count of table rows */ + g_assert_cmpint (db_get_int (db, "SELECT COUNT(*) FROM main.sqlite_master;"), + ==, + db_get_int (db, "SELECT COUNT(*) FROM test.sqlite_master;")); + + /* As duplicate rows are removed, SELECT count should match the size of one table. */ + compare_table (db, + "SELECT COUNT (*) FROM (" + "SELECT username,json_data FROM main.users " + "UNION " + "SELECT username,json_data FROM test.users " + ");", + db_get_int (db, "SELECT COUNT(*) FROM main.users;"), + db_get_int (db, "SELECT COUNT(*) FROM test.users;")); + + /* sqlite doesn't guarantee `id` to be always incremented by one. It + * may depend on the order they are updated. And so + * compare by values. + */ + compare_table (db, + "SELECT COUNT (*) FROM (" + "SELECT username,device,users.json_data FROM main.user_devices " + "INNER JOIN main.users ON user_devices.user_id=users.id " + "UNION " + "SELECT username,device,users.json_data FROM test.user_devices " + "INNER JOIN main.users ON user_devices.user_id=users.id " + ");", + db_get_int (db, "SELECT COUNT(*) FROM main.user_devices;"), + db_get_int (db, "SELECT COUNT(*) FROM test.user_devices;")); + + compare_table (db, + "SELECT COUNT (*) FROM (" + "SELECT username,device,next_batch,pickle,enabled,accounts.json_data FROM main.accounts " + "INNER JOIN main.user_devices ON user_devices.id=accounts.user_device_id " + "INNER JOIN main.users ON users.id=user_devices.user_id " + "UNION " + "SELECT username,device,next_batch,pickle,enabled,accounts.json_data FROM test.accounts " + "INNER JOIN test.user_devices ON user_devices.id=accounts.user_device_id " + "INNER JOIN test.users ON users.id=user_devices.user_id " + ");", + db_get_int (db, "SELECT COUNT(*) FROM main.accounts;"), + db_get_int (db, "SELECT COUNT(*) FROM test.accounts;")); + + compare_table (db, + "SELECT COUNT (*) FROM (" + "SELECT username,device,room_name,prev_batch,rooms.json_data FROM main.rooms " + "INNER JOIN main.accounts ON accounts.id=rooms.account_id " + "INNER JOIN main.user_devices ON user_devices.id=accounts.user_device_id " + "INNER JOIN main.users ON user_devices.user_id=users.id " + "UNION " + "SELECT username,device,room_name,prev_batch,rooms.json_data FROM test.rooms " + "INNER JOIN test.accounts ON accounts.id=rooms.account_id " + "INNER JOIN test.user_devices ON user_devices.id=accounts.user_device_id " + "INNER JOIN test.users ON user_devices.user_id=users.id " + ");", + db_get_int (db, "SELECT COUNT(*) FROM main.rooms;"), + db_get_int (db, "SELECT COUNT(*) FROM test.rooms;")); + + compare_table (db, + "SELECT COUNT (*) FROM (" + "SELECT file_url,file_sha256,iv,version,algorithm,key,type," + "extractable,json_data FROM main.encryption_keys " + "UNION " + "SELECT file_url,file_sha256,iv,version,algorithm,key,type," + "extractable,json_data FROM test.encryption_keys " + ");", + db_get_int (db, "SELECT COUNT(*) FROM main.encryption_keys;"), + db_get_int (db, "SELECT COUNT(*) FROM test.encryption_keys;")); + + compare_table (db, + "SELECT COUNT (*) FROM (" + "SELECT username,device,sender_key,session_id,type,sessions.pickle,time,sessions.json_data FROM main.sessions " + "INNER JOIN main.accounts ON accounts.id=sessions.account_id " + "INNER JOIN main.user_devices ON user_devices.id=accounts.user_device_id " + "INNER JOIN main.users ON user_devices.user_id=users.id " + "UNION " + "SELECT username,device,sender_key,session_id,type,sessions.pickle,time,sessions.json_data FROM test.sessions " + "INNER JOIN test.accounts ON accounts.id=sessions.account_id " + "INNER JOIN test.user_devices ON user_devices.id=accounts.user_device_id " + "INNER JOIN test.users ON user_devices.user_id=users.id " + ");", + db_get_int (db, "SELECT COUNT(*) FROM main.sessions;"), + db_get_int (db, "SELECT COUNT(*) FROM test.sessions;")); +} + +static void +test_cm_db_migration (void) +{ + g_autoptr(GDir) dir = NULL; + g_autofree char *path = NULL; + g_autoptr(GError) error = NULL; + const char *name; + + path = g_test_build_filename (G_TEST_DIST, "cm-db", NULL); + dir = g_dir_open (path, 0, &error); + g_assert_no_error (error); + + while ((name = g_dir_read_name (dir)) != NULL) { + g_autofree char *expected_file = NULL; + g_autofree char *input_file = NULL; + g_autofree char *input = NULL; + CmDb *cm_db; + sqlite3 *db = NULL; + GTask *task; + int status; + + if (g_str_has_suffix (name, "v2.sql")) + continue; + + g_assert_true (g_str_has_suffix (name, "sql")); + g_debug ("Migrating %s", name); + + /* Export old version sql file */ + matrix_export_sql_file (path, name, &db); + sqlite3_close (db); + + /* Export migrated version sql file */ + expected_file = g_strdelimit (g_strdup (name), "01", '2'); + matrix_export_sql_file (path, expected_file, &db); + + /* Open history with old db, which will result in db migration */ + input_file = g_strdup (name); + strcpy (input_file + strlen (input_file) - strlen ("sql"), "db"); + + cm_db = cm_db_new (); + task = g_task_new (NULL, NULL, NULL, NULL); + cm_db_open_async (cm_db, g_strdup (g_test_get_dir (G_TEST_BUILT)), + input_file, finish_bool_cb, task); + + while (!g_task_get_completed (task)) + g_main_context_iteration (NULL, TRUE); + + status = g_task_propagate_boolean (task, NULL); + g_assert_true (cm_db_is_open (cm_db)); + g_assert_true (status); + g_assert_finalize_object (task); + + task = g_task_new (NULL, NULL, NULL, NULL); + cm_db_close_async (cm_db, finish_bool_cb, task); + + while (!g_task_get_completed (task)) + g_main_context_iteration (NULL, TRUE); + + status = g_task_propagate_boolean (task, NULL); + g_assert_true (status); + g_assert_false (cm_db_is_open (cm_db)); + g_assert_finalize_object (task); + /* xxx: g_assert_finalize_object (cm_db) fails sometimes when used as subproject */ + g_object_unref (cm_db); + g_free (input_file); + + /* Attach old (now migrated) db with expected migrated db */ + input_file = g_test_build_filename (G_TEST_BUILT, name, NULL); + strcpy (input_file + strlen (input_file) - strlen ("sql"), "db"); + /* The db that's verified is 'main' (shipped as testcase), and the + * generated one by matrix-db is named 'test', which is to be tested */ + input = g_strconcat ("ATTACH '", input_file, "' as test;", NULL); + status = sqlite3_exec (db, input, NULL, NULL, NULL); + g_assert_cmpint (status, ==, SQLITE_OK); + + compare_matrix_db (db); + sqlite3_close (db); + } +} + +int +main (int argc, + char *argv[]) +{ + g_autoptr(CmMatrix) matrix = NULL; + + g_test_init (&argc, &argv, NULL); + + cm_init (TRUE); + matrix = cm_matrix_new (g_test_get_dir (G_TEST_BUILT), + g_test_get_dir (G_TEST_BUILT), + "org.example.CMatrix", + FALSE); + + g_test_add_func ("/cm-db/new", test_cm_db_new); + g_test_add_func ("/cm-db/account", test_cm_db_account); + g_test_add_func ("/cm-db/migration", test_cm_db_migration); + + return g_test_run (); +} diff --git a/subprojects/libcmatrix/tests/cm-db/content-v0.sql b/subprojects/libcmatrix/tests/cm-db/content-v0.sql new file mode 100644 index 0000000000000000000000000000000000000000..a4e24c74c59600b48ff9baf85ef96f4cc8d2a51d --- /dev/null +++ b/subprojects/libcmatrix/tests/cm-db/content-v0.sql @@ -0,0 +1,87 @@ +BEGIN TRANSACTION; + +CREATE TABLE devices( + id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, + device TEXT NOT NULL UNIQUE +); + +CREATE TABLE users( + id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, + username TEXT NOT NULL, + device_id INTEGER REFERENCES devices(id), + UNIQUE (username, device_id) +); + +CREATE TABLE accounts( + id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, + user_id INTEGER NOT NULL REFERENCES users(id), + next_batch TEXT, + pickle TEXT, + enabled INTEGER DEFAULT 0, + UNIQUE (user_id) +); + +CREATE TABLE rooms( + id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, + account_id INTEGER NOT NULL REFERENCES accounts(id), + room_name TEXT NOT NULL, + prev_batch TEXT, + UNIQUE (account_id, room_name) +); + +CREATE TABLE encryption_keys( + id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, + file_url TEXT NOT NULL, + file_sha256 TEXT, + iv TEXT NOT NULL, + version INT DEFAULT 2 NOT NULL, + algorithm INT NOT NULL, + key TEXT NOT NULL, + type INT NOT NULL, + extractable INT DEFAULT 1 NOT NULL, + UNIQUE (file_url) +); + +CREATE TABLE session( + id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, + account_id INTEGER NOT NULL REFERENCES accounts(id), + sender_key TEXT NOT NULL, + session_id TEXT NOT NULL, + type INTEGER NOT NULL, + pickle TEXT NOT NULL, + time INT, + UNIQUE (account_id, sender_key, session_id) +); + + +INSERT INTO devices VALUES(3,'ALICE EXAMPLE COM'); +INSERT INTO devices VALUES(2,'BOB EXAMPLE COM'); +INSERT INTO devices VALUES(4,'ALICE EXAMPLE NET'); +INSERT INTO devices VALUES(5,'ALICE EXAMPLE NET 2'); +INSERT INTO devices VALUES(1,'ALICE EXAMPLE NET 3'); + +INSERT INTO users VALUES(1,'@alice:example.com', 3); +INSERT INTO users VALUES(3,'@bob:example.com', 2); +INSERT INTO users VALUES(2,'@alice:example.net', 4); +INSERT INTO users VALUES(5,'@alice:example.net', 5); +INSERT INTO users VALUES(4,'@alice:example.net', 1); + +INSERT INTO accounts VALUES(3, 4, 'alice example net batch', 'alice example net pickle', 1); +INSERT INTO accounts VALUES(2, 1, 'alice example com batch', 'alice example com pickle', 1); +INSERT INTO accounts VALUES(4, 3, 'bob example com batch', 'bob example com pickle', 0); + +INSERT INTO rooms VALUES(7, 3, 'alice example net room A', 'prev batch 1'); +INSERT INTO rooms VALUES(2, 3, 'alice example net room B', 'prev batch 2'); +INSERT INTO rooms VALUES(4, 4, 'bob example com room C', 'bob com batch 3'); +INSERT INTO rooms VALUES(5, 3, 'alice example net room C', 'prev batch 3'); +INSERT INTO rooms VALUES(1, 3, 'alice example net room D', 'prev batch 4'); +INSERT INTO rooms VALUES(3, 4, 'bob example com room A', 'bob com batch 1'); +INSERT INTO rooms VALUES(9, 4, 'bob example com room B', 'bob com batch 2'); + +INSERT INTO session VALUES(1, 2, 'alice com key 1', 'alice com id 1', 1, 'alice com id 1', 11111111); +INSERT INTO session VALUES(2, 4, 'bob key 1', 'bob id 1', 1, 'bob id 1', 22222222); +INSERT INTO session VALUES(3, 4, 'bob key 2', 'bob id 2', 1, 'bob id 2', 33333333); +INSERT INTO session VALUES(4, 4, 'bob key 3', 'bob id 3', 2, 'bob id 3', 44444444); +INSERT INTO session VALUES(5, 3, 'net key 1', 'net id 1', 1, 'netid 1', 555555); + +COMMIT; diff --git a/subprojects/libcmatrix/tests/cm-db/content-v1.sql b/subprojects/libcmatrix/tests/cm-db/content-v1.sql new file mode 100644 index 0000000000000000000000000000000000000000..5e39d994545bbbcb12c2723f40be6e8a00ad990d --- /dev/null +++ b/subprojects/libcmatrix/tests/cm-db/content-v1.sql @@ -0,0 +1,98 @@ +BEGIN TRANSACTION; + +PRAGMA user_version = 1; +PRAGMA foreign_keys = ON; + +CREATE TABLE users( + id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, + username TEXT NOT NULL UNIQUE, + outdated INTEGER DEFAULT 1, + json_data TEXT +); + +CREATE TABLE user_devices( + id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, + user_id INTEGER NOT NULL REFERENCES users(id), + device TEXT NOT NULL, + curve25519_key TEXT, + ed25519_key TEXT, + verification INTEGER DEFAULT 0, + json_data TEXT, + UNIQUE (user_id, device) +); + +CREATE TABLE accounts( + id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, + user_device_id INTEGER NOT NULL REFERENCES user_devices(id), + next_batch TEXT, + pickle TEXT, + enabled INTEGER DEFAULT 0, + json_data TEXT, + UNIQUE (user_device_id) +); + +CREATE TABLE rooms( + id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, + account_id INTEGER NOT NULL REFERENCES accounts(id), + room_name TEXT NOT NULL, + prev_batch TEXT, + replacement_room_id INTEGER REFERENCES rooms(id), + json_data TEXT, + UNIQUE (account_id, room_name) +); + +CREATE TABLE encryption_keys( + id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, + file_url TEXT NOT NULL, + file_sha256 TEXT, + iv TEXT NOT NULL, + version INT DEFAULT 2 NOT NULL, + algorithm INT NOT NULL, + key TEXT NOT NULL, + type INT NOT NULL, + extractable INT DEFAULT 1 NOT NULL, + json_data TEXT, + UNIQUE (file_url) +); + +CREATE TABLE session( + id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, + account_id INTEGER NOT NULL REFERENCES accounts(id), + sender_key TEXT NOT NULL, + session_id TEXT NOT NULL, + type INTEGER NOT NULL, + pickle TEXT NOT NULL, + time INT, + json_data TEXT, + UNIQUE (account_id, sender_key, session_id) +); + +INSERT INTO users VALUES(1,'@alice:example.com', 1, NULL); +INSERT INTO users VALUES(2,'@alice:example.net', 1, NULL); +INSERT INTO users VALUES(3,'@bob:example.com', 1, NULL); + +INSERT INTO user_devices VALUES(3, 1, 'ALICE EXAMPLE COM', NULL, NULL, 0, NULL); +INSERT INTO user_devices VALUES(2, 2, 'ALICE EXAMPLE NET 3', NULL, NULL, 0, NULL); +INSERT INTO user_devices VALUES(4, 3, 'BOB EXAMPLE COM', NULL, NULL, 0, NULL); +INSERT INTO user_devices VALUES(6, 2, 'ALICE EXAMPLE NET', NULL, NULL, 0, NULL); +INSERT INTO user_devices VALUES(5, 2, 'ALICE EXAMPLE NET 2', NULL, NULL, 0, NULL); + +INSERT INTO accounts VALUES(3, 2, 'alice example net batch', 'alice example net pickle', 1, NULL); +INSERT INTO accounts VALUES(1, 3, 'alice example com batch', 'alice example com pickle', 1, NULL); +INSERT INTO accounts VALUES(4, 4, 'bob example com batch', 'bob example com pickle', 0, NULL); + +INSERT INTO rooms VALUES(8, 3, 'alice example net room A', 'prev batch 1', NULL, NULL); +INSERT INTO rooms VALUES(6, 3, 'alice example net room B', 'prev batch 2', NULL, NULL); +INSERT INTO rooms VALUES(4, 4, 'bob example com room C', 'bob com batch 3', NULL, NULL); +INSERT INTO rooms VALUES(3, 4, 'bob example com room A', 'bob com batch 1', NULL, NULL); +INSERT INTO rooms VALUES(5, 3, 'alice example net room C', 'prev batch 3', NULL, NULL); +INSERT INTO rooms VALUES(9, 4, 'bob example com room B', 'bob com batch 2', NULL, NULL); +INSERT INTO rooms VALUES(2, 3, 'alice example net room D', 'prev batch 4', NULL, NULL); + +INSERT INTO session VALUES(1, 1, 'alice com key 1', 'alice com id 1', 1, 'alice com id 1', 11111111, NULL); +INSERT INTO session VALUES(2, 4, 'bob key 1', 'bob id 1', 1, 'bob id 1', 22222222, NULL); +INSERT INTO session VALUES(3, 4, 'bob key 2', 'bob id 2', 1, 'bob id 2', 33333333, NULL); +INSERT INTO session VALUES(4, 4, 'bob key 3', 'bob id 3', 2, 'bob id 3', 44444444, NULL); +INSERT INTO session VALUES(5, 3, 'net key 1', 'net id 1', 1, 'netid 1', 555555, NULL); + +COMMIT; diff --git a/subprojects/libcmatrix/tests/cm-db/content-v2.sql b/subprojects/libcmatrix/tests/cm-db/content-v2.sql new file mode 100644 index 0000000000000000000000000000000000000000..6c83311ae68eafb13a7b35d182d4aa47e983ebbd --- /dev/null +++ b/subprojects/libcmatrix/tests/cm-db/content-v2.sql @@ -0,0 +1,168 @@ +BEGIN TRANSACTION; + +PRAGMA user_version = 2; +PRAGMA foreign_keys = ON; + +CREATE TABLE users( + id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, + account_id INTEGER REFERENCES accounts(id) ON DELETE CASCADE, + username TEXT NOT NULL, + tracking INTEGER NOT NULL DEFAULT 0, + outdated INTEGER DEFAULT 1, + json_data TEXT, + UNIQUE (account_id, username) +); + +CREATE TABLE user_devices( + id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, + user_id INTEGER NOT NULL REFERENCES users(id), + device TEXT NOT NULL, + curve25519_key TEXT, + ed25519_key TEXT, + verification INTEGER DEFAULT 0, + json_data TEXT, + UNIQUE (user_id, device) +); + +CREATE TABLE accounts( + id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, + user_device_id INTEGER NOT NULL REFERENCES user_devices(id), + next_batch TEXT, + pickle TEXT, + enabled INTEGER DEFAULT 0, + json_data TEXT, + UNIQUE (user_device_id) +); + +CREATE TABLE rooms( + id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, + account_id INTEGER NOT NULL REFERENCES accounts(id) ON DELETE CASCADE, + room_name TEXT NOT NULL, + prev_batch TEXT, + replacement_room_id INTEGER REFERENCES rooms(id), + room_state INTEGER NOT NULL DEFAULT 0, + json_data TEXT, + UNIQUE (account_id, room_name) +); + +CREATE TABLE IF NOT EXISTS room_members ( + id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, + room_id INTEGER NOT NULL REFERENCES rooms(id) ON DELETE CASCADE, + user_id INTEGER NOT NULL REFERENCES users(id), + user_state INTEGER NOT NULL DEFAULT 0, + json_data TEXT, + UNIQUE (room_id, user_id) +); + +CREATE TABLE IF NOT EXISTS room_events_cache ( + id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, + room_id INTEGER NOT NULL REFERENCES rooms(id) ON DELETE CASCADE, + sender_id INTEGER REFERENCES room_members(id), + event_uid TEXT NOT NULL, + origin_server_ts INTEGER, + json_data TEXT, + UNIQUE (room_id, event_uid) +); + +CREATE TABLE IF NOT EXISTS room_events ( + id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, + sorted_id INTEGER NOT NULL, + room_id INTEGER NOT NULL REFERENCES rooms(id) ON DELETE CASCADE, + sender_id INTEGER NOT NULL REFERENCES room_members(id), + event_type INTEGER NOT NULL, + event_uid TEXT, + txnid TEXT, + replaces_event_id INTEGER REFERENCES room_events(id), + replaces_event_cache_id INTEGER REFERENCES room_events_cache(id), + replaced_with_id INTEGER REFERENCES room_events(id), + reply_to_id INTEGER REFERENCES room_events(id), + event_state INTEGER, + state_key TEXT, + origin_server_ts INTEGER NOT NULL, + decryption INTEGER NOT NULL DEFAULT 0, + json_data TEXT, + UNIQUE (room_id, event_uid) +); + +CREATE TABLE encryption_keys( + id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, + account_id INTEGER REFERENCES accounts(id) ON DELETE CASCADE, + file_url TEXT NOT NULL, + file_sha256 TEXT, + iv TEXT NOT NULL, + version INT DEFAULT 2 NOT NULL, + algorithm INT NOT NULL, + key TEXT NOT NULL, + type INT NOT NULL, + extractable INT DEFAULT 1 NOT NULL, + json_data TEXT, + UNIQUE (account_id, file_url) +); + +CREATE TABLE sessions ( + id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, + account_id INTEGER NOT NULL REFERENCES accounts(id) ON DELETE CASCADE, + sender_key TEXT NOT NULL, + session_id TEXT NOT NULL, + type INTEGER NOT NULL, + pickle TEXT NOT NULL, + time INT, + origin_server_ts INTEGER, + chain_index INTEGER, + session_state INTEGER NOT NULL DEFAULT 0, + json_data TEXT, + UNIQUE (account_id, sender_key, session_id) +); + +CREATE UNIQUE INDEX IF NOT EXISTS room_event_idx ON room_events (room_id, event_uid); +CREATE UNIQUE INDEX IF NOT EXISTS room_event_txn_idx ON room_events (room_id, txnid); +CREATE UNIQUE INDEX IF NOT EXISTS user_device_idx ON user_devices (user_id, device); +CREATE INDEX IF NOT EXISTS room_event_state_idx ON room_events (state_key); +CREATE UNIQUE INDEX IF NOT EXISTS room_event_cache_idx ON room_events_cache (room_id, event_uid); +CREATE UNIQUE INDEX IF NOT EXISTS encryption_key_idx ON encryption_keys (account_id, file_url); +CREATE INDEX IF NOT EXISTS session_sender_idx ON sessions (account_id, sender_key); +CREATE INDEX IF NOT EXISTS user_idx ON users (username); + +CREATE TRIGGER IF NOT EXISTS insert_replaced_with_id AFTER INSERT +ON room_events FOR EACH ROW WHEN NEW.replaces_event_id IS NOT NULL +BEGIN + UPDATE room_events SET replaced_with_id=NEW.id + WHERE id=NEW.replaces_event_id AND (replaced_with_id IS NULL or replaced_with_id < NEW.id); +END; + +CREATE TRIGGER IF NOT EXISTS update_replaced_with_id AFTER UPDATE OF replaces_event_id +ON room_events FOR EACH ROW WHEN NEW.replaces_event_id IS NOT NULL +BEGIN + UPDATE room_events SET replaced_with_id=NEW.id + WHERE id=NEW.replaces_event_id AND (replaced_with_id IS NULL or replaced_with_id < NEW.id); +END; + +INSERT INTO users VALUES(1,NULL,'@alice:example.com', 0, 1, NULL); +INSERT INTO users VALUES(2,NULL,'@alice:example.net', 0, 1, NULL); +INSERT INTO users VALUES(3,NULL,'@bob:example.com', 0, 1, NULL); + +INSERT INTO user_devices VALUES(3, 1, 'ALICE EXAMPLE COM', NULL, NULL, 0, NULL); +INSERT INTO user_devices VALUES(2, 2, 'ALICE EXAMPLE NET 3', NULL, NULL, 0, NULL); +INSERT INTO user_devices VALUES(4, 3, 'BOB EXAMPLE COM', NULL, NULL, 0, NULL); +INSERT INTO user_devices VALUES(6, 2, 'ALICE EXAMPLE NET', NULL, NULL, 0, NULL); +INSERT INTO user_devices VALUES(5, 2, 'ALICE EXAMPLE NET 2', NULL, NULL, 0, NULL); + +INSERT INTO accounts VALUES(3, 2, 'alice example net batch', 'alice example net pickle', 1, NULL); +INSERT INTO accounts VALUES(1, 3, 'alice example com batch', 'alice example com pickle', 1, NULL); +INSERT INTO accounts VALUES(4, 4, 'bob example com batch', 'bob example com pickle', 0, NULL); + +INSERT INTO rooms VALUES(8, 3, 'alice example net room A', 'prev batch 1', NULL, 0, NULL); +INSERT INTO rooms VALUES(6, 3, 'alice example net room B', 'prev batch 2', NULL, 0, NULL); +INSERT INTO rooms VALUES(4, 4, 'bob example com room C', 'bob com batch 3', NULL, 0, NULL); +INSERT INTO rooms VALUES(3, 4, 'bob example com room A', 'bob com batch 1', NULL, 0, NULL); +INSERT INTO rooms VALUES(5, 3, 'alice example net room C', 'prev batch 3', NULL, 0, NULL); +INSERT INTO rooms VALUES(9, 4, 'bob example com room B', 'bob com batch 2', NULL, 0, NULL); +INSERT INTO rooms VALUES(2, 3, 'alice example net room D', 'prev batch 4', NULL, 0, NULL); + +INSERT INTO sessions VALUES(1, 1, 'alice com key 1', 'alice com id 1', 1, 'alice com id 1', 11111111, NULL, NULL, 0, NULL); +INSERT INTO sessions VALUES(2, 4, 'bob key 1', 'bob id 1', 1, 'bob id 1', 22222222, NULL, NULL, 0, NULL); +INSERT INTO sessions VALUES(3, 4, 'bob key 2', 'bob id 2', 1, 'bob id 2', 33333333, NULL, NULL, 0, NULL); +INSERT INTO sessions VALUES(4, 4, 'bob key 3', 'bob id 3', 2, 'bob id 3', 44444444, NULL, NULL, 0, NULL); +INSERT INTO sessions VALUES(5, 3, 'net key 1', 'net id 1', 1, 'netid 1', 555555, NULL, NULL, 0, NULL); + +COMMIT; diff --git a/subprojects/libcmatrix/tests/cm-db/empty-v0.sql b/subprojects/libcmatrix/tests/cm-db/empty-v0.sql new file mode 100644 index 0000000000000000000000000000000000000000..093911633265e00267192e93baebb512787ab4c9 --- /dev/null +++ b/subprojects/libcmatrix/tests/cm-db/empty-v0.sql @@ -0,0 +1,56 @@ +BEGIN TRANSACTION; + +CREATE TABLE devices( + id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, + device TEXT NOT NULL UNIQUE +); + +CREATE TABLE users( + id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, + username TEXT NOT NULL, + device_id INTEGER REFERENCES devices(id), + UNIQUE (username, device_id) +); + +CREATE TABLE accounts( + id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, + user_id INTEGER NOT NULL REFERENCES users(id), + next_batch TEXT, + pickle TEXT, + enabled INTEGER DEFAULT 0, + UNIQUE (user_id) +); + +CREATE TABLE rooms( + id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, + account_id INTEGER NOT NULL REFERENCES accounts(id), + room_name TEXT NOT NULL, + prev_batch TEXT, + UNIQUE (account_id, room_name) +); + +CREATE TABLE encryption_keys( + id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, + file_url TEXT NOT NULL, + file_sha256 TEXT, + iv TEXT NOT NULL, + version INT DEFAULT 2 NOT NULL, + algorithm INT NOT NULL, + key TEXT NOT NULL, + type INT NOT NULL, + extractable INT DEFAULT 1 NOT NULL, + UNIQUE (file_url) +); + +CREATE TABLE session( + id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, + account_id INTEGER NOT NULL REFERENCES accounts(id), + sender_key TEXT NOT NULL, + session_id TEXT NOT NULL, + type INTEGER NOT NULL, + pickle TEXT NOT NULL, + time INT, + UNIQUE (account_id, sender_key, session_id) +); + +COMMIT; diff --git a/subprojects/libcmatrix/tests/cm-db/empty-v1.sql b/subprojects/libcmatrix/tests/cm-db/empty-v1.sql new file mode 100644 index 0000000000000000000000000000000000000000..a3a30515bacb202d4f38a51a431c48f7a03c62cb --- /dev/null +++ b/subprojects/libcmatrix/tests/cm-db/empty-v1.sql @@ -0,0 +1,70 @@ +BEGIN TRANSACTION; + +PRAGMA user_version = 1; +PRAGMA foreign_keys = ON; + +CREATE TABLE users( + id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, + username TEXT NOT NULL UNIQUE, + outdated INTEGER DEFAULT 1, + json_data TEXT +); + +CREATE TABLE user_devices( + id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, + user_id INTEGER NOT NULL REFERENCES users(id), + device TEXT NOT NULL, + curve25519_key TEXT, + ed25519_key TEXT, + verification INTEGER DEFAULT 0, + json_data TEXT, + UNIQUE (user_id, device) +); + +CREATE TABLE accounts( + id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, + user_device_id INTEGER NOT NULL REFERENCES user_devices(id), + next_batch TEXT, + pickle TEXT, + enabled INTEGER DEFAULT 0, + json_data TEXT, + UNIQUE (user_device_id) +); + +CREATE TABLE rooms( + id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, + account_id INTEGER NOT NULL REFERENCES accounts(id), + room_name TEXT NOT NULL, + prev_batch TEXT, + replacement_room_id INTEGER REFERENCES rooms(id), + json_data TEXT, + UNIQUE (account_id, room_name) +); + +CREATE TABLE encryption_keys( + id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, + file_url TEXT NOT NULL, + file_sha256 TEXT, + iv TEXT NOT NULL, + version INT DEFAULT 2 NOT NULL, + algorithm INT NOT NULL, + key TEXT NOT NULL, + type INT NOT NULL, + extractable INT DEFAULT 1 NOT NULL, + json_data TEXT, + UNIQUE (file_url) +); + +CREATE TABLE session( + id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, + account_id INTEGER NOT NULL REFERENCES accounts(id), + sender_key TEXT NOT NULL, + session_id TEXT NOT NULL, + type INTEGER NOT NULL, + pickle TEXT NOT NULL, + time INT, + json_data TEXT, + UNIQUE (account_id, sender_key, session_id) +); + +COMMIT; diff --git a/subprojects/libcmatrix/tests/cm-db/empty-v2.sql b/subprojects/libcmatrix/tests/cm-db/empty-v2.sql new file mode 100644 index 0000000000000000000000000000000000000000..698ddd5b8a773d440e325a1f0601bd10b6f6ff33 --- /dev/null +++ b/subprojects/libcmatrix/tests/cm-db/empty-v2.sql @@ -0,0 +1,140 @@ +BEGIN TRANSACTION; + +PRAGMA user_version = 2; +PRAGMA foreign_keys = ON; + +CREATE TABLE users( + id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, + account_id INTEGER REFERENCES accounts(id) ON DELETE CASCADE, + username TEXT NOT NULL, + tracking INTEGER NOT NULL DEFAULT 0, + outdated INTEGER DEFAULT 1, + json_data TEXT, + UNIQUE (account_id, username) +); + +CREATE TABLE user_devices( + id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, + user_id INTEGER NOT NULL REFERENCES users(id), + device TEXT NOT NULL, + curve25519_key TEXT, + ed25519_key TEXT, + verification INTEGER DEFAULT 0, + json_data TEXT, + UNIQUE (user_id, device) +); + +CREATE TABLE accounts( + id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, + user_device_id INTEGER NOT NULL REFERENCES user_devices(id), + next_batch TEXT, + pickle TEXT, + enabled INTEGER DEFAULT 0, + json_data TEXT, + UNIQUE (user_device_id) +); + +CREATE TABLE rooms( + id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, + account_id INTEGER NOT NULL REFERENCES accounts(id) ON DELETE CASCADE, + room_name TEXT NOT NULL, + prev_batch TEXT, + replacement_room_id INTEGER REFERENCES rooms(id), + room_state INTEGER NOT NULL DEFAULT 0, + json_data TEXT, + UNIQUE (account_id, room_name) +); + +CREATE TABLE IF NOT EXISTS room_members ( + id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, + room_id INTEGER NOT NULL REFERENCES rooms(id) ON DELETE CASCADE, + user_id INTEGER NOT NULL REFERENCES users(id), + user_state INTEGER NOT NULL DEFAULT 0, + json_data TEXT, + UNIQUE (room_id, user_id) +); + +CREATE TABLE IF NOT EXISTS room_events_cache ( + id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, + room_id INTEGER NOT NULL REFERENCES rooms(id) ON DELETE CASCADE, + sender_id INTEGER REFERENCES room_members(id), + event_uid TEXT NOT NULL, + origin_server_ts INTEGER, + json_data TEXT, + UNIQUE (room_id, event_uid) +); + +CREATE TABLE IF NOT EXISTS room_events ( + id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, + sorted_id INTEGER NOT NULL, + room_id INTEGER NOT NULL REFERENCES rooms(id) ON DELETE CASCADE, + sender_id INTEGER NOT NULL REFERENCES room_members(id), + event_type INTEGER NOT NULL, + event_uid TEXT, + txnid TEXT, + replaces_event_id INTEGER REFERENCES room_events(id), + replaces_event_cache_id INTEGER REFERENCES room_events_cache(id), + replaced_with_id INTEGER REFERENCES room_events(id), + reply_to_id INTEGER REFERENCES room_events(id), + event_state INTEGER, + state_key TEXT, + origin_server_ts INTEGER NOT NULL, + decryption INTEGER NOT NULL DEFAULT 0, + json_data TEXT, + UNIQUE (room_id, event_uid) +); + +CREATE TABLE encryption_keys( + id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, + account_id INTEGER REFERENCES accounts(id) ON DELETE CASCADE, + file_url TEXT NOT NULL, + file_sha256 TEXT, + iv TEXT NOT NULL, + version INT DEFAULT 2 NOT NULL, + algorithm INT NOT NULL, + key TEXT NOT NULL, + type INT NOT NULL, + extractable INT DEFAULT 1 NOT NULL, + json_data TEXT, + UNIQUE (account_id, file_url) +); + +CREATE TABLE sessions ( + id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, + account_id INTEGER NOT NULL REFERENCES accounts(id) ON DELETE CASCADE, + sender_key TEXT NOT NULL, + session_id TEXT NOT NULL, + type INTEGER NOT NULL, + pickle TEXT NOT NULL, + time INT, + origin_server_ts INTEGER, + chain_index INTEGER, + session_state INTEGER NOT NULL DEFAULT 0, + json_data TEXT, + UNIQUE (account_id, sender_key, session_id) +); + +CREATE UNIQUE INDEX IF NOT EXISTS room_event_idx ON room_events (room_id, event_uid); +CREATE UNIQUE INDEX IF NOT EXISTS room_event_txn_idx ON room_events (room_id, txnid); +CREATE UNIQUE INDEX IF NOT EXISTS user_device_idx ON user_devices (user_id, device); +CREATE INDEX IF NOT EXISTS room_event_state_idx ON room_events (state_key); +CREATE UNIQUE INDEX IF NOT EXISTS room_event_cache_idx ON room_events_cache (room_id, event_uid); +CREATE UNIQUE INDEX IF NOT EXISTS encryption_key_idx ON encryption_keys (account_id, file_url); +CREATE INDEX IF NOT EXISTS session_sender_idx ON sessions (account_id, sender_key); +CREATE INDEX IF NOT EXISTS user_idx ON users (username); + +CREATE TRIGGER IF NOT EXISTS insert_replaced_with_id AFTER INSERT +ON room_events FOR EACH ROW WHEN NEW.replaces_event_id IS NOT NULL +BEGIN + UPDATE room_events SET replaced_with_id=NEW.id + WHERE id=NEW.replaces_event_id AND (replaced_with_id IS NULL or replaced_with_id < NEW.id); +END; + +CREATE TRIGGER IF NOT EXISTS update_replaced_with_id AFTER UPDATE OF replaces_event_id +ON room_events FOR EACH ROW WHEN NEW.replaces_event_id IS NOT NULL +BEGIN + UPDATE room_events SET replaced_with_id=NEW.id + WHERE id=NEW.replaces_event_id AND (replaced_with_id IS NULL or replaced_with_id < NEW.id); +END; + +COMMIT; diff --git a/tests/matrix-enc.c b/subprojects/libcmatrix/tests/cm-enc.c similarity index 78% rename from tests/matrix-enc.c rename to subprojects/libcmatrix/tests/cm-enc.c index c1ad7c550f61b0494c7b7239e45b7fc4fc3fe428..38d887c340c5a5ccd9f9041af6cc809f7adcf07c 100644 --- a/tests/matrix-enc.c +++ b/subprojects/libcmatrix/tests/cm-enc.c @@ -1,7 +1,7 @@ /* -*- mode: c; c-basic-offset: 2; indent-tabs-mode: nil; -*- */ /* matrix-utils.c * - * Copyright 2020 Purism SPC + * Copyright 2022 Purism SPC * * Author(s): * Mohammed Sadiq <sadiq@sadiqpk.org> @@ -23,8 +23,8 @@ #include <olm/olm.h> #include <sys/random.h> -#include "matrix-utils.h" -#include "matrix-enc.h" +#include "cm-utils-private.h" +#include "cm-enc-private.h" typedef struct EncData { char *user_id; @@ -72,27 +72,27 @@ EncData enc[] = { static void -test_matrix_enc_verify (void) +test_cm_enc_verify (void) { - g_autoptr(MatrixEnc) enc1 = NULL; - g_autoptr(MatrixEnc) enc2 = NULL; + g_autoptr(CmEnc) enc1 = NULL; + g_autoptr(CmEnc) enc2 = NULL; JsonObject *object, *root; char *sign, *json, *key_label; const char *message; EncData data; data = enc[0]; - enc1 = matrix_enc_new (NULL, enc[0].olm2_pickle, enc[0].pickle_key); - enc2 = matrix_enc_new (NULL, enc[1].olm2_pickle, enc[1].pickle_key); - g_assert (MATRIX_IS_ENC (enc1)); - g_assert (MATRIX_IS_ENC (enc2)); + enc1 = cm_enc_new (NULL, enc[0].olm2_pickle, enc[0].pickle_key); + enc2 = cm_enc_new (NULL, enc[1].olm2_pickle, enc[1].pickle_key); + g_assert (CM_IS_ENC (enc1)); + g_assert (CM_IS_ENC (enc2)); /* @message is in canonical form */ message = "{\"timeout\":20000,\"type\":\"m.message\"}"; - sign = matrix_enc_sign_string (enc1, message, -1); + sign = cm_enc_sign_string (enc1, message, -1); g_assert_nonnull (sign); - root = matrix_utils_string_to_json_object (message); + root = cm_utils_string_to_json_object (message); g_assert_nonnull (root); json_object_set_object_member (root, "signatures", json_object_new ()); @@ -103,63 +103,63 @@ test_matrix_enc_verify (void) key_label = g_strconcat ("ed25519:", data.device_id, NULL); json_object_set_string_member (object, key_label, sign); - json = matrix_utils_json_object_to_string (root, FALSE); + json = cm_utils_json_object_to_string (root, FALSE); g_assert_nonnull (json); g_free (sign); g_free (key_label); json_object_unref (root); - root = matrix_utils_string_to_json_object (json); + root = cm_utils_string_to_json_object (json); g_assert_nonnull (root); - g_assert_true (matrix_enc_verify (enc1, root, data.user_id, - data.device_id, data.ed_key)); - g_assert_true (matrix_enc_verify (enc2, root, data.user_id, - data.device_id, data.ed_key)); - g_assert_false (matrix_enc_verify (enc1, root, data.user_id, - data.device_id, data.curve_key)); - g_assert_false (matrix_enc_verify (enc1, root, enc[1].user_id, - data.device_id, data.ed_key)); + g_assert_true (cm_enc_verify (enc1, root, data.user_id, + data.device_id, data.ed_key)); + g_assert_true (cm_enc_verify (enc2, root, data.user_id, + data.device_id, data.ed_key)); + g_assert_false (cm_enc_verify (enc1, root, data.user_id, + data.device_id, data.curve_key)); + g_assert_false (cm_enc_verify (enc1, root, enc[1].user_id, + data.device_id, data.ed_key)); json_object_unref (root); g_free (json); } static void -test_matrix_enc_new (void) +test_cm_enc_new (void) { const char *value; char *pickle; EncData data; for (guint i = 0; i < G_N_ELEMENTS (enc); i++) { - g_autoptr(MatrixEnc) matrix_enc = NULL; + g_autoptr(CmEnc) cm_enc = NULL; data = enc[i]; #ifdef HAVE_OLM3 - matrix_enc = matrix_enc_new (NULL, data.olm3_pickle, data.pickle_key); - g_assert (MATRIX_IS_ENC (matrix_enc)); + cm_enc = cm_enc_new (NULL, data.olm3_pickle, data.pickle_key); + g_assert (CM_IS_ENC (cm_enc)); - pickle = matrix_enc_get_account_pickle (matrix_enc); + pickle = cm_enc_get_pickle (cm_enc); # ifdef OLM_ACCOUNT_PICKLE_V4 g_assert_cmpstr (pickle, ==, data.olm3_v4_pickle); # else g_assert_cmpstr (pickle, ==, data.olm3_pickle); # endif g_clear_pointer (&pickle, g_free); - g_object_unref (matrix_enc); + g_object_unref (cm_enc); #endif - matrix_enc = matrix_enc_new (NULL, data.olm2_pickle, data.pickle_key); - g_assert (MATRIX_IS_ENC (matrix_enc)); + cm_enc = cm_enc_new (NULL, data.olm2_pickle, data.pickle_key); + g_assert (CM_IS_ENC (cm_enc)); - value = matrix_enc_get_curve25519_key (matrix_enc); + value = cm_enc_get_curve25519_key (cm_enc); g_assert_cmpstr (value, ==, data.curve_key); - value = matrix_enc_get_ed25519_key (matrix_enc); + value = cm_enc_get_ed25519_key (cm_enc); g_assert_cmpstr (value, ==, data.ed_key); - pickle = matrix_enc_get_account_pickle (matrix_enc); + pickle = cm_enc_get_pickle (cm_enc); #ifdef HAVE_OLM3 # ifdef OLM_ACCOUNT_PICKLE_V4 g_assert_cmpstr (pickle, ==, data.olm3_v4_pickle); @@ -179,8 +179,8 @@ main (int argc, { g_test_init (&argc, &argv, NULL); - g_test_add_func ("/matrix/enc/new", test_matrix_enc_new); - g_test_add_func ("/matrix/enc/verify", test_matrix_enc_verify); + g_test_add_func ("/matrix/enc/new", test_cm_enc_new); + g_test_add_func ("/matrix/enc/verify", test_cm_enc_verify); return g_test_run (); } diff --git a/subprojects/libcmatrix/tests/cm-utils.c b/subprojects/libcmatrix/tests/cm-utils.c new file mode 100644 index 0000000000000000000000000000000000000000..900802650b45c803e15dfde62ab2d3c9d0ac85f8 --- /dev/null +++ b/subprojects/libcmatrix/tests/cm-utils.c @@ -0,0 +1,209 @@ +/* -*- mode: c; c-basic-offset: 2; indent-tabs-mode: nil; -*- */ +/* cm-utils.c + * + * Copyright 2022 Purism SPC + * + * Author(s): + * Mohammed Sadiq <sadiq@sadiqpk.org> + * + * SPDX-License-Identifier: LGPL-2.1-or-later + */ + +#undef NDEBUG +#undef G_DISABLE_ASSERT +#undef G_DISABLE_CHECKS +#undef G_DISABLE_CAST_CHECKS +#undef G_LOG_DOMAIN + +#include "cm-utils-private.h" + +static JsonObject * +get_json_object_for_file (const char *dir, + const char *file_name) +{ + g_autoptr(JsonParser) parser = NULL; + g_autoptr(GError) error = NULL; + g_autofree char *path = NULL; + + path = g_build_filename (dir, file_name, NULL); + parser = json_parser_new (); + json_parser_load_from_file (parser, path, &error); + g_assert_no_error (error); + + return json_node_dup_object (json_parser_get_root (parser)); +} + +static void +test_utils_canonical (void) +{ + g_autoptr(GError) error = NULL; + g_autoptr(GDir) dir = NULL; + g_autofree char *path = NULL; + const char *name; + + path = g_test_build_filename (G_TEST_DIST, "cm-utils", NULL); + dir = g_dir_open (path, 0, &error); + g_assert_no_error (error); + + while ((name = g_dir_read_name (dir)) != NULL) { + g_autofree char *expected_file = NULL; + g_autofree char *expected_name = NULL; + g_autofree char *expected_json = NULL; + g_autoptr(JsonObject) object = NULL; + g_autoptr(GString) json_str = NULL; + + if (!g_str_has_suffix (name, ".json")) + continue; + + expected_name = g_strconcat (name, ".expected", NULL); + expected_file = g_build_filename (path, expected_name, NULL); + g_file_get_contents (expected_file, &expected_json, NULL, &error); + g_assert_no_error (error); + + object = get_json_object_for_file (path, name); + json_str = cm_utils_json_get_canonical (object, NULL); + g_assert_cmpstr (json_str->str, ==, expected_json); + } +} + +static void +test_utils_valid_user_name (void) +{ + struct Data + { + const char *user_name; + gboolean valid; + } data[] = { + {NULL, FALSE}, + {"", FALSE}, + {"@:.", FALSE}, + {"@bob:", FALSE}, + {"@:example.org", FALSE}, + {"abc", FALSE}, + {"good@bad:com", FALSE}, + {"@a:example.org", TRUE}, + {"@alice:example.org", TRUE}, + {"@alice:example.org@alice:example.org", FALSE}, + {"@alice:sub.example.org", TRUE}, + {"@bob:localhost", TRUE}, + }; + + for (guint i = 0; i < G_N_ELEMENTS (data); i++) + { + const char *user_name = data[i].user_name; + gboolean valid = data[i].valid; + + if (valid) + g_assert_true (cm_utils_user_name_valid (user_name)); + else + g_assert_false (cm_utils_user_name_valid (user_name)); + } +} + +static void +test_utils_valid_email (void) +{ + struct Data + { + const char *email; + gboolean valid; + } data[] = { + {"", FALSE}, + {"@:.", FALSE}, + {"@bob:", FALSE}, + {"@:example.org", FALSE}, + {"abc", FALSE}, + {"good@bad:com", FALSE}, + {"@a:example.org", FALSE}, + {"@alice:example.org", FALSE}, + {"test@user.com", TRUE}, + {"test@user.comtest@user.com", FALSE}, + {"തറ@home.com", TRUE}, + }; + + for (guint i = 0; i < G_N_ELEMENTS (data); i++) + { + const char *email = data[i].email; + gboolean valid = data[i].valid; + + if (valid) + g_assert_true (cm_utils_user_name_is_email (email)); + else + g_assert_false (cm_utils_user_name_is_email (email)); + } +} + +static void +test_utils_valid_phone (void) +{ + struct Data + { + const char *phone; + gboolean valid; + } data[] = { + {"", FALSE}, + {"123", FALSE}, + {"+9123", FALSE}, + {"+91223344", FALSE}, + {"+91123456789", TRUE}, + {"+13123456789", TRUE}, + {"+13123456789002211443", FALSE}, + }; + + for (guint i = 0; i < G_N_ELEMENTS (data); i++) + { + const char *phone = data[i].phone; + gboolean valid = data[i].valid; + + if (valid) + g_assert_true (cm_utils_mobile_is_valid (phone)); + else + g_assert_false (cm_utils_mobile_is_valid (phone)); + } +} + +static void +test_utils_valid_home_server (void) +{ + struct Data + { + const char *uri; + gboolean valid; + } data[] = { + {"", FALSE}, + {"http://", FALSE}, + {"ftp://example.com", FALSE}, + {"http://example.com", TRUE}, + {"https://example.com", TRUE}, + {"http://example.com/", TRUE}, + {"http://example.com.", FALSE}, + {"http://localhost:8008", TRUE}, + {"http://localhost:8008/path", FALSE}, + }; + + for (guint i = 0; i < G_N_ELEMENTS (data); i++) + { + const char *uri = data[i].uri; + gboolean valid = data[i].valid; + + if (valid) + g_assert_true (cm_utils_home_server_valid (uri)); + else + g_assert_false (cm_utils_home_server_valid (uri)); + } +} + +int +main (int argc, + char *argv[]) +{ + g_test_init (&argc, &argv, NULL); + + g_test_add_func ("/cm-utils/canonical", test_utils_canonical); + g_test_add_func ("/cm-utils/valid-user-name", test_utils_valid_user_name); + g_test_add_func ("/cm-utils/valid-email", test_utils_valid_email); + g_test_add_func ("/cm-utils/valid-phone", test_utils_valid_phone); + g_test_add_func ("/cm-utils/valid-home-server", test_utils_valid_home_server); + + return g_test_run (); +} diff --git a/tests/matrix-utils/canonical-0.json b/subprojects/libcmatrix/tests/cm-utils/canonical-0.json similarity index 100% rename from tests/matrix-utils/canonical-0.json rename to subprojects/libcmatrix/tests/cm-utils/canonical-0.json diff --git a/tests/matrix-utils/canonical-0.json.expected b/subprojects/libcmatrix/tests/cm-utils/canonical-0.json.expected similarity index 100% rename from tests/matrix-utils/canonical-0.json.expected rename to subprojects/libcmatrix/tests/cm-utils/canonical-0.json.expected diff --git a/tests/matrix-utils/canonical-1.json b/subprojects/libcmatrix/tests/cm-utils/canonical-1.json similarity index 100% rename from tests/matrix-utils/canonical-1.json rename to subprojects/libcmatrix/tests/cm-utils/canonical-1.json diff --git a/tests/matrix-utils/canonical-1.json.expected b/subprojects/libcmatrix/tests/cm-utils/canonical-1.json.expected similarity index 100% rename from tests/matrix-utils/canonical-1.json.expected rename to subprojects/libcmatrix/tests/cm-utils/canonical-1.json.expected diff --git a/tests/matrix-utils/canonical-2.json b/subprojects/libcmatrix/tests/cm-utils/canonical-2.json similarity index 100% rename from tests/matrix-utils/canonical-2.json rename to subprojects/libcmatrix/tests/cm-utils/canonical-2.json diff --git a/tests/matrix-utils/canonical-2.json.expected b/subprojects/libcmatrix/tests/cm-utils/canonical-2.json.expected similarity index 100% rename from tests/matrix-utils/canonical-2.json.expected rename to subprojects/libcmatrix/tests/cm-utils/canonical-2.json.expected diff --git a/tests/matrix-utils/canonical-3.json b/subprojects/libcmatrix/tests/cm-utils/canonical-3.json similarity index 100% rename from tests/matrix-utils/canonical-3.json rename to subprojects/libcmatrix/tests/cm-utils/canonical-3.json diff --git a/tests/matrix-utils/canonical-3.json.expected b/subprojects/libcmatrix/tests/cm-utils/canonical-3.json.expected similarity index 100% rename from tests/matrix-utils/canonical-3.json.expected rename to subprojects/libcmatrix/tests/cm-utils/canonical-3.json.expected diff --git a/tests/matrix-utils/canonical-4.json b/subprojects/libcmatrix/tests/cm-utils/canonical-4.json similarity index 100% rename from tests/matrix-utils/canonical-4.json rename to subprojects/libcmatrix/tests/cm-utils/canonical-4.json diff --git a/tests/matrix-utils/canonical-4.json.expected b/subprojects/libcmatrix/tests/cm-utils/canonical-4.json.expected similarity index 100% rename from tests/matrix-utils/canonical-4.json.expected rename to subprojects/libcmatrix/tests/cm-utils/canonical-4.json.expected diff --git a/tests/matrix-utils/canonical-5.json b/subprojects/libcmatrix/tests/cm-utils/canonical-5.json similarity index 100% rename from tests/matrix-utils/canonical-5.json rename to subprojects/libcmatrix/tests/cm-utils/canonical-5.json diff --git a/tests/matrix-utils/canonical-5.json.expected b/subprojects/libcmatrix/tests/cm-utils/canonical-5.json.expected similarity index 100% rename from tests/matrix-utils/canonical-5.json.expected rename to subprojects/libcmatrix/tests/cm-utils/canonical-5.json.expected diff --git a/tests/matrix-utils/canonical-6.json b/subprojects/libcmatrix/tests/cm-utils/canonical-6.json similarity index 100% rename from tests/matrix-utils/canonical-6.json rename to subprojects/libcmatrix/tests/cm-utils/canonical-6.json diff --git a/tests/matrix-utils/canonical-6.json.expected b/subprojects/libcmatrix/tests/cm-utils/canonical-6.json.expected similarity index 100% rename from tests/matrix-utils/canonical-6.json.expected rename to subprojects/libcmatrix/tests/cm-utils/canonical-6.json.expected diff --git a/tests/matrix-utils/canonical-7.json b/subprojects/libcmatrix/tests/cm-utils/canonical-7.json similarity index 100% rename from tests/matrix-utils/canonical-7.json rename to subprojects/libcmatrix/tests/cm-utils/canonical-7.json diff --git a/tests/matrix-utils/canonical-7.json.expected b/subprojects/libcmatrix/tests/cm-utils/canonical-7.json.expected similarity index 100% rename from tests/matrix-utils/canonical-7.json.expected rename to subprojects/libcmatrix/tests/cm-utils/canonical-7.json.expected diff --git a/tests/matrix-utils/canonical-8.json b/subprojects/libcmatrix/tests/cm-utils/canonical-8.json similarity index 100% rename from tests/matrix-utils/canonical-8.json rename to subprojects/libcmatrix/tests/cm-utils/canonical-8.json diff --git a/tests/matrix-utils/canonical-8.json.expected b/subprojects/libcmatrix/tests/cm-utils/canonical-8.json.expected similarity index 100% rename from tests/matrix-utils/canonical-8.json.expected rename to subprojects/libcmatrix/tests/cm-utils/canonical-8.json.expected diff --git a/subprojects/libcmatrix/tests/enc-chat.c b/subprojects/libcmatrix/tests/enc-chat.c new file mode 100644 index 0000000000000000000000000000000000000000..0f0a96b879f2456284ea3b66fbf07986c7212ff1 --- /dev/null +++ b/subprojects/libcmatrix/tests/enc-chat.c @@ -0,0 +1,171 @@ +/* -*- mode: c; c-basic-offset: 2; indent-tabs-mode: nil; -*- */ +/* session.c + * + * Copyright 2022 Purism SPC + * + * Author(s): + * Mohammed Sadiq <sadiq@sadiqpk.org> + * + * SPDX-License-Identifier: LGPL-2.1-or-later + */ + +#undef NDEBUG +#undef G_DISABLE_ASSERT +#undef G_DISABLE_CHECKS +#undef G_DISABLE_CAST_CHECKS +#undef G_LOG_DOMAIN + +#include "src/cm-enc.c" +#include "cm-matrix.h" +#include "cm-matrix-private.h" + +static void +finish_bool_cb (GObject *object, + GAsyncResult *result, + gpointer user_data) +{ + g_autoptr(GError) error = NULL; + GTask *task = user_data; + gboolean status; + + g_assert_true (G_IS_TASK (task)); + + status = g_task_propagate_boolean (G_TASK (result), &error); + g_assert_no_error (error); + g_task_return_boolean (task, status); +} + +/* 'n' starts from 1 */ +static JsonObject * +get_nth_member (JsonObject *root, + guint n) +{ + g_autoptr(GList) members = NULL; + + members = json_object_get_members (root); + + if (g_list_length (members) < n) + return NULL; + + return json_object_get_object_member (root, g_list_nth_data (members, n)); +} + +static void +test_enc_chat_new (void) +{ + char *alice_one_time, *bob_one_time; + CmEnc *alice_enc, *bob_enc; + JsonObject *obj, *root; + CmMatrix *matrix; + size_t len; + + matrix = g_object_new (CM_TYPE_MATRIX, NULL); + + { + GTask *task; + GError *error = NULL; + + task = g_task_new (NULL, NULL, NULL, NULL); + cm_matrix_open_async (matrix, + g_test_get_dir (G_TEST_BUILT), + "test-chat.c", + NULL, + finish_bool_cb, task); + + while (!g_task_get_completed (task)) + g_main_context_iteration (NULL, TRUE); + + g_assert_true (g_task_propagate_boolean (task, &error)); + g_assert_no_error (error); + g_assert_finalize_object (task); + } + + /* Generate new keys for each */ + { + GRefString *matrix_id; + + matrix_id = g_ref_string_new_intern ("@alice:example.org"); + alice_enc = cm_enc_new (cm_matrix_get_db (matrix), NULL, NULL); + cm_enc_set_details (alice_enc, matrix_id, "SYNAPSE"); + g_ref_string_release (matrix_id); + + matrix_id = g_ref_string_new_intern ("@bob:example.org"); + bob_enc = cm_enc_new (cm_matrix_get_db (matrix), NULL, NULL); + cm_enc_set_details (bob_enc, matrix_id, "DENDRITE"); + g_ref_string_release (matrix_id); + } + + /* Generate one time keys */ + len = cm_enc_create_one_time_keys (alice_enc, 3); + g_assert_cmpint (len, ==, 3); + len = cm_enc_create_one_time_keys (bob_enc, 3); + g_assert_cmpint (len, ==, 3); + + alice_one_time = cm_enc_get_one_time_keys_json (alice_enc); + bob_one_time = cm_enc_get_one_time_keys_json (bob_enc); + g_assert_nonnull (alice_one_time); + g_assert_nonnull (bob_one_time); + + cm_enc_publish_one_time_keys (alice_enc); + cm_enc_publish_one_time_keys (bob_enc); + g_assert_null (cm_enc_get_one_time_keys_json (alice_enc)); + g_assert_null (cm_enc_get_one_time_keys_json (bob_enc)); + + root = cm_utils_string_to_json_object (alice_one_time); + g_assert_nonnull (root); + obj = cm_utils_json_object_get_object (root, "one_time_keys"); + obj = get_nth_member (obj, 1); + g_assert_nonnull (obj); + /* Verify Alice's one time key with Bob's device */ + g_assert_true (cm_enc_verify (bob_enc, obj, + cm_enc_get_user_id (alice_enc), + cm_enc_get_device_id (alice_enc), + cm_enc_get_ed25519_key (alice_enc))); + json_object_unref (root); + + root = cm_utils_string_to_json_object (bob_one_time); + g_assert_nonnull (root); + obj = cm_utils_json_object_get_object (root, "one_time_keys"); + obj = get_nth_member (obj, 1); + g_assert_nonnull (obj); + /* Verify Bob's one time key with Alice's device */ + g_assert_true (cm_enc_verify (alice_enc, obj, + cm_enc_get_user_id (bob_enc), + cm_enc_get_device_id (bob_enc), + cm_enc_get_ed25519_key (bob_enc))); + /* Verify Bob's one time key with Bob's device */ + g_assert_true (cm_enc_verify (bob_enc, obj, + cm_enc_get_user_id (bob_enc), + cm_enc_get_device_id (bob_enc), + cm_enc_get_ed25519_key (bob_enc))); + g_assert_false (cm_enc_verify (bob_enc, obj, + cm_enc_get_user_id (alice_enc), + cm_enc_get_device_id (alice_enc), + cm_enc_get_ed25519_key (alice_enc))); + json_object_unref (root); + + g_free (alice_one_time); + g_free (bob_one_time); + + g_assert_finalize_object (alice_enc); + g_assert_finalize_object (bob_enc); + g_assert_finalize_object (matrix); +} + +int +main (int argc, + char *argv[]) +{ + g_autoptr(CmMatrix) matrix = NULL; + + g_test_init (&argc, &argv, NULL); + + cm_init (TRUE); + matrix = cm_matrix_new (g_test_get_dir (G_TEST_BUILT), + g_test_get_dir (G_TEST_BUILT), + "org.example.CMatrix", + FALSE); + g_test_add_func ("/enc-chat/new", test_enc_chat_new); + + return g_test_run (); +} diff --git a/subprojects/libcmatrix/tests/meson.build b/subprojects/libcmatrix/tests/meson.build new file mode 100644 index 0000000000000000000000000000000000000000..dafe69c32766a08237b81891d1c6625f2bb2c384 --- /dev/null +++ b/subprojects/libcmatrix/tests/meson.build @@ -0,0 +1,32 @@ +tests_inc = [ + root_inc, + src_inc, +] + +env = environment() +env.set('G_TEST_SRCDIR', meson.current_source_dir()) +env.set('G_TEST_BUILDDIR', meson.current_build_dir()) +env.set('MALLOC_CHECK_', '2') + +test_items = [] + +test_items = [ + 'client', + 'enc-chat', + 'cm-db', + 'cm-enc', + 'room', + 'room-member', + 'cm-utils', +] + +foreach item: test_items + t = executable( + item, + item + '.c', + include_directories: tests_inc, + link_with: cmatrix_lib, + dependencies: cmatrix_deps, + ) + test(item, t, env: env, timeout: 120) +endforeach diff --git a/subprojects/libcmatrix/tests/room-member.c b/subprojects/libcmatrix/tests/room-member.c new file mode 100644 index 0000000000000000000000000000000000000000..d0c86986a4ba68ed485e3251d4242a3d8d7f9b83 --- /dev/null +++ b/subprojects/libcmatrix/tests/room-member.c @@ -0,0 +1,57 @@ +/* -*- mode: c; c-basic-offset: 2; indent-tabs-mode: nil; -*- */ +/* room-member.c + * + * Copyright 2022 Purism SPC + * + * Author(s): + * Mohammed Sadiq <sadiq@sadiqpk.org> + * + * SPDX-License-Identifier: LGPL-2.1-or-later + */ + +#undef NDEBUG +#undef G_DISABLE_ASSERT +#undef G_DISABLE_CHECKS +#undef G_DISABLE_CAST_CHECKS +#undef G_LOG_DOMAIN + +#include "cm-room-private.h" + +#undef G_LOG_DOMAIN +#include "users/cm-room-member.c" + +static void +test_room_member_new (void) +{ + CmRoomMember *member; + GRefString *user_id; + CmClient *client; + CmRoom *room; + CmUser *user; + + room = cm_room_new ("random room"); + client = cm_client_new (); + cm_room_set_client (room, client); + user_id = g_ref_string_new_intern ("@alice:example.co"); + member = cm_room_member_new (user_id); + g_ref_string_release (user_id); + user = CM_USER (member); + cm_user_set_client (user, client); + g_object_unref (client); + g_assert (CM_IS_ROOM_MEMBER (member)); + + g_assert_cmpstr (cm_user_get_id (user), ==, "@alice:example.co"); + + g_assert_finalize_object (room); +} + +int +main (int argc, + char *argv[]) +{ + g_test_init (&argc, &argv, NULL); + + g_test_add_func ("/room-member/new", test_room_member_new); + + return g_test_run (); +} diff --git a/subprojects/libcmatrix/tests/room.c b/subprojects/libcmatrix/tests/room.c new file mode 100644 index 0000000000000000000000000000000000000000..3a01e8eb36fe754b3dab80c80a900afd6a8ed193 --- /dev/null +++ b/subprojects/libcmatrix/tests/room.c @@ -0,0 +1,44 @@ +/* -*- mode: c; c-basic-offset: 2; indent-tabs-mode: nil; -*- */ +/* room.c + * + * Copyright 2022 Purism SPC + * + * Author(s): + * Mohammed Sadiq <sadiq@sadiqpk.org> + * + * SPDX-License-Identifier: LGPL-2.1-or-later + */ + +#undef NDEBUG +#undef G_DISABLE_ASSERT +#undef G_DISABLE_CHECKS +#undef G_DISABLE_CAST_CHECKS +#undef G_LOG_DOMAIN + +#include "cm-room.c" + +static void +test_room_new (void) +{ + CmRoom *room; + + room = cm_room_new ("some-room-id"); + g_assert (CM_IS_ROOM (room)); + + g_assert_cmpstr (cm_room_get_id (room), ==, "some-room-id"); + + g_assert_false (cm_room_is_encrypted (room)); + + g_assert_finalize_object (room); +} + +int +main (int argc, + char *argv[]) +{ + g_test_init (&argc, &argv, NULL); + + g_test_add_func ("/room/new", test_room_new); + + return g_test_run (); +} diff --git a/tests/history.c b/tests/history.c index 954d3589cce53f5df477570a097df8e130015315..3f5707968e92f22dc8e39000a80126c9b2c8b1c4 100644 --- a/tests/history.c +++ b/tests/history.c @@ -738,69 +738,6 @@ add_chatty_message (ChattyHistory *history, } } -static void -add_chat_and_test (ChattyHistory *history, - ChattyMaAccount *account, - const char *room_id, - const char *room_name, - gboolean hidden, - guint total_count) -{ - g_autoptr(GPtrArray) chat_list = NULL; - g_autoptr(GTask) task = NULL; - - if (room_id) { - g_autoptr(ChattyMaChat) chat = NULL; - - chat = chatty_ma_chat_new (room_id, room_name, NULL, FALSE); - g_assert (CHATTY_IS_MA_CHAT (chat)); - if (hidden) - chatty_item_set_state (CHATTY_ITEM (chat), CHATTY_ITEM_HIDDEN); - chatty_ma_account_add_chat (account, CHATTY_CHAT (chat)); - g_assert_true (chatty_history_update_chat (history, CHATTY_CHAT (chat))); - } - - task = g_task_new (NULL, NULL, NULL, NULL); - chatty_history_get_chats_async (history, CHATTY_ACCOUNT (account), - finish_pointer_cb, task); - - while (!g_task_get_completed (task)) - g_main_context_iteration (NULL, TRUE); - chat_list = g_task_propagate_pointer (task, NULL); - if (total_count) { - g_assert_nonnull (chat_list); - g_assert_cmpint (chat_list->len, ==, total_count); - } else { - g_assert_null (chat_list); - } -} - -static void -test_history_chat (void) -{ - g_autoptr(ChattyHistory) history = NULL; - g_autoptr(ChattyMaAccount) ma_account = NULL; - const char *account; - - g_remove (g_test_get_filename (G_TEST_BUILT, "test-history.db", NULL)); - - history = chatty_history_new (); - chatty_history_open (history, g_test_get_dir (G_TEST_BUILT), "test-history.db"); - g_assert_true (chatty_history_is_open (history)); - - account = "@alice:example.com"; - ma_account = chatty_ma_account_new (account, NULL); - g_assert (CHATTY_IS_MA_ACCOUNT (ma_account)); - - add_chat_and_test (history, ma_account, NULL, NULL, FALSE, 0); - add_chat_and_test (history, ma_account, "!xdesSDcdsSXXs", "Test room", FALSE, 1); - add_chat_and_test (history, ma_account, "!aabbdesSDcdsS", "Another test room", FALSE, 2); - add_chat_and_test (history, ma_account, "!aabbdesSDcdsS", "Name changed", FALSE, 2); - add_chat_and_test (history, ma_account, "!aabbdesSDcdsS", "Name changed", TRUE, 1); - - chatty_history_close (history); -} - static void test_history_message (void) { @@ -1236,7 +1173,6 @@ main (int argc, g_setenv ("GSETTINGS_BACKEND", "memory", TRUE); g_test_add_func ("/history/new", test_history_new); - g_test_add_func ("/history/chat", test_history_chat); g_test_add_func ("/history/message", test_history_message); g_test_add_func ("/history/raw_message", test_history_raw_message); g_test_add_func ("/history/db", test_history_db); diff --git a/tests/matrix-api.c b/tests/matrix-api.c deleted file mode 100644 index 8be72fc24dfca6c773fd79e4f355eacc996a3159..0000000000000000000000000000000000000000 --- a/tests/matrix-api.c +++ /dev/null @@ -1,71 +0,0 @@ -/* -*- mode: c; c-basic-offset: 2; indent-tabs-mode: nil; -*- */ -/* matrix-api.c - * - * Copyright 2020 Purism SPC - * - * Author(s): - * Mohammed Sadiq <sadiq@sadiqpk.org> - * - * SPDX-License-Identifier: GPL-3.0-or-later - */ - -#include "matrix-api.h" - -static void -test_matrix_api_new (void) -{ - MatrixApi *api; - const char *name; - - name = "@alice:example.com"; - api = matrix_api_new (NULL); - g_assert (MATRIX_IS_API (api)); - g_assert_cmpstr (matrix_api_get_username (api), ==, NULL); - matrix_api_set_login_username (api, name); - g_assert_cmpstr (matrix_api_get_login_username (api), ==, name); - g_object_unref (api); - - name = "@alice:example.org"; - api = matrix_api_new (name); - g_assert (MATRIX_IS_API (api)); - g_assert_cmpstr (matrix_api_get_login_username (api), ==, name); - g_assert_cmpstr (matrix_api_get_password (api), ==, NULL); - g_assert_cmpstr (matrix_api_get_homeserver (api), ==, NULL); - - matrix_api_set_password (api, "hunter2"); - g_assert_cmpstr (matrix_api_get_password (api), ==, "hunter2"); - matrix_api_set_password (api, NULL); - g_assert_cmpstr (matrix_api_get_password (api), ==, "hunter2"); - - matrix_api_set_homeserver (api, "example.net"); - g_assert_cmpstr (matrix_api_get_homeserver (api), ==, NULL); - matrix_api_set_homeserver (api, "https://example.com"); - g_assert_cmpstr (matrix_api_get_homeserver (api), ==, "https://example.com"); - matrix_api_set_homeserver (api, NULL); - g_assert_cmpstr (matrix_api_get_homeserver (api), ==, "https://example.com"); - matrix_api_set_homeserver (api, "https://example.org/"); - g_assert_cmpstr (matrix_api_get_homeserver (api), ==, "https://example.org"); - matrix_api_set_homeserver (api, "https://chat.example.net/page"); - g_assert_cmpstr (matrix_api_get_homeserver (api), ==, "https://chat.example.net"); - matrix_api_set_homeserver (api, "http://talk.example.com"); - g_assert_cmpstr (matrix_api_get_homeserver (api), ==, "http://talk.example.com"); - matrix_api_set_homeserver (api, "http://example.com:80"); - g_assert_cmpstr (matrix_api_get_homeserver (api), ==, "http://example.com"); - matrix_api_set_homeserver (api, "http://example.com:80"); - g_assert_cmpstr (matrix_api_get_homeserver (api), ==, "http://example.com"); - matrix_api_set_homeserver (api, "https://talk.example.net:80"); - g_assert_cmpstr (matrix_api_get_homeserver (api), ==, "https://talk.example.net:80"); - - g_object_unref (api); -} - -int -main (int argc, - char *argv[]) -{ - g_test_init (&argc, &argv, NULL); - - g_test_add_func ("/matrix/api/new", test_matrix_api_new); - - return g_test_run (); -} diff --git a/tests/matrix-db.c b/tests/matrix-db.c deleted file mode 100644 index 2ddd66850e8347dbccf055def34adbb72dc7a301..0000000000000000000000000000000000000000 --- a/tests/matrix-db.c +++ /dev/null @@ -1,230 +0,0 @@ -/* -*- mode: c; c-basic-offset: 2; indent-tabs-mode: nil; -*- */ -/* matrix-db.c - * - * Copyright 2020 Purism SPC - * - * Author(s): - * Mohammed Sadiq <sadiq@sadiqpk.org> - * - * SPDX-License-Identifier: GPL-3.0-or-later - */ - -#undef NDEBUG -#undef G_DISABLE_ASSERT -#undef G_DISABLE_CHECKS -#undef G_DISABLE_CAST_CHECKS -#undef G_LOG_DOMAIN - -#include <glib/gstdio.h> -#include <sqlite3.h> - -#include "chatty-ma-account.h" -#include "matrix-db.h" - -static void -finish_bool_cb (GObject *object, - GAsyncResult *result, - gpointer user_data) -{ - g_autoptr(GError) error = NULL; - GTask *task = user_data; - GObject *obj; - gboolean status; - - g_assert_true (G_IS_TASK (task)); - - status = g_task_propagate_boolean (G_TASK (result), &error); - g_assert_no_error (error); - - obj = G_OBJECT (result); - g_object_set_data (user_data, "enabled", g_object_get_data (obj, "enabled")); - g_object_set_data_full (user_data, "pickle", g_object_steal_data (obj, "pickle"), g_free); - g_object_set_data_full (user_data, "device", g_object_steal_data (obj, "device"), g_free); - g_object_set_data_full (user_data, "username", g_object_steal_data (obj, "username"), g_free); - g_task_return_boolean (task, status); -} - -static gboolean -account_matches_username (gconstpointer account, - gconstpointer username) -{ - const char *id = username; - - g_assert_true (CHATTY_IS_MA_ACCOUNT ((gpointer)account)); - g_assert_true (id && *id); - - return g_strcmp0 (id, chatty_item_get_username ((gpointer)account)) == 0; -} - -static void -add_matrix_account (MatrixDb *db, - GPtrArray *account_array, - const char *username, - const char *pickle, - const char *device_id, - gboolean enabled) -{ - ChattyMaAccount *account; - GObject *object; - GTask *task; - GError *error = NULL; - gboolean success; - guint i; - - g_assert_true (MATRIX_IS_DB (db)); - g_assert_nonnull (account_array); - g_assert_nonnull (username); - - if (g_ptr_array_find_with_equal_func (account_array, username, - account_matches_username, &i)) { - account = account_array->pdata[i]; - } else { - account = chatty_ma_account_new (username, NULL); - g_ptr_array_add (account_array, account); - } - - g_assert_true (CHATTY_IS_MA_ACCOUNT (account)); - object = G_OBJECT (account); - - g_object_set_data (object, "enabled", GINT_TO_POINTER (enabled)); - g_object_set_data_full (object, "pickle", g_strdup (pickle), g_free); - g_object_set_data_full (object, "device", g_strdup (device_id), g_free); - - task = g_task_new (NULL, NULL, NULL, NULL); - matrix_db_save_account_async (db, CHATTY_ACCOUNT (account), enabled, g_strdup (pickle), device_id, - NULL, finish_bool_cb, task); - - while (!g_task_get_completed (task)) - g_main_context_iteration (NULL, TRUE); - - success = g_task_propagate_boolean (task, &error); - g_assert_no_error (error); - g_assert_true (success); - g_clear_object (&task); - - g_assert_true (g_ptr_array_find (account_array, account, &i)); - account = account_array->pdata[i]; - task = g_task_new (NULL, NULL, NULL, NULL); - matrix_db_load_account_async (db, CHATTY_ACCOUNT (account), device_id, finish_bool_cb, task); - - while (!g_task_get_completed (task)) - g_main_context_iteration (NULL, TRUE); - - success = g_task_propagate_boolean (task, &error); - g_assert_no_error (error); - g_assert_true (success); - g_assert_cmpstr (g_object_get_data (G_OBJECT (task), "username"), ==, - chatty_item_get_username (CHATTY_ITEM (account))); - g_assert_cmpint (GPOINTER_TO_INT (g_object_get_data (G_OBJECT (task), "enabled")), - ==, GPOINTER_TO_INT (g_object_get_data (object, "enabled"))); - g_assert_cmpstr (g_object_get_data (G_OBJECT (task), "pickle"), ==, - g_object_get_data (object, "pickle")); - g_assert_cmpstr (g_object_get_data (G_OBJECT (task), "device"), ==, - g_object_get_data (object, "device")); - g_clear_object (&task); -} - -static void -test_matrix_db_account (void) -{ - GTask *task; - MatrixDb *db; - gboolean status; - GPtrArray *account_array; - - g_remove (g_test_get_filename (G_TEST_BUILT, "test-matrix.db", NULL)); - - db = matrix_db_new (); - task = g_task_new (NULL, NULL, NULL, NULL); - matrix_db_open_async (db, g_strdup (g_test_get_dir (G_TEST_BUILT)), - "test-matrix.db", finish_bool_cb, task); - - while (!g_task_get_completed (task)) - g_main_context_iteration (NULL, TRUE); - - status = g_task_propagate_boolean (task, NULL); - g_assert_finalize_object (task); - g_assert_true (status); - - account_array = g_ptr_array_new (); - g_ptr_array_set_free_func (account_array, (GDestroyNotify)g_object_unref); - - add_matrix_account (db, account_array, "@alice:example.org", - NULL, NULL, TRUE); - add_matrix_account (db, account_array, "@alice:example.org", - NULL, NULL, FALSE); - add_matrix_account (db, account_array, "@alice:example.com", - NULL, "XXAABBDD", FALSE); - add_matrix_account (db, account_array, "@alice:example.com", - "Some Pickle", "XXAABBDD", TRUE); - add_matrix_account (db, account_array, "@alice:example.org", - NULL, NULL, TRUE); - - add_matrix_account (db, account_array, "@bob:example.org", - NULL, NULL, FALSE); - add_matrix_account (db, account_array, "@alice:example.org", - NULL, NULL, FALSE); - add_matrix_account (db, account_array, "@bob:example.org", - NULL, NULL, TRUE); - - add_matrix_account (db, account_array, "@alice:example.net", - NULL, NULL, TRUE); - add_matrix_account (db, account_array, "@alice:example.com", - NULL, NULL, FALSE); - - g_ptr_array_unref (account_array); -} - -static void -test_matrix_db_new (void) -{ - const char *file_name; - MatrixDb *db; - GTask *task; - gboolean status; - - file_name = g_test_get_filename (G_TEST_BUILT, "test-matrix.db", NULL); - g_remove (file_name); - g_assert_false (g_file_test (file_name, G_FILE_TEST_EXISTS)); - - db = matrix_db_new (); - task = g_task_new (NULL, NULL, NULL, NULL); - matrix_db_open_async (db, g_strdup (g_test_get_dir (G_TEST_BUILT)), - "test-matrix.db", finish_bool_cb, task); - - while (!g_task_get_completed (task)) - g_main_context_iteration (NULL, TRUE); - - status = g_task_propagate_boolean (task, NULL); - g_assert_true (g_file_test (file_name, G_FILE_TEST_IS_REGULAR)); - g_assert_true (matrix_db_is_open (db)); - g_assert_true (status); - g_clear_object (&task); - - task = g_task_new (NULL, NULL, NULL, NULL); - matrix_db_close_async (db, finish_bool_cb, task); - - while (!g_task_get_completed (task)) - g_main_context_iteration (NULL, TRUE); - - status = g_task_propagate_boolean (task, NULL); - g_assert_true (status); - g_assert_false (matrix_db_is_open (db)); - g_clear_object (&db); - g_clear_object (&task); - - g_remove (file_name); - g_assert_false (g_file_test (file_name, G_FILE_TEST_EXISTS)); -} - -int -main (int argc, - char *argv[]) -{ - g_test_init (&argc, &argv, NULL); - - g_test_add_func ("/matrix-db/new", test_matrix_db_new); - g_test_add_func ("/matrix-db/account", test_matrix_db_account); - - return g_test_run (); -} diff --git a/tests/matrix-utils.c b/tests/matrix-utils.c deleted file mode 100644 index f64bfd6fec8c8c8618e022aab3c1c90bba88e680..0000000000000000000000000000000000000000 --- a/tests/matrix-utils.c +++ /dev/null @@ -1,83 +0,0 @@ -/* -*- mode: c; c-basic-offset: 2; indent-tabs-mode: nil; -*- */ -/* matrix-utils.c - * - * Copyright 2020 Purism SPC - * - * Author(s): - * Mohammed Sadiq <sadiq@sadiqpk.org> - * - * SPDX-License-Identifier: GPL-3.0-or-later - */ - -#undef NDEBUG -#undef G_DISABLE_ASSERT -#undef G_DISABLE_CHECKS -#undef G_DISABLE_CAST_CHECKS -#undef G_LOG_DOMAIN - -#include <glib.h> - -#include "matrix-utils.h" - -static JsonObject * -get_json_object_for_file (const char *dir, - const char *file_name) -{ - g_autoptr(JsonParser) parser = NULL; - g_autoptr(GError) error = NULL; - g_autofree char *path = NULL; - - path = g_build_filename (dir, file_name, NULL); - parser = json_parser_new (); - json_parser_load_from_file (parser, path, &error); - g_assert_no_error (error); - - return json_node_dup_object (json_parser_get_root (parser)); -} - -static void -test_matrix_utils_canonical (void) -{ - g_autoptr(GError) error = NULL; - g_autoptr(GDir) dir = NULL; - g_autofree char *path = NULL; - const char *name; - - path = g_test_build_filename (G_TEST_DIST, "matrix-utils", NULL); - dir = g_dir_open (path, 0, &error); - g_assert_no_error (error); - - while ((name = g_dir_read_name (dir)) != NULL) { - g_autofree char *expected_file = NULL; - g_autofree char *expected_name = NULL; - g_autofree char *expected_json = NULL; - g_autoptr(JsonObject) object = NULL; - g_autoptr(GString) json_str = NULL; - - if (!g_str_has_suffix (name, ".json")) - continue; - - expected_name = g_strconcat (name, ".expected", NULL); - expected_file = g_build_filename (path, expected_name, NULL); - g_file_get_contents (expected_file, &expected_json, NULL, &error); - g_assert_no_error (error); - - object = get_json_object_for_file (path, name); - json_str = matrix_utils_json_get_canonical (object, NULL); - g_assert_cmpstr (json_str->str, ==, expected_json); - } -} - -int -main (int argc, - char *argv[]) -{ - /* DEBUG */ - g_setenv ("G_TEST_SRCDIR", "/media/sadiq/temp/jhbuild/checkout/chatty/tests/", FALSE); - - g_test_init (&argc, &argv, NULL); - - g_test_add_func ("/matrix/utils/canonical", test_matrix_utils_canonical); - - return g_test_run (); -} diff --git a/tests/meson.build b/tests/meson.build index 78069afb0f4adda8050759ec414e620904a7b1aa..86147b0ef467eaf66bb8ce43df85469cad282113 100644 --- a/tests/meson.build +++ b/tests/meson.build @@ -6,7 +6,8 @@ tests_inc = [ env = environment() env.set('G_TEST_SRCDIR', meson.current_source_dir()) env.set('G_TEST_BUILDDIR', meson.current_build_dir()) -env.set('GSETTINGS_SCHEMA_DIR', join_paths(meson.build_root(), 'data')) +env.set('GSETTINGS_BACKEND', 'memory') +env.set('GSETTINGS_SCHEMA_DIR', join_paths(meson.project_build_root(), 'data')) env.set('MALLOC_CHECK_', '2') test_items = [] @@ -20,10 +21,6 @@ test_items = [ 'history', 'settings', 'utils', - 'matrix-api', - 'matrix-db', - 'matrix-enc', - 'matrix-utils', 'message-text-item', 'mm-account', 'sms-uri', @@ -34,7 +31,7 @@ foreach item: test_items item, item + '.c', include_directories: tests_inc, - link_with: libchatty.get_static_lib(), + link_with: libchatty_static, dependencies: chatty_deps, ) test(item, t, env: env, timeout: 300) diff --git a/tests/sms-uri.c b/tests/sms-uri.c index da02b34acdb5a46e49bcbaf3586b71ff3cacfc95..3830d6868e4e1f4fd2788ff7965f223ffcd5be34 100644 --- a/tests/sms-uri.c +++ b/tests/sms-uri.c @@ -42,6 +42,7 @@ data array[] = { { "sms://+919995112233?body=I'm busy", "I'm busy", "+919995112233", 1, TRUE, TRUE}, { "sms://123,453?body=a ചെറിയ test", "a ചെറിയ test", "123,453", 2, TRUE, TRUE}, { "sms://453,123,145,123,453?body=HELP", "HELP", "123,145,453", 3, TRUE, TRUE}, + { "sms://123,123,123,123,123?body=HELP", "HELP", "123", 1, TRUE, TRUE}, { "sms://453,123,145,123,453?body=HELP%20me", "HELP me", "123,145,453", 3, TRUE, TRUE}, { "sms:9995 123 123?body=Call me later", "Call me later", "+919995123123", 1, TRUE, TRUE, "IN"}, { "sms:+919995 123 123?body= before and after ", " before and after ", "+919995123123", 1, TRUE, TRUE, "US"},