From 29093379b707bfe620234c2968b37aa86666542a Mon Sep 17 00:00:00 2001 From: Kitsune Ral Date: Fri, 30 Mar 2018 14:03:24 +0900 Subject: Introduce install target for the library Closes #113. --- .travis.yml | 7 +++++-- CMakeLists.txt | 28 ++++++++++++++++++++++++++++ 2 files changed, 33 insertions(+), 2 deletions(-) diff --git a/.travis.yml b/.travis.yml index a5938670..2a03267e 100644 --- a/.travis.yml +++ b/.travis.yml @@ -26,10 +26,13 @@ before_install: script: - mkdir build && cd build -- cmake .. +- cmake -DCMAKE_INSTALL_PREFIX=../install .. - cmake --build . --target all +- cmake --build . --target install - cd .. -- qmake qmc-example.pro "CONFIG += debug" "CONFIG -= app_bundle" "QMAKE_CC = $CC" "QMAKE_CXX = $CXX" +- ls -R install +- mv lib lib.back # Force the below script to use the installed copy instead +- qmake qmc-example.pro "CONFIG += debug" "CONFIG -= app_bundle" "QMAKE_CC = $CC" "QMAKE_CXX = $CXX" "INCLUDES += -I./install/include" "LIBS += -L./install/lib" - make all - if [ "$QMC_TEST_USER" != "" ]; then $VALGRIND ./qmc-example "$QMC_TEST_USER" "$QMC_TEST_PWD" qmc-example-travis '#qmc-test:matrix.org' "Travis CI job $TRAVIS_JOB_NUMBER"; fi diff --git a/CMakeLists.txt b/CMakeLists.txt index 82ab2b55..3ee2200d 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -3,6 +3,9 @@ cmake_minimum_required(VERSION 3.1) project(qmatrixclient CXX) include(CheckCXXCompilerFlag) +if (NOT WIN32) + include(GNUInstallDirs) +endif(NOT WIN32) # Find includes in corresponding build directories set(CMAKE_INCLUDE_CURRENT_DIR ON) @@ -86,6 +89,31 @@ target_link_libraries(qmatrixclient Qt5::Core Qt5::Network Qt5::Gui) add_executable(qmc-example ${example_SRCS}) target_link_libraries(qmc-example Qt5::Core qmatrixclient) +# Installation + +if (NOT CMAKE_INSTALL_LIBDIR) + set(CMAKE_INSTALL_LIBDIR ".") +endif() + +if (NOT CMAKE_INSTALL_BINDIR) + set(CMAKE_INSTALL_BINDIR ".") +endif() + +if (NOT CMAKE_INSTALL_INCLUDEDIR) + set(CMAKE_INSTALL_INCLUDEDIR "include") +endif() + +install (TARGETS qmatrixclient EXPORT QMatrixClient + ARCHIVE DESTINATION ${CMAKE_INSTALL_LIBDIR} + LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR} + INCLUDES DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}) +install (DIRECTORY . DESTINATION ${CMAKE_INSTALL_INCLUDEDIR} + FILES_MATCHING PATTERN "*.h") + +install (TARGETS qmc-example RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR}) + +install (EXPORT QMatrixClient DESTINATION ${CMAKE_INSTALL_LIBDIR}/cmake/qmatrixclient) + if (WIN32) install (FILES mime/packages/freedesktop.org.xml DESTINATION mime/packages) -- cgit v1.2.3 From efeb50a46ad824aa258472f6ac8da74810f05a55 Mon Sep 17 00:00:00 2001 From: Kitsune Ral Date: Sat, 31 Mar 2018 13:16:02 +0900 Subject: Move source files to a separate folder It's been long overdue to separate them from the rest of the stuff (docs etc.). Also, this allows installing to a directory within the checked out git tree (say, ./install/, similar to ./build/). --- .travis.yml | 6 +- CMakeLists.txt | 93 +- avatar.cpp | 187 --- avatar.h | 61 - connection.cpp | 943 ------------- connection.h | 475 ------- connectiondata.cpp | 108 -- connectiondata.h | 55 - converters.h | 177 --- events/accountdataevents.h | 78 -- events/directchatevent.cpp | 36 - events/directchatevent.h | 34 - events/event.cpp | 182 --- events/event.h | 314 ----- events/eventcontent.cpp | 85 -- events/eventcontent.h | 314 ----- events/receiptevent.cpp | 70 - events/receiptevent.h | 50 - events/redactionevent.cpp | 1 - events/redactionevent.h | 43 - events/roomavatarevent.cpp | 23 - events/roomavatarevent.h | 43 - events/roommemberevent.cpp | 69 - events/roommemberevent.h | 78 -- events/roommessageevent.cpp | 193 --- events/roommessageevent.h | 194 --- events/simplestateevents.h | 53 - events/typingevent.cpp | 32 - events/typingevent.h | 39 - jobs/basejob.cpp | 508 ------- jobs/basejob.h | 303 ---- jobs/checkauthmethods.cpp | 53 - jobs/checkauthmethods.h | 40 - jobs/downloadfilejob.cpp | 120 -- jobs/downloadfilejob.h | 30 - jobs/generated/account-data.cpp | 28 - jobs/generated/account-data.h | 27 - jobs/generated/administrative_contact.cpp | 122 -- jobs/generated/administrative_contact.h | 83 -- jobs/generated/banning.cpp | 34 - jobs/generated/banning.h | 26 - jobs/generated/content-repo.cpp | 254 ---- jobs/generated/content-repo.h | 129 -- jobs/generated/create_room.cpp | 115 -- jobs/generated/create_room.h | 55 - jobs/generated/directory.cpp | 76 - jobs/generated/directory.h | 58 - jobs/generated/inviting.cpp | 23 - jobs/generated/inviting.h | 20 - jobs/generated/kicking.cpp | 25 - jobs/generated/kicking.h | 20 - jobs/generated/leaving.cpp | 38 - jobs/generated/leaving.h | 40 - jobs/generated/list_public_rooms.cpp | 266 ---- jobs/generated/list_public_rooms.h | 106 -- jobs/generated/login.cpp | 79 -- jobs/generated/login.h | 33 - jobs/generated/logout.cpp | 26 - jobs/generated/logout.h | 27 - jobs/generated/profile.cpp | 140 -- jobs/generated/profile.h | 96 -- jobs/generated/receipts.cpp | 21 - jobs/generated/receipts.h | 21 - jobs/generated/redaction.cpp | 45 - jobs/generated/redaction.h | 30 - jobs/generated/third_party_membership.cpp | 25 - jobs/generated/third_party_membership.h | 20 - jobs/generated/typing.cpp | 24 - jobs/generated/typing.h | 20 - jobs/generated/versions.cpp | 47 - jobs/generated/versions.h | 38 - jobs/generated/whoami.cpp | 50 - jobs/generated/whoami.h | 37 - jobs/joinroomjob.cpp | 58 - jobs/joinroomjob.h | 40 - jobs/mediathumbnailjob.cpp | 63 - jobs/mediathumbnailjob.h | 47 - jobs/passwordlogin.cpp | 74 - jobs/passwordlogin.h | 42 - jobs/postreadmarkersjob.h | 37 - jobs/postreceiptjob.cpp | 27 - jobs/postreceiptjob.h | 30 - jobs/requestdata.cpp | 38 - jobs/requestdata.h | 59 - jobs/roommessagesjob.cpp | 65 - jobs/roommessagesjob.h | 47 - jobs/sendeventjob.cpp | 45 - jobs/sendeventjob.h | 57 - jobs/setroomstatejob.cpp | 32 - jobs/setroomstatejob.h | 64 - jobs/syncjob.cpp | 133 -- jobs/syncjob.h | 99 -- joinstate.h | 48 - lib/avatar.cpp | 187 +++ lib/avatar.h | 61 + lib/connection.cpp | 943 +++++++++++++ lib/connection.h | 475 +++++++ lib/connectiondata.cpp | 108 ++ lib/connectiondata.h | 55 + lib/converters.h | 177 +++ lib/events/accountdataevents.h | 78 ++ lib/events/directchatevent.cpp | 36 + lib/events/directchatevent.h | 34 + lib/events/event.cpp | 182 +++ lib/events/event.h | 314 +++++ lib/events/eventcontent.cpp | 85 ++ lib/events/eventcontent.h | 314 +++++ lib/events/receiptevent.cpp | 70 + lib/events/receiptevent.h | 50 + lib/events/redactionevent.cpp | 1 + lib/events/redactionevent.h | 43 + lib/events/roomavatarevent.cpp | 23 + lib/events/roomavatarevent.h | 43 + lib/events/roommemberevent.cpp | 69 + lib/events/roommemberevent.h | 78 ++ lib/events/roommessageevent.cpp | 193 +++ lib/events/roommessageevent.h | 194 +++ lib/events/simplestateevents.h | 53 + lib/events/typingevent.cpp | 32 + lib/events/typingevent.h | 39 + lib/jobs/basejob.cpp | 508 +++++++ lib/jobs/basejob.h | 303 ++++ lib/jobs/checkauthmethods.cpp | 53 + lib/jobs/checkauthmethods.h | 40 + lib/jobs/downloadfilejob.cpp | 120 ++ lib/jobs/downloadfilejob.h | 30 + lib/jobs/generated/account-data.cpp | 28 + lib/jobs/generated/account-data.h | 27 + lib/jobs/generated/administrative_contact.cpp | 122 ++ lib/jobs/generated/administrative_contact.h | 83 ++ lib/jobs/generated/banning.cpp | 34 + lib/jobs/generated/banning.h | 26 + lib/jobs/generated/content-repo.cpp | 254 ++++ lib/jobs/generated/content-repo.h | 129 ++ lib/jobs/generated/create_room.cpp | 115 ++ lib/jobs/generated/create_room.h | 55 + lib/jobs/generated/directory.cpp | 76 + lib/jobs/generated/directory.h | 58 + lib/jobs/generated/inviting.cpp | 23 + lib/jobs/generated/inviting.h | 20 + lib/jobs/generated/kicking.cpp | 25 + lib/jobs/generated/kicking.h | 20 + lib/jobs/generated/leaving.cpp | 38 + lib/jobs/generated/leaving.h | 40 + lib/jobs/generated/list_public_rooms.cpp | 266 ++++ lib/jobs/generated/list_public_rooms.h | 106 ++ lib/jobs/generated/login.cpp | 79 ++ lib/jobs/generated/login.h | 33 + lib/jobs/generated/logout.cpp | 26 + lib/jobs/generated/logout.h | 27 + lib/jobs/generated/profile.cpp | 140 ++ lib/jobs/generated/profile.h | 96 ++ lib/jobs/generated/receipts.cpp | 21 + lib/jobs/generated/receipts.h | 21 + lib/jobs/generated/redaction.cpp | 45 + lib/jobs/generated/redaction.h | 30 + lib/jobs/generated/third_party_membership.cpp | 25 + lib/jobs/generated/third_party_membership.h | 20 + lib/jobs/generated/typing.cpp | 24 + lib/jobs/generated/typing.h | 20 + lib/jobs/generated/versions.cpp | 47 + lib/jobs/generated/versions.h | 38 + lib/jobs/generated/whoami.cpp | 50 + lib/jobs/generated/whoami.h | 37 + lib/jobs/joinroomjob.cpp | 58 + lib/jobs/joinroomjob.h | 40 + lib/jobs/mediathumbnailjob.cpp | 63 + lib/jobs/mediathumbnailjob.h | 47 + lib/jobs/passwordlogin.cpp | 74 + lib/jobs/passwordlogin.h | 42 + lib/jobs/postreadmarkersjob.h | 37 + lib/jobs/postreceiptjob.cpp | 27 + lib/jobs/postreceiptjob.h | 30 + lib/jobs/requestdata.cpp | 38 + lib/jobs/requestdata.h | 59 + lib/jobs/roommessagesjob.cpp | 65 + lib/jobs/roommessagesjob.h | 47 + lib/jobs/sendeventjob.cpp | 45 + lib/jobs/sendeventjob.h | 57 + lib/jobs/setroomstatejob.cpp | 32 + lib/jobs/setroomstatejob.h | 64 + lib/jobs/syncjob.cpp | 133 ++ lib/jobs/syncjob.h | 99 ++ lib/joinstate.h | 48 + lib/logging.cpp | 33 + lib/logging.h | 78 ++ lib/networkaccessmanager.cpp | 75 + lib/networkaccessmanager.h | 49 + lib/networksettings.cpp | 31 + lib/networksettings.h | 44 + lib/room.cpp | 1851 +++++++++++++++++++++++++ lib/room.h | 424 ++++++ lib/settings.cpp | 123 ++ lib/settings.h | 134 ++ lib/user.cpp | 399 ++++++ lib/user.h | 125 ++ lib/util.h | 205 +++ libqmatrixclient.pri | 132 +- logging.cpp | 33 - logging.h | 78 -- networkaccessmanager.cpp | 75 - networkaccessmanager.h | 49 - networksettings.cpp | 31 - networksettings.h | 44 - room.cpp | 1851 ------------------------- room.h | 424 ------ settings.cpp | 123 -- settings.h | 134 -- state.cpp | 69 - state.h | 47 - user.cpp | 399 ------ user.h | 125 -- util.h | 205 --- 213 files changed, 12111 insertions(+), 12218 deletions(-) delete mode 100644 avatar.cpp delete mode 100644 avatar.h delete mode 100644 connection.cpp delete mode 100644 connection.h delete mode 100644 connectiondata.cpp delete mode 100644 connectiondata.h delete mode 100644 converters.h delete mode 100644 events/accountdataevents.h delete mode 100644 events/directchatevent.cpp delete mode 100644 events/directchatevent.h delete mode 100644 events/event.cpp delete mode 100644 events/event.h delete mode 100644 events/eventcontent.cpp delete mode 100644 events/eventcontent.h delete mode 100644 events/receiptevent.cpp delete mode 100644 events/receiptevent.h delete mode 100644 events/redactionevent.cpp delete mode 100644 events/redactionevent.h delete mode 100644 events/roomavatarevent.cpp delete mode 100644 events/roomavatarevent.h delete mode 100644 events/roommemberevent.cpp delete mode 100644 events/roommemberevent.h delete mode 100644 events/roommessageevent.cpp delete mode 100644 events/roommessageevent.h delete mode 100644 events/simplestateevents.h delete mode 100644 events/typingevent.cpp delete mode 100644 events/typingevent.h delete mode 100644 jobs/basejob.cpp delete mode 100644 jobs/basejob.h delete mode 100644 jobs/checkauthmethods.cpp delete mode 100644 jobs/checkauthmethods.h delete mode 100644 jobs/downloadfilejob.cpp delete mode 100644 jobs/downloadfilejob.h delete mode 100644 jobs/generated/account-data.cpp delete mode 100644 jobs/generated/account-data.h delete mode 100644 jobs/generated/administrative_contact.cpp delete mode 100644 jobs/generated/administrative_contact.h delete mode 100644 jobs/generated/banning.cpp delete mode 100644 jobs/generated/banning.h delete mode 100644 jobs/generated/content-repo.cpp delete mode 100644 jobs/generated/content-repo.h delete mode 100644 jobs/generated/create_room.cpp delete mode 100644 jobs/generated/create_room.h delete mode 100644 jobs/generated/directory.cpp delete mode 100644 jobs/generated/directory.h delete mode 100644 jobs/generated/inviting.cpp delete mode 100644 jobs/generated/inviting.h delete mode 100644 jobs/generated/kicking.cpp delete mode 100644 jobs/generated/kicking.h delete mode 100644 jobs/generated/leaving.cpp delete mode 100644 jobs/generated/leaving.h delete mode 100644 jobs/generated/list_public_rooms.cpp delete mode 100644 jobs/generated/list_public_rooms.h delete mode 100644 jobs/generated/login.cpp delete mode 100644 jobs/generated/login.h delete mode 100644 jobs/generated/logout.cpp delete mode 100644 jobs/generated/logout.h delete mode 100644 jobs/generated/profile.cpp delete mode 100644 jobs/generated/profile.h delete mode 100644 jobs/generated/receipts.cpp delete mode 100644 jobs/generated/receipts.h delete mode 100644 jobs/generated/redaction.cpp delete mode 100644 jobs/generated/redaction.h delete mode 100644 jobs/generated/third_party_membership.cpp delete mode 100644 jobs/generated/third_party_membership.h delete mode 100644 jobs/generated/typing.cpp delete mode 100644 jobs/generated/typing.h delete mode 100644 jobs/generated/versions.cpp delete mode 100644 jobs/generated/versions.h delete mode 100644 jobs/generated/whoami.cpp delete mode 100644 jobs/generated/whoami.h delete mode 100644 jobs/joinroomjob.cpp delete mode 100644 jobs/joinroomjob.h delete mode 100644 jobs/mediathumbnailjob.cpp delete mode 100644 jobs/mediathumbnailjob.h delete mode 100644 jobs/passwordlogin.cpp delete mode 100644 jobs/passwordlogin.h delete mode 100644 jobs/postreadmarkersjob.h delete mode 100644 jobs/postreceiptjob.cpp delete mode 100644 jobs/postreceiptjob.h delete mode 100644 jobs/requestdata.cpp delete mode 100644 jobs/requestdata.h delete mode 100644 jobs/roommessagesjob.cpp delete mode 100644 jobs/roommessagesjob.h delete mode 100644 jobs/sendeventjob.cpp delete mode 100644 jobs/sendeventjob.h delete mode 100644 jobs/setroomstatejob.cpp delete mode 100644 jobs/setroomstatejob.h delete mode 100644 jobs/syncjob.cpp delete mode 100644 jobs/syncjob.h delete mode 100644 joinstate.h create mode 100644 lib/avatar.cpp create mode 100644 lib/avatar.h create mode 100644 lib/connection.cpp create mode 100644 lib/connection.h create mode 100644 lib/connectiondata.cpp create mode 100644 lib/connectiondata.h create mode 100644 lib/converters.h create mode 100644 lib/events/accountdataevents.h create mode 100644 lib/events/directchatevent.cpp create mode 100644 lib/events/directchatevent.h create mode 100644 lib/events/event.cpp create mode 100644 lib/events/event.h create mode 100644 lib/events/eventcontent.cpp create mode 100644 lib/events/eventcontent.h create mode 100644 lib/events/receiptevent.cpp create mode 100644 lib/events/receiptevent.h create mode 100644 lib/events/redactionevent.cpp create mode 100644 lib/events/redactionevent.h create mode 100644 lib/events/roomavatarevent.cpp create mode 100644 lib/events/roomavatarevent.h create mode 100644 lib/events/roommemberevent.cpp create mode 100644 lib/events/roommemberevent.h create mode 100644 lib/events/roommessageevent.cpp create mode 100644 lib/events/roommessageevent.h create mode 100644 lib/events/simplestateevents.h create mode 100644 lib/events/typingevent.cpp create mode 100644 lib/events/typingevent.h create mode 100644 lib/jobs/basejob.cpp create mode 100644 lib/jobs/basejob.h create mode 100644 lib/jobs/checkauthmethods.cpp create mode 100644 lib/jobs/checkauthmethods.h create mode 100644 lib/jobs/downloadfilejob.cpp create mode 100644 lib/jobs/downloadfilejob.h create mode 100644 lib/jobs/generated/account-data.cpp create mode 100644 lib/jobs/generated/account-data.h create mode 100644 lib/jobs/generated/administrative_contact.cpp create mode 100644 lib/jobs/generated/administrative_contact.h create mode 100644 lib/jobs/generated/banning.cpp create mode 100644 lib/jobs/generated/banning.h create mode 100644 lib/jobs/generated/content-repo.cpp create mode 100644 lib/jobs/generated/content-repo.h create mode 100644 lib/jobs/generated/create_room.cpp create mode 100644 lib/jobs/generated/create_room.h create mode 100644 lib/jobs/generated/directory.cpp create mode 100644 lib/jobs/generated/directory.h create mode 100644 lib/jobs/generated/inviting.cpp create mode 100644 lib/jobs/generated/inviting.h create mode 100644 lib/jobs/generated/kicking.cpp create mode 100644 lib/jobs/generated/kicking.h create mode 100644 lib/jobs/generated/leaving.cpp create mode 100644 lib/jobs/generated/leaving.h create mode 100644 lib/jobs/generated/list_public_rooms.cpp create mode 100644 lib/jobs/generated/list_public_rooms.h create mode 100644 lib/jobs/generated/login.cpp create mode 100644 lib/jobs/generated/login.h create mode 100644 lib/jobs/generated/logout.cpp create mode 100644 lib/jobs/generated/logout.h create mode 100644 lib/jobs/generated/profile.cpp create mode 100644 lib/jobs/generated/profile.h create mode 100644 lib/jobs/generated/receipts.cpp create mode 100644 lib/jobs/generated/receipts.h create mode 100644 lib/jobs/generated/redaction.cpp create mode 100644 lib/jobs/generated/redaction.h create mode 100644 lib/jobs/generated/third_party_membership.cpp create mode 100644 lib/jobs/generated/third_party_membership.h create mode 100644 lib/jobs/generated/typing.cpp create mode 100644 lib/jobs/generated/typing.h create mode 100644 lib/jobs/generated/versions.cpp create mode 100644 lib/jobs/generated/versions.h create mode 100644 lib/jobs/generated/whoami.cpp create mode 100644 lib/jobs/generated/whoami.h create mode 100644 lib/jobs/joinroomjob.cpp create mode 100644 lib/jobs/joinroomjob.h create mode 100644 lib/jobs/mediathumbnailjob.cpp create mode 100644 lib/jobs/mediathumbnailjob.h create mode 100644 lib/jobs/passwordlogin.cpp create mode 100644 lib/jobs/passwordlogin.h create mode 100644 lib/jobs/postreadmarkersjob.h create mode 100644 lib/jobs/postreceiptjob.cpp create mode 100644 lib/jobs/postreceiptjob.h create mode 100644 lib/jobs/requestdata.cpp create mode 100644 lib/jobs/requestdata.h create mode 100644 lib/jobs/roommessagesjob.cpp create mode 100644 lib/jobs/roommessagesjob.h create mode 100644 lib/jobs/sendeventjob.cpp create mode 100644 lib/jobs/sendeventjob.h create mode 100644 lib/jobs/setroomstatejob.cpp create mode 100644 lib/jobs/setroomstatejob.h create mode 100644 lib/jobs/syncjob.cpp create mode 100644 lib/jobs/syncjob.h create mode 100644 lib/joinstate.h create mode 100644 lib/logging.cpp create mode 100644 lib/logging.h create mode 100644 lib/networkaccessmanager.cpp create mode 100644 lib/networkaccessmanager.h create mode 100644 lib/networksettings.cpp create mode 100644 lib/networksettings.h create mode 100644 lib/room.cpp create mode 100644 lib/room.h create mode 100644 lib/settings.cpp create mode 100644 lib/settings.h create mode 100644 lib/user.cpp create mode 100644 lib/user.h create mode 100644 lib/util.h delete mode 100644 logging.cpp delete mode 100644 logging.h delete mode 100644 networkaccessmanager.cpp delete mode 100644 networkaccessmanager.h delete mode 100644 networksettings.cpp delete mode 100644 networksettings.h delete mode 100644 room.cpp delete mode 100644 room.h delete mode 100644 settings.cpp delete mode 100644 settings.h delete mode 100644 state.cpp delete mode 100644 state.h delete mode 100644 user.cpp delete mode 100644 user.h delete mode 100644 util.h diff --git a/.travis.yml b/.travis.yml index 2a03267e..c0008af0 100644 --- a/.travis.yml +++ b/.travis.yml @@ -29,10 +29,10 @@ script: - cmake -DCMAKE_INSTALL_PREFIX=../install .. - cmake --build . --target all - cmake --build . --target install +- cd ../examples +- $CC qmc-example.cpp -I../install/include -l../install/lib/libqmatrixclient.a -o qmc-example-lib-installed - cd .. -- ls -R install -- mv lib lib.back # Force the below script to use the installed copy instead -- qmake qmc-example.pro "CONFIG += debug" "CONFIG -= app_bundle" "QMAKE_CC = $CC" "QMAKE_CXX = $CXX" "INCLUDES += -I./install/include" "LIBS += -L./install/lib" +- qmake qmc-example.pro "CONFIG += debug" "CONFIG -= app_bundle" "QMAKE_CC = $CC" "QMAKE_CXX = $CXX" - make all - if [ "$QMC_TEST_USER" != "" ]; then $VALGRIND ./qmc-example "$QMC_TEST_USER" "$QMC_TEST_PWD" qmc-example-travis '#qmc-test:matrix.org' "Travis CI job $TRAVIS_JOB_NUMBER"; fi diff --git a/CMakeLists.txt b/CMakeLists.txt index 3ee2200d..c5511935 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -8,7 +8,7 @@ if (NOT WIN32) endif(NOT WIN32) # Find includes in corresponding build directories -set(CMAKE_INCLUDE_CURRENT_DIR ON) +#set(CMAKE_INCLUDE_CURRENT_DIR ON) # Instruct CMake to run moc automatically when needed. set(CMAKE_AUTOMOC ON) @@ -21,6 +21,18 @@ if(NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES) "MinSizeRel" "RelWithDebInfo") endif() +if (NOT CMAKE_INSTALL_LIBDIR) + set(CMAKE_INSTALL_LIBDIR ".") +endif() + +if (NOT CMAKE_INSTALL_BINDIR) + set(CMAKE_INSTALL_BINDIR ".") +endif() + +if (NOT CMAKE_INSTALL_INCLUDEDIR) + set(CMAKE_INSTALL_INCLUDEDIR "include") +endif() + set(CMAKE_CXX_STANDARD 14) foreach (FLAG all "" pedantic extra error=return-type no-unused-parameter no-gnu-zero-variadic-macro-arguments) @@ -47,36 +59,36 @@ message( STATUS ) # Set up source files set(libqmatrixclient_SRCS - networkaccessmanager.cpp - connectiondata.cpp - connection.cpp - logging.cpp - room.cpp - user.cpp - avatar.cpp - settings.cpp - networksettings.cpp - events/event.cpp - events/eventcontent.cpp - events/roommessageevent.cpp - events/roommemberevent.cpp - events/roomavatarevent.cpp - events/typingevent.cpp - events/receiptevent.cpp - events/directchatevent.cpp - jobs/requestdata.cpp - jobs/basejob.cpp - jobs/checkauthmethods.cpp - jobs/sendeventjob.cpp - jobs/setroomstatejob.cpp - jobs/joinroomjob.cpp - jobs/roommessagesjob.cpp - jobs/syncjob.cpp - jobs/mediathumbnailjob.cpp - jobs/downloadfilejob.cpp + lib/networkaccessmanager.cpp + lib/connectiondata.cpp + lib/connection.cpp + lib/logging.cpp + lib/room.cpp + lib/user.cpp + lib/avatar.cpp + lib/settings.cpp + lib/networksettings.cpp + lib/events/event.cpp + lib/events/eventcontent.cpp + lib/events/roommessageevent.cpp + lib/events/roommemberevent.cpp + lib/events/roomavatarevent.cpp + lib/events/typingevent.cpp + lib/events/receiptevent.cpp + lib/events/directchatevent.cpp + lib/jobs/requestdata.cpp + lib/jobs/basejob.cpp + lib/jobs/checkauthmethods.cpp + lib/jobs/sendeventjob.cpp + lib/jobs/setroomstatejob.cpp + lib/jobs/joinroomjob.cpp + lib/jobs/roommessagesjob.cpp + lib/jobs/syncjob.cpp + lib/jobs/mediathumbnailjob.cpp + lib/jobs/downloadfilejob.cpp ) -aux_source_directory(jobs/generated libqmatrixclient_job_SRCS) +aux_source_directory(lib/jobs/generated libqmatrixclient_job_SRCS) set(example_SRCS examples/qmc-example.cpp) @@ -84,34 +96,31 @@ add_library(qmatrixclient ${libqmatrixclient_SRCS} ${libqmatrixclient_job_SRCS}) set_property(TARGET qmatrixclient PROPERTY VERSION "0.2.0") set_property(TARGET qmatrixclient PROPERTY SOVERSION 0 ) +target_include_directories(qmatrixclient PUBLIC + $ + $ +) +target_include_directories(qmatrixclient PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/lib) target_link_libraries(qmatrixclient Qt5::Core Qt5::Network Qt5::Gui) add_executable(qmc-example ${example_SRCS}) +target_include_directories(qmc-example PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/lib) target_link_libraries(qmc-example Qt5::Core qmatrixclient) # Installation -if (NOT CMAKE_INSTALL_LIBDIR) - set(CMAKE_INSTALL_LIBDIR ".") -endif() - -if (NOT CMAKE_INSTALL_BINDIR) - set(CMAKE_INSTALL_BINDIR ".") -endif() - -if (NOT CMAKE_INSTALL_INCLUDEDIR) - set(CMAKE_INSTALL_INCLUDEDIR "include") -endif() - install (TARGETS qmatrixclient EXPORT QMatrixClient ARCHIVE DESTINATION ${CMAKE_INSTALL_LIBDIR} LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR} INCLUDES DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}) -install (DIRECTORY . DESTINATION ${CMAKE_INSTALL_INCLUDEDIR} +install (DIRECTORY lib/ DESTINATION ${CMAKE_INSTALL_INCLUDEDIR} FILES_MATCHING PATTERN "*.h") install (TARGETS qmc-example RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR}) +#export(TARGETS qmatrixclient FILE QMatrixClientConfig.cmake EXPORT_LINK_INTERFACE_LIBRARIES) +export(EXPORT QMatrixClient) +export(PACKAGE QMatrixClient) install (EXPORT QMatrixClient DESTINATION ${CMAKE_INSTALL_LIBDIR}/cmake/qmatrixclient) if (WIN32) diff --git a/avatar.cpp b/avatar.cpp deleted file mode 100644 index 1ff2aae1..00000000 --- a/avatar.cpp +++ /dev/null @@ -1,187 +0,0 @@ -/****************************************************************************** - * Copyright (C) 2017 Kitsune Ral - * - * 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 - */ - -#include "avatar.h" - -#include "jobs/mediathumbnailjob.h" -#include "events/eventcontent.h" -#include "connection.h" - -#include -#include - -using namespace QMatrixClient; - -class Avatar::Private -{ - public: - explicit Private(QIcon di, QUrl url = {}) - : _defaultIcon(di), _url(url) - { } - QImage get(Connection* connection, QSize size, - get_callback_t callback) const; - bool upload(UploadContentJob* job, upload_callback_t callback); - - bool checkUrl(QUrl url) const; - - const QIcon _defaultIcon; - QUrl _url; - - // The below are related to image caching, hence mutable - mutable QImage _originalImage; - mutable std::vector> _scaledImages; - mutable QSize _requestedSize; - mutable bool _bannedUrl = false; - mutable bool _fetched = false; - mutable QPointer _thumbnailRequest = nullptr; - mutable QPointer _uploadRequest = nullptr; - mutable std::vector callbacks; - mutable get_callback_t uploadCallback; -}; - -Avatar::Avatar(QIcon defaultIcon) - : d(std::make_unique(std::move(defaultIcon))) -{ } - -Avatar::Avatar(QUrl url, QIcon defaultIcon) - : d(std::make_unique(std::move(defaultIcon), std::move(url))) -{ } - -Avatar::Avatar(Avatar&&) = default; - -Avatar::~Avatar() = default; - -Avatar& Avatar::operator=(Avatar&&) = default; - -QImage Avatar::get(Connection* connection, int dimension, - get_callback_t callback) const -{ - return d->get(connection, {dimension, dimension}, callback); -} - -QImage Avatar::get(Connection* connection, int width, int height, - get_callback_t callback) const -{ - return d->get(connection, {width, height}, callback); -} - -bool Avatar::upload(Connection* connection, const QString& fileName, - upload_callback_t callback) const -{ - if (isJobRunning(d->_uploadRequest)) - return false; - return d->upload(connection->uploadFile(fileName), callback); -} - -bool Avatar::upload(Connection* connection, QIODevice* source, - upload_callback_t callback) const -{ - if (isJobRunning(d->_uploadRequest) || !source->isReadable()) - return false; - return d->upload(connection->uploadContent(source), callback); -} - -QString Avatar::mediaId() const -{ - return d->_url.authority() + d->_url.path(); -} - -QImage Avatar::Private::get(Connection* connection, QSize size, - get_callback_t callback) const -{ - // FIXME: Alternating between longer-width and longer-height requests - // is a sure way to trick the below code into constantly getting another - // image from the server because the existing one is alleged unsatisfactory. - // This is plain abuse by the client, though; so not critical for now. - if( ( !(_fetched || _thumbnailRequest) - || size.width() > _requestedSize.width() - || size.height() > _requestedSize.height() ) && checkUrl(_url) ) - { - qCDebug(MAIN) << "Getting avatar from" << _url.toString(); - _requestedSize = size; - if (isJobRunning(_thumbnailRequest)) - _thumbnailRequest->abandon(); - callbacks.emplace_back(std::move(callback)); - _thumbnailRequest = connection->getThumbnail(_url, size); - QObject::connect( _thumbnailRequest, &MediaThumbnailJob::success, [this] - { - _fetched = true; - _originalImage = _thumbnailRequest->scaledThumbnail(_requestedSize); - _scaledImages.clear(); - for (auto n: callbacks) - n(); - }); - } - - if( _originalImage.isNull() ) - { - if (_defaultIcon.isNull()) - return _originalImage; - - QPainter p { &_originalImage }; - _defaultIcon.paint(&p, { QPoint(), _defaultIcon.actualSize(size) }); - } - - for (auto p: _scaledImages) - if (p.first == size) - return p.second; - auto result = _originalImage.scaled(size, - Qt::KeepAspectRatio, Qt::SmoothTransformation); - _scaledImages.emplace_back(size, result); - return result; -} - -bool Avatar::Private::upload(UploadContentJob* job, upload_callback_t callback) -{ - _uploadRequest = job; - if (!isJobRunning(_uploadRequest)) - return false; - _uploadRequest->connect(_uploadRequest, &BaseJob::success, - [job,callback] { callback(job->contentUri()); }); - return true; -} - -bool Avatar::Private::checkUrl(QUrl url) const -{ - if (_bannedUrl || url.isEmpty()) - return false; - - // FIXME: Make "mxc" a library-wide constant and maybe even make - // the URL checker a Connection(?) method. - _bannedUrl = !(url.isValid() && - url.scheme() == "mxc" && url.path().count('/') == 1); - if (_bannedUrl) - qCWarning(MAIN) << "Avatar URL is invalid or not mxc-based:" - << url.toDisplayString(); - return !_bannedUrl; -} - -QUrl Avatar::url() const { return d->_url; } - -bool Avatar::updateUrl(const QUrl& newUrl) -{ - if (newUrl == d->_url) - return false; - - d->_url = newUrl; - d->_fetched = false; - if (isJobRunning(d->_thumbnailRequest)) - d->_thumbnailRequest->abandon(); - return true; -} - diff --git a/avatar.h b/avatar.h deleted file mode 100644 index 0166ae9e..00000000 --- a/avatar.h +++ /dev/null @@ -1,61 +0,0 @@ -/****************************************************************************** - * Copyright (C) 2017 Kitsune Ral - * - * 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 - */ - -#pragma once - -#include -#include - -#include -#include - -namespace QMatrixClient -{ - class Connection; - - class Avatar - { - public: - explicit Avatar(QIcon defaultIcon = {}); - explicit Avatar(QUrl url, QIcon defaultIcon = {}); - Avatar(Avatar&&); - ~Avatar(); - Avatar& operator=(Avatar&&); - - using get_callback_t = std::function; - using upload_callback_t = std::function; - - QImage get(Connection* connection, int dimension, - get_callback_t callback) const; - QImage get(Connection* connection, int w, int h, - get_callback_t callback) const; - - bool upload(Connection* connection, const QString& fileName, - upload_callback_t callback) const; - bool upload(Connection* connection, QIODevice* source, - upload_callback_t callback) const; - - QString mediaId() const; - QUrl url() const; - bool updateUrl(const QUrl& newUrl); - - private: - class Private; - std::unique_ptr d; - }; -} // namespace QMatrixClient diff --git a/connection.cpp b/connection.cpp deleted file mode 100644 index 2d7235b9..00000000 --- a/connection.cpp +++ /dev/null @@ -1,943 +0,0 @@ -/****************************************************************************** - * Copyright (C) 2015 Felix Rohrbach - * - * 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 - */ - -#include "connection.h" -#include "connectiondata.h" -#include "user.h" -#include "events/event.h" -#include "events/directchatevent.h" -#include "room.h" -#include "settings.h" -#include "jobs/generated/login.h" -#include "jobs/generated/logout.h" -#include "jobs/generated/receipts.h" -#include "jobs/generated/leaving.h" -#include "jobs/generated/account-data.h" -#include "jobs/sendeventjob.h" -#include "jobs/joinroomjob.h" -#include "jobs/roommessagesjob.h" -#include "jobs/syncjob.h" -#include "jobs/mediathumbnailjob.h" -#include "jobs/downloadfilejob.h" - -#include -#include -#include -#include -#include -#include -#include -#include -#include - -using namespace QMatrixClient; - -using DirectChatsMap = QMultiHash; - -class Connection::Private -{ - public: - explicit Private(std::unique_ptr&& connection) - : data(move(connection)) - { } - Q_DISABLE_COPY(Private) - Private(Private&&) = delete; - Private operator=(Private&&) = delete; - - Connection* q = nullptr; - std::unique_ptr data; - // A complex key below is a pair of room name and whether its - // state is Invited. The spec mandates to keep Invited room state - // separately so we should, e.g., keep objects for Invite and - // Leave state of the same room. - QHash, Room*> roomMap; - QVector roomIdsToForget; - QMap userMap; - DirectChatsMap directChats; - QHash accountData; - QString userId; - - SyncJob* syncJob = nullptr; - - bool cacheState = true; - bool cacheToBinary = SettingsGroup("libqmatrixclient") - .value("cache_type").toString() != "json"; - - void connectWithToken(const QString& user, const QString& accessToken, - const QString& deviceId); - void broadcastDirectChatUpdates(); -}; - -Connection::Connection(const QUrl& server, QObject* parent) - : QObject(parent) - , d(std::make_unique(std::make_unique(server))) -{ - d->q = this; // All d initialization should occur before this line -} - -Connection::Connection(QObject* parent) - : Connection({}, parent) -{ } - -Connection::~Connection() -{ - qCDebug(MAIN) << "deconstructing connection object for" << d->userId; - stopSync(); -} - -void Connection::resolveServer(const QString& mxidOrDomain) -{ - // At this point we may have something as complex as - // @username:[IPv6:address]:port, or as simple as a plain domain name. - - // Try to parse as an FQID; if there's no @ part, assume it's a domain name. - QRegularExpression parser( - "^(@.+?:)?" // Optional username (allow everything for compatibility) - "(\\[[^]]+\\]|[^:@]+)" // Either IPv6 address or hostname/IPv4 address - "(:\\d{1,5})?$", // Optional port - QRegularExpression::UseUnicodePropertiesOption); // Because asian digits - auto match = parser.match(mxidOrDomain); - - QUrl maybeBaseUrl = QUrl::fromUserInput(match.captured(2)); - maybeBaseUrl.setScheme("https"); // Instead of the Qt-default "http" - if (!match.hasMatch() || !maybeBaseUrl.isValid()) - { - emit resolveError( - tr("%1 is not a valid homeserver address") - .arg(maybeBaseUrl.toString())); - return; - } - - setHomeserver(maybeBaseUrl); - emit resolved(); - return; - - // FIXME, #178: The below code is incorrect and is no more executed. The - // correct server resolution should be done from .well-known/matrix/client - auto domain = maybeBaseUrl.host(); - qCDebug(MAIN) << "Finding the server" << domain; - // Check if the Matrix server has a dedicated service record. - QDnsLookup* dns = new QDnsLookup(); - dns->setType(QDnsLookup::SRV); - dns->setName("_matrix._tcp." + domain); - - connect(dns, &QDnsLookup::finished, [this,dns,maybeBaseUrl]() { - QUrl baseUrl { maybeBaseUrl }; - if (dns->error() == QDnsLookup::NoError && - dns->serviceRecords().isEmpty()) - { - auto record = dns->serviceRecords().front(); - baseUrl.setHost(record.target()); - baseUrl.setPort(record.port()); - qCDebug(MAIN) << "SRV record for" << maybeBaseUrl.host() - << "is" << baseUrl.authority(); - } else { - qCDebug(MAIN) << baseUrl.host() << "doesn't have SRV record" - << dns->name() << "- using the hostname as is"; - } - setHomeserver(baseUrl); - emit resolved(); - dns->deleteLater(); - }); - dns->lookup(); -} - -void Connection::connectToServer(const QString& user, const QString& password, - const QString& initialDeviceName, - const QString& deviceId) -{ - checkAndConnect(user, - [=] { - doConnectToServer(user, password, initialDeviceName, deviceId); - }); -} -void Connection::doConnectToServer(const QString& user, const QString& password, - const QString& initialDeviceName, - const QString& deviceId) -{ - auto loginJob = callApi(QStringLiteral("m.login.password"), - user, /*medium*/ "", /*address*/ "", password, /*token*/ "", - deviceId, initialDeviceName); - connect(loginJob, &BaseJob::success, this, - [this, loginJob] { - d->connectWithToken(loginJob->userId(), loginJob->accessToken(), - loginJob->deviceId()); - }); - connect(loginJob, &BaseJob::failure, this, - [this, loginJob] { - emit loginError(loginJob->errorString()); - }); -} - -void Connection::connectWithToken(const QString& userId, - const QString& accessToken, - const QString& deviceId) -{ - checkAndConnect(userId, - [=] { d->connectWithToken(userId, accessToken, deviceId); }); -} - -void Connection::Private::connectWithToken(const QString& user, - const QString& accessToken, - const QString& deviceId) -{ - userId = user; - data->setToken(accessToken.toLatin1()); - data->setDeviceId(deviceId); - qCDebug(MAIN) << "Using server" << data->baseUrl().toDisplayString() - << "by user" << userId << "from device" << deviceId; - emit q->connected(); - -} - -void Connection::checkAndConnect(const QString& userId, - std::function connectFn) -{ - if (d->data->baseUrl().isValid()) - { - connectFn(); - return; - } - // Not good to go, try to fix the homeserver URL. - if (userId.startsWith('@') && userId.indexOf(':') != -1) - { - // The below construct makes a single-shot connection that triggers - // on the signal and then self-disconnects. - // NB: doResolveServer can emit resolveError, so this is a part of - // checkAndConnect function contract. - QMetaObject::Connection connection; - connection = connect(this, &Connection::homeserverChanged, - this, [=] { connectFn(); disconnect(connection); }); - resolveServer(userId); - } else - emit resolveError( - tr("%1 is an invalid homeserver URL") - .arg(d->data->baseUrl().toString())); -} - -void Connection::logout() -{ - auto job = callApi(); - connect( job, &LogoutJob::success, this, [this] { - stopSync(); - emit loggedOut(); - }); -} - -void Connection::sync(int timeout) -{ - if (d->syncJob) - return; - - // Raw string: http://en.cppreference.com/w/cpp/language/string_literal - const QString filter { R"({"room": { "timeline": { "limit": 100 } } })" }; - auto job = d->syncJob = - callApi(d->data->lastEvent(), filter, timeout); - connect( job, &SyncJob::success, [this, job] { - onSyncSuccess(job->takeData()); - d->syncJob = nullptr; - emit syncDone(); - }); - connect( job, &SyncJob::retryScheduled, this, &Connection::networkError); - connect( job, &SyncJob::failure, [this, job] { - d->syncJob = nullptr; - if (job->error() == BaseJob::ContentAccessError) - emit loginError(job->errorString()); - else - emit syncError(job->errorString()); - }); -} - -void Connection::onSyncSuccess(SyncData &&data) { - d->data->setLastEvent(data.nextBatch()); - for (auto&& roomData: data.takeRoomData()) - { - const auto forgetIdx = d->roomIdsToForget.indexOf(roomData.roomId); - if (forgetIdx != -1) - { - d->roomIdsToForget.removeAt(forgetIdx); - if (roomData.joinState == JoinState::Leave) - { - qDebug(MAIN) << "Room" << roomData.roomId - << "has been forgotten, ignoring /sync response for it"; - continue; - } - qWarning(MAIN) << "Room" << roomData.roomId - << "has just been forgotten but /sync returned it in" - << toCString(roomData.joinState) - << "state - suspiciously fast turnaround"; - } - if ( auto* r = provideRoom(roomData.roomId, roomData.joinState) ) - r->updateData(std::move(roomData)); - QCoreApplication::processEvents(); - } - for (auto&& accountEvent: data.takeAccountData()) - { - if (accountEvent->type() == EventType::DirectChat) - { - DirectChatsMap newDirectChats; - const auto* event = static_cast(accountEvent.get()); - auto usersToDCs = event->usersToDirectChats(); - for (auto it = usersToDCs.begin(); it != usersToDCs.end(); ++it) - { - newDirectChats.insert(user(it.key()), it.value()); - qCDebug(MAIN) << "Marked room" << it.value() - << "as a direct chat with" << it.key(); - } - if (newDirectChats != d->directChats) - { - d->directChats = newDirectChats; - emit directChatsListChanged(); - } - continue; - } - d->accountData[accountEvent->jsonType()] = - accountEvent->contentJson().toVariantHash(); - } -} - -void Connection::stopSync() -{ - if (d->syncJob) - { - d->syncJob->abandon(); - d->syncJob = nullptr; - } -} - -void Connection::postMessage(Room* room, const QString& type, const QString& message) const -{ - callApi(room->id(), type, message); -} - -PostReceiptJob* Connection::postReceipt(Room* room, RoomEvent* event) const -{ - return callApi(room->id(), "m.read", event->id()); -} - -JoinRoomJob* Connection::joinRoom(const QString& roomAlias) -{ - auto job = callApi(roomAlias); - connect(job, &JoinRoomJob::success, - this, [this, job] { provideRoom(job->roomId(), JoinState::Join); }); - return job; -} - -void Connection::leaveRoom(Room* room) -{ - callApi(room->id()); -} - -RoomMessagesJob* Connection::getMessages(Room* room, const QString& from) const -{ - return callApi(room->id(), from); -} - -inline auto splitMediaId(const QString& mediaId) -{ - auto idParts = mediaId.split('/'); - Q_ASSERT_X(idParts.size() == 2, __FUNCTION__, - ("'" + mediaId + - "' doesn't look like 'serverName/localMediaId'").toLatin1()); - return idParts; -} - -MediaThumbnailJob* Connection::getThumbnail(const QString& mediaId, QSize requestedSize) const -{ - auto idParts = splitMediaId(mediaId); - return callApi(idParts.front(), idParts.back(), - requestedSize); -} - -MediaThumbnailJob* Connection::getThumbnail(const QUrl& url, QSize requestedSize) const -{ - return getThumbnail(url.authority() + url.path(), requestedSize); -} - -MediaThumbnailJob* Connection::getThumbnail(const QUrl& url, int requestedWidth, - int requestedHeight) const -{ - return getThumbnail(url, QSize(requestedWidth, requestedHeight)); -} - -UploadContentJob* Connection::uploadContent(QIODevice* contentSource, - const QString& filename, const QString& contentType) const -{ - return callApi(contentSource, filename, contentType); -} - -UploadContentJob* Connection::uploadFile(const QString& fileName, - const QString& contentType) -{ - auto sourceFile = new QFile(fileName); - if (sourceFile->open(QIODevice::ReadOnly)) - { - qCWarning(MAIN) << "Couldn't open" << sourceFile->fileName() - << "for reading"; - return nullptr; - } - return uploadContent(sourceFile, QFileInfo(*sourceFile).fileName(), - contentType); -} - -GetContentJob* Connection::getContent(const QString& mediaId) const -{ - auto idParts = splitMediaId(mediaId); - return callApi(idParts.front(), idParts.back()); -} - -GetContentJob* Connection::getContent(const QUrl& url) const -{ - return getContent(url.authority() + url.path()); -} - -DownloadFileJob* Connection::downloadFile(const QUrl& url, - const QString& localFilename) const -{ - auto mediaId = url.authority() + url.path(); - auto idParts = splitMediaId(mediaId); - auto* job = callApi(idParts.front(), idParts.back(), - localFilename); - return job; -} - -CreateRoomJob* Connection::createRoom(RoomVisibility visibility, - const QString& alias, const QString& name, const QString& topic, - const QVector& invites, const QString& presetName, - bool isDirect, bool guestsCanJoin, - const QVector& initialState, - const QVector& invite3pids, - const QJsonObject creationContent) -{ - auto job = callApi( - visibility == PublishRoom ? "public" : "private", alias, name, - topic, invites, invite3pids, creationContent, initialState, - presetName, isDirect, guestsCanJoin); - connect(job, &BaseJob::success, this, [this,job] { - emit createdRoom(provideRoom(job->roomId(), JoinState::Join)); - }); - return job; -} - -void Connection::requestDirectChat(const QString& userId) -{ - doInDirectChat(userId, [this] (Room* r) { emit directChatAvailable(r); }); -} - -void Connection::doInDirectChat(const QString& userId, - std::function operation) -{ - // There can be more than one DC; find the first valid, and delete invalid - // (left/forgotten) ones along the way. - for (auto roomId: d->directChats.values(user(userId))) - { - if (auto r = room(roomId, JoinState::Join)) - { - Q_ASSERT(r->id() == roomId); - qCDebug(MAIN) << "Requested direct chat with" << userId - << "is already available as" << r->id(); - operation(r); - return; - } - if (auto ir = invitation(roomId)) - { - Q_ASSERT(ir->id() == roomId); - auto j = joinRoom(ir->id()); - connect(j, &BaseJob::success, this, [this,roomId,userId,operation] { - qCDebug(MAIN) << "Joined the already invited direct chat with" - << userId << "as" << roomId; - operation(room(roomId, JoinState::Join)); - }); - } - qCWarning(MAIN) << "Direct chat with" << userId << "known as room" - << roomId << "is not valid, discarding it"; - removeFromDirectChats(roomId); - } - - auto j = createDirectChat(userId); - connect(j, &BaseJob::success, this, [this,j,userId,operation] { - qCDebug(MAIN) << "Direct chat with" << userId - << "has been created as" << j->roomId(); - operation(room(j->roomId(), JoinState::Join)); - }); -} - -CreateRoomJob* Connection::createDirectChat(const QString& userId, - const QString& topic, const QString& name) -{ - return createRoom(UnpublishRoom, "", name, topic, {userId}, - "trusted_private_chat", true); -} - -ForgetRoomJob* Connection::forgetRoom(const QString& id) -{ - // To forget is hard :) First we should ensure the local user is not - // in the room (by leaving it, if necessary); once it's done, the /forget - // endpoint can be called; and once this is through, the local Room object - // (if any existed) is deleted. At the same time, we still have to - // (basically immediately) return a pointer to ForgetRoomJob. Therefore - // a ForgetRoomJob is created in advance and can be returned in a probably - // not-yet-started state (it will start once /leave completes). - auto forgetJob = new ForgetRoomJob(id); - auto room = d->roomMap.value({id, false}); - if (!room) - room = d->roomMap.value({id, true}); - if (room && room->joinState() != JoinState::Leave) - { - auto leaveJob = room->leaveRoom(); - connect(leaveJob, &BaseJob::success, this, [this, forgetJob, room] { - forgetJob->start(connectionData()); - // If the matching /sync response hasn't arrived yet, mark the room - // for explicit deletion - if (room->joinState() != JoinState::Leave) - d->roomIdsToForget.push_back(room->id()); - }); - connect(leaveJob, &BaseJob::failure, forgetJob, &BaseJob::abandon); - } - else - forgetJob->start(connectionData()); - connect(forgetJob, &BaseJob::success, this, [this, id] - { - // If the room is in the map (possibly in both forms), delete all forms. - for (auto f: {false, true}) - if (auto r = d->roomMap.take({ id, f })) - { - emit aboutToDeleteRoom(r); - qCDebug(MAIN) << "Room" << id - << "in join state" << toCString(r->joinState()) - << "will be deleted"; - r->deleteLater(); - } - }); - return forgetJob; -} - -QUrl Connection::homeserver() const -{ - return d->data->baseUrl(); -} - -Room* Connection::room(const QString& roomId, JoinStates states) const -{ - Room* room = d->roomMap.value({roomId, false}, nullptr); - if (states.testFlag(JoinState::Join) && - room && room->joinState() == JoinState::Join) - return room; - - if (states.testFlag(JoinState::Invite)) - if (Room* invRoom = invitation(roomId)) - return invRoom; - - if (states.testFlag(JoinState::Leave) && - room && room->joinState() == JoinState::Leave) - return room; - - return nullptr; -} - -Room* Connection::invitation(const QString& roomId) const -{ - return d->roomMap.value({roomId, true}, nullptr); -} - -User* Connection::user(const QString& userId) -{ - if( d->userMap.contains(userId) ) - return d->userMap.value(userId); - auto* user = userFactory(this, userId); - d->userMap.insert(userId, user); - emit newUser(user); - return user; -} - -const User* Connection::user() const -{ - return d->userId.isEmpty() ? nullptr : d->userMap.value(d->userId, nullptr); -} - -User* Connection::user() -{ - return d->userId.isEmpty() ? nullptr : user(d->userId); -} - -QString Connection::userId() const -{ - return d->userId; -} - -QString Connection::deviceId() const -{ - return d->data->deviceId(); -} - -QString Connection::token() const -{ - return accessToken(); -} - -QByteArray Connection::accessToken() const -{ - return d->data->accessToken(); -} - -SyncJob* Connection::syncJob() const -{ - return d->syncJob; -} - -int Connection::millisToReconnect() const -{ - return d->syncJob ? d->syncJob->millisToRetry() : 0; -} - -QHash< QPair, Room* > Connection::roomMap() const -{ - // Copy-on-write-and-remove-elements is faster than copying elements one by one. - QHash< QPair, Room* > roomMap = d->roomMap; - for (auto it = roomMap.begin(); it != roomMap.end(); ) - { - if (it.value()->joinState() == JoinState::Leave) - it = roomMap.erase(it); - else - ++it; - } - return roomMap; -} - -QHash> Connection::tagsToRooms() const -{ - QHash> result; - for (auto* r: d->roomMap) - { - for (const auto& tagName: r->tagNames()) - result[tagName].push_back(r); - } - for (auto it = result.begin(); it != result.end(); ++it) - std::sort(it->begin(), it->end(), - [t=it.key()] (Room* r1, Room* r2) { - return r1->tags().value(t).order < r2->tags().value(t).order; - }); - return result; -} - -QStringList Connection::tagNames() const -{ - QStringList tags ({FavouriteTag}); - for (auto* r: d->roomMap) - for (const auto& tag: r->tagNames()) - if (tag != LowPriorityTag && !tags.contains(tag)) - tags.push_back(tag); - tags.push_back(LowPriorityTag); - return tags; -} - -QVector Connection::roomsWithTag(const QString& tagName) const -{ - QVector rooms; - std::copy_if(d->roomMap.begin(), d->roomMap.end(), std::back_inserter(rooms), - [&tagName] (Room* r) { return r->tags().contains(tagName); }); - return rooms; -} - -QJsonObject toJson(const DirectChatsMap& directChats) -{ - QJsonObject json; - for (auto it = directChats.keyBegin(); it != directChats.keyEnd(); ++it) - json.insert((*it)->id(), toJson(directChats.values(*it))); - return json; -} - -void Connection::Private::broadcastDirectChatUpdates() -{ - q->callApi(userId, QStringLiteral("m.direct"), - toJson(directChats)); - emit q->directChatsListChanged(); -} - -void Connection::addToDirectChats(const Room* room, const User* user) -{ - Q_ASSERT(room != nullptr && user != nullptr); - if (d->directChats.contains(user, room->id())) - return; - d->directChats.insert(user, room->id()); - d->broadcastDirectChatUpdates(); -} - -void Connection::removeFromDirectChats(const QString& roomId, const User* user) -{ - Q_ASSERT(!roomId.isEmpty()); - if ((user != nullptr && !d->directChats.contains(user, roomId)) || - d->directChats.key(roomId) == nullptr) - return; - if (user != nullptr) - d->directChats.remove(user, roomId); - else - for (auto it = d->directChats.begin(); it != d->directChats.end();) - { - if (it.value() == roomId) - it = d->directChats.erase(it); - else - ++it; - } - d->broadcastDirectChatUpdates(); -} - -bool Connection::isDirectChat(const QString& roomId) const -{ - return d->directChats.key(roomId) != nullptr; -} - -QList Connection::directChatUsers(const Room* room) const -{ - Q_ASSERT(room != nullptr); - return d->directChats.keys(room->id()); -} - -QMap Connection::users() const -{ - return d->userMap; -} - -const ConnectionData* Connection::connectionData() const -{ - return d->data.get(); -} - -Room* Connection::provideRoom(const QString& id, JoinState joinState) -{ - // TODO: This whole function is a strong case for a RoomManager class. - Q_ASSERT_X(!id.isEmpty(), __FUNCTION__, "Empty room id"); - - const auto roomKey = qMakePair(id, joinState == JoinState::Invite); - auto* room = d->roomMap.value(roomKey, nullptr); - if (room) - { - // Leave is a special case because in transition (5a) (see the .h file) - // joinState == room->joinState but we still have to preempt the Invite - // and emit a signal. For Invite and Join, there's no such problem. - if (room->joinState() == joinState && joinState != JoinState::Leave) - return room; - } - else - { - room = roomFactory(this, id, joinState); - if (!room) - { - qCCritical(MAIN) << "Failed to create a room" << id; - return nullptr; - } - d->roomMap.insert(roomKey, room); - emit newRoom(room); - } - if (joinState == JoinState::Invite) - { - // prev is either Leave or nullptr - auto* prev = d->roomMap.value({id, false}, nullptr); - emit invitedRoom(room, prev); - } - else - { - room->setJoinState(joinState); - // Preempt the Invite room (if any) with a room in Join/Leave state. - auto* prevInvite = d->roomMap.take({id, true}); - if (joinState == JoinState::Join) - emit joinedRoom(room, prevInvite); - else if (joinState == JoinState::Leave) - emit leftRoom(room, prevInvite); - if (prevInvite) - { - qCDebug(MAIN) << "Deleting Invite state for room" << prevInvite->id(); - emit aboutToDeleteRoom(prevInvite); - prevInvite->deleteLater(); - } - } - - return room; -} - -Connection::room_factory_t Connection::roomFactory = - [](Connection* c, const QString& id, JoinState joinState) - { return new Room(c, id, joinState); }; - -Connection::user_factory_t Connection::userFactory = - [](Connection* c, const QString& id) { return new User(id, c); }; - -QByteArray Connection::generateTxnId() -{ - return d->data->generateTxnId(); -} - -void Connection::setHomeserver(const QUrl& url) -{ - if (homeserver() == url) - return; - - d->data->setBaseUrl(url); - emit homeserverChanged(homeserver()); -} - -static constexpr int CACHE_VERSION_MAJOR = 7; -static constexpr int CACHE_VERSION_MINOR = 0; - -void Connection::saveState(const QUrl &toFile) const -{ - if (!d->cacheState) - return; - - QElapsedTimer et; et.start(); - - QFileInfo stateFile { - toFile.isEmpty() ? stateCachePath() : toFile.toLocalFile() - }; - if (!stateFile.dir().exists()) - stateFile.dir().mkpath("."); - - QFile outfile { stateFile.absoluteFilePath() }; - if (!outfile.open(QFile::WriteOnly)) - { - qCWarning(MAIN) << "Error opening" << stateFile.absoluteFilePath() - << ":" << outfile.errorString(); - qCWarning(MAIN) << "Caching the rooms state disabled"; - d->cacheState = false; - return; - } - - QJsonObject rootObj; - { - QJsonObject rooms; - QJsonObject inviteRooms; - for (const auto* i : roomMap()) // Pass on rooms in Leave state - { - if (i->joinState() == JoinState::Invite) - inviteRooms.insert(i->id(), i->toJson()); - else - rooms.insert(i->id(), i->toJson()); - QElapsedTimer et1; et1.start(); - QCoreApplication::processEvents(); - if (et1.elapsed() > 1) - qCDebug(PROFILER) << "processEvents() borrowed" << et1; - } - - QJsonObject roomObj; - if (!rooms.isEmpty()) - roomObj.insert("join", rooms); - if (!inviteRooms.isEmpty()) - roomObj.insert("invite", inviteRooms); - - rootObj.insert("next_batch", d->data->lastEvent()); - rootObj.insert("rooms", roomObj); - } - { - QJsonArray accountDataEvents { - QJsonObject { - { QStringLiteral("type"), QStringLiteral("m.direct") }, - { QStringLiteral("content"), toJson(d->directChats) } - } - }; - - for (auto it = d->accountData.begin(); it != d->accountData.end(); ++it) - accountDataEvents.append(QJsonObject { - {"type", it.key()}, - {"content", QJsonObject::fromVariantHash(it.value())} - }); - rootObj.insert("account_data", - QJsonObject {{ QStringLiteral("events"), accountDataEvents }}); - } - - QJsonObject versionObj; - versionObj.insert("major", CACHE_VERSION_MAJOR); - versionObj.insert("minor", CACHE_VERSION_MINOR); - rootObj.insert("cache_version", versionObj); - - QJsonDocument json { rootObj }; - auto data = d->cacheToBinary ? json.toBinaryData() : - json.toJson(QJsonDocument::Compact); - qCDebug(PROFILER) << "Cache for" << userId() << "generated in" << et; - - outfile.write(data.data(), data.size()); - qCDebug(MAIN) << "State cache saved to" << outfile.fileName(); -} - -void Connection::loadState(const QUrl &fromFile) -{ - if (!d->cacheState) - return; - - QElapsedTimer et; et.start(); - QFile file { - fromFile.isEmpty() ? stateCachePath() : fromFile.toLocalFile() - }; - if (!file.exists()) - { - qCDebug(MAIN) << "No state cache file found"; - return; - } - if(!file.open(QFile::ReadOnly)) - { - qCWarning(MAIN) << "file " << file.fileName() << "failed to open for read"; - return; - } - QByteArray data = file.readAll(); - - auto jsonDoc = d->cacheToBinary ? QJsonDocument::fromBinaryData(data) : - QJsonDocument::fromJson(data); - if (jsonDoc.isNull()) - { - qCWarning(MAIN) << "Cache file broken, discarding"; - return; - } - auto actualCacheVersionMajor = - jsonDoc.object() - .value("cache_version").toObject() - .value("major").toInt(); - if (actualCacheVersionMajor < CACHE_VERSION_MAJOR) - { - qCWarning(MAIN) - << "Major version of the cache file is" << actualCacheVersionMajor - << "but" << CACHE_VERSION_MAJOR << "required; discarding the cache"; - return; - } - - SyncData sync; - sync.parseJson(jsonDoc); - onSyncSuccess(std::move(sync)); - qCDebug(PROFILER) << "*** Cached state for" << userId() << "loaded in" << et; -} - -QString Connection::stateCachePath() const -{ - auto safeUserId = userId(); - safeUserId.replace(':', '_'); - return QStandardPaths::writableLocation(QStandardPaths::CacheLocation) - % '/' % safeUserId % "_state.json"; -} - -bool Connection::cacheState() const -{ - return d->cacheState; -} - -void Connection::setCacheState(bool newValue) -{ - if (d->cacheState != newValue) - { - d->cacheState = newValue; - emit cacheStateChanged(); - } -} - diff --git a/connection.h b/connection.h deleted file mode 100644 index c6d543ec..00000000 --- a/connection.h +++ /dev/null @@ -1,475 +0,0 @@ -/****************************************************************************** - * Copyright (C) 2015 Felix Rohrbach - * - * 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 - */ - -#pragma once - -#include "jobs/generated/create_room.h" -#include "joinstate.h" - -#include -#include -#include - -#include -#include - -namespace QMatrixClient -{ - class Room; - class User; - class RoomEvent; - class ConnectionData; - - class SyncJob; - class SyncData; - class RoomMessagesJob; - class PostReceiptJob; - class ForgetRoomJob; - class MediaThumbnailJob; - class JoinRoomJob; - class UploadContentJob; - class GetContentJob; - class DownloadFileJob; - - class Connection: public QObject { - Q_OBJECT - - /** Whether or not the rooms state should be cached locally - * \sa loadState(), saveState() - */ - Q_PROPERTY(User* localUser READ user CONSTANT) - Q_PROPERTY(QString localUserId READ userId CONSTANT) - Q_PROPERTY(QString deviceId READ deviceId CONSTANT) - Q_PROPERTY(QByteArray accessToken READ accessToken CONSTANT) - Q_PROPERTY(QUrl homeserver READ homeserver WRITE setHomeserver NOTIFY homeserverChanged) - Q_PROPERTY(bool cacheState READ cacheState WRITE setCacheState NOTIFY cacheStateChanged) - public: - using room_factory_t = - std::function; - using user_factory_t = - std::function; - - enum RoomVisibility { PublishRoom, UnpublishRoom }; // FIXME: Should go inside CreateRoomJob - - explicit Connection(QObject* parent = nullptr); - explicit Connection(const QUrl& server, QObject* parent = nullptr); - virtual ~Connection(); - - /** Get all Invited and Joined rooms - * \return a hashmap from a composite key - room name and whether - * it's an Invite rather than Join - to room pointers - */ - QHash, Room*> roomMap() const; - - /** Get all Invited and Joined rooms grouped by tag - * \return a hashmap from tag name to a vector of room pointers, - * sorted by their order in the tag - details are at - * https://matrix.org/speculator/spec/drafts%2Fe2e/client_server/unstable.html#id95 - */ - QHash> tagsToRooms() const; - - /** Get all room tags known on this connection */ - QStringList tagNames() const; - - /** Get the list of rooms with the specified tag */ - QVector roomsWithTag(const QString& tagName) const; - - /** Mark the room as a direct chat with the user - * This function marks \p room as a direct chat with \p user. - * Emits the signal synchronously, without waiting to complete - * synchronisation with the server. - * - * \sa directChatsListChanged - */ - void addToDirectChats(const Room* room, const User* user); - - /** Unmark the room from direct chats - * This function removes the room id from direct chats either for - * a specific \p user or for all users if \p user in nullptr. - * The room id is used to allow removal of, e.g., ids of forgotten - * rooms; a Room object need not exist. Emits the signal - * immediately, without waiting to complete synchronisation with - * the server. - * - * \sa directChatsListChanged - */ - void removeFromDirectChats(const QString& roomId, - const User* user = nullptr); - - /** Check whether the room id corresponds to a direct chat */ - bool isDirectChat(const QString& roomId) const; - - /** Retrieve the list of users the room is a direct chat with - * @return The list of users for which this room is marked as - * a direct chat; an empty list if the room is not a direct chat - */ - QList directChatUsers(const Room* room) const; - - QMap users() const; - - // FIXME: Convert Q_INVOKABLEs to Q_PROPERTIES - // (breaks back-compatibility) - QUrl homeserver() const; - Q_INVOKABLE Room* room(const QString& roomId, - JoinStates states = JoinState::Invite|JoinState::Join) const; - Q_INVOKABLE Room* invitation(const QString& roomId) const; - Q_INVOKABLE User* user(const QString& userId); - const User* user() const; - User* user(); - QString userId() const; - QString deviceId() const; - /** @deprecated Use accessToken() instead. */ - Q_INVOKABLE QString token() const; - QByteArray accessToken() const; - Q_INVOKABLE SyncJob* syncJob() const; - Q_INVOKABLE int millisToReconnect() const; - - /** - * Call this before first sync to load from previously saved file. - * - * \param fromFile A local path to read the state from. Uses QUrl - * to be QML-friendly. Empty parameter means using a path - * defined by stateCachePath(). - */ - Q_INVOKABLE void loadState(const QUrl &fromFile = {}); - /** - * This method saves the current state of rooms (but not messages - * in them) to a local cache file, so that it could be loaded by - * loadState() on a next run of the client. - * - * \param toFile A local path to save the state to. Uses QUrl to be - * QML-friendly. Empty parameter means using a path defined by - * stateCachePath(). - */ - Q_INVOKABLE void saveState(const QUrl &toFile = {}) const; - - /** - * The default path to store the cached room state, defined as - * follows: - * QStandardPaths::writeableLocation(QStandardPaths::CacheLocation) + _safeUserId + "_state.json" - * where `_safeUserId` is userId() with `:` (colon) replaced with - * `_` (underscore) - * /see loadState(), saveState() - */ - Q_INVOKABLE QString stateCachePath() const; - - bool cacheState() const; - void setCacheState(bool newValue); - - /** - * This is a universal method to start a job of a type passed - * as a template parameter. Arguments to callApi() are arguments - * to the job constructor _except_ the first ConnectionData* - * argument - callApi() will pass it automatically. - */ - template - JobT* callApi(JobArgTs&&... jobArgs) const - { - auto job = new JobT(std::forward(jobArgs)...); - job->start(connectionData()); - return job; - } - - /** Generates a new transaction id. Transaction id's are unique within - * a single Connection object - */ - Q_INVOKABLE QByteArray generateTxnId(); - - template - static void setRoomType() - { - roomFactory = - [](Connection* c, const QString& id, JoinState joinState) - { return new T(c, id, joinState); }; - } - - template - static void setUserType() - { - userFactory = - [](Connection* c, const QString& id) { return new T(id, c); }; - } - - public slots: - /** Set the homeserver base URL */ - void setHomeserver(const QUrl& baseUrl); - - /** Determine and set the homeserver from domain or MXID */ - void resolveServer(const QString& mxidOrDomain); - - void connectToServer(const QString& user, const QString& password, - const QString& initialDeviceName, - const QString& deviceId = {}); - void connectWithToken(const QString& userId, const QString& accessToken, - const QString& deviceId); - - /** @deprecated Use stopSync() instead */ - void disconnectFromServer() { stopSync(); } - void logout(); - - void sync(int timeout = -1); - void stopSync(); - - virtual MediaThumbnailJob* getThumbnail(const QString& mediaId, - QSize requestedSize) const; - MediaThumbnailJob* getThumbnail(const QUrl& url, - QSize requestedSize) const; - MediaThumbnailJob* getThumbnail(const QUrl& url, - int requestedWidth, - int requestedHeight) const; - - // QIODevice* should already be open - UploadContentJob* uploadContent(QIODevice* contentSource, - const QString& filename = {}, - const QString& contentType = {}) const; - UploadContentJob* uploadFile(const QString& fileName, - const QString& contentType = {}); - GetContentJob* getContent(const QString& mediaId) const; - GetContentJob* getContent(const QUrl& url) const; - // If localFilename is empty, a temporary file will be created - DownloadFileJob* downloadFile(const QUrl& url, - const QString& localFilename = {}) const; - - /** - * \brief Create a room (generic method) - * This method allows to customize room entirely to your liking, - * providing all the attributes the original CS API provides. - */ - CreateRoomJob* createRoom(RoomVisibility visibility, - const QString& alias, const QString& name, const QString& topic, - const QVector& invites, const QString& presetName = {}, bool isDirect = false, - bool guestsCanJoin = false, - const QVector& initialState = {}, - const QVector& invite3pids = {}, - const QJsonObject creationContent = {}); - - /** Get a direct chat with a single user - * This method may return synchronously or asynchoronously depending - * on whether a direct chat room with the respective person exists - * already. - * - * \sa directChatAvailable - */ - Q_INVOKABLE void requestDirectChat(const QString& userId); - - /** Run an operation in a direct chat with the user - * This method may return synchronously or asynchoronously depending - * on whether a direct chat room with the respective person exists - * already. Instead of emitting a signal it executes the passed - * function object with the direct chat room as its parameter. - */ - Q_INVOKABLE void doInDirectChat(const QString& userId, - std::function operation); - - /** Create a direct chat with a single user, optional name and topic - * A room will always be created, unlike in requestDirectChat. - * It is advised to use requestDirectChat as a default way of getting - * one-on-one with a person, and only use createDirectChat when - * a new creation is explicitly desired. - */ - CreateRoomJob* createDirectChat(const QString& userId, - const QString& topic = {}, const QString& name = {}); - - virtual JoinRoomJob* joinRoom(const QString& roomAlias); - - /** Sends /forget to the server and also deletes room locally. - * This method is in Connection, not in Room, since it's a - * room lifecycle operation, and Connection is an acting room manager. - * It ensures that the local user is not a member of a room (running /leave, - * if necessary) then issues a /forget request and if that one doesn't fail - * deletion of the local Room object is ensured. - * \param id - the room id to forget - * \return - the ongoing /forget request to the server; note that the - * success() signal of this request is connected to deleteLater() - * of a respective room so by the moment this finishes, there might be no - * Room object anymore. - */ - ForgetRoomJob* forgetRoom(const QString& id); - - // Old API that will be abolished any time soon. DO NOT USE. - - /** @deprecated Use callApi() or Room::postMessage() instead */ - virtual void postMessage(Room* room, const QString& type, - const QString& message) const; - /** @deprecated Use callApi() or Room::postReceipt() instead */ - virtual PostReceiptJob* postReceipt(Room* room, - RoomEvent* event) const; - /** @deprecated Use callApi() or Room::leaveRoom() instead */ - virtual void leaveRoom( Room* room ); - /** @deprecated User callApi() or Room::getPreviousContent() instead */ - virtual RoomMessagesJob* getMessages(Room* room, - const QString& from) const; - signals: - /** - * @deprecated - * This was a signal resulting from a successful resolveServer(). - * Since Connection now provides setHomeserver(), the HS URL - * may change even without resolveServer() invocation. Use - * homeserverChanged() instead of resolved(). You can also use - * connectToServer and connectWithToken without the HS URL set in - * advance (i.e. without calling resolveServer), as they now trigger - * server name resolution from MXID if the server URL is not valid. - */ - void resolved(); - void resolveError(QString error); - - void homeserverChanged(QUrl baseUrl); - - void connected(); - void reconnected(); //< Unused; use connected() instead - void loggedOut(); - void loginError(QString error); - void networkError(int retriesTaken, int inMilliseconds); - - void syncDone(); - void syncError(QString error); - - void newUser(User* user); - - /** - * \group Signals emitted on room transitions - * - * Note: Rooms in Invite state are always stored separately from - * rooms in Join/Leave state, because of special treatment of - * invite_state in Matrix CS API (see The Spec on /sync for details). - * Therefore, objects below are: r - room in Join/Leave state; - * i - room in Invite state - * - * 1. none -> Invite: newRoom(r), invitedRoom(r,nullptr) - * 2. none -> Join: newRoom(r), joinedRoom(r,nullptr) - * 3. none -> Leave: newRoom(r), leftRoom(r,nullptr) - * 4. Invite -> Join: - * newRoom(r), joinedRoom(r,i), aboutToDeleteRoom(i) - * 4a. Leave and Invite -> Join: - * joinedRoom(r,i), aboutToDeleteRoom(i) - * 5. Invite -> Leave: - * newRoom(r), leftRoom(r,i), aboutToDeleteRoom(i) - * 5a. Leave and Invite -> Leave: - * leftRoom(r,i), aboutToDeleteRoom(i) - * 6. Join -> Leave: leftRoom(r) - * 7. Leave -> Invite: newRoom(i), invitedRoom(i,r) - * 8. Leave -> Join: joinedRoom(r) - * The following transitions are only possible via forgetRoom() - * so far; if a room gets forgotten externally, sync won't tell - * about it: - * 9. any -> none: as any -> Leave, then aboutToDeleteRoom(r) - */ - - /** A new room object has been created */ - void newRoom(Room* room); - - /** Invitation to a room received - * - * If the same room is in Left state, it's passed in prev. - */ - void invitedRoom(Room* room, Room* prev); - - /** A room has just been joined - * - * It's not the same as receiving a room in "join" section of sync - * response (rooms will be there even after joining). If this room - * was in Invite state before, the respective object is passed in - * prev (and it will be deleted shortly afterwards). - */ - void joinedRoom(Room* room, Room* prev); - - /** A room has just been left - * - * If this room has been in Invite state (as in case of rejecting - * an invitation), the respective object will be passed in prev - * (and will be deleted shortly afterwards). - */ - void leftRoom(Room* room, Room* prev); - - /** The room object is about to be deleted */ - void aboutToDeleteRoom(Room* room); - - /** The room has just been created by createRoom or requestDirectChat - * - * This signal is not emitted in usual room state transitions, - * only as an outcome of room creation operations invoked by - * the client. - * \note requestDirectChat doesn't necessarily create a new chat; - * use directChatAvailable signal if you just need to obtain - * a direct chat room. - */ - void createdRoom(Room* room); - - /** The direct chat room is ready for using - * This signal is emitted upon any successful outcome from - * requestDirectChat. - */ - void directChatAvailable(Room* directChat); - - /** The list of direct chats has changed - * This signal is emitted every time when the mapping of users - * to direct chat rooms is changed (because of either local updates - * or a different list arrived from the server). - */ - void directChatsListChanged(); - - void cacheStateChanged(); - - protected: - /** - * @brief Access the underlying ConnectionData class - */ - const ConnectionData* connectionData() const; - - /** - * @brief Find a (possibly new) Room object for the specified id - * Use this method whenever you need to find a Room object in - * the local list of rooms. Note that this does not interact with - * the server; in particular, does not automatically create rooms - * on the server. - * @return a pointer to a Room object with the specified id; nullptr - * if roomId is empty if roomFactory() failed to create a Room object. - */ - Room* provideRoom(const QString& roomId, JoinState joinState); - - /** - * Completes loading sync data. - */ - void onSyncSuccess(SyncData &&data); - - private: - class Private; - std::unique_ptr d; - - /** - * A single entry for functions that need to check whether the - * homeserver is valid before running. May either execute connectFn - * synchronously or asynchronously (if tryResolve is true and - * a DNS lookup is initiated); in case of errors, emits resolveError - * if the homeserver URL is not valid and cannot be resolved from - * userId. - * - * @param userId - fully-qualified MXID to resolve HS from - * @param connectFn - a function to execute once the HS URL is good - */ - void checkAndConnect(const QString& userId, - std::function connectFn); - void doConnectToServer(const QString& user, const QString& password, - const QString& initialDeviceName, - const QString& deviceId = {}); - - static room_factory_t roomFactory; - static user_factory_t userFactory; - }; -} // namespace QMatrixClient -Q_DECLARE_METATYPE(QMatrixClient::Connection*) diff --git a/connectiondata.cpp b/connectiondata.cpp deleted file mode 100644 index 4e9bc77e..00000000 --- a/connectiondata.cpp +++ /dev/null @@ -1,108 +0,0 @@ -/****************************************************************************** - * Copyright (C) 2015 Felix Rohrbach - * - * 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 - */ - -#include "connectiondata.h" - -#include "networkaccessmanager.h" -#include "logging.h" - -using namespace QMatrixClient; - -struct ConnectionData::Private -{ - explicit Private(const QUrl& url) : baseUrl(url) { } - - QUrl baseUrl; - QByteArray accessToken; - QString lastEvent; - QString deviceId; - - mutable unsigned int txnCounter = 0; - const qint64 id = QDateTime::currentMSecsSinceEpoch(); -}; - -ConnectionData::ConnectionData(QUrl baseUrl) - : d(std::make_unique(baseUrl)) -{ } - -ConnectionData::~ConnectionData() = default; - -QByteArray ConnectionData::accessToken() const -{ - return d->accessToken; -} - -QUrl ConnectionData::baseUrl() const -{ - return d->baseUrl; -} - -QNetworkAccessManager* ConnectionData::nam() const -{ - return NetworkAccessManager::instance(); -} - -void ConnectionData::setBaseUrl(QUrl baseUrl) -{ - d->baseUrl = baseUrl; - qCDebug(MAIN) << "updated baseUrl to" << d->baseUrl; -} - -void ConnectionData::setToken(QByteArray token) -{ - d->accessToken = token; -} - -void ConnectionData::setHost(QString host) -{ - d->baseUrl.setHost(host); - qCDebug(MAIN) << "updated baseUrl to" << d->baseUrl; -} - -void ConnectionData::setPort(int port) -{ - d->baseUrl.setPort(port); - qCDebug(MAIN) << "updated baseUrl to" << d->baseUrl; -} - -const QString& ConnectionData::deviceId() const -{ - return d->deviceId; -} - -void ConnectionData::setDeviceId(const QString& deviceId) -{ - d->deviceId = deviceId; - qCDebug(MAIN) << "updated deviceId to" << d->deviceId; -} - -QString ConnectionData::lastEvent() const -{ - return d->lastEvent; -} - -void ConnectionData::setLastEvent(QString identifier) -{ - d->lastEvent = identifier; -} - -QByteArray ConnectionData::generateTxnId() const -{ - return QByteArray::number(d->id) + 'q' + - QByteArray::number(++d->txnCounter); -} diff --git a/connectiondata.h b/connectiondata.h deleted file mode 100644 index 7a2f2e90..00000000 --- a/connectiondata.h +++ /dev/null @@ -1,55 +0,0 @@ -/****************************************************************************** - * Copyright (C) 2015 Felix Rohrbach - * - * 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 - */ - -#pragma once - -#include - -#include - -class QNetworkAccessManager; - -namespace QMatrixClient -{ - class ConnectionData - { - public: - explicit ConnectionData(QUrl baseUrl); - virtual ~ConnectionData(); - - QByteArray accessToken() const; - QUrl baseUrl() const; - const QString& deviceId() const; - - QNetworkAccessManager* nam() const; - void setBaseUrl(QUrl baseUrl); - void setToken(QByteArray accessToken); - void setHost( QString host ); - void setPort( int port ); - void setDeviceId(const QString& deviceId); - - QString lastEvent() const; - void setLastEvent( QString identifier ); - - QByteArray generateTxnId() const; - - private: - struct Private; - std::unique_ptr d; - }; -} // namespace QMatrixClient diff --git a/converters.h b/converters.h deleted file mode 100644 index bba298e0..00000000 --- a/converters.h +++ /dev/null @@ -1,177 +0,0 @@ -/****************************************************************************** -* Copyright (C) 2017 Kitsune Ral -* -* 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 -*/ - -#pragma once - -#include -#include // Includes -#include - -namespace QMatrixClient -{ - // This catches anything implicitly convertible to QJsonValue/Object/Array - inline QJsonValue toJson(const QJsonValue& val) { return val; } - inline QJsonObject toJson(const QJsonObject& o) { return o; } - inline QJsonArray toJson(const QJsonArray& arr) { return arr; } -#ifdef _MSC_VER // MSVC gets lost and doesn't know which overload to use - inline QJsonValue toJson(const QString& s) { return s; } -#endif - - template - inline QJsonArray toJson(const QVector& vals) - { - QJsonArray ar; - for (const auto& v: vals) - ar.push_back(toJson(v)); - return ar; - } - - inline QJsonArray toJson(const QStringList& strings) - { - return QJsonArray::fromStringList(strings); - } - - inline QJsonValue toJson(const QByteArray& bytes) - { - return QJsonValue(bytes.constData()); - } - - template - inline QJsonObject toJson(const QHash& hashMap) - { - QJsonObject json; - for (auto it = hashMap.begin(); it != hashMap.end(); ++it) - json.insert(it.key(), toJson(it.value())); - return json; - } - - template - struct FromJson - { - T operator()(const QJsonValue& jv) const { return static_cast(jv); } - }; - - template - inline T fromJson(const QJsonValue& jv) - { - return FromJson()(jv); - } - - template <> struct FromJson - { - bool operator()(const QJsonValue& jv) const { return jv.toBool(); } - }; - - template <> struct FromJson - { - int operator()(const QJsonValue& jv) const { return jv.toInt(); } - }; - - template <> struct FromJson - { - double operator()(const QJsonValue& jv) const { return jv.toDouble(); } - }; - - template <> struct FromJson - { - qint64 operator()(const QJsonValue& jv) const { return qint64(jv.toDouble()); } - }; - - template <> struct FromJson - { - QString operator()(const QJsonValue& jv) const { return jv.toString(); } - }; - - template <> struct FromJson - { - QDateTime operator()(const QJsonValue& jv) const - { - return QDateTime::fromMSecsSinceEpoch(fromJson(jv), Qt::UTC); - } - }; - - template <> struct FromJson - { - QDate operator()(const QJsonValue& jv) const - { - return fromJson(jv).date(); - } - }; - - template <> struct FromJson - { - QJsonObject operator()(const QJsonValue& jv) const - { - return jv.toObject(); - } - }; - - template <> struct FromJson - { - QJsonArray operator()(const QJsonValue& jv) const - { - return jv.toArray(); - } - }; - - template struct FromJson> - { - QVector operator()(const QJsonValue& jv) const - { - const auto jsonArray = jv.toArray(); - QVector vect; vect.resize(jsonArray.size()); - std::transform(jsonArray.begin(), jsonArray.end(), - vect.begin(), FromJson()); - return vect; - } - }; - - template struct FromJson> - { - QList operator()(const QJsonValue& jv) const - { - const auto jsonArray = jv.toArray(); - QList sl; sl.reserve(jsonArray.size()); - std::transform(jsonArray.begin(), jsonArray.end(), - std::back_inserter(sl), FromJson()); - return sl; - } - }; - - template <> struct FromJson : FromJson> { }; - - template <> struct FromJson - { - inline QByteArray operator()(const QJsonValue& jv) const - { - return fromJson(jv).toLatin1(); - } - }; - - template struct FromJson> - { - QHash operator()(const QJsonValue& jv) const - { - const auto json = jv.toObject(); - QHash h; h.reserve(json.size()); - for (auto it = json.begin(); it != json.end(); ++it) - h.insert(it.key(), fromJson(it.value())); - return h; - } - }; -} // namespace QMatrixClient diff --git a/events/accountdataevents.h b/events/accountdataevents.h deleted file mode 100644 index f3ba27bb..00000000 --- a/events/accountdataevents.h +++ /dev/null @@ -1,78 +0,0 @@ -#include - -/****************************************************************************** - * Copyright (C) 2018 Kitsune Ral - * - * 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 - */ - -#pragma once - -#include "event.h" -#include "eventcontent.h" - -namespace QMatrixClient -{ - static constexpr const char* FavouriteTag = "m.favourite"; - static constexpr const char* LowPriorityTag = "m.lowpriority"; - - struct TagRecord - { - TagRecord (QString order = {}) : order(std::move(order)) { } - explicit TagRecord(const QJsonValue& jv) - : order(jv.toObject().value("order").toString()) - { } - - QString order; - - bool operator==(const TagRecord& other) const - { return order == other.order; } - bool operator!=(const TagRecord& other) const - { return !operator==(other); } - }; - - inline QJsonValue toJson(const TagRecord& rec) - { - return QJsonObject {{ QStringLiteral("order"), rec.order }}; - } - - using TagsMap = QHash; - -#define DEFINE_SIMPLE_EVENT(_Name, _TypeId, _EnumType, _ContentType, _ContentKey) \ - class _Name : public Event \ - { \ - public: \ - static constexpr const char* TypeId = _TypeId; \ - static const char* typeId() { return TypeId; } \ - explicit _Name(const QJsonObject& obj) \ - : Event((_EnumType), obj) \ - , _content(contentJson(), QStringLiteral(#_ContentKey)) \ - { } \ - template \ - explicit _Name(Ts&&... contentArgs) \ - : Event(_EnumType) \ - , _content(QStringLiteral(#_ContentKey), \ - std::forward(contentArgs)...) \ - { } \ - const _ContentType& _ContentKey() const { return _content.value; } \ - QJsonObject toJson() const { return _content.toJson(); } \ - protected: \ - EventContent::SimpleContent<_ContentType> _content; \ - }; - - DEFINE_SIMPLE_EVENT(TagEvent, "m.tag", EventType::Tag, TagsMap, tags) - DEFINE_SIMPLE_EVENT(ReadMarkerEvent, "m.fully_read", EventType::ReadMarker, - QString, event_id) -} diff --git a/events/directchatevent.cpp b/events/directchatevent.cpp deleted file mode 100644 index 7049d967..00000000 --- a/events/directchatevent.cpp +++ /dev/null @@ -1,36 +0,0 @@ -/****************************************************************************** - * Copyright (C) 2018 Kitsune Ral - * - * 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 - */ - -#include "directchatevent.h" - -#include "converters.h" - -using namespace QMatrixClient; - -DirectChatEvent::DirectChatEvent(const QJsonObject& obj) - : Event(Type::DirectChat, obj) -{ } - -QMultiHash DirectChatEvent::usersToDirectChats() const -{ - QMultiHash result; - for (auto it = contentJson().begin(); it != contentJson().end(); ++it) - for (auto roomIdValue: it.value().toArray()) - result.insert(it.key(), roomIdValue.toString()); - return result; -} diff --git a/events/directchatevent.h b/events/directchatevent.h deleted file mode 100644 index 2b0ad0a0..00000000 --- a/events/directchatevent.h +++ /dev/null @@ -1,34 +0,0 @@ -/****************************************************************************** - * Copyright (C) 2018 Kitsune Ral - * - * 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 - */ - -#pragma once - -#include "event.h" - -namespace QMatrixClient -{ - class DirectChatEvent : public Event - { - public: - explicit DirectChatEvent(const QJsonObject& obj); - - QMultiHash usersToDirectChats() const; - - static constexpr const char * TypeId = "m.direct"; - }; -} diff --git a/events/event.cpp b/events/event.cpp deleted file mode 100644 index 8ddf3945..00000000 --- a/events/event.cpp +++ /dev/null @@ -1,182 +0,0 @@ -/****************************************************************************** - * Copyright (C) 2015 Felix Rohrbach - * - * 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 - */ - -#include "event.h" - -#include "roommessageevent.h" -#include "simplestateevents.h" -#include "roommemberevent.h" -#include "roomavatarevent.h" -#include "typingevent.h" -#include "receiptevent.h" -#include "accountdataevents.h" -#include "directchatevent.h" -#include "redactionevent.h" -#include "logging.h" - -#include - -using namespace QMatrixClient; - -Event::Event(Type type, const QJsonObject& rep) - : _type(type), _originalJson(rep) -{ - if (!rep.contains("content") && - !rep.value("unsigned").toObject().contains("redacted_because")) - { - qCWarning(EVENTS) << "Event without 'content' node"; - qCWarning(EVENTS) << formatJson << rep; - } -} - -Event::~Event() = default; - -QString Event::jsonType() const -{ - return originalJsonObject().value("type").toString(); -} - -QByteArray Event::originalJson() const -{ - return QJsonDocument(_originalJson).toJson(); -} - -QJsonObject Event::originalJsonObject() const -{ - return _originalJson; -} - -const QJsonObject Event::contentJson() const -{ - return _originalJson["content"].toObject(); -} - -template -inline BaseEventT* makeIfMatches(const QJsonObject&, const QString&) -{ - return nullptr; -} - -template -inline BaseEventT* makeIfMatches(const QJsonObject& o, const QString& selector) -{ - if (selector == EventT::TypeId) - return new EventT(o); - - return makeIfMatches(o, selector); -} - -template <> -EventPtr _impl::doMakeEvent(const QJsonObject& obj) -{ - // Check more specific event types first - if (auto e = doMakeEvent(obj)) - return EventPtr(move(e)); - - return EventPtr { makeIfMatches( - obj, obj["type"].toString()) }; -} - -RoomEvent::RoomEvent(Event::Type type) : Event(type) { } - -RoomEvent::RoomEvent(Type type, const QJsonObject& rep) - : Event(type, rep) - , _id(rep["event_id"].toString()) -// , _roomId(rep["room_id"].toString()) -// , _senderId(rep["sender"].toString()) -// , _serverTimestamp( -// QMatrixClient::fromJson(rep["origin_server_ts"])) -{ -// if (_id.isEmpty()) -// { -// qCWarning(EVENTS) << "Can't find event_id in a room event"; -// qCWarning(EVENTS) << formatJson << rep; -// } -// if (!rep.contains("origin_server_ts")) -// { -// qCWarning(EVENTS) << "Can't find server timestamp in a room event"; -// qCWarning(EVENTS) << formatJson << rep; -// } -// if (_senderId.isEmpty()) -// { -// qCWarning(EVENTS) << "Can't find sender in a room event"; -// qCWarning(EVENTS) << formatJson << rep; -// } - auto unsignedData = rep["unsigned"].toObject(); - auto redaction = unsignedData.value("redacted_because"); - if (redaction.isObject()) - { - _redactedBecause = - std::make_unique(redaction.toObject()); - return; - } - - _txnId = unsignedData.value("transactionId").toString(); - if (!_txnId.isEmpty()) - qCDebug(EVENTS) << "Event transactionId:" << _txnId; -} - -RoomEvent::~RoomEvent() = default; // Let the smart pointer do its job - -QDateTime RoomEvent::timestamp() const -{ - return QMatrixClient::fromJson( - originalJsonObject().value("origin_server_ts")); -} - -QString RoomEvent::roomId() const -{ - return originalJsonObject().value("room_id").toString(); -} - -QString RoomEvent::senderId() const -{ - return originalJsonObject().value("sender").toString(); -} - -QString RoomEvent::redactionReason() const -{ - return isRedacted() ? _redactedBecause->reason() : QString{}; -} - -void RoomEvent::addId(const QString& id) -{ - Q_ASSERT(_id.isEmpty()); Q_ASSERT(!id.isEmpty()); - _id = id; -} - -template <> -RoomEventPtr _impl::doMakeEvent(const QJsonObject& obj) -{ - return RoomEventPtr { makeIfMatches - (obj, obj["type"].toString()) }; -} - -StateEventBase::~StateEventBase() = default; - -bool StateEventBase::repeatsState() const -{ - auto contentJson = originalJsonObject().value("content"); - auto prevContentJson = originalJsonObject().value("unsigned") - .toObject().value("prev_content"); - return contentJson == prevContentJson; -} diff --git a/events/event.h b/events/event.h deleted file mode 100644 index eccfec41..00000000 --- a/events/event.h +++ /dev/null @@ -1,314 +0,0 @@ -/****************************************************************************** - * Copyright (C) 2015 Felix Rohrbach - * - * 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 - */ - -#pragma once - -#include -#include -#include -#include - -#include "util.h" - -#include - -namespace QMatrixClient -{ - template - using event_ptr_tt = std::unique_ptr; - - namespace _impl - { - template - event_ptr_tt doMakeEvent(const QJsonObject& obj); - } - - class Event - { - Q_GADGET - public: - enum class Type : quint16 - { - Unknown = 0, - Typing, Receipt, Tag, DirectChat, ReadMarker, - RoomEventBase = 0x1000, - RoomMessage = RoomEventBase + 1, - RoomEncryptedMessage, Redaction, - RoomStateEventBase = 0x1800, - RoomName = RoomStateEventBase + 1, - RoomAliases, RoomCanonicalAlias, RoomMember, RoomTopic, - RoomAvatar, RoomEncryption, RoomCreate, RoomJoinRules, - RoomPowerLevels, - Reserved = 0x2000 - }; - - explicit Event(Type type) : _type(type) { } - Event(Type type, const QJsonObject& rep); - Event(const Event&) = delete; - virtual ~Event(); - - Type type() const { return _type; } - QString jsonType() const; - bool isStateEvent() const - { - return (quint16(_type) & 0x1800) == 0x1800; - } - QByteArray originalJson() const; - QJsonObject originalJsonObject() const; - - // According to the CS API spec, every event also has - // a "content" object; but since its structure is different for - // different types, we're implementing it per-event type - // (and in most cases it will be a combination of other fields - // instead of "content" field). - - const QJsonObject contentJson() const; - - private: - Type _type; - QJsonObject _originalJson; - - REGISTER_ENUM(Type) - Q_PROPERTY(Type type READ type CONSTANT) - Q_PROPERTY(QJsonObject contentJson READ contentJson CONSTANT) - }; - using EventType = Event::Type; - using EventPtr = event_ptr_tt; - - /** Create an event with proper type from a JSON object - * Use this factory template to detect the type from the JSON object - * contents (the detected event type should derive from the template - * parameter type) and create an event object of that type. - */ - template - event_ptr_tt makeEvent(const QJsonObject& obj) - { - auto e = _impl::doMakeEvent(obj); - if (!e) - e = std::make_unique(EventType::Unknown, obj); - return e; - } - - namespace _impl - { - template <> - EventPtr doMakeEvent(const QJsonObject& obj); - } - - /** - * \brief A vector of pointers to events with deserialisation capabilities - * - * This is a simple wrapper over a generic vector type that adds - * a convenience method to deserialise events from QJsonArray. - * \tparam EventT base type of all events in the vector - */ - template - class EventsBatch : public std::vector> - { - public: - /** - * \brief Deserialise events from an array - * - * Given the following JSON construct, creates events from - * the array stored at key "node": - * \code - * "container": { - * "node": [ { "event_id": "!evt1:srv.org", ... }, ... ] - * } - * \endcode - * \param container - the wrapping JSON object - * \param node - the key in container that holds the array of events - */ - void fromJson(const QJsonObject& container, const QString& node) - { - const auto objs = container.value(node).toArray(); - using size_type = typename std::vector>::size_type; - // The below line accommodates the difference in size types of - // STL and Qt containers. - this->reserve(static_cast(objs.size())); - for (auto objValue: objs) - this->emplace_back(makeEvent(objValue.toObject())); - } - }; - using Events = EventsBatch; - - class RedactionEvent; - - /** This class corresponds to m.room.* events */ - class RoomEvent : public Event - { - Q_GADGET - Q_PROPERTY(QString id READ id) - Q_PROPERTY(QDateTime timestamp READ timestamp CONSTANT) - Q_PROPERTY(QString roomId READ roomId CONSTANT) - Q_PROPERTY(QString senderId READ senderId CONSTANT) - Q_PROPERTY(QString redactionReason READ redactionReason) - Q_PROPERTY(bool isRedacted READ isRedacted) - Q_PROPERTY(QString transactionId READ transactionId) - public: - // RedactionEvent is an incomplete type here so we cannot inline - // constructors and destructors - explicit RoomEvent(Type type); - RoomEvent(Type type, const QJsonObject& rep); - ~RoomEvent(); - - QString id() const { return _id; } - QDateTime timestamp() const; - QString roomId() const; - QString senderId() const; - bool isRedacted() const { return bool(_redactedBecause); } - const RedactionEvent* redactedBecause() const - { - return _redactedBecause.get(); - } - QString redactionReason() const; - const QString& transactionId() const { return _txnId; } - - /** - * Sets the transaction id for locally created events. This should be - * done before the event is exposed to any code using the respective - * Q_PROPERTY. - * - * \param txnId - transaction id, normally obtained from - * Connection::generateTxnId() - */ - void setTransactionId(const QString& txnId) { _txnId = txnId; } - - /** - * Sets event id for locally created events - * - * When a new event is created locally, it has no server id yet. - * This function allows to add the id once the confirmation from - * the server is received. There should be no id set previously - * in the event. It's the responsibility of the code calling addId() - * to notify clients that use Q_PROPERTY(id) about its change - */ - void addId(const QString& id); - - private: - QString _id; -// QString _roomId; -// QString _senderId; -// QDateTime _serverTimestamp; - event_ptr_tt _redactedBecause; - QString _txnId; - }; - using RoomEvents = EventsBatch; - using RoomEventPtr = event_ptr_tt; - - namespace _impl - { - template <> - RoomEventPtr doMakeEvent(const QJsonObject& obj); - } - - /** - * Conceptually similar to QStringView (but much more primitive), it's a - * simple abstraction over a pair of RoomEvents::const_iterator values - * referring to the beginning and the end of a range in a RoomEvents - * container. - */ - struct RoomEventsRange - { - RoomEvents::iterator from; - RoomEvents::iterator to; - - RoomEvents::size_type size() const - { - Q_ASSERT(std::distance(from, to) >= 0); - return RoomEvents::size_type(std::distance(from, to)); - } - bool empty() const { return from == to; } - RoomEvents::const_iterator begin() const { return from; } - RoomEvents::const_iterator end() const { return to; } - RoomEvents::iterator begin() { return from; } - RoomEvents::iterator end() { return to; } - }; - - class StateEventBase: public RoomEvent - { - public: - explicit StateEventBase(Type type, const QJsonObject& obj) - : RoomEvent(obj.contains("state_key") ? type : Type::Unknown, - obj) - { } - explicit StateEventBase(Type type) - : RoomEvent(type) - { } - ~StateEventBase() override = 0; - - virtual bool repeatsState() const; - }; - - template - struct Prev - { - template - explicit Prev(const QJsonObject& unsignedJson, - ContentParamTs&&... contentParams) - : senderId(unsignedJson.value("prev_sender").toString()) - , content(unsignedJson.value("prev_content").toObject(), - std::forward(contentParams)...) - { } - - QString senderId; - ContentT content; - }; - - template - class StateEvent: public StateEventBase - { - public: - using content_type = ContentT; - - template - explicit StateEvent(Type type, const QJsonObject& obj, - ContentParamTs&&... contentParams) - : StateEventBase(type, obj) - , _content(contentJson(), - std::forward(contentParams)...) - { - auto unsignedData = obj.value("unsigned").toObject(); - if (unsignedData.contains("prev_content")) - _prev = std::make_unique>(unsignedData, - std::forward(contentParams)...); - } - template - explicit StateEvent(Type type, ContentParamTs&&... contentParams) - : StateEventBase(type) - , _content(std::forward(contentParams)...) - { } - - QJsonObject toJson() const { return _content.toJson(); } - - const ContentT& content() const { return _content; } - /** @deprecated Use prevContent instead */ - const ContentT* prev_content() const { return prevContent(); } - const ContentT* prevContent() const - { return _prev ? &_prev->content : nullptr; } - QString prevSenderId() const { return _prev ? _prev->senderId : ""; } - - protected: - ContentT _content; - std::unique_ptr> _prev; - }; -} // namespace QMatrixClient -Q_DECLARE_METATYPE(QMatrixClient::Event*) -Q_DECLARE_METATYPE(QMatrixClient::RoomEvent*) -Q_DECLARE_METATYPE(const QMatrixClient::Event*) -Q_DECLARE_METATYPE(const QMatrixClient::RoomEvent*) diff --git a/events/eventcontent.cpp b/events/eventcontent.cpp deleted file mode 100644 index f5974b46..00000000 --- a/events/eventcontent.cpp +++ /dev/null @@ -1,85 +0,0 @@ -/****************************************************************************** - * Copyright (C) 2017 Kitsune Ral - * - * 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 - */ - -#include "eventcontent.h" - -#include -#include - -using namespace QMatrixClient::EventContent; - -QJsonObject Base::toJson() const -{ - QJsonObject o; - fillJson(&o); - return o; -} - -FileInfo::FileInfo(const QUrl& u, int payloadSize, const QMimeType& mimeType, - const QString& originalFilename) - : mimeType(mimeType), url(u), payloadSize(payloadSize) - , originalName(originalFilename) -{ } - -FileInfo::FileInfo(const QUrl& u, const QJsonObject& infoJson, - const QString& originalFilename) - : originalInfoJson(infoJson) - , mimeType(QMimeDatabase().mimeTypeForName(infoJson["mimetype"].toString())) - , url(u) - , payloadSize(infoJson["size"].toInt()) - , originalName(originalFilename) -{ - if (!mimeType.isValid()) - mimeType = QMimeDatabase().mimeTypeForData(QByteArray()); -} - -void FileInfo::fillInfoJson(QJsonObject* infoJson) const -{ - Q_ASSERT(infoJson); - infoJson->insert("size", payloadSize); - infoJson->insert("mimetype", mimeType.name()); -} - -ImageInfo::ImageInfo(const QUrl& u, int fileSize, QMimeType mimeType, - const QSize& imageSize) - : FileInfo(u, fileSize, mimeType), imageSize(imageSize) -{ } - -ImageInfo::ImageInfo(const QUrl& u, const QJsonObject& infoJson, - const QString& originalFilename) - : FileInfo(u, infoJson, originalFilename) - , imageSize(infoJson["w"].toInt(), infoJson["h"].toInt()) -{ } - -void ImageInfo::fillInfoJson(QJsonObject* infoJson) const -{ - FileInfo::fillInfoJson(infoJson); - infoJson->insert("w", imageSize.width()); - infoJson->insert("h", imageSize.height()); -} - -Thumbnail::Thumbnail(const QJsonObject& infoJson) - : ImageInfo(infoJson["thumbnail_url"].toString(), - infoJson["thumbnail_info"].toObject()) -{ } - -void Thumbnail::fillInfoJson(QJsonObject* infoJson) const -{ - infoJson->insert("thumbnail_url", url.toString()); - infoJson->insert("thumbnail_info", toInfoJson(*this)); -} diff --git a/events/eventcontent.h b/events/eventcontent.h deleted file mode 100644 index 9d44aec0..00000000 --- a/events/eventcontent.h +++ /dev/null @@ -1,314 +0,0 @@ -/****************************************************************************** - * Copyright (C) 2017 Kitsune Ral - * - * 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 - */ - -#pragma once - -// This file contains generic event content definitions, applicable to room -// message events as well as other events (e.g., avatars). - -#include "converters.h" - -#include -#include -#include - -#include - -namespace QMatrixClient -{ - namespace EventContent - { - /** - * A base class for all content types that can be stored - * in a RoomMessageEvent - * - * Each content type class should have a constructor taking - * a QJsonObject and override fillJson() with an implementation - * that will fill the target QJsonObject with stored values. It is - * assumed but not required that a content object can also be created - * from plain data. - */ - class Base - { - public: - explicit Base (const QJsonObject& o = {}) : originalJson(o) { } - virtual ~Base() = default; - - QJsonObject toJson() const; - - public: - QJsonObject originalJson; - - protected: - virtual void fillJson(QJsonObject* o) const = 0; - }; - - template - class SimpleContent: public Base - { - public: - using value_type = T; - - // The constructor is templated to enable perfect forwarding - template - SimpleContent(QString keyName, TT&& value) - : value(std::forward(value)), key(std::move(keyName)) - { } - SimpleContent(const QJsonObject& json, QString keyName) - : Base(json) - , value(QMatrixClient::fromJson(json[keyName])) - , key(std::move(keyName)) - { } - - public: - T value; - - protected: - QString key; - - private: - void fillJson(QJsonObject* json) const override - { - Q_ASSERT(json); - json->insert(key, QMatrixClient::toJson(value)); - } - }; - - // The below structures fairly follow CS spec 11.2.1.6. The overall - // set of attributes for each content types is a superset of the spec - // but specific aggregation structure is altered. See doc comments to - // each type for the list of available attributes. - - // A quick classes inheritance structure follows: - // FileInfo - // FileContent : UrlBasedContent - // AudioContent : UrlBasedContent - // ImageInfo : FileInfo + imageSize attribute - // ImageContent : UrlBasedContent - // VideoContent : UrlBasedContent - - /** - * A base/mixin class for structures representing an "info" object for - * some content types. These include most attachment types currently in - * the CS API spec. - * - * In order to use it in a content class, derive both from TypedBase - * (or Base) and from FileInfo (or its derivative, such as \p ImageInfo) - * and call fillInfoJson() to fill the "info" subobject. Make sure - * to pass an "info" part of JSON to FileInfo constructor, not the whole - * JSON content, as well as contents of "url" (or a similar key) and - * optionally "filename" node from the main JSON content. Assuming you - * don't do unusual things, you should use \p UrlBasedContent<> instead - * of doing multiple inheritance and overriding Base::fillJson() by hand. - * - * This class is not polymorphic. - */ - class FileInfo - { - public: - explicit FileInfo(const QUrl& u, int payloadSize = -1, - const QMimeType& mimeType = {}, - const QString& originalFilename = {}); - FileInfo(const QUrl& u, const QJsonObject& infoJson, - const QString& originalFilename = {}); - - void fillInfoJson(QJsonObject* infoJson) const; - - /** - * \brief Extract media id from the URL - * - * This can be used, e.g., to construct a QML-facing image:// - * URI as follows: - * \code "image://provider/" + info.mediaId() \endcode - */ - QString mediaId() const { return url.authority() + url.path(); } - - public: - QJsonObject originalInfoJson; - QMimeType mimeType; - QUrl url; - int payloadSize; - QString originalName; - }; - - template - QJsonObject toInfoJson(const InfoT& info) - { - QJsonObject infoJson; - info.fillInfoJson(&infoJson); - return infoJson; - } - - /** - * A content info class for image content types: image, thumbnail, video - */ - class ImageInfo : public FileInfo - { - public: - explicit ImageInfo(const QUrl& u, int fileSize = -1, - QMimeType mimeType = {}, - const QSize& imageSize = {}); - ImageInfo(const QUrl& u, const QJsonObject& infoJson, - const QString& originalFilename = {}); - - void fillInfoJson(QJsonObject* infoJson) const; - - public: - QSize imageSize; - }; - - /** - * An auxiliary class for an info type that carries a thumbnail - * - * This class saves/loads a thumbnail to/from "info" subobject of - * the JSON representation of event content; namely, - * "info/thumbnail_url" and "info/thumbnail_info" fields are used. - */ - class Thumbnail : public ImageInfo - { - public: - Thumbnail(const QJsonObject& infoJson); - Thumbnail(const ImageInfo& info) - : ImageInfo(info) - { } - - /** - * Writes thumbnail information to "thumbnail_info" subobject - * and thumbnail URL to "thumbnail_url" node inside "info". - */ - void fillInfoJson(QJsonObject* infoJson) const; - }; - - class TypedBase: public Base - { - public: - explicit TypedBase(const QJsonObject& o = {}) : Base(o) { } - virtual QMimeType type() const = 0; - virtual const FileInfo* fileInfo() const { return nullptr; } - virtual const Thumbnail* thumbnailInfo() const { return nullptr; } - }; - - /** - * A base class for content types that have a URL and additional info - * - * Types that derive from this class template take "url" and, - * optionally, "filename" values from the top-level JSON object and - * the rest of information from the "info" subobject, as defined by - * the parameter type. - * - * \tparam InfoT base info class - */ - template - class UrlBasedContent : public TypedBase, public InfoT - { - public: - UrlBasedContent(QUrl url, InfoT&& info, QString filename = {}) - : InfoT(url, std::forward(info), filename) - { } - explicit UrlBasedContent(const QJsonObject& json) - : TypedBase(json) - , InfoT(json["url"].toString(), json["info"].toObject(), - json["filename"].toString()) - { - // A small hack to facilitate links creation in QML. - originalJson.insert("mediaId", InfoT::mediaId()); - } - - QMimeType type() const override { return InfoT::mimeType; } - const FileInfo* fileInfo() const override { return this; } - - protected: - void fillJson(QJsonObject* json) const override - { - Q_ASSERT(json); - json->insert("url", InfoT::url.toString()); - if (!InfoT::originalName.isEmpty()) - json->insert("filename", InfoT::originalName); - json->insert("info", toInfoJson(*this)); - } - }; - - template - class UrlWithThumbnailContent : public UrlBasedContent - { - public: - // TODO: POD constructor - explicit UrlWithThumbnailContent(const QJsonObject& json) - : UrlBasedContent(json) - , thumbnail(InfoT::originalInfoJson) - { - // Another small hack, to simplify making a thumbnail link - UrlBasedContent::originalJson.insert( - "thumbnailMediaId", thumbnail.mediaId()); - } - - const Thumbnail* thumbnailInfo() const override - { return &thumbnail; } - - public: - Thumbnail thumbnail; - - protected: - void fillJson(QJsonObject* json) const override - { - UrlBasedContent::fillJson(json); - auto infoJson = json->take("info").toObject(); - thumbnail.fillInfoJson(&infoJson); - json->insert("info", infoJson); - } - }; - - /** - * Content class for m.image - * - * Available fields: - * - corresponding to the top-level JSON: - * - url - * - filename (extension to the spec) - * - corresponding to the "info" subobject: - * - payloadSize ("size" in JSON) - * - mimeType ("mimetype" in JSON) - * - imageSize (QSize for a combination of "h" and "w" in JSON) - * - thumbnail.url ("thumbnail_url" in JSON) - * - corresponding to the "info/thumbnail_info" subobject: contents of - * thumbnail field, in the same vein as for the main image: - * - payloadSize - * - mimeType - * - imageSize - */ - using ImageContent = UrlWithThumbnailContent; - - /** - * Content class for m.file - * - * Available fields: - * - corresponding to the top-level JSON: - * - url - * - filename - * - corresponding to the "info" subobject: - * - payloadSize ("size" in JSON) - * - mimeType ("mimetype" in JSON) - * - thumbnail.url ("thumbnail_url" in JSON) - * - corresponding to the "info/thumbnail_info" subobject: - * - thumbnail.payloadSize - * - thumbnail.mimeType - * - thumbnail.imageSize (QSize for "h" and "w" in JSON) - */ - using FileContent = UrlWithThumbnailContent; - } // namespace EventContent -} // namespace QMatrixClient diff --git a/events/receiptevent.cpp b/events/receiptevent.cpp deleted file mode 100644 index 7555db82..00000000 --- a/events/receiptevent.cpp +++ /dev/null @@ -1,70 +0,0 @@ -/****************************************************************************** - * Copyright (C) 2016 Felix Rohrbach - * - * 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 - */ - -/* -Example of a Receipt Event: -{ - "content": { - "$1435641916114394fHBLK:matrix.org": { - "m.read": { - "@rikj:jki.re": { - "ts": 1436451550453 - } - } - } - }, - "room_id": "!KpjVgQyZpzBwvMBsnT:matrix.org", - "type": "m.receipt" -} -*/ - -#include "receiptevent.h" - -#include "converters.h" -#include "logging.h" - -using namespace QMatrixClient; - -ReceiptEvent::ReceiptEvent(const QJsonObject& obj) - : Event(Type::Receipt, obj) -{ - Q_ASSERT(obj["type"].toString() == TypeId); - - const QJsonObject contents = contentJson(); - _eventsWithReceipts.reserve(contents.size()); - for( auto eventIt = contents.begin(); eventIt != contents.end(); ++eventIt ) - { - if (eventIt.key().isEmpty()) - { - qCWarning(EPHEMERAL) << "ReceiptEvent has an empty event id, skipping"; - qCDebug(EPHEMERAL) << "ReceiptEvent content follows:\n" << contents; - continue; - } - const QJsonObject reads = eventIt.value().toObject().value("m.read").toObject(); - QVector receipts; - receipts.reserve(reads.size()); - for( auto userIt = reads.begin(); userIt != reads.end(); ++userIt ) - { - const QJsonObject user = userIt.value().toObject(); - receipts.push_back({userIt.key(), - QMatrixClient::fromJson(user["ts"])}); - } - _eventsWithReceipts.push_back({eventIt.key(), std::move(receipts)}); - } -} - diff --git a/events/receiptevent.h b/events/receiptevent.h deleted file mode 100644 index 5b99ae3f..00000000 --- a/events/receiptevent.h +++ /dev/null @@ -1,50 +0,0 @@ -/****************************************************************************** - * Copyright (C) 2016 Felix Rohrbach - * - * 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 - */ - -#pragma once - -#include "event.h" - -namespace QMatrixClient -{ - struct Receipt - { - QString userId; - QDateTime timestamp; - }; - struct ReceiptsForEvent - { - QString evtId; - QVector receipts; - }; - using EventsWithReceipts = QVector; - - class ReceiptEvent: public Event - { - public: - explicit ReceiptEvent(const QJsonObject& obj); - - EventsWithReceipts eventsWithReceipts() const - { return _eventsWithReceipts; } - - static constexpr const char* const TypeId = "m.receipt"; - - private: - EventsWithReceipts _eventsWithReceipts; - }; -} // namespace QMatrixClient diff --git a/events/redactionevent.cpp b/events/redactionevent.cpp deleted file mode 100644 index bf467718..00000000 --- a/events/redactionevent.cpp +++ /dev/null @@ -1 +0,0 @@ -#include "redactionevent.h" diff --git a/events/redactionevent.h b/events/redactionevent.h deleted file mode 100644 index fa6902ab..00000000 --- a/events/redactionevent.h +++ /dev/null @@ -1,43 +0,0 @@ -/****************************************************************************** - * Copyright (C) 2017 Kitsune Ral - * - * 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 - */ - -#pragma once - -#include "event.h" - -namespace QMatrixClient -{ - class RedactionEvent : public RoomEvent - { - public: - static constexpr const char* const TypeId = "m.room.redaction"; - - RedactionEvent(const QJsonObject& obj) - : RoomEvent(Type::Redaction, obj) - , _redactedEvent(obj.value("redacts").toString()) - , _reason(contentJson().value("reason").toString()) - { } - - const QString& redactedEvent() const { return _redactedEvent; } - const QString& reason() const { return _reason; } - - private: - QString _redactedEvent; - QString _reason; - }; -} // namespace QMatrixClient diff --git a/events/roomavatarevent.cpp b/events/roomavatarevent.cpp deleted file mode 100644 index 7a5f82a1..00000000 --- a/events/roomavatarevent.cpp +++ /dev/null @@ -1,23 +0,0 @@ -/****************************************************************************** - * Copyright (C) 2017 Kitsune Ral - * - * 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 - */ - -#include "roomavatarevent.h" - -using namespace QMatrixClient; - - diff --git a/events/roomavatarevent.h b/events/roomavatarevent.h deleted file mode 100644 index ccfe8fbf..00000000 --- a/events/roomavatarevent.h +++ /dev/null @@ -1,43 +0,0 @@ -/****************************************************************************** - * Copyright (C) 2017 Kitsune Ral - * - * 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 - */ - -#pragma once - -#include "event.h" - -#include - -#include "eventcontent.h" - -namespace QMatrixClient -{ - class RoomAvatarEvent: public StateEvent - { - // It's a bit of an overkill to use a full-fledged ImageContent - // because in reality m.room.avatar usually only has a single URL, - // without a thumbnail. But The Spec says there be thumbnails, and - // we follow The Spec. - public: - explicit RoomAvatarEvent(const QJsonObject& obj) - : StateEvent(Type::RoomAvatar, obj) - { } - - static constexpr const char* TypeId = "m.room.avatar"; - }; - -} // namespace QMatrixClient diff --git a/events/roommemberevent.cpp b/events/roommemberevent.cpp deleted file mode 100644 index 76b003c2..00000000 --- a/events/roommemberevent.cpp +++ /dev/null @@ -1,69 +0,0 @@ -/****************************************************************************** - * Copyright (C) 2015 Felix Rohrbach - * - * 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 - */ - -#include "roommemberevent.h" - -#include "logging.h" - -#include - -using namespace QMatrixClient; - -static const std::array membershipStrings = { { - QStringLiteral("invite"), QStringLiteral("join"), - QStringLiteral("knock"), QStringLiteral("leave"), - QStringLiteral("ban") -} }; - -namespace QMatrixClient -{ - template <> - struct FromJson - { - MembershipType operator()(const QJsonValue& jv) const - { - const auto& membershipString = jv.toString(); - for (auto it = membershipStrings.begin(); - it != membershipStrings.end(); ++it) - if (membershipString == *it) - return MembershipType(it - membershipStrings.begin()); - - qCWarning(EVENTS) << "Unknown MembershipType: " << membershipString; - return MembershipType::Undefined; - } - }; -} - -MemberEventContent::MemberEventContent(const QJsonObject& json) - : membership(fromJson(json["membership"])) - , isDirect(json["is_direct"].toBool()) - , displayName(json["displayname"].toString()) - , avatarUrl(json["avatar_url"].toString()) -{ } - -void MemberEventContent::fillJson(QJsonObject* o) const -{ - Q_ASSERT(o); - Q_ASSERT_X(membership != MembershipType::Undefined, __FUNCTION__, - "The key 'membership' must be explicit in MemberEventContent"); - if (membership != MembershipType::Undefined) - o->insert("membership", membershipStrings[membership]); - o->insert("displayname", displayName); - if (avatarUrl.isValid()) - o->insert("avatar_url", avatarUrl.toString()); -} diff --git a/events/roommemberevent.h b/events/roommemberevent.h deleted file mode 100644 index 89b970c9..00000000 --- a/events/roommemberevent.h +++ /dev/null @@ -1,78 +0,0 @@ -/****************************************************************************** - * Copyright (C) 2015 Felix Rohrbach - * - * 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 - */ - -#pragma once - -#include "event.h" - -#include "eventcontent.h" - -#include - -namespace QMatrixClient -{ - class MemberEventContent: public EventContent::Base - { - public: - enum MembershipType : size_t { Invite = 0, Join, Knock, Leave, Ban, - Undefined }; - - explicit MemberEventContent(MembershipType mt = MembershipType::Join) - : membership(mt) - { } - explicit MemberEventContent(const QJsonObject& json); - - MembershipType membership; - bool isDirect = false; - QString displayName; - QUrl avatarUrl; - - protected: - void fillJson(QJsonObject* o) const override; - }; - - using MembershipType = MemberEventContent::MembershipType; - - class RoomMemberEvent: public StateEvent - { - Q_GADGET - public: - static constexpr const char* TypeId = "m.room.member"; - - using MembershipType = MemberEventContent::MembershipType; - - RoomMemberEvent(MemberEventContent&& c) - : StateEvent(Type::RoomMember, c) - { } - explicit RoomMemberEvent(const QJsonObject& obj) - : StateEvent(Type::RoomMember, obj) -// , _userId(obj["state_key"].toString()) - { } - - MembershipType membership() const { return content().membership; } - QString userId() const - { return originalJsonObject().value("state_key").toString(); } - bool isDirect() const { return content().isDirect; } - QString displayName() const { return content().displayName; } - QUrl avatarUrl() const { return content().avatarUrl; } - - private: -// QString _userId; - REGISTER_ENUM(MembershipType) - }; -} // namespace QMatrixClient diff --git a/events/roommessageevent.cpp b/events/roommessageevent.cpp deleted file mode 100644 index dec0ca50..00000000 --- a/events/roommessageevent.cpp +++ /dev/null @@ -1,193 +0,0 @@ -/****************************************************************************** - * Copyright (C) 2015 Felix Rohrbach - * - * 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 - */ - -#include "roommessageevent.h" - -#include "logging.h" - -#include - -using namespace QMatrixClient; -using namespace EventContent; - -using MsgType = RoomMessageEvent::MsgType; - -template -TypedBase* make(const QJsonObject& json) -{ - return new ContentT(json); -} - -struct MsgTypeDesc -{ - QString jsonType; - MsgType enumType; - TypedBase* (*maker)(const QJsonObject&); -}; - -const std::vector msgTypes = - { { QStringLiteral("m.text"), MsgType::Text, make } - , { QStringLiteral("m.emote"), MsgType::Emote, make } - , { QStringLiteral("m.notice"), MsgType::Notice, make } - , { QStringLiteral("m.image"), MsgType::Image, make } - , { QStringLiteral("m.file"), MsgType::File, make } - , { QStringLiteral("m.location"), MsgType::Location, make } - , { QStringLiteral("m.video"), MsgType::Video, make } - , { QStringLiteral("m.audio"), MsgType::Audio, make } - }; - -QString msgTypeToJson(MsgType enumType) -{ - auto it = std::find_if(msgTypes.begin(), msgTypes.end(), - [=](const MsgTypeDesc& mtd) { return mtd.enumType == enumType; }); - if (it != msgTypes.end()) - return it->jsonType; - - return {}; -} - -MsgType jsonToMsgType(const QString& jsonType) -{ - auto it = std::find_if(msgTypes.begin(), msgTypes.end(), - [=](const MsgTypeDesc& mtd) { return mtd.jsonType == jsonType; }); - if (it != msgTypes.end()) - return it->enumType; - - return MsgType::Unknown; -} - -RoomMessageEvent::RoomMessageEvent(const QString& plainBody, - MsgType msgType, TypedBase* content) - : RoomMessageEvent(plainBody, msgTypeToJson(msgType), content) -{ } - -RoomMessageEvent::RoomMessageEvent(const QJsonObject& obj) - : RoomEvent(Type::RoomMessage, obj), _content(nullptr) -{ - if (isRedacted()) - return; - const QJsonObject content = contentJson(); - if ( content.contains("msgtype") && content.contains("body") ) - { - _plainBody = content["body"].toString(); - - _msgtype = content["msgtype"].toString(); - for (auto mt: msgTypes) - if (mt.jsonType == _msgtype) - _content.reset(mt.maker(content)); - - if (!_content) - { - qCWarning(EVENTS) << "RoomMessageEvent: couldn't load content," - << " full content dump follows"; - qCWarning(EVENTS) << formatJson << content; - } - } - else - { - qCWarning(EVENTS) << "No body or msgtype in room message event"; - qCWarning(EVENTS) << formatJson << obj; - } -} - -RoomMessageEvent::MsgType RoomMessageEvent::msgtype() const -{ - return jsonToMsgType(_msgtype); -} - -QMimeType RoomMessageEvent::mimeType() const -{ - return _content ? _content->type() : - QMimeDatabase().mimeTypeForName("text/plain"); -} - -bool RoomMessageEvent::hasTextContent() const -{ - return content() && - (msgtype() == MsgType::Text || msgtype() == MsgType::Emote || - msgtype() == MsgType::Notice); // FIXME: Unbind from specific msgtypes -} - -bool RoomMessageEvent::hasFileContent() const -{ - return content() && content()->fileInfo(); -} - -bool RoomMessageEvent::hasThumbnail() const -{ - return content() && content()->thumbnailInfo(); -} - -QJsonObject RoomMessageEvent::toJson() const -{ - QJsonObject obj = _content ? _content->toJson() : QJsonObject(); - obj.insert("msgtype", msgTypeToJson(msgtype())); - obj.insert("body", plainBody()); - return obj; -} - -TextContent::TextContent(const QString& text, const QString& contentType) - : mimeType(QMimeDatabase().mimeTypeForName(contentType)), body(text) -{ } - -TextContent::TextContent(const QJsonObject& json) -{ - QMimeDatabase db; - - // Special-casing the custom matrix.org's (actually, Riot's) way - // of sending HTML messages. - if (json["format"].toString() == "org.matrix.custom.html") - { - mimeType = db.mimeTypeForName("text/html"); - body = json["formatted_body"].toString(); - } else { - // Falling back to plain text, as there's no standard way to describe - // rich text in messages. - mimeType = db.mimeTypeForName("text/plain"); - body = json["body"].toString(); - } -} - -void TextContent::fillJson(QJsonObject* json) const -{ - Q_ASSERT(json); - json->insert("format", QStringLiteral("org.matrix.custom.html")); - json->insert("formatted_body", body); -} - -LocationContent::LocationContent(const QString& geoUri, const ImageInfo& thumbnail) - : geoUri(geoUri), thumbnail(thumbnail) -{ } - -LocationContent::LocationContent(const QJsonObject& json) - : TypedBase(json) - , geoUri(json["geo_uri"].toString()) - , thumbnail(json["info"].toObject()) -{ } - -QMimeType LocationContent::type() const -{ - return QMimeDatabase().mimeTypeForData(geoUri.toLatin1()); -} - -void LocationContent::fillJson(QJsonObject* o) const -{ - Q_ASSERT(o); - o->insert("geo_uri", geoUri); - o->insert("info", toInfoJson(thumbnail)); -} diff --git a/events/roommessageevent.h b/events/roommessageevent.h deleted file mode 100644 index a55564ed..00000000 --- a/events/roommessageevent.h +++ /dev/null @@ -1,194 +0,0 @@ -/****************************************************************************** - * Copyright (C) 2015 Felix Rohrbach - * - * 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 - */ - -#pragma once - -#include "event.h" - -#include "eventcontent.h" - -namespace QMatrixClient -{ - namespace MessageEventContent = EventContent; // Back-compatibility - - /** - * The event class corresponding to m.room.message events - */ - class RoomMessageEvent: public RoomEvent - { - Q_GADGET - Q_PROPERTY(QString msgType READ rawMsgtype CONSTANT) - Q_PROPERTY(QString plainBody READ plainBody CONSTANT) - Q_PROPERTY(QMimeType mimeType READ mimeType STORED false CONSTANT) - Q_PROPERTY(EventContent::TypedBase* content READ content CONSTANT) - public: - enum class MsgType - { - Text, Emote, Notice, Image, File, Location, Video, Audio, Unknown - }; - - RoomMessageEvent(const QString& plainBody, - const QString& jsonMsgType, - EventContent::TypedBase* content = nullptr) - : RoomEvent(Type::RoomMessage) - , _msgtype(jsonMsgType), _plainBody(plainBody), _content(content) - { } - explicit RoomMessageEvent(const QString& plainBody, - MsgType msgType = MsgType::Text, - EventContent::TypedBase* content = nullptr); - explicit RoomMessageEvent(const QJsonObject& obj); - - MsgType msgtype() const; - QString rawMsgtype() const { return _msgtype; } - const QString& plainBody() const { return _plainBody; } - EventContent::TypedBase* content() const - { return _content.data(); } - QMimeType mimeType() const; - bool hasTextContent() const; - bool hasFileContent() const; - bool hasThumbnail() const; - - QJsonObject toJson() const; - - static constexpr const char* TypeId = "m.room.message"; - - private: - QString _msgtype; - QString _plainBody; - QScopedPointer _content; - - REGISTER_ENUM(MsgType) - }; - using MessageEventType = RoomMessageEvent::MsgType; - - namespace EventContent - { - // Additional event content types - - /** - * Rich text content for m.text, m.emote, m.notice - * - * Available fields: mimeType, body. The body can be either rich text - * or plain text, depending on what mimeType specifies. - */ - class TextContent: public TypedBase - { - public: - TextContent(const QString& text, const QString& contentType); - explicit TextContent(const QJsonObject& json); - - QMimeType type() const override { return mimeType; } - - QMimeType mimeType; - QString body; - - protected: - void fillJson(QJsonObject* json) const override; - }; - - /** - * Content class for m.location - * - * Available fields: - * - corresponding to the top-level JSON: - * - geoUri ("geo_uri" in JSON) - * - corresponding to the "info" subobject: - * - thumbnail.url ("thumbnail_url" in JSON) - * - corresponding to the "info/thumbnail_info" subobject: - * - thumbnail.payloadSize - * - thumbnail.mimeType - * - thumbnail.imageSize - */ - class LocationContent: public TypedBase - { - public: - LocationContent(const QString& geoUri, - const ImageInfo& thumbnail); - explicit LocationContent(const QJsonObject& json); - - QMimeType type() const override; - - public: - QString geoUri; - Thumbnail thumbnail; - - protected: - void fillJson(QJsonObject* o) const override; - }; - - /** - * A base class for info types that include duration: audio and video - */ - template - class PlayableContent : public ContentT - { - public: - PlayableContent(const QJsonObject& json) - : ContentT(json) - , duration(ContentT::originalInfoJson["duration"].toInt()) - { } - - protected: - void fillJson(QJsonObject* json) const override - { - ContentT::fillJson(json); - auto infoJson = json->take("info").toObject(); - infoJson.insert("duration", duration); - json->insert("info", infoJson); - } - - public: - int duration; - }; - - /** - * Content class for m.video - * - * Available fields: - * - corresponding to the top-level JSON: - * - url - * - filename (extension to the CS API spec) - * - corresponding to the "info" subobject: - * - payloadSize ("size" in JSON) - * - mimeType ("mimetype" in JSON) - * - duration - * - imageSize (QSize for a combination of "h" and "w" in JSON) - * - thumbnail.url ("thumbnail_url" in JSON) - * - corresponding to the "info/thumbnail_info" subobject: contents of - * thumbnail field, in the same vein as for "info": - * - payloadSize - * - mimeType - * - imageSize - */ - using VideoContent = PlayableContent>; - - /** - * Content class for m.audio - * - * Available fields: - * - corresponding to the top-level JSON: - * - url - * - filename (extension to the CS API spec) - * - corresponding to the "info" subobject: - * - payloadSize ("size" in JSON) - * - mimeType ("mimetype" in JSON) - * - duration - */ - using AudioContent = PlayableContent>; - } // namespace EventContent -} // namespace QMatrixClient diff --git a/events/simplestateevents.h b/events/simplestateevents.h deleted file mode 100644 index 6b0cd51a..00000000 --- a/events/simplestateevents.h +++ /dev/null @@ -1,53 +0,0 @@ -/****************************************************************************** - * Copyright (C) 2017 Kitsune Ral - * - * 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 - */ - -#pragma once - -#include "event.h" -#include "eventcontent.h" - -namespace QMatrixClient -{ -#define DEFINE_SIMPLE_STATE_EVENT(_Name, _TypeId, _EnumType, _ContentType, _ContentKey) \ - class _Name \ - : public StateEvent> \ - { \ - public: \ - static constexpr const char* TypeId = _TypeId; \ - explicit _Name(const QJsonObject& obj) \ - : StateEvent(_EnumType, obj, QStringLiteral(#_ContentKey)) \ - { } \ - template \ - explicit _Name(T&& value) \ - : StateEvent(_EnumType, QStringLiteral(#_ContentKey), \ - std::forward(value)) \ - { } \ - const _ContentType& _ContentKey() const { return content().value; } \ - }; - - DEFINE_SIMPLE_STATE_EVENT(RoomNameEvent, "m.room.name", - Event::Type::RoomName, QString, name) - DEFINE_SIMPLE_STATE_EVENT(RoomAliasesEvent, "m.room.aliases", - Event::Type::RoomAliases, QStringList, aliases) - DEFINE_SIMPLE_STATE_EVENT(RoomCanonicalAliasEvent, "m.room.canonical_alias", - Event::Type::RoomCanonicalAlias, QString, alias) - DEFINE_SIMPLE_STATE_EVENT(RoomTopicEvent, "m.room.topic", - Event::Type::RoomTopic, QString, topic) - DEFINE_SIMPLE_STATE_EVENT(EncryptionEvent, "m.room.encryption", - Event::Type::RoomEncryption, QString, algorithm) -} // namespace QMatrixClient diff --git a/events/typingevent.cpp b/events/typingevent.cpp deleted file mode 100644 index a4d3bae4..00000000 --- a/events/typingevent.cpp +++ /dev/null @@ -1,32 +0,0 @@ -/****************************************************************************** - * Copyright (C) 2015 Felix Rohrbach - * - * 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 - */ - -#include "typingevent.h" - -using namespace QMatrixClient; - -TypingEvent::TypingEvent(const QJsonObject& obj) - : Event(Type::Typing, obj) -{ - QJsonValue result; - result= contentJson()["user_ids"]; - QJsonArray array = result.toArray(); - for( const QJsonValue& user: array ) - _users.push_back(user.toString()); -} - diff --git a/events/typingevent.h b/events/typingevent.h deleted file mode 100644 index 8c9551a4..00000000 --- a/events/typingevent.h +++ /dev/null @@ -1,39 +0,0 @@ -/****************************************************************************** - * Copyright (C) 2015 Felix Rohrbach - * - * 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 - */ - -#pragma once - -#include "event.h" - -#include - -namespace QMatrixClient -{ - class TypingEvent: public Event - { - public: - static constexpr const char* const TypeId = "m.typing"; - - TypingEvent(const QJsonObject& obj); - - QStringList users() const { return _users; } - - private: - QStringList _users; - }; -} // namespace QMatrixClient diff --git a/jobs/basejob.cpp b/jobs/basejob.cpp deleted file mode 100644 index 3cde7c50..00000000 --- a/jobs/basejob.cpp +++ /dev/null @@ -1,508 +0,0 @@ -/****************************************************************************** - * Copyright (C) 2015 Felix Rohrbach - * - * 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 - */ - -#include "basejob.h" - -#include "connectiondata.h" - -#include -#include -#include -#include -#include -//#include - -#include - -using namespace QMatrixClient; - -struct NetworkReplyDeleter : public QScopedPointerDeleteLater -{ - static inline void cleanup(QNetworkReply* reply) - { - if (reply && reply->isRunning()) - reply->abort(); - QScopedPointerDeleteLater::cleanup(reply); - } -}; - -class BaseJob::Private -{ - public: - // Using an idiom from clang-tidy: - // http://clang.llvm.org/extra/clang-tidy/checks/modernize-pass-by-value.html - Private(HttpVerb v, QString endpoint, QUrlQuery q, Data&& data, bool nt) - : verb(v), apiEndpoint(std::move(endpoint)) - , requestQuery(std::move(q)), requestData(std::move(data)) - , needsToken(nt) - { } - - void sendRequest(); - const JobTimeoutConfig& getCurrentTimeoutConfig() const; - - const ConnectionData* connection = nullptr; - - // Contents for the network request - HttpVerb verb; - QString apiEndpoint; - QHash requestHeaders; - QUrlQuery requestQuery; - Data requestData; - bool needsToken; - - // There's no use of QMimeType here because we don't want to match - // content types against the known MIME type hierarchy; and at the same - // type QMimeType is of little help with MIME type globs (`text/*` etc.) - QByteArrayList expectedContentTypes; - - QScopedPointer reply; - Status status = Pending; - - QTimer timer; - QTimer retryTimer; - - QVector errorStrategy = - { { 90, 5 }, { 90, 10 }, { 120, 30 } }; - int maxRetries = errorStrategy.size(); - int retriesTaken = 0; - - LoggingCategory logCat = JOBS; -}; - -BaseJob::BaseJob(HttpVerb verb, const QString& name, const QString& endpoint, bool needsToken) - : BaseJob(verb, name, endpoint, Query { }, Data { }, needsToken) -{ } - -BaseJob::BaseJob(HttpVerb verb, const QString& name, const QString& endpoint, - const Query& query, Data&& data, bool needsToken) - : d(new Private(verb, endpoint, query, std::move(data), needsToken)) -{ - setObjectName(name); - setExpectedContentTypes({ "application/json" }); - d->timer.setSingleShot(true); - connect (&d->timer, &QTimer::timeout, this, &BaseJob::timeout); - d->retryTimer.setSingleShot(true); - connect (&d->retryTimer, &QTimer::timeout, this, &BaseJob::sendRequest); -} - -BaseJob::~BaseJob() -{ - stop(); - qCDebug(d->logCat) << this << "destroyed"; -} - -const QString& BaseJob::apiEndpoint() const -{ - return d->apiEndpoint; -} - -void BaseJob::setApiEndpoint(const QString& apiEndpoint) -{ - d->apiEndpoint = apiEndpoint; -} - -const BaseJob::headers_t&BaseJob::requestHeaders() const -{ - return d->requestHeaders; -} - -void BaseJob::setRequestHeader(const headers_t::key_type& headerName, - const headers_t::mapped_type& headerValue) -{ - d->requestHeaders[headerName] = headerValue; -} - -void BaseJob::setRequestHeaders(const BaseJob::headers_t& headers) -{ - d->requestHeaders = headers; -} - -const QUrlQuery& BaseJob::query() const -{ - return d->requestQuery; -} - -void BaseJob::setRequestQuery(const QUrlQuery& query) -{ - d->requestQuery = query; -} - -const BaseJob::Data& BaseJob::requestData() const -{ - return d->requestData; -} - -void BaseJob::setRequestData(Data&& data) -{ - std::swap(d->requestData, data); -} - -const QByteArrayList& BaseJob::expectedContentTypes() const -{ - return d->expectedContentTypes; -} - -void BaseJob::addExpectedContentType(const QByteArray& contentType) -{ - d->expectedContentTypes << contentType; -} - -void BaseJob::setExpectedContentTypes(const QByteArrayList& contentTypes) -{ - d->expectedContentTypes = contentTypes; -} - -QUrl BaseJob::makeRequestUrl(QUrl baseUrl, - const QString& path, const QUrlQuery& query) -{ - auto pathBase = baseUrl.path(); - if (!pathBase.endsWith('/') && !path.startsWith('/')) - pathBase.push_back('/'); - - baseUrl.setPath( pathBase + path ); - baseUrl.setQuery(query); - return baseUrl; -} - -void BaseJob::Private::sendRequest() -{ - QNetworkRequest req - { makeRequestUrl(connection->baseUrl(), apiEndpoint, requestQuery) }; - if (!requestHeaders.contains("Content-Type")) - req.setHeader(QNetworkRequest::ContentTypeHeader, "application/json"); - req.setRawHeader(QByteArray("Authorization"), - QByteArray("Bearer ") + connection->accessToken()); -#if (QT_VERSION >= QT_VERSION_CHECK(5, 6, 0)) - req.setAttribute(QNetworkRequest::FollowRedirectsAttribute, true); - req.setMaximumRedirectsAllowed(10); -#endif - for (auto it = requestHeaders.cbegin(); it != requestHeaders.cend(); ++it) - req.setRawHeader(it.key(), it.value()); - switch( verb ) - { - case HttpVerb::Get: - reply.reset( connection->nam()->get(req) ); - break; - case HttpVerb::Post: - reply.reset( connection->nam()->post(req, requestData.source()) ); - break; - case HttpVerb::Put: - reply.reset( connection->nam()->put(req, requestData.source()) ); - break; - case HttpVerb::Delete: - reply.reset( connection->nam()->deleteResource(req) ); - break; - } -} - -void BaseJob::beforeStart(const ConnectionData*) -{ } - -void BaseJob::afterStart(const ConnectionData*, QNetworkReply*) -{ } - -void BaseJob::beforeAbandon(QNetworkReply*) -{ } - -void BaseJob::start(const ConnectionData* connData) -{ - d->connection = connData; - beforeStart(connData); - if (status().good()) - sendRequest(); - if (status().good()) - afterStart(connData, d->reply.data()); - if (!status().good()) - QTimer::singleShot(0, this, &BaseJob::finishJob); -} - -void BaseJob::sendRequest() -{ - emit aboutToStart(); - d->retryTimer.stop(); // In case we were counting down at the moment - qCDebug(d->logCat) << this << "sending request to" << d->apiEndpoint; - if (!d->requestQuery.isEmpty()) - qCDebug(d->logCat) << " query:" << d->requestQuery.toString(); - d->sendRequest(); - connect( d->reply.data(), &QNetworkReply::finished, this, &BaseJob::gotReply ); - if (d->reply->isRunning()) - { - connect( d->reply.data(), &QNetworkReply::uploadProgress, - this, &BaseJob::uploadProgress); - connect( d->reply.data(), &QNetworkReply::downloadProgress, - this, &BaseJob::downloadProgress); - d->timer.start(getCurrentTimeout()); - qCDebug(d->logCat) << this << "request has been sent"; - emit started(); - } - else - qCWarning(d->logCat) << this << "request could not start"; -} - -void BaseJob::gotReply() -{ - setStatus(checkReply(d->reply.data())); - if (status().good()) - setStatus(parseReply(d->reply.data())); - else - { - const auto body = d->reply->readAll(); - if (!body.isEmpty()) - { - qCDebug(d->logCat).noquote() << "Error body:" << body; - auto json = QJsonDocument::fromJson(body).object(); - if (json.isEmpty()) - setStatus(IncorrectRequestError, body); - else { - if (error() == TooManyRequestsError || - json.value("errcode").toString() == "M_LIMIT_EXCEEDED") - { - QString msg = tr("Too many requests"); - auto retryInterval = json.value("retry_after_ms").toInt(-1); - if (retryInterval != -1) - msg += tr(", next retry advised after %1 ms") - .arg(retryInterval); - else // We still have to figure some reasonable interval - retryInterval = getNextRetryInterval(); - - setStatus(TooManyRequestsError, msg); - - // Shortcut to retry instead of executing finishJob() - stop(); - qCWarning(d->logCat) - << this << "will retry in" << retryInterval; - d->retryTimer.start(retryInterval); - emit retryScheduled(d->retriesTaken, retryInterval); - return; - } - setStatus(IncorrectRequestError, json.value("error").toString()); - } - } - } - - finishJob(); -} - -bool checkContentType(const QByteArray& type, const QByteArrayList& patterns) -{ - if (patterns.isEmpty()) - return true; - - // ignore possible appendixes of the content type - const auto ctype = type.split(';').front(); - - for (const auto& pattern: patterns) - { - if (pattern.startsWith('*') || ctype == pattern) // Fast lane - return true; - - auto patternParts = pattern.split('/'); - Q_ASSERT_X(patternParts.size() <= 2, __FUNCTION__, - "BaseJob: Expected content type should have up to two" - " /-separated parts; violating pattern: " + pattern); - - if (ctype.split('/').front() == patternParts.front() && - patternParts.back() == "*") - return true; // Exact match already went on fast lane - } - - return false; -} - -BaseJob::Status BaseJob::checkReply(QNetworkReply* reply) const -{ - const auto httpCode = - reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt(); - qCDebug(d->logCat).nospace().noquote() << this << " returned HTTP code " - << httpCode << ": " - << (reply->error() == QNetworkReply::NoError ? - "Success" : reply->errorString()) - << " (URL: " << reply->url().toDisplayString() << ")"; - - if (httpCode == 429) // Qt doesn't know about it yet - return { TooManyRequestsError, tr("Too many requests") }; - - // Should we check httpCode instead? Maybe even use it in BaseJob::Status? - // That would make codes in logs slightly more readable. - switch( reply->error() ) - { - case QNetworkReply::NoError: - if (checkContentType(reply->rawHeader("Content-Type"), - d->expectedContentTypes)) - return NoError; - else // A warning in the logs might be more proper instead - return { IncorrectResponseError, - "Incorrect content type of the response" }; - - case QNetworkReply::AuthenticationRequiredError: - case QNetworkReply::ContentAccessDenied: - case QNetworkReply::ContentOperationNotPermittedError: - return { ContentAccessError, reply->errorString() }; - - case QNetworkReply::ProtocolInvalidOperationError: - case QNetworkReply::UnknownContentError: - return { IncorrectRequestError, reply->errorString() }; - - case QNetworkReply::ContentNotFoundError: - return { NotFoundError, reply->errorString() }; - - default: - return { NetworkError, reply->errorString() }; - } -} - -BaseJob::Status BaseJob::parseReply(QNetworkReply* reply) -{ - QJsonParseError error; - QJsonDocument json = QJsonDocument::fromJson(reply->readAll(), &error); - if( error.error == QJsonParseError::NoError ) - return parseJson(json); - else - return { JsonParseError, error.errorString() }; -} - -BaseJob::Status BaseJob::parseJson(const QJsonDocument&) -{ - return Success; -} - -void BaseJob::stop() -{ - d->timer.stop(); - if (d->reply) - { - d->reply->disconnect(this); // Ignore whatever comes from the reply - if (d->reply->isRunning()) - { - qCWarning(d->logCat) << this << "stopped without ready network reply"; - d->reply->abort(); - } - } - else - qCWarning(d->logCat) << this << "stopped with empty network reply"; -} - -void BaseJob::finishJob() -{ - stop(); - if ((error() == NetworkError || error() == TimeoutError) - && d->retriesTaken < d->maxRetries) - { - // TODO: The whole retrying thing should be put to ConnectionManager - // otherwise independently retrying jobs make a bit of notification - // storm towards the UI. - const auto retryInterval = - error() == TimeoutError ? 0 : getNextRetryInterval(); - ++d->retriesTaken; - qCWarning(d->logCat) << this << "will retry" << d->retriesTaken - << "in" << retryInterval/1000 << "s"; - d->retryTimer.start(retryInterval); - emit retryScheduled(d->retriesTaken, retryInterval); - return; - } - - // Notify those interested in any completion of the job (including killing) - emit finished(this); - - emit result(this); - if (error()) - emit failure(this); - else - emit success(this); - - deleteLater(); -} - -const JobTimeoutConfig& BaseJob::Private::getCurrentTimeoutConfig() const -{ - return errorStrategy[std::min(retriesTaken, errorStrategy.size() - 1)]; -} - -BaseJob::duration_t BaseJob::getCurrentTimeout() const -{ - return d->getCurrentTimeoutConfig().jobTimeout * 1000; -} - -BaseJob::duration_t BaseJob::getNextRetryInterval() const -{ - return d->getCurrentTimeoutConfig().nextRetryInterval * 1000; -} - -BaseJob::duration_t BaseJob::millisToRetry() const -{ - return d->retryTimer.isActive() ? d->retryTimer.remainingTime() : 0; -} - -int BaseJob::maxRetries() const -{ - return d->maxRetries; -} - -void BaseJob::setMaxRetries(int newMaxRetries) -{ - d->maxRetries = newMaxRetries; -} - -BaseJob::Status BaseJob::status() const -{ - return d->status; -} - -int BaseJob::error() const -{ - return d->status.code; -} - -QString BaseJob::errorString() const -{ - return d->status.message; -} - -void BaseJob::setStatus(Status s) -{ - d->status = s; - if (!s.good()) - qCWarning(d->logCat) << this << "status" << s; -} - -void BaseJob::setStatus(int code, QString message) -{ - message.replace(d->connection->accessToken(), "(REDACTED)"); - setStatus({ code, message }); -} - -void BaseJob::abandon() -{ - beforeAbandon(d->reply.data()); - setStatus(Abandoned); - this->disconnect(); - if (d->reply) - d->reply->disconnect(this); - deleteLater(); -} - -void BaseJob::timeout() -{ - setStatus( TimeoutError, "The job has timed out" ); - finishJob(); -} - -void BaseJob::setLoggingCategory(LoggingCategory lcf) -{ - d->logCat = lcf; -} diff --git a/jobs/basejob.h b/jobs/basejob.h deleted file mode 100644 index ed630a67..00000000 --- a/jobs/basejob.h +++ /dev/null @@ -1,303 +0,0 @@ -/****************************************************************************** - * Copyright (C) 2015 Felix Rohrbach - * - * 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 - */ - -#pragma once - -#include "../logging.h" -#include "requestdata.h" - -#include -#include - -// Any job that parses the response will need the below two. -#include -#include - -class QNetworkReply; -class QSslError; - -namespace QMatrixClient -{ - class ConnectionData; - - enum class HttpVerb { Get, Put, Post, Delete }; - - struct JobTimeoutConfig - { - int jobTimeout; - int nextRetryInterval; - }; - - class BaseJob: public QObject - { - Q_OBJECT - Q_PROPERTY(int maxRetries READ maxRetries WRITE setMaxRetries) - public: - /* Just in case, the values are compatible with KJob - * (which BaseJob used to inherit from). */ - enum StatusCode { NoError = 0 // To be compatible with Qt conventions - , Success = 0 - , Pending = 1 - , Abandoned = 50 //< A very brief period between abandoning and object deletion - , ErrorLevel = 100 //< Errors have codes starting from this - , NetworkError = 100 - , JsonParseError - , TimeoutError - , ContentAccessError - , NotFoundError - , IncorrectRequestError - , IncorrectResponseError - , TooManyRequestsError - , UserDefinedError = 200 - }; - - /** - * A simple wrapper around QUrlQuery that allows its creation from - * a list of string pairs - */ - class Query : public QUrlQuery - { - public: - using QUrlQuery::QUrlQuery; - Query() = default; - Query(const std::initializer_list< QPair >& l) - { - setQueryItems(l); - } - }; - - using Data = RequestData; - - /** - * This structure stores the status of a server call job. The status consists - * of a code, that is described (but not delimited) by the respective enum, - * and a freeform message. - * - * To extend the list of error codes, define an (anonymous) enum - * along the lines of StatusCode, with additional values - * starting at UserDefinedError - */ - class Status - { - public: - Status(StatusCode c) : code(c) { } - Status(int c, QString m) : code(c), message(std::move(m)) { } - - bool good() const { return code < ErrorLevel; } - friend QDebug operator<<(QDebug dbg, const Status& s) - { - QDebug(dbg).noquote().nospace() - << s.code << ": " << s.message; - return dbg; - } - - int code; - QString message; - }; - - using duration_t = int; // milliseconds - - public: - BaseJob(HttpVerb verb, const QString& name, const QString& endpoint, - bool needsToken = true); - BaseJob(HttpVerb verb, const QString& name, const QString& endpoint, - const Query& query, Data&& data = {}, - bool needsToken = true); - - Status status() const; - int error() const; - virtual QString errorString() const; - - int maxRetries() const; - void setMaxRetries(int newMaxRetries); - - Q_INVOKABLE duration_t getCurrentTimeout() const; - Q_INVOKABLE duration_t getNextRetryInterval() const; - Q_INVOKABLE duration_t millisToRetry() const; - - friend QDebug operator<<(QDebug dbg, const BaseJob* j) - { - return dbg << j->objectName(); - } - - public slots: - void start(const ConnectionData* connData); - - /** - * Abandons the result of this job, arrived or unarrived. - * - * This aborts waiting for a reply from the server (if there was - * any pending) and deletes the job object. It is always done quietly - * (as opposed to KJob::kill() that can trigger emitting the result). - */ - void abandon(); - - signals: - /** The job is about to send a network request */ - void aboutToStart(); - - /** The job has sent a network request */ - void started(); - - /** - * The previous network request has failed; the next attempt will - * be done in the specified time - * @param nextAttempt the 1-based number of attempt (will always be more than 1) - * @param inMilliseconds the interval after which the next attempt will be taken - */ - void retryScheduled(int nextAttempt, int inMilliseconds); - - /** - * Emitted when the job is finished, in any case. It is used to notify - * observers that the job is terminated and that progress can be hidden. - * - * This should not be emitted directly by subclasses; - * use finishJob() instead. - * - * In general, to be notified of a job's completion, client code - * should connect to success() and failure() - * rather than finished(), so that kill() is indeed quiet. - * However if you store a list of jobs and they might get killed - * silently, then you must connect to this instead of result(), - * to avoid dangling pointers in your list. - * - * @param job the job that emitted this signal - * - * @see success, failure - */ - void finished(BaseJob* job); - - /** - * Emitted when the job is finished (except when killed). - * - * Use error() to know if the job was finished with error. - * - * @param job the job that emitted this signal - * - * @see success, failure - */ - void result(BaseJob* job); - - /** - * Emitted together with result() but only if there's no error. - */ - void success(BaseJob*); - - /** - * Emitted together with result() if there's an error. - * Same as result(), this won't be emitted in case of kill(). - */ - void failure(BaseJob*); - - void downloadProgress(qint64 bytesReceived, qint64 bytesTotal); - void uploadProgress(qint64 bytesSent, qint64 bytesTotal); - - protected: - using headers_t = QHash; - - const QString& apiEndpoint() const; - void setApiEndpoint(const QString& apiEndpoint); - const headers_t& requestHeaders() const; - void setRequestHeader(const headers_t::key_type& headerName, - const headers_t::mapped_type& headerValue); - void setRequestHeaders(const headers_t& headers); - const QUrlQuery& query() const; - void setRequestQuery(const QUrlQuery& query); - const Data& requestData() const; - void setRequestData(Data&& data); - const QByteArrayList& expectedContentTypes() const; - void addExpectedContentType(const QByteArray& contentType); - void setExpectedContentTypes(const QByteArrayList& contentTypes); - - /** Construct a URL out of baseUrl, path and query - * The function automatically adds '/' between baseUrl's path and - * \p path if necessary. The query component of \p baseUrl - * is ignored. - */ - static QUrl makeRequestUrl(QUrl baseUrl, const QString& path, - const QUrlQuery& query = {}); - - virtual void beforeStart(const ConnectionData* connData); - virtual void afterStart(const ConnectionData* connData, - QNetworkReply* reply); - virtual void beforeAbandon(QNetworkReply*); - - /** - * Used by gotReply() to check the received reply for general - * issues such as network errors or access denial. - * Returning anything except NoError/Success prevents - * further parseReply()/parseJson() invocation. - * - * @param reply the reply received from the server - * @return the result of checking the reply - * - * @see gotReply - */ - virtual Status checkReply(QNetworkReply* reply) const; - - /** - * Processes the reply. By default, parses the reply into - * a QJsonDocument and calls parseJson() if it's a valid JSON. - * - * @param reply raw contents of a HTTP reply from the server (without headers) - * - * @see gotReply, parseJson - */ - virtual Status parseReply(QNetworkReply* reply); - - /** - * Processes the JSON document received from the Matrix server. - * By default returns succesful status without analysing the JSON. - * - * @param json valid JSON document received from the server - * - * @see parseReply - */ - virtual Status parseJson(const QJsonDocument&); - - void setStatus(Status s); - void setStatus(int code, QString message); - - // Q_DECLARE_LOGGING_CATEGORY return different function types - // in different versions - using LoggingCategory = decltype(JOBS)*; - void setLoggingCategory(LoggingCategory lcf); - - // Job objects should only be deleted via QObject::deleteLater - ~BaseJob() override; - - protected slots: - void timeout(); - - private slots: - void sendRequest(); - void gotReply(); - - private: - void stop(); - void finishJob(); - - class Private; - QScopedPointer d; - }; - - inline bool isJobRunning(BaseJob* job) - { - return job && job->error() == BaseJob::Pending; - } -} // namespace QMatrixClient diff --git a/jobs/checkauthmethods.cpp b/jobs/checkauthmethods.cpp deleted file mode 100644 index 117def89..00000000 --- a/jobs/checkauthmethods.cpp +++ /dev/null @@ -1,53 +0,0 @@ -/****************************************************************************** - * Copyright (C) 2015 Felix Rohrbach - * - * 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 - */ - -#include "checkauthmethods.h" - -#include -#include - -using namespace QMatrixClient; - -class CheckAuthMethods::Private -{ - public: - QString session; -}; - -CheckAuthMethods::CheckAuthMethods() - : BaseJob(HttpVerb::Get, "CheckAuthMethods", - QStringLiteral("_matrix/client/r0/login"), Query(), Data(), false) - , d(new Private) -{ -} - -CheckAuthMethods::~CheckAuthMethods() -{ - delete d; -} - -QString CheckAuthMethods::session() -{ - return d->session; -} - -BaseJob::Status CheckAuthMethods::parseJson(const QJsonDocument& data) -{ - // TODO - return { BaseJob::StatusCode::UserDefinedError, "Not implemented" }; -} diff --git a/jobs/checkauthmethods.h b/jobs/checkauthmethods.h deleted file mode 100644 index 647f3db6..00000000 --- a/jobs/checkauthmethods.h +++ /dev/null @@ -1,40 +0,0 @@ -/****************************************************************************** - * Copyright (C) 2015 Felix Rohrbach - * - * 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 - */ - -#pragma once - -#include "basejob.h" - -namespace QMatrixClient -{ - class CheckAuthMethods : public BaseJob - { - public: - CheckAuthMethods(); - virtual ~CheckAuthMethods(); - - QString session(); - - protected: - Status parseJson(const QJsonDocument& data) override; - - private: - class Private; - Private* d; - }; -} diff --git a/jobs/downloadfilejob.cpp b/jobs/downloadfilejob.cpp deleted file mode 100644 index 6a3d8483..00000000 --- a/jobs/downloadfilejob.cpp +++ /dev/null @@ -1,120 +0,0 @@ -#include "downloadfilejob.h" - -#include -#include -#include - -using namespace QMatrixClient; - -class DownloadFileJob::Private -{ - public: - Private() : tempFile(new QTemporaryFile()) { } - - explicit Private(const QString& localFilename) - : targetFile(new QFile(localFilename)) - , tempFile(new QFile(targetFile->fileName() + ".qmcdownload")) - { } - - QScopedPointer targetFile; - QScopedPointer tempFile; -}; - -QUrl DownloadFileJob::makeRequestUrl(QUrl baseUrl, const QUrl& mxcUri) -{ - return makeRequestUrl(baseUrl, mxcUri.authority(), mxcUri.path().mid(1)); -} - -DownloadFileJob::DownloadFileJob(const QString& serverName, - const QString& mediaId, - const QString& localFilename) - : GetContentJob(serverName, mediaId) - , d(localFilename.isEmpty() ? new Private : new Private(localFilename)) -{ - setObjectName("DownloadFileJob"); -} - -QString DownloadFileJob::targetFileName() const -{ - return (d->targetFile ? d->targetFile : d->tempFile)->fileName(); -} - -void DownloadFileJob::beforeStart(const ConnectionData*) -{ - if (d->targetFile && !d->targetFile->isReadable() && - !d->targetFile->open(QIODevice::WriteOnly)) - { - qCWarning(JOBS) << "Couldn't open the file" - << d->targetFile->fileName() << "for writing"; - setStatus(FileError, "Could not open the target file for writing"); - return; - } - if (!d->tempFile->isReadable() && !d->tempFile->open(QIODevice::WriteOnly)) - { - qCWarning(JOBS) << "Couldn't open the temporary file" - << d->tempFile->fileName() << "for writing"; - setStatus(FileError, "Could not open the temporary download file"); - return; - } - qCDebug(JOBS) << "Downloading to" << d->tempFile->fileName(); -} - -void DownloadFileJob::afterStart(const ConnectionData*, QNetworkReply* reply) -{ - connect(reply, &QNetworkReply::metaDataChanged, this, [this,reply] { - auto sizeHeader = reply->header(QNetworkRequest::ContentLengthHeader); - if (sizeHeader.isValid()) - { - auto targetSize = sizeHeader.value(); - if (targetSize != -1) - if (!d->tempFile->resize(targetSize)) - { - qCWarning(JOBS) << "Failed to allocate" << targetSize - << "bytes for" << d->tempFile->fileName(); - setStatus(FileError, - "Could not reserve disk space for download"); - } - } - }); - connect(reply, &QIODevice::readyRead, this, [this,reply] { - auto bytes = reply->read(reply->bytesAvailable()); - if (bytes.isEmpty()) - { - qCWarning(JOBS) - << "Unexpected empty chunk when downloading from" - << reply->url() << "to" << d->tempFile->fileName(); - } else { - d->tempFile->write(bytes); - } - }); -} - -void DownloadFileJob::beforeAbandon(QNetworkReply*) -{ - if (d->targetFile) - d->targetFile->remove(); - d->tempFile->remove(); -} - -BaseJob::Status DownloadFileJob::parseReply(QNetworkReply*) -{ - if (d->targetFile) - { - d->targetFile->close(); - if (!d->targetFile->remove()) - { - qCWarning(JOBS) << "Failed to remove the target file placeholder"; - return { FileError, "Couldn't finalise the download" }; - } - if (!d->tempFile->rename(d->targetFile->fileName())) - { - qCWarning(JOBS) << "Failed to rename" << d->tempFile->fileName() - << "to" << d->targetFile->fileName(); - return { FileError, "Couldn't finalise the download" }; - } - } - else - d->tempFile->close(); - qCDebug(JOBS) << "Saved a file as" << targetFileName(); - return Success; -} diff --git a/jobs/downloadfilejob.h b/jobs/downloadfilejob.h deleted file mode 100644 index 1815a7c8..00000000 --- a/jobs/downloadfilejob.h +++ /dev/null @@ -1,30 +0,0 @@ -#pragma once - -#include "generated/content-repo.h" - -namespace QMatrixClient -{ - class DownloadFileJob : public GetContentJob - { - public: - enum { FileError = BaseJob::UserDefinedError + 1 }; - - using GetContentJob::makeRequestUrl; - static QUrl makeRequestUrl(QUrl baseUrl, const QUrl& mxcUri); - - DownloadFileJob(const QString& serverName, const QString& mediaId, - const QString& localFilename = {}); - - QString targetFileName() const; - - private: - class Private; - QScopedPointer d; - - void beforeStart(const ConnectionData*) override; - void afterStart(const ConnectionData*, - QNetworkReply* reply) override; - void beforeAbandon(QNetworkReply*) override; - Status parseReply(QNetworkReply*) override; - }; -} diff --git a/jobs/generated/account-data.cpp b/jobs/generated/account-data.cpp deleted file mode 100644 index 35ee94c0..00000000 --- a/jobs/generated/account-data.cpp +++ /dev/null @@ -1,28 +0,0 @@ -/****************************************************************************** - * THIS FILE IS GENERATED - ANY EDITS WILL BE OVERWRITTEN - */ - -#include "account-data.h" - -#include "converters.h" - -#include - -using namespace QMatrixClient; - -static const auto basePath = QStringLiteral("/_matrix/client/r0"); - -SetAccountDataJob::SetAccountDataJob(const QString& userId, const QString& type, const QJsonObject& content) - : BaseJob(HttpVerb::Put, "SetAccountDataJob", - basePath % "/user/" % userId % "/account_data/" % type) -{ - setRequestData(Data(content)); -} - -SetAccountDataPerRoomJob::SetAccountDataPerRoomJob(const QString& userId, const QString& roomId, const QString& type, const QJsonObject& content) - : BaseJob(HttpVerb::Put, "SetAccountDataPerRoomJob", - basePath % "/user/" % userId % "/rooms/" % roomId % "/account_data/" % type) -{ - setRequestData(Data(content)); -} - diff --git a/jobs/generated/account-data.h b/jobs/generated/account-data.h deleted file mode 100644 index 69ad9fb4..00000000 --- a/jobs/generated/account-data.h +++ /dev/null @@ -1,27 +0,0 @@ -/****************************************************************************** - * THIS FILE IS GENERATED - ANY EDITS WILL BE OVERWRITTEN - */ - -#pragma once - -#include "../basejob.h" - -#include - - -namespace QMatrixClient -{ - // Operations - - class SetAccountDataJob : public BaseJob - { - public: - explicit SetAccountDataJob(const QString& userId, const QString& type, const QJsonObject& content = {}); - }; - - class SetAccountDataPerRoomJob : public BaseJob - { - public: - explicit SetAccountDataPerRoomJob(const QString& userId, const QString& roomId, const QString& type, const QJsonObject& content = {}); - }; -} // namespace QMatrixClient diff --git a/jobs/generated/administrative_contact.cpp b/jobs/generated/administrative_contact.cpp deleted file mode 100644 index 1af57941..00000000 --- a/jobs/generated/administrative_contact.cpp +++ /dev/null @@ -1,122 +0,0 @@ -/****************************************************************************** - * THIS FILE IS GENERATED - ANY EDITS WILL BE OVERWRITTEN - */ - -#include "administrative_contact.h" - -#include - -using namespace QMatrixClient; - -static const auto basePath = QStringLiteral("/_matrix/client/r0"); - -GetAccount3PIDsJob::ThirdPartyIdentifier::operator QJsonObject() const -{ - QJsonObject o; - o.insert("medium", toJson(medium)); - o.insert("address", toJson(address)); - - return o; -} -namespace QMatrixClient -{ - template <> struct FromJson - { - GetAccount3PIDsJob::ThirdPartyIdentifier operator()(QJsonValue jv) - { - QJsonObject o = jv.toObject(); - GetAccount3PIDsJob::ThirdPartyIdentifier result; - result.medium = - fromJson(o.value("medium")); - result.address = - fromJson(o.value("address")); - - return result; - } - }; -} // namespace QMatrixClient - -class GetAccount3PIDsJob::Private -{ - public: - QVector threepids; -}; - -QUrl GetAccount3PIDsJob::makeRequestUrl(QUrl baseUrl) -{ - return BaseJob::makeRequestUrl(baseUrl, - basePath % "/account/3pid"); -} - -GetAccount3PIDsJob::GetAccount3PIDsJob() - : BaseJob(HttpVerb::Get, "GetAccount3PIDsJob", - basePath % "/account/3pid") - , d(new Private) -{ -} - -GetAccount3PIDsJob::~GetAccount3PIDsJob() = default; - -const QVector& GetAccount3PIDsJob::threepids() const -{ - return d->threepids; -} - -BaseJob::Status GetAccount3PIDsJob::parseJson(const QJsonDocument& data) -{ - auto json = data.object(); - d->threepids = fromJson>(json.value("threepids")); - return Success; -} - -Post3PIDsJob::ThreePidCredentials::operator QJsonObject() const -{ - QJsonObject o; - o.insert("client_secret", toJson(clientSecret)); - o.insert("id_server", toJson(idServer)); - o.insert("sid", toJson(sid)); - - return o; -} -namespace QMatrixClient -{ - template <> struct FromJson - { - Post3PIDsJob::ThreePidCredentials operator()(QJsonValue jv) - { - QJsonObject o = jv.toObject(); - Post3PIDsJob::ThreePidCredentials result; - result.clientSecret = - fromJson(o.value("client_secret")); - result.idServer = - fromJson(o.value("id_server")); - result.sid = - fromJson(o.value("sid")); - - return result; - } - }; -} // namespace QMatrixClient - -Post3PIDsJob::Post3PIDsJob(const ThreePidCredentials& threePidCreds, bool bind) - : BaseJob(HttpVerb::Post, "Post3PIDsJob", - basePath % "/account/3pid") -{ - QJsonObject _data; - _data.insert("three_pid_creds", toJson(threePidCreds)); - _data.insert("bind", toJson(bind)); - setRequestData(_data); -} - -QUrl RequestTokenTo3PIDJob::makeRequestUrl(QUrl baseUrl) -{ - return BaseJob::makeRequestUrl(baseUrl, - basePath % "/account/3pid/email/requestToken"); -} - -RequestTokenTo3PIDJob::RequestTokenTo3PIDJob() - : BaseJob(HttpVerb::Post, "RequestTokenTo3PIDJob", - basePath % "/account/3pid/email/requestToken", false) -{ -} - diff --git a/jobs/generated/administrative_contact.h b/jobs/generated/administrative_contact.h deleted file mode 100644 index c8429d39..00000000 --- a/jobs/generated/administrative_contact.h +++ /dev/null @@ -1,83 +0,0 @@ -/****************************************************************************** - * THIS FILE IS GENERATED - ANY EDITS WILL BE OVERWRITTEN - */ - -#pragma once - -#include "../basejob.h" - -#include - -#include "converters.h" - -namespace QMatrixClient -{ - // Operations - - class GetAccount3PIDsJob : public BaseJob - { - public: - // Inner data structures - - struct ThirdPartyIdentifier - { - QString medium; - QString address; - - operator QJsonObject() const; - }; - - // End of inner data structures - - /** Construct a URL out of baseUrl and usual parameters passed to - * GetAccount3PIDsJob. This function can be used when - * a URL for GetAccount3PIDsJob is necessary but the job - * itself isn't. - */ - static QUrl makeRequestUrl(QUrl baseUrl); - - explicit GetAccount3PIDsJob(); - ~GetAccount3PIDsJob() override; - - const QVector& threepids() const; - - protected: - Status parseJson(const QJsonDocument& data) override; - - private: - class Private; - QScopedPointer d; - }; - - class Post3PIDsJob : public BaseJob - { - public: - // Inner data structures - - struct ThreePidCredentials - { - QString clientSecret; - QString idServer; - QString sid; - - operator QJsonObject() const; - }; - - // End of inner data structures - - explicit Post3PIDsJob(const ThreePidCredentials& threePidCreds, bool bind = {}); - }; - - class RequestTokenTo3PIDJob : public BaseJob - { - public: - /** Construct a URL out of baseUrl and usual parameters passed to - * RequestTokenTo3PIDJob. This function can be used when - * a URL for RequestTokenTo3PIDJob is necessary but the job - * itself isn't. - */ - static QUrl makeRequestUrl(QUrl baseUrl); - - explicit RequestTokenTo3PIDJob(); - }; -} // namespace QMatrixClient diff --git a/jobs/generated/banning.cpp b/jobs/generated/banning.cpp deleted file mode 100644 index f66b27b6..00000000 --- a/jobs/generated/banning.cpp +++ /dev/null @@ -1,34 +0,0 @@ -/****************************************************************************** - * THIS FILE IS GENERATED - ANY EDITS WILL BE OVERWRITTEN - */ - -#include "banning.h" - -#include "converters.h" - -#include - -using namespace QMatrixClient; - -static const auto basePath = QStringLiteral("/_matrix/client/r0"); - -BanJob::BanJob(const QString& roomId, const QString& userId, const QString& reason) - : BaseJob(HttpVerb::Post, "BanJob", - basePath % "/rooms/" % roomId % "/ban") -{ - QJsonObject _data; - _data.insert("user_id", toJson(userId)); - if (!reason.isEmpty()) - _data.insert("reason", toJson(reason)); - setRequestData(_data); -} - -UnbanJob::UnbanJob(const QString& roomId, const QString& userId) - : BaseJob(HttpVerb::Post, "UnbanJob", - basePath % "/rooms/" % roomId % "/unban") -{ - QJsonObject _data; - _data.insert("user_id", toJson(userId)); - setRequestData(_data); -} - diff --git a/jobs/generated/banning.h b/jobs/generated/banning.h deleted file mode 100644 index 2d6fbd9b..00000000 --- a/jobs/generated/banning.h +++ /dev/null @@ -1,26 +0,0 @@ -/****************************************************************************** - * THIS FILE IS GENERATED - ANY EDITS WILL BE OVERWRITTEN - */ - -#pragma once - -#include "../basejob.h" - - - -namespace QMatrixClient -{ - // Operations - - class BanJob : public BaseJob - { - public: - explicit BanJob(const QString& roomId, const QString& userId, const QString& reason = {}); - }; - - class UnbanJob : public BaseJob - { - public: - explicit UnbanJob(const QString& roomId, const QString& userId); - }; -} // namespace QMatrixClient diff --git a/jobs/generated/content-repo.cpp b/jobs/generated/content-repo.cpp deleted file mode 100644 index 51011251..00000000 --- a/jobs/generated/content-repo.cpp +++ /dev/null @@ -1,254 +0,0 @@ -/****************************************************************************** - * THIS FILE IS GENERATED - ANY EDITS WILL BE OVERWRITTEN - */ - -#include "content-repo.h" - -#include "converters.h" - -#include -#include - -using namespace QMatrixClient; - -static const auto basePath = QStringLiteral("/_matrix/media/r0"); - -class UploadContentJob::Private -{ - public: - QString contentUri; -}; - -BaseJob::Query queryToUploadContent(const QString& filename) -{ - BaseJob::Query _q; - if (!filename.isEmpty()) - _q.addQueryItem("filename", filename); - return _q; -} - -UploadContentJob::UploadContentJob(QIODevice* content, const QString& filename, const QString& contentType) - : BaseJob(HttpVerb::Post, "UploadContentJob", - basePath % "/upload", - queryToUploadContent(filename)) - , d(new Private) -{ - setRequestHeader("Content-Type", contentType.toLatin1()); - - setRequestData(Data(content)); -} - -UploadContentJob::~UploadContentJob() = default; - -const QString& UploadContentJob::contentUri() const -{ - return d->contentUri; -} - -BaseJob::Status UploadContentJob::parseJson(const QJsonDocument& data) -{ - auto json = data.object(); - if (!json.contains("content_uri")) - return { JsonParseError, - "The key 'content_uri' not found in the response" }; - d->contentUri = fromJson(json.value("content_uri")); - return Success; -} - -class GetContentJob::Private -{ - public: - QString contentType; - QString contentDisposition; - QIODevice* content; -}; - -QUrl GetContentJob::makeRequestUrl(QUrl baseUrl, const QString& serverName, const QString& mediaId) -{ - return BaseJob::makeRequestUrl(baseUrl, - basePath % "/download/" % serverName % "/" % mediaId); -} - -GetContentJob::GetContentJob(const QString& serverName, const QString& mediaId) - : BaseJob(HttpVerb::Get, "GetContentJob", - basePath % "/download/" % serverName % "/" % mediaId, false) - , d(new Private) -{ - setExpectedContentTypes({ "*/*" }); -} - -GetContentJob::~GetContentJob() = default; - -const QString& GetContentJob::contentType() const -{ - return d->contentType; -} - -const QString& GetContentJob::contentDisposition() const -{ - return d->contentDisposition; -} - -QIODevice* GetContentJob::content() const -{ - return d->content; -} - -BaseJob::Status GetContentJob::parseReply(QNetworkReply* reply) -{ - d->contentType = reply->rawHeader("Content-Type"); - d->contentDisposition = reply->rawHeader("Content-Disposition"); - d->content = reply; - return Success; -} - -class GetContentOverrideNameJob::Private -{ - public: - QString contentType; - QString contentDisposition; - QIODevice* content; -}; - -QUrl GetContentOverrideNameJob::makeRequestUrl(QUrl baseUrl, const QString& serverName, const QString& mediaId, const QString& fileName) -{ - return BaseJob::makeRequestUrl(baseUrl, - basePath % "/download/" % serverName % "/" % mediaId % "/" % fileName); -} - -GetContentOverrideNameJob::GetContentOverrideNameJob(const QString& serverName, const QString& mediaId, const QString& fileName) - : BaseJob(HttpVerb::Get, "GetContentOverrideNameJob", - basePath % "/download/" % serverName % "/" % mediaId % "/" % fileName, false) - , d(new Private) -{ - setExpectedContentTypes({ "*/*" }); -} - -GetContentOverrideNameJob::~GetContentOverrideNameJob() = default; - -const QString& GetContentOverrideNameJob::contentType() const -{ - return d->contentType; -} - -const QString& GetContentOverrideNameJob::contentDisposition() const -{ - return d->contentDisposition; -} - -QIODevice* GetContentOverrideNameJob::content() const -{ - return d->content; -} - -BaseJob::Status GetContentOverrideNameJob::parseReply(QNetworkReply* reply) -{ - d->contentType = reply->rawHeader("Content-Type"); - d->contentDisposition = reply->rawHeader("Content-Disposition"); - d->content = reply; - return Success; -} - -class GetContentThumbnailJob::Private -{ - public: - QString contentType; - QIODevice* content; -}; - -BaseJob::Query queryToGetContentThumbnail(int width, int height, const QString& method) -{ - BaseJob::Query _q; - _q.addQueryItem("width", QString("%1").arg(width)); - _q.addQueryItem("height", QString("%1").arg(height)); - if (!method.isEmpty()) - _q.addQueryItem("method", method); - return _q; -} - -QUrl GetContentThumbnailJob::makeRequestUrl(QUrl baseUrl, const QString& serverName, const QString& mediaId, int width, int height, const QString& method) -{ - return BaseJob::makeRequestUrl(baseUrl, - basePath % "/thumbnail/" % serverName % "/" % mediaId, - queryToGetContentThumbnail(width, height, method)); -} - -GetContentThumbnailJob::GetContentThumbnailJob(const QString& serverName, const QString& mediaId, int width, int height, const QString& method) - : BaseJob(HttpVerb::Get, "GetContentThumbnailJob", - basePath % "/thumbnail/" % serverName % "/" % mediaId, - queryToGetContentThumbnail(width, height, method), - {}, false) - , d(new Private) -{ - setExpectedContentTypes({ "image/jpeg", "image/png" }); -} - -GetContentThumbnailJob::~GetContentThumbnailJob() = default; - -const QString& GetContentThumbnailJob::contentType() const -{ - return d->contentType; -} - -QIODevice* GetContentThumbnailJob::content() const -{ - return d->content; -} - -BaseJob::Status GetContentThumbnailJob::parseReply(QNetworkReply* reply) -{ - d->contentType = reply->rawHeader("Content-Type"); - d->content = reply; - return Success; -} - -class GetUrlPreviewJob::Private -{ - public: - double matrixImageSize; - QString ogImage; -}; - -BaseJob::Query queryToGetUrlPreview(const QString& url, double ts) -{ - BaseJob::Query _q; - _q.addQueryItem("url", url); - _q.addQueryItem("ts", QString("%1").arg(ts)); - return _q; -} - -QUrl GetUrlPreviewJob::makeRequestUrl(QUrl baseUrl, const QString& url, double ts) -{ - return BaseJob::makeRequestUrl(baseUrl, - basePath % "/preview_url", - queryToGetUrlPreview(url, ts)); -} - -GetUrlPreviewJob::GetUrlPreviewJob(const QString& url, double ts) - : BaseJob(HttpVerb::Get, "GetUrlPreviewJob", - basePath % "/preview_url", - queryToGetUrlPreview(url, ts)) - , d(new Private) -{ -} - -GetUrlPreviewJob::~GetUrlPreviewJob() = default; - -double GetUrlPreviewJob::matrixImageSize() const -{ - return d->matrixImageSize; -} - -const QString& GetUrlPreviewJob::ogImage() const -{ - return d->ogImage; -} - -BaseJob::Status GetUrlPreviewJob::parseJson(const QJsonDocument& data) -{ - auto json = data.object(); - d->matrixImageSize = fromJson(json.value("matrix:image:size")); - d->ogImage = fromJson(json.value("og:image")); - return Success; -} - diff --git a/jobs/generated/content-repo.h b/jobs/generated/content-repo.h deleted file mode 100644 index b4ea562f..00000000 --- a/jobs/generated/content-repo.h +++ /dev/null @@ -1,129 +0,0 @@ -/****************************************************************************** - * THIS FILE IS GENERATED - ANY EDITS WILL BE OVERWRITTEN - */ - -#pragma once - -#include "../basejob.h" - -#include - - -namespace QMatrixClient -{ - // Operations - - class UploadContentJob : public BaseJob - { - public: - explicit UploadContentJob(QIODevice* content, const QString& filename = {}, const QString& contentType = {}); - ~UploadContentJob() override; - - const QString& contentUri() const; - - protected: - Status parseJson(const QJsonDocument& data) override; - - private: - class Private; - QScopedPointer d; - }; - - class GetContentJob : public BaseJob - { - public: - /** Construct a URL out of baseUrl and usual parameters passed to - * GetContentJob. This function can be used when - * a URL for GetContentJob is necessary but the job - * itself isn't. - */ - static QUrl makeRequestUrl(QUrl baseUrl, const QString& serverName, const QString& mediaId); - - explicit GetContentJob(const QString& serverName, const QString& mediaId); - ~GetContentJob() override; - - const QString& contentType() const; - const QString& contentDisposition() const; - QIODevice* content() const; - - protected: - Status parseReply(QNetworkReply* reply) override; - - private: - class Private; - QScopedPointer d; - }; - - class GetContentOverrideNameJob : public BaseJob - { - public: - /** Construct a URL out of baseUrl and usual parameters passed to - * GetContentOverrideNameJob. This function can be used when - * a URL for GetContentOverrideNameJob is necessary but the job - * itself isn't. - */ - static QUrl makeRequestUrl(QUrl baseUrl, const QString& serverName, const QString& mediaId, const QString& fileName); - - explicit GetContentOverrideNameJob(const QString& serverName, const QString& mediaId, const QString& fileName); - ~GetContentOverrideNameJob() override; - - const QString& contentType() const; - const QString& contentDisposition() const; - QIODevice* content() const; - - protected: - Status parseReply(QNetworkReply* reply) override; - - private: - class Private; - QScopedPointer d; - }; - - class GetContentThumbnailJob : public BaseJob - { - public: - /** Construct a URL out of baseUrl and usual parameters passed to - * GetContentThumbnailJob. This function can be used when - * a URL for GetContentThumbnailJob is necessary but the job - * itself isn't. - */ - static QUrl makeRequestUrl(QUrl baseUrl, const QString& serverName, const QString& mediaId, int width = {}, int height = {}, const QString& method = {}); - - explicit GetContentThumbnailJob(const QString& serverName, const QString& mediaId, int width = {}, int height = {}, const QString& method = {}); - ~GetContentThumbnailJob() override; - - const QString& contentType() const; - QIODevice* content() const; - - protected: - Status parseReply(QNetworkReply* reply) override; - - private: - class Private; - QScopedPointer d; - }; - - class GetUrlPreviewJob : public BaseJob - { - public: - /** Construct a URL out of baseUrl and usual parameters passed to - * GetUrlPreviewJob. This function can be used when - * a URL for GetUrlPreviewJob is necessary but the job - * itself isn't. - */ - static QUrl makeRequestUrl(QUrl baseUrl, const QString& url, double ts = {}); - - explicit GetUrlPreviewJob(const QString& url, double ts = {}); - ~GetUrlPreviewJob() override; - - double matrixImageSize() const; - const QString& ogImage() const; - - protected: - Status parseJson(const QJsonDocument& data) override; - - private: - class Private; - QScopedPointer d; - }; -} // namespace QMatrixClient diff --git a/jobs/generated/create_room.cpp b/jobs/generated/create_room.cpp deleted file mode 100644 index de7807b5..00000000 --- a/jobs/generated/create_room.cpp +++ /dev/null @@ -1,115 +0,0 @@ -/****************************************************************************** - * THIS FILE IS GENERATED - ANY EDITS WILL BE OVERWRITTEN - */ - -#include "create_room.h" - -#include - -using namespace QMatrixClient; - -static const auto basePath = QStringLiteral("/_matrix/client/r0"); - -CreateRoomJob::Invite3pid::operator QJsonObject() const -{ - QJsonObject o; - o.insert("id_server", toJson(idServer)); - o.insert("medium", toJson(medium)); - o.insert("address", toJson(address)); - - return o; -} -namespace QMatrixClient -{ - template <> struct FromJson - { - CreateRoomJob::Invite3pid operator()(QJsonValue jv) - { - QJsonObject o = jv.toObject(); - CreateRoomJob::Invite3pid result; - result.idServer = - fromJson(o.value("id_server")); - result.medium = - fromJson(o.value("medium")); - result.address = - fromJson(o.value("address")); - - return result; - } - }; -} // namespace QMatrixClient - -CreateRoomJob::StateEvent::operator QJsonObject() const -{ - QJsonObject o; - o.insert("type", toJson(type)); - o.insert("state_key", toJson(stateKey)); - o.insert("content", toJson(content)); - - return o; -} -namespace QMatrixClient -{ - template <> struct FromJson - { - CreateRoomJob::StateEvent operator()(QJsonValue jv) - { - QJsonObject o = jv.toObject(); - CreateRoomJob::StateEvent result; - result.type = - fromJson(o.value("type")); - result.stateKey = - fromJson(o.value("state_key")); - result.content = - fromJson(o.value("content")); - - return result; - } - }; -} // namespace QMatrixClient - -class CreateRoomJob::Private -{ - public: - QString roomId; -}; - -CreateRoomJob::CreateRoomJob(const QString& visibility, const QString& roomAliasName, const QString& name, const QString& topic, const QVector& invite, const QVector& invite3pid, const QJsonObject& creationContent, const QVector& initialState, const QString& preset, bool isDirect, bool guestCanJoin) - : BaseJob(HttpVerb::Post, "CreateRoomJob", - basePath % "/createRoom") - , d(new Private) -{ - QJsonObject _data; - if (!visibility.isEmpty()) - _data.insert("visibility", toJson(visibility)); - if (!roomAliasName.isEmpty()) - _data.insert("room_alias_name", toJson(roomAliasName)); - if (!name.isEmpty()) - _data.insert("name", toJson(name)); - if (!topic.isEmpty()) - _data.insert("topic", toJson(topic)); - _data.insert("invite", toJson(invite)); - _data.insert("invite_3pid", toJson(invite3pid)); - _data.insert("creation_content", toJson(creationContent)); - _data.insert("initial_state", toJson(initialState)); - if (!preset.isEmpty()) - _data.insert("preset", toJson(preset)); - _data.insert("is_direct", toJson(isDirect)); - _data.insert("guest_can_join", toJson(guestCanJoin)); - setRequestData(_data); -} - -CreateRoomJob::~CreateRoomJob() = default; - -const QString& CreateRoomJob::roomId() const -{ - return d->roomId; -} - -BaseJob::Status CreateRoomJob::parseJson(const QJsonDocument& data) -{ - auto json = data.object(); - d->roomId = fromJson(json.value("room_id")); - return Success; -} - diff --git a/jobs/generated/create_room.h b/jobs/generated/create_room.h deleted file mode 100644 index b479615a..00000000 --- a/jobs/generated/create_room.h +++ /dev/null @@ -1,55 +0,0 @@ -/****************************************************************************** - * THIS FILE IS GENERATED - ANY EDITS WILL BE OVERWRITTEN - */ - -#pragma once - -#include "../basejob.h" - -#include -#include - -#include "converters.h" - -namespace QMatrixClient -{ - // Operations - - class CreateRoomJob : public BaseJob - { - public: - // Inner data structures - - struct Invite3pid - { - QString idServer; - QString medium; - QString address; - - operator QJsonObject() const; - }; - - struct StateEvent - { - QString type; - QString stateKey; - QJsonObject content; - - operator QJsonObject() const; - }; - - // End of inner data structures - - explicit CreateRoomJob(const QString& visibility = {}, const QString& roomAliasName = {}, const QString& name = {}, const QString& topic = {}, const QVector& invite = {}, const QVector& invite3pid = {}, const QJsonObject& creationContent = {}, const QVector& initialState = {}, const QString& preset = {}, bool isDirect = {}, bool guestCanJoin = {}); - ~CreateRoomJob() override; - - const QString& roomId() const; - - protected: - Status parseJson(const QJsonDocument& data) override; - - private: - class Private; - QScopedPointer d; - }; -} // namespace QMatrixClient diff --git a/jobs/generated/directory.cpp b/jobs/generated/directory.cpp deleted file mode 100644 index 9428dcee..00000000 --- a/jobs/generated/directory.cpp +++ /dev/null @@ -1,76 +0,0 @@ -/****************************************************************************** - * THIS FILE IS GENERATED - ANY EDITS WILL BE OVERWRITTEN - */ - -#include "directory.h" - -#include "converters.h" - -#include - -using namespace QMatrixClient; - -static const auto basePath = QStringLiteral("/_matrix/client/r0/directory"); - -SetRoomAliasJob::SetRoomAliasJob(const QString& roomAlias, const QString& roomId) - : BaseJob(HttpVerb::Put, "SetRoomAliasJob", - basePath % "/room/" % roomAlias) -{ - QJsonObject _data; - if (!roomId.isEmpty()) - _data.insert("room_id", toJson(roomId)); - setRequestData(_data); -} - -class GetRoomIdByAliasJob::Private -{ - public: - QString roomId; - QVector servers; -}; - -QUrl GetRoomIdByAliasJob::makeRequestUrl(QUrl baseUrl, const QString& roomAlias) -{ - return BaseJob::makeRequestUrl(baseUrl, - basePath % "/room/" % roomAlias); -} - -GetRoomIdByAliasJob::GetRoomIdByAliasJob(const QString& roomAlias) - : BaseJob(HttpVerb::Get, "GetRoomIdByAliasJob", - basePath % "/room/" % roomAlias, false) - , d(new Private) -{ -} - -GetRoomIdByAliasJob::~GetRoomIdByAliasJob() = default; - -const QString& GetRoomIdByAliasJob::roomId() const -{ - return d->roomId; -} - -const QVector& GetRoomIdByAliasJob::servers() const -{ - return d->servers; -} - -BaseJob::Status GetRoomIdByAliasJob::parseJson(const QJsonDocument& data) -{ - auto json = data.object(); - d->roomId = fromJson(json.value("room_id")); - d->servers = fromJson>(json.value("servers")); - return Success; -} - -QUrl DeleteRoomAliasJob::makeRequestUrl(QUrl baseUrl, const QString& roomAlias) -{ - return BaseJob::makeRequestUrl(baseUrl, - basePath % "/room/" % roomAlias); -} - -DeleteRoomAliasJob::DeleteRoomAliasJob(const QString& roomAlias) - : BaseJob(HttpVerb::Delete, "DeleteRoomAliasJob", - basePath % "/room/" % roomAlias) -{ -} - diff --git a/jobs/generated/directory.h b/jobs/generated/directory.h deleted file mode 100644 index 87591240..00000000 --- a/jobs/generated/directory.h +++ /dev/null @@ -1,58 +0,0 @@ -/****************************************************************************** - * THIS FILE IS GENERATED - ANY EDITS WILL BE OVERWRITTEN - */ - -#pragma once - -#include "../basejob.h" - -#include - - -namespace QMatrixClient -{ - // Operations - - class SetRoomAliasJob : public BaseJob - { - public: - explicit SetRoomAliasJob(const QString& roomAlias, const QString& roomId = {}); - }; - - class GetRoomIdByAliasJob : public BaseJob - { - public: - /** Construct a URL out of baseUrl and usual parameters passed to - * GetRoomIdByAliasJob. This function can be used when - * a URL for GetRoomIdByAliasJob is necessary but the job - * itself isn't. - */ - static QUrl makeRequestUrl(QUrl baseUrl, const QString& roomAlias); - - explicit GetRoomIdByAliasJob(const QString& roomAlias); - ~GetRoomIdByAliasJob() override; - - const QString& roomId() const; - const QVector& servers() const; - - protected: - Status parseJson(const QJsonDocument& data) override; - - private: - class Private; - QScopedPointer d; - }; - - class DeleteRoomAliasJob : public BaseJob - { - public: - /** Construct a URL out of baseUrl and usual parameters passed to - * DeleteRoomAliasJob. This function can be used when - * a URL for DeleteRoomAliasJob is necessary but the job - * itself isn't. - */ - static QUrl makeRequestUrl(QUrl baseUrl, const QString& roomAlias); - - explicit DeleteRoomAliasJob(const QString& roomAlias); - }; -} // namespace QMatrixClient diff --git a/jobs/generated/inviting.cpp b/jobs/generated/inviting.cpp deleted file mode 100644 index d2ee2107..00000000 --- a/jobs/generated/inviting.cpp +++ /dev/null @@ -1,23 +0,0 @@ -/****************************************************************************** - * THIS FILE IS GENERATED - ANY EDITS WILL BE OVERWRITTEN - */ - -#include "inviting.h" - -#include "converters.h" - -#include - -using namespace QMatrixClient; - -static const auto basePath = QStringLiteral("/_matrix/client/r0"); - -InviteUserJob::InviteUserJob(const QString& roomId, const QString& userId) - : BaseJob(HttpVerb::Post, "InviteUserJob", - basePath % "/rooms/" % roomId % "/invite") -{ - QJsonObject _data; - _data.insert("user_id", toJson(userId)); - setRequestData(_data); -} - diff --git a/jobs/generated/inviting.h b/jobs/generated/inviting.h deleted file mode 100644 index eaa884df..00000000 --- a/jobs/generated/inviting.h +++ /dev/null @@ -1,20 +0,0 @@ -/****************************************************************************** - * THIS FILE IS GENERATED - ANY EDITS WILL BE OVERWRITTEN - */ - -#pragma once - -#include "../basejob.h" - - - -namespace QMatrixClient -{ - // Operations - - class InviteUserJob : public BaseJob - { - public: - explicit InviteUserJob(const QString& roomId, const QString& userId); - }; -} // namespace QMatrixClient diff --git a/jobs/generated/kicking.cpp b/jobs/generated/kicking.cpp deleted file mode 100644 index bf2490b7..00000000 --- a/jobs/generated/kicking.cpp +++ /dev/null @@ -1,25 +0,0 @@ -/****************************************************************************** - * THIS FILE IS GENERATED - ANY EDITS WILL BE OVERWRITTEN - */ - -#include "kicking.h" - -#include "converters.h" - -#include - -using namespace QMatrixClient; - -static const auto basePath = QStringLiteral("/_matrix/client/r0"); - -KickJob::KickJob(const QString& roomId, const QString& userId, const QString& reason) - : BaseJob(HttpVerb::Post, "KickJob", - basePath % "/rooms/" % roomId % "/kick") -{ - QJsonObject _data; - _data.insert("user_id", toJson(userId)); - if (!reason.isEmpty()) - _data.insert("reason", toJson(reason)); - setRequestData(_data); -} - diff --git a/jobs/generated/kicking.h b/jobs/generated/kicking.h deleted file mode 100644 index 3814bef7..00000000 --- a/jobs/generated/kicking.h +++ /dev/null @@ -1,20 +0,0 @@ -/****************************************************************************** - * THIS FILE IS GENERATED - ANY EDITS WILL BE OVERWRITTEN - */ - -#pragma once - -#include "../basejob.h" - - - -namespace QMatrixClient -{ - // Operations - - class KickJob : public BaseJob - { - public: - explicit KickJob(const QString& roomId, const QString& userId, const QString& reason = {}); - }; -} // namespace QMatrixClient diff --git a/jobs/generated/leaving.cpp b/jobs/generated/leaving.cpp deleted file mode 100644 index fbc40d11..00000000 --- a/jobs/generated/leaving.cpp +++ /dev/null @@ -1,38 +0,0 @@ -/****************************************************************************** - * THIS FILE IS GENERATED - ANY EDITS WILL BE OVERWRITTEN - */ - -#include "leaving.h" - -#include "converters.h" - -#include - -using namespace QMatrixClient; - -static const auto basePath = QStringLiteral("/_matrix/client/r0"); - -QUrl LeaveRoomJob::makeRequestUrl(QUrl baseUrl, const QString& roomId) -{ - return BaseJob::makeRequestUrl(baseUrl, - basePath % "/rooms/" % roomId % "/leave"); -} - -LeaveRoomJob::LeaveRoomJob(const QString& roomId) - : BaseJob(HttpVerb::Post, "LeaveRoomJob", - basePath % "/rooms/" % roomId % "/leave") -{ -} - -QUrl ForgetRoomJob::makeRequestUrl(QUrl baseUrl, const QString& roomId) -{ - return BaseJob::makeRequestUrl(baseUrl, - basePath % "/rooms/" % roomId % "/forget"); -} - -ForgetRoomJob::ForgetRoomJob(const QString& roomId) - : BaseJob(HttpVerb::Post, "ForgetRoomJob", - basePath % "/rooms/" % roomId % "/forget") -{ -} - diff --git a/jobs/generated/leaving.h b/jobs/generated/leaving.h deleted file mode 100644 index 9bae2363..00000000 --- a/jobs/generated/leaving.h +++ /dev/null @@ -1,40 +0,0 @@ -/****************************************************************************** - * THIS FILE IS GENERATED - ANY EDITS WILL BE OVERWRITTEN - */ - -#pragma once - -#include "../basejob.h" - - - -namespace QMatrixClient -{ - // Operations - - class LeaveRoomJob : public BaseJob - { - public: - /** Construct a URL out of baseUrl and usual parameters passed to - * LeaveRoomJob. This function can be used when - * a URL for LeaveRoomJob is necessary but the job - * itself isn't. - */ - static QUrl makeRequestUrl(QUrl baseUrl, const QString& roomId); - - explicit LeaveRoomJob(const QString& roomId); - }; - - class ForgetRoomJob : public BaseJob - { - public: - /** Construct a URL out of baseUrl and usual parameters passed to - * ForgetRoomJob. This function can be used when - * a URL for ForgetRoomJob is necessary but the job - * itself isn't. - */ - static QUrl makeRequestUrl(QUrl baseUrl, const QString& roomId); - - explicit ForgetRoomJob(const QString& roomId); - }; -} // namespace QMatrixClient diff --git a/jobs/generated/list_public_rooms.cpp b/jobs/generated/list_public_rooms.cpp deleted file mode 100644 index 39653300..00000000 --- a/jobs/generated/list_public_rooms.cpp +++ /dev/null @@ -1,266 +0,0 @@ -/****************************************************************************** - * THIS FILE IS GENERATED - ANY EDITS WILL BE OVERWRITTEN - */ - -#include "list_public_rooms.h" - -#include - -using namespace QMatrixClient; - -static const auto basePath = QStringLiteral("/_matrix/client/r0"); - -GetPublicRoomsJob::PublicRoomsChunk::operator QJsonObject() const -{ - QJsonObject o; - o.insert("aliases", toJson(aliases)); - o.insert("canonical_alias", toJson(canonicalAlias)); - o.insert("name", toJson(name)); - o.insert("num_joined_members", toJson(numJoinedMembers)); - o.insert("room_id", toJson(roomId)); - o.insert("topic", toJson(topic)); - o.insert("world_readable", toJson(worldReadable)); - o.insert("guest_can_join", toJson(guestCanJoin)); - o.insert("avatar_url", toJson(avatarUrl)); - - return o; -} -namespace QMatrixClient -{ - template <> struct FromJson - { - GetPublicRoomsJob::PublicRoomsChunk operator()(QJsonValue jv) - { - QJsonObject o = jv.toObject(); - GetPublicRoomsJob::PublicRoomsChunk result; - result.aliases = - fromJson>(o.value("aliases")); - result.canonicalAlias = - fromJson(o.value("canonical_alias")); - result.name = - fromJson(o.value("name")); - result.numJoinedMembers = - fromJson(o.value("num_joined_members")); - result.roomId = - fromJson(o.value("room_id")); - result.topic = - fromJson(o.value("topic")); - result.worldReadable = - fromJson(o.value("world_readable")); - result.guestCanJoin = - fromJson(o.value("guest_can_join")); - result.avatarUrl = - fromJson(o.value("avatar_url")); - - return result; - } - }; -} // namespace QMatrixClient - -class GetPublicRoomsJob::Private -{ - public: - QVector chunk; - QString nextBatch; - QString prevBatch; - double totalRoomCountEstimate; -}; - -BaseJob::Query queryToGetPublicRooms(double limit, const QString& since, const QString& server) -{ - BaseJob::Query _q; - _q.addQueryItem("limit", QString("%1").arg(limit)); - if (!since.isEmpty()) - _q.addQueryItem("since", since); - if (!server.isEmpty()) - _q.addQueryItem("server", server); - return _q; -} - -QUrl GetPublicRoomsJob::makeRequestUrl(QUrl baseUrl, double limit, const QString& since, const QString& server) -{ - return BaseJob::makeRequestUrl(baseUrl, - basePath % "/publicRooms", - queryToGetPublicRooms(limit, since, server)); -} - -GetPublicRoomsJob::GetPublicRoomsJob(double limit, const QString& since, const QString& server) - : BaseJob(HttpVerb::Get, "GetPublicRoomsJob", - basePath % "/publicRooms", - queryToGetPublicRooms(limit, since, server), - {}, false) - , d(new Private) -{ -} - -GetPublicRoomsJob::~GetPublicRoomsJob() = default; - -const QVector& GetPublicRoomsJob::chunk() const -{ - return d->chunk; -} - -const QString& GetPublicRoomsJob::nextBatch() const -{ - return d->nextBatch; -} - -const QString& GetPublicRoomsJob::prevBatch() const -{ - return d->prevBatch; -} - -double GetPublicRoomsJob::totalRoomCountEstimate() const -{ - return d->totalRoomCountEstimate; -} - -BaseJob::Status GetPublicRoomsJob::parseJson(const QJsonDocument& data) -{ - auto json = data.object(); - if (!json.contains("chunk")) - return { JsonParseError, - "The key 'chunk' not found in the response" }; - d->chunk = fromJson>(json.value("chunk")); - d->nextBatch = fromJson(json.value("next_batch")); - d->prevBatch = fromJson(json.value("prev_batch")); - d->totalRoomCountEstimate = fromJson(json.value("total_room_count_estimate")); - return Success; -} - -QueryPublicRoomsJob::Filter::operator QJsonObject() const -{ - QJsonObject o; - o.insert("generic_search_term", toJson(genericSearchTerm)); - - return o; -} -namespace QMatrixClient -{ - template <> struct FromJson - { - QueryPublicRoomsJob::Filter operator()(QJsonValue jv) - { - QJsonObject o = jv.toObject(); - QueryPublicRoomsJob::Filter result; - result.genericSearchTerm = - fromJson(o.value("generic_search_term")); - - return result; - } - }; -} // namespace QMatrixClient - -QueryPublicRoomsJob::PublicRoomsChunk::operator QJsonObject() const -{ - QJsonObject o; - o.insert("aliases", toJson(aliases)); - o.insert("canonical_alias", toJson(canonicalAlias)); - o.insert("name", toJson(name)); - o.insert("num_joined_members", toJson(numJoinedMembers)); - o.insert("room_id", toJson(roomId)); - o.insert("topic", toJson(topic)); - o.insert("world_readable", toJson(worldReadable)); - o.insert("guest_can_join", toJson(guestCanJoin)); - o.insert("avatar_url", toJson(avatarUrl)); - - return o; -} -namespace QMatrixClient -{ - template <> struct FromJson - { - QueryPublicRoomsJob::PublicRoomsChunk operator()(QJsonValue jv) - { - QJsonObject o = jv.toObject(); - QueryPublicRoomsJob::PublicRoomsChunk result; - result.aliases = - fromJson>(o.value("aliases")); - result.canonicalAlias = - fromJson(o.value("canonical_alias")); - result.name = - fromJson(o.value("name")); - result.numJoinedMembers = - fromJson(o.value("num_joined_members")); - result.roomId = - fromJson(o.value("room_id")); - result.topic = - fromJson(o.value("topic")); - result.worldReadable = - fromJson(o.value("world_readable")); - result.guestCanJoin = - fromJson(o.value("guest_can_join")); - result.avatarUrl = - fromJson(o.value("avatar_url")); - - return result; - } - }; -} // namespace QMatrixClient - -class QueryPublicRoomsJob::Private -{ - public: - QVector chunk; - QString nextBatch; - QString prevBatch; - double totalRoomCountEstimate; -}; - -BaseJob::Query queryToQueryPublicRooms(const QString& server) -{ - BaseJob::Query _q; - if (!server.isEmpty()) - _q.addQueryItem("server", server); - return _q; -} - -QueryPublicRoomsJob::QueryPublicRoomsJob(const QString& server, double limit, const QString& since, const Filter& filter) - : BaseJob(HttpVerb::Post, "QueryPublicRoomsJob", - basePath % "/publicRooms", - queryToQueryPublicRooms(server)) - , d(new Private) -{ - QJsonObject _data; - _data.insert("limit", toJson(limit)); - if (!since.isEmpty()) - _data.insert("since", toJson(since)); - _data.insert("filter", toJson(filter)); - setRequestData(_data); -} - -QueryPublicRoomsJob::~QueryPublicRoomsJob() = default; - -const QVector& QueryPublicRoomsJob::chunk() const -{ - return d->chunk; -} - -const QString& QueryPublicRoomsJob::nextBatch() const -{ - return d->nextBatch; -} - -const QString& QueryPublicRoomsJob::prevBatch() const -{ - return d->prevBatch; -} - -double QueryPublicRoomsJob::totalRoomCountEstimate() const -{ - return d->totalRoomCountEstimate; -} - -BaseJob::Status QueryPublicRoomsJob::parseJson(const QJsonDocument& data) -{ - auto json = data.object(); - if (!json.contains("chunk")) - return { JsonParseError, - "The key 'chunk' not found in the response" }; - d->chunk = fromJson>(json.value("chunk")); - d->nextBatch = fromJson(json.value("next_batch")); - d->prevBatch = fromJson(json.value("prev_batch")); - d->totalRoomCountEstimate = fromJson(json.value("total_room_count_estimate")); - return Success; -} - diff --git a/jobs/generated/list_public_rooms.h b/jobs/generated/list_public_rooms.h deleted file mode 100644 index 5c281de3..00000000 --- a/jobs/generated/list_public_rooms.h +++ /dev/null @@ -1,106 +0,0 @@ -/****************************************************************************** - * THIS FILE IS GENERATED - ANY EDITS WILL BE OVERWRITTEN - */ - -#pragma once - -#include "../basejob.h" - -#include - -#include "converters.h" - -namespace QMatrixClient -{ - // Operations - - class GetPublicRoomsJob : public BaseJob - { - public: - // Inner data structures - - struct PublicRoomsChunk - { - QVector aliases; - QString canonicalAlias; - QString name; - double numJoinedMembers; - QString roomId; - QString topic; - bool worldReadable; - bool guestCanJoin; - QString avatarUrl; - - operator QJsonObject() const; - }; - - // End of inner data structures - - /** Construct a URL out of baseUrl and usual parameters passed to - * GetPublicRoomsJob. This function can be used when - * a URL for GetPublicRoomsJob is necessary but the job - * itself isn't. - */ - static QUrl makeRequestUrl(QUrl baseUrl, double limit = {}, const QString& since = {}, const QString& server = {}); - - explicit GetPublicRoomsJob(double limit = {}, const QString& since = {}, const QString& server = {}); - ~GetPublicRoomsJob() override; - - const QVector& chunk() const; - const QString& nextBatch() const; - const QString& prevBatch() const; - double totalRoomCountEstimate() const; - - protected: - Status parseJson(const QJsonDocument& data) override; - - private: - class Private; - QScopedPointer d; - }; - - class QueryPublicRoomsJob : public BaseJob - { - public: - // Inner data structures - - struct Filter - { - QString genericSearchTerm; - - operator QJsonObject() const; - }; - - struct PublicRoomsChunk - { - QVector aliases; - QString canonicalAlias; - QString name; - double numJoinedMembers; - QString roomId; - QString topic; - bool worldReadable; - bool guestCanJoin; - QString avatarUrl; - - operator QJsonObject() const; - }; - - // End of inner data structures - - explicit QueryPublicRoomsJob(const QString& server = {}, double limit = {}, const QString& since = {}, const Filter& filter = {}); - ~QueryPublicRoomsJob() override; - - const QVector& chunk() const; - const QString& nextBatch() const; - const QString& prevBatch() const; - double totalRoomCountEstimate() const; - - protected: - Status parseJson(const QJsonDocument& data) override; - - private: - class Private; - QScopedPointer d; - }; -} // namespace QMatrixClient diff --git a/jobs/generated/login.cpp b/jobs/generated/login.cpp deleted file mode 100644 index a4dab428..00000000 --- a/jobs/generated/login.cpp +++ /dev/null @@ -1,79 +0,0 @@ -/****************************************************************************** - * THIS FILE IS GENERATED - ANY EDITS WILL BE OVERWRITTEN - */ - -#include "login.h" - -#include "converters.h" - -#include - -using namespace QMatrixClient; - -static const auto basePath = QStringLiteral("/_matrix/client/r0"); - -class LoginJob::Private -{ - public: - QString userId; - QString accessToken; - QString homeServer; - QString deviceId; -}; - -LoginJob::LoginJob(const QString& type, const QString& user, const QString& medium, const QString& address, const QString& password, const QString& token, const QString& deviceId, const QString& initialDeviceDisplayName) - : BaseJob(HttpVerb::Post, "LoginJob", - basePath % "/login", false) - , d(new Private) -{ - QJsonObject _data; - _data.insert("type", toJson(type)); - if (!user.isEmpty()) - _data.insert("user", toJson(user)); - if (!medium.isEmpty()) - _data.insert("medium", toJson(medium)); - if (!address.isEmpty()) - _data.insert("address", toJson(address)); - if (!password.isEmpty()) - _data.insert("password", toJson(password)); - if (!token.isEmpty()) - _data.insert("token", toJson(token)); - if (!deviceId.isEmpty()) - _data.insert("device_id", toJson(deviceId)); - if (!initialDeviceDisplayName.isEmpty()) - _data.insert("initial_device_display_name", toJson(initialDeviceDisplayName)); - setRequestData(_data); -} - -LoginJob::~LoginJob() = default; - -const QString& LoginJob::userId() const -{ - return d->userId; -} - -const QString& LoginJob::accessToken() const -{ - return d->accessToken; -} - -const QString& LoginJob::homeServer() const -{ - return d->homeServer; -} - -const QString& LoginJob::deviceId() const -{ - return d->deviceId; -} - -BaseJob::Status LoginJob::parseJson(const QJsonDocument& data) -{ - auto json = data.object(); - d->userId = fromJson(json.value("user_id")); - d->accessToken = fromJson(json.value("access_token")); - d->homeServer = fromJson(json.value("home_server")); - d->deviceId = fromJson(json.value("device_id")); - return Success; -} - diff --git a/jobs/generated/login.h b/jobs/generated/login.h deleted file mode 100644 index 3ac955d4..00000000 --- a/jobs/generated/login.h +++ /dev/null @@ -1,33 +0,0 @@ -/****************************************************************************** - * THIS FILE IS GENERATED - ANY EDITS WILL BE OVERWRITTEN - */ - -#pragma once - -#include "../basejob.h" - - - -namespace QMatrixClient -{ - // Operations - - class LoginJob : public BaseJob - { - public: - explicit LoginJob(const QString& type, const QString& user = {}, const QString& medium = {}, const QString& address = {}, const QString& password = {}, const QString& token = {}, const QString& deviceId = {}, const QString& initialDeviceDisplayName = {}); - ~LoginJob() override; - - const QString& userId() const; - const QString& accessToken() const; - const QString& homeServer() const; - const QString& deviceId() const; - - protected: - Status parseJson(const QJsonDocument& data) override; - - private: - class Private; - QScopedPointer d; - }; -} // namespace QMatrixClient diff --git a/jobs/generated/logout.cpp b/jobs/generated/logout.cpp deleted file mode 100644 index 83139842..00000000 --- a/jobs/generated/logout.cpp +++ /dev/null @@ -1,26 +0,0 @@ -/****************************************************************************** - * THIS FILE IS GENERATED - ANY EDITS WILL BE OVERWRITTEN - */ - -#include "logout.h" - -#include "converters.h" - -#include - -using namespace QMatrixClient; - -static const auto basePath = QStringLiteral("/_matrix/client/r0"); - -QUrl LogoutJob::makeRequestUrl(QUrl baseUrl) -{ - return BaseJob::makeRequestUrl(baseUrl, - basePath % "/logout"); -} - -LogoutJob::LogoutJob() - : BaseJob(HttpVerb::Post, "LogoutJob", - basePath % "/logout") -{ -} - diff --git a/jobs/generated/logout.h b/jobs/generated/logout.h deleted file mode 100644 index 7640ba55..00000000 --- a/jobs/generated/logout.h +++ /dev/null @@ -1,27 +0,0 @@ -/****************************************************************************** - * THIS FILE IS GENERATED - ANY EDITS WILL BE OVERWRITTEN - */ - -#pragma once - -#include "../basejob.h" - - - -namespace QMatrixClient -{ - // Operations - - class LogoutJob : public BaseJob - { - public: - /** Construct a URL out of baseUrl and usual parameters passed to - * LogoutJob. This function can be used when - * a URL for LogoutJob is necessary but the job - * itself isn't. - */ - static QUrl makeRequestUrl(QUrl baseUrl); - - explicit LogoutJob(); - }; -} // namespace QMatrixClient diff --git a/jobs/generated/profile.cpp b/jobs/generated/profile.cpp deleted file mode 100644 index 1f7092d7..00000000 --- a/jobs/generated/profile.cpp +++ /dev/null @@ -1,140 +0,0 @@ -/****************************************************************************** - * THIS FILE IS GENERATED - ANY EDITS WILL BE OVERWRITTEN - */ - -#include "profile.h" - -#include "converters.h" - -#include - -using namespace QMatrixClient; - -static const auto basePath = QStringLiteral("/_matrix/client/r0"); - -SetDisplayNameJob::SetDisplayNameJob(const QString& userId, const QString& displayname) - : BaseJob(HttpVerb::Put, "SetDisplayNameJob", - basePath % "/profile/" % userId % "/displayname") -{ - QJsonObject _data; - if (!displayname.isEmpty()) - _data.insert("displayname", toJson(displayname)); - setRequestData(_data); -} - -class GetDisplayNameJob::Private -{ - public: - QString displayname; -}; - -QUrl GetDisplayNameJob::makeRequestUrl(QUrl baseUrl, const QString& userId) -{ - return BaseJob::makeRequestUrl(baseUrl, - basePath % "/profile/" % userId % "/displayname"); -} - -GetDisplayNameJob::GetDisplayNameJob(const QString& userId) - : BaseJob(HttpVerb::Get, "GetDisplayNameJob", - basePath % "/profile/" % userId % "/displayname", false) - , d(new Private) -{ -} - -GetDisplayNameJob::~GetDisplayNameJob() = default; - -const QString& GetDisplayNameJob::displayname() const -{ - return d->displayname; -} - -BaseJob::Status GetDisplayNameJob::parseJson(const QJsonDocument& data) -{ - auto json = data.object(); - d->displayname = fromJson(json.value("displayname")); - return Success; -} - -SetAvatarUrlJob::SetAvatarUrlJob(const QString& userId, const QString& avatarUrl) - : BaseJob(HttpVerb::Put, "SetAvatarUrlJob", - basePath % "/profile/" % userId % "/avatar_url") -{ - QJsonObject _data; - if (!avatarUrl.isEmpty()) - _data.insert("avatar_url", toJson(avatarUrl)); - setRequestData(_data); -} - -class GetAvatarUrlJob::Private -{ - public: - QString avatarUrl; -}; - -QUrl GetAvatarUrlJob::makeRequestUrl(QUrl baseUrl, const QString& userId) -{ - return BaseJob::makeRequestUrl(baseUrl, - basePath % "/profile/" % userId % "/avatar_url"); -} - -GetAvatarUrlJob::GetAvatarUrlJob(const QString& userId) - : BaseJob(HttpVerb::Get, "GetAvatarUrlJob", - basePath % "/profile/" % userId % "/avatar_url", false) - , d(new Private) -{ -} - -GetAvatarUrlJob::~GetAvatarUrlJob() = default; - -const QString& GetAvatarUrlJob::avatarUrl() const -{ - return d->avatarUrl; -} - -BaseJob::Status GetAvatarUrlJob::parseJson(const QJsonDocument& data) -{ - auto json = data.object(); - d->avatarUrl = fromJson(json.value("avatar_url")); - return Success; -} - -class GetUserProfileJob::Private -{ - public: - QString avatarUrl; - QString displayname; -}; - -QUrl GetUserProfileJob::makeRequestUrl(QUrl baseUrl, const QString& userId) -{ - return BaseJob::makeRequestUrl(baseUrl, - basePath % "/profile/" % userId); -} - -GetUserProfileJob::GetUserProfileJob(const QString& userId) - : BaseJob(HttpVerb::Get, "GetUserProfileJob", - basePath % "/profile/" % userId, false) - , d(new Private) -{ -} - -GetUserProfileJob::~GetUserProfileJob() = default; - -const QString& GetUserProfileJob::avatarUrl() const -{ - return d->avatarUrl; -} - -const QString& GetUserProfileJob::displayname() const -{ - return d->displayname; -} - -BaseJob::Status GetUserProfileJob::parseJson(const QJsonDocument& data) -{ - auto json = data.object(); - d->avatarUrl = fromJson(json.value("avatar_url")); - d->displayname = fromJson(json.value("displayname")); - return Success; -} - diff --git a/jobs/generated/profile.h b/jobs/generated/profile.h deleted file mode 100644 index 024130f5..00000000 --- a/jobs/generated/profile.h +++ /dev/null @@ -1,96 +0,0 @@ -/****************************************************************************** - * THIS FILE IS GENERATED - ANY EDITS WILL BE OVERWRITTEN - */ - -#pragma once - -#include "../basejob.h" - - - -namespace QMatrixClient -{ - // Operations - - class SetDisplayNameJob : public BaseJob - { - public: - explicit SetDisplayNameJob(const QString& userId, const QString& displayname = {}); - }; - - class GetDisplayNameJob : public BaseJob - { - public: - /** Construct a URL out of baseUrl and usual parameters passed to - * GetDisplayNameJob. This function can be used when - * a URL for GetDisplayNameJob is necessary but the job - * itself isn't. - */ - static QUrl makeRequestUrl(QUrl baseUrl, const QString& userId); - - explicit GetDisplayNameJob(const QString& userId); - ~GetDisplayNameJob() override; - - const QString& displayname() const; - - protected: - Status parseJson(const QJsonDocument& data) override; - - private: - class Private; - QScopedPointer d; - }; - - class SetAvatarUrlJob : public BaseJob - { - public: - explicit SetAvatarUrlJob(const QString& userId, const QString& avatarUrl = {}); - }; - - class GetAvatarUrlJob : public BaseJob - { - public: - /** Construct a URL out of baseUrl and usual parameters passed to - * GetAvatarUrlJob. This function can be used when - * a URL for GetAvatarUrlJob is necessary but the job - * itself isn't. - */ - static QUrl makeRequestUrl(QUrl baseUrl, const QString& userId); - - explicit GetAvatarUrlJob(const QString& userId); - ~GetAvatarUrlJob() override; - - const QString& avatarUrl() const; - - protected: - Status parseJson(const QJsonDocument& data) override; - - private: - class Private; - QScopedPointer d; - }; - - class GetUserProfileJob : public BaseJob - { - public: - /** Construct a URL out of baseUrl and usual parameters passed to - * GetUserProfileJob. This function can be used when - * a URL for GetUserProfileJob is necessary but the job - * itself isn't. - */ - static QUrl makeRequestUrl(QUrl baseUrl, const QString& userId); - - explicit GetUserProfileJob(const QString& userId); - ~GetUserProfileJob() override; - - const QString& avatarUrl() const; - const QString& displayname() const; - - protected: - Status parseJson(const QJsonDocument& data) override; - - private: - class Private; - QScopedPointer d; - }; -} // namespace QMatrixClient diff --git a/jobs/generated/receipts.cpp b/jobs/generated/receipts.cpp deleted file mode 100644 index 83c38b6f..00000000 --- a/jobs/generated/receipts.cpp +++ /dev/null @@ -1,21 +0,0 @@ -/****************************************************************************** - * THIS FILE IS GENERATED - ANY EDITS WILL BE OVERWRITTEN - */ - -#include "receipts.h" - -#include "converters.h" - -#include - -using namespace QMatrixClient; - -static const auto basePath = QStringLiteral("/_matrix/client/r0"); - -PostReceiptJob::PostReceiptJob(const QString& roomId, const QString& receiptType, const QString& eventId, const QJsonObject& receipt) - : BaseJob(HttpVerb::Post, "PostReceiptJob", - basePath % "/rooms/" % roomId % "/receipt/" % receiptType % "/" % eventId) -{ - setRequestData(Data(receipt)); -} - diff --git a/jobs/generated/receipts.h b/jobs/generated/receipts.h deleted file mode 100644 index 9eb7a489..00000000 --- a/jobs/generated/receipts.h +++ /dev/null @@ -1,21 +0,0 @@ -/****************************************************************************** - * THIS FILE IS GENERATED - ANY EDITS WILL BE OVERWRITTEN - */ - -#pragma once - -#include "../basejob.h" - -#include - - -namespace QMatrixClient -{ - // Operations - - class PostReceiptJob : public BaseJob - { - public: - explicit PostReceiptJob(const QString& roomId, const QString& receiptType, const QString& eventId, const QJsonObject& receipt = {}); - }; -} // namespace QMatrixClient diff --git a/jobs/generated/redaction.cpp b/jobs/generated/redaction.cpp deleted file mode 100644 index 0da35dfc..00000000 --- a/jobs/generated/redaction.cpp +++ /dev/null @@ -1,45 +0,0 @@ -/****************************************************************************** - * THIS FILE IS GENERATED - ANY EDITS WILL BE OVERWRITTEN - */ - -#include "redaction.h" - -#include "converters.h" - -#include - -using namespace QMatrixClient; - -static const auto basePath = QStringLiteral("/_matrix/client/r0"); - -class RedactEventJob::Private -{ - public: - QString eventId; -}; - -RedactEventJob::RedactEventJob(const QString& roomId, const QString& eventId, const QString& txnId, const QString& reason) - : BaseJob(HttpVerb::Put, "RedactEventJob", - basePath % "/rooms/" % roomId % "/redact/" % eventId % "/" % txnId) - , d(new Private) -{ - QJsonObject _data; - if (!reason.isEmpty()) - _data.insert("reason", toJson(reason)); - setRequestData(_data); -} - -RedactEventJob::~RedactEventJob() = default; - -const QString& RedactEventJob::eventId() const -{ - return d->eventId; -} - -BaseJob::Status RedactEventJob::parseJson(const QJsonDocument& data) -{ - auto json = data.object(); - d->eventId = fromJson(json.value("event_id")); - return Success; -} - diff --git a/jobs/generated/redaction.h b/jobs/generated/redaction.h deleted file mode 100644 index e3b3ff4f..00000000 --- a/jobs/generated/redaction.h +++ /dev/null @@ -1,30 +0,0 @@ -/****************************************************************************** - * THIS FILE IS GENERATED - ANY EDITS WILL BE OVERWRITTEN - */ - -#pragma once - -#include "../basejob.h" - - - -namespace QMatrixClient -{ - // Operations - - class RedactEventJob : public BaseJob - { - public: - explicit RedactEventJob(const QString& roomId, const QString& eventId, const QString& txnId, const QString& reason = {}); - ~RedactEventJob() override; - - const QString& eventId() const; - - protected: - Status parseJson(const QJsonDocument& data) override; - - private: - class Private; - QScopedPointer d; - }; -} // namespace QMatrixClient diff --git a/jobs/generated/third_party_membership.cpp b/jobs/generated/third_party_membership.cpp deleted file mode 100644 index b637d481..00000000 --- a/jobs/generated/third_party_membership.cpp +++ /dev/null @@ -1,25 +0,0 @@ -/****************************************************************************** - * THIS FILE IS GENERATED - ANY EDITS WILL BE OVERWRITTEN - */ - -#include "third_party_membership.h" - -#include "converters.h" - -#include - -using namespace QMatrixClient; - -static const auto basePath = QStringLiteral("/_matrix/client/r0"); - -InviteBy3PIDJob::InviteBy3PIDJob(const QString& roomId, const QString& idServer, const QString& medium, const QString& address) - : BaseJob(HttpVerb::Post, "InviteBy3PIDJob", - basePath % "/rooms/" % roomId % "/invite") -{ - QJsonObject _data; - _data.insert("id_server", toJson(idServer)); - _data.insert("medium", toJson(medium)); - _data.insert("address", toJson(address)); - setRequestData(_data); -} - diff --git a/jobs/generated/third_party_membership.h b/jobs/generated/third_party_membership.h deleted file mode 100644 index c7b5214e..00000000 --- a/jobs/generated/third_party_membership.h +++ /dev/null @@ -1,20 +0,0 @@ -/****************************************************************************** - * THIS FILE IS GENERATED - ANY EDITS WILL BE OVERWRITTEN - */ - -#pragma once - -#include "../basejob.h" - - - -namespace QMatrixClient -{ - // Operations - - class InviteBy3PIDJob : public BaseJob - { - public: - explicit InviteBy3PIDJob(const QString& roomId, const QString& idServer, const QString& medium, const QString& address); - }; -} // namespace QMatrixClient diff --git a/jobs/generated/typing.cpp b/jobs/generated/typing.cpp deleted file mode 100644 index fa700290..00000000 --- a/jobs/generated/typing.cpp +++ /dev/null @@ -1,24 +0,0 @@ -/****************************************************************************** - * THIS FILE IS GENERATED - ANY EDITS WILL BE OVERWRITTEN - */ - -#include "typing.h" - -#include "converters.h" - -#include - -using namespace QMatrixClient; - -static const auto basePath = QStringLiteral("/_matrix/client/r0"); - -SetTypingJob::SetTypingJob(const QString& userId, const QString& roomId, bool typing, int timeout) - : BaseJob(HttpVerb::Put, "SetTypingJob", - basePath % "/rooms/" % roomId % "/typing/" % userId) -{ - QJsonObject _data; - _data.insert("typing", toJson(typing)); - _data.insert("timeout", toJson(timeout)); - setRequestData(_data); -} - diff --git a/jobs/generated/typing.h b/jobs/generated/typing.h deleted file mode 100644 index 0495ed0a..00000000 --- a/jobs/generated/typing.h +++ /dev/null @@ -1,20 +0,0 @@ -/****************************************************************************** - * THIS FILE IS GENERATED - ANY EDITS WILL BE OVERWRITTEN - */ - -#pragma once - -#include "../basejob.h" - - - -namespace QMatrixClient -{ - // Operations - - class SetTypingJob : public BaseJob - { - public: - explicit SetTypingJob(const QString& userId, const QString& roomId, bool typing, int timeout = {}); - }; -} // namespace QMatrixClient diff --git a/jobs/generated/versions.cpp b/jobs/generated/versions.cpp deleted file mode 100644 index b12594ca..00000000 --- a/jobs/generated/versions.cpp +++ /dev/null @@ -1,47 +0,0 @@ -/****************************************************************************** - * THIS FILE IS GENERATED - ANY EDITS WILL BE OVERWRITTEN - */ - -#include "versions.h" - -#include "converters.h" - -#include - -using namespace QMatrixClient; - -static const auto basePath = QStringLiteral("/_matrix/client"); - -class GetVersionsJob::Private -{ - public: - QVector versions; -}; - -QUrl GetVersionsJob::makeRequestUrl(QUrl baseUrl) -{ - return BaseJob::makeRequestUrl(baseUrl, - basePath % "/versions"); -} - -GetVersionsJob::GetVersionsJob() - : BaseJob(HttpVerb::Get, "GetVersionsJob", - basePath % "/versions", false) - , d(new Private) -{ -} - -GetVersionsJob::~GetVersionsJob() = default; - -const QVector& GetVersionsJob::versions() const -{ - return d->versions; -} - -BaseJob::Status GetVersionsJob::parseJson(const QJsonDocument& data) -{ - auto json = data.object(); - d->versions = fromJson>(json.value("versions")); - return Success; -} - diff --git a/jobs/generated/versions.h b/jobs/generated/versions.h deleted file mode 100644 index 18f6bb44..00000000 --- a/jobs/generated/versions.h +++ /dev/null @@ -1,38 +0,0 @@ -/****************************************************************************** - * THIS FILE IS GENERATED - ANY EDITS WILL BE OVERWRITTEN - */ - -#pragma once - -#include "../basejob.h" - -#include - - -namespace QMatrixClient -{ - // Operations - - class GetVersionsJob : public BaseJob - { - public: - /** Construct a URL out of baseUrl and usual parameters passed to - * GetVersionsJob. This function can be used when - * a URL for GetVersionsJob is necessary but the job - * itself isn't. - */ - static QUrl makeRequestUrl(QUrl baseUrl); - - explicit GetVersionsJob(); - ~GetVersionsJob() override; - - const QVector& versions() const; - - protected: - Status parseJson(const QJsonDocument& data) override; - - private: - class Private; - QScopedPointer d; - }; -} // namespace QMatrixClient diff --git a/jobs/generated/whoami.cpp b/jobs/generated/whoami.cpp deleted file mode 100644 index cc38fa4d..00000000 --- a/jobs/generated/whoami.cpp +++ /dev/null @@ -1,50 +0,0 @@ -/****************************************************************************** - * THIS FILE IS GENERATED - ANY EDITS WILL BE OVERWRITTEN - */ - -#include "whoami.h" - -#include "converters.h" - -#include - -using namespace QMatrixClient; - -static const auto basePath = QStringLiteral("/_matrix/client/r0"); - -class GetTokenOwnerJob::Private -{ - public: - QString userId; -}; - -QUrl GetTokenOwnerJob::makeRequestUrl(QUrl baseUrl) -{ - return BaseJob::makeRequestUrl(baseUrl, - basePath % "/account/whoami"); -} - -GetTokenOwnerJob::GetTokenOwnerJob() - : BaseJob(HttpVerb::Get, "GetTokenOwnerJob", - basePath % "/account/whoami") - , d(new Private) -{ -} - -GetTokenOwnerJob::~GetTokenOwnerJob() = default; - -const QString& GetTokenOwnerJob::userId() const -{ - return d->userId; -} - -BaseJob::Status GetTokenOwnerJob::parseJson(const QJsonDocument& data) -{ - auto json = data.object(); - if (!json.contains("user_id")) - return { JsonParseError, - "The key 'user_id' not found in the response" }; - d->userId = fromJson(json.value("user_id")); - return Success; -} - diff --git a/jobs/generated/whoami.h b/jobs/generated/whoami.h deleted file mode 100644 index 835232ee..00000000 --- a/jobs/generated/whoami.h +++ /dev/null @@ -1,37 +0,0 @@ -/****************************************************************************** - * THIS FILE IS GENERATED - ANY EDITS WILL BE OVERWRITTEN - */ - -#pragma once - -#include "../basejob.h" - - - -namespace QMatrixClient -{ - // Operations - - class GetTokenOwnerJob : public BaseJob - { - public: - /** Construct a URL out of baseUrl and usual parameters passed to - * GetTokenOwnerJob. This function can be used when - * a URL for GetTokenOwnerJob is necessary but the job - * itself isn't. - */ - static QUrl makeRequestUrl(QUrl baseUrl); - - explicit GetTokenOwnerJob(); - ~GetTokenOwnerJob() override; - - const QString& userId() const; - - protected: - Status parseJson(const QJsonDocument& data) override; - - private: - class Private; - QScopedPointer d; - }; -} // namespace QMatrixClient diff --git a/jobs/joinroomjob.cpp b/jobs/joinroomjob.cpp deleted file mode 100644 index 66a75089..00000000 --- a/jobs/joinroomjob.cpp +++ /dev/null @@ -1,58 +0,0 @@ -/****************************************************************************** - * Copyright (C) 2015 Felix Rohrbach - * - * 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 - */ - -#include "joinroomjob.h" -#include "util.h" - -using namespace QMatrixClient; - -class JoinRoomJob::Private -{ - public: - QString roomId; -}; - -JoinRoomJob::JoinRoomJob(const QString& roomAlias) - : BaseJob(HttpVerb::Post, "JoinRoomJob", - QStringLiteral("_matrix/client/r0/join/%1").arg(roomAlias)) - , d(new Private) -{ -} - -JoinRoomJob::~JoinRoomJob() -{ - delete d; -} - -QString JoinRoomJob::roomId() -{ - return d->roomId; -} - -BaseJob::Status JoinRoomJob::parseJson(const QJsonDocument& data) -{ - QJsonObject json = data.object(); - if( json.contains("room_id") ) - { - d->roomId = json.value("room_id").toString(); - return Success; - } - - qCDebug(JOBS) << data; - return { UserDefinedError, "No room_id in the JSON response" }; -} diff --git a/jobs/joinroomjob.h b/jobs/joinroomjob.h deleted file mode 100644 index f3ba216f..00000000 --- a/jobs/joinroomjob.h +++ /dev/null @@ -1,40 +0,0 @@ -/****************************************************************************** - * Copyright (C) 2015 Felix Rohrbach - * - * 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 - */ - -#pragma once - -#include "basejob.h" - -namespace QMatrixClient -{ - class JoinRoomJob: public BaseJob - { - public: - explicit JoinRoomJob(const QString& roomAlias); - virtual ~JoinRoomJob(); - - QString roomId(); - - protected: - Status parseJson(const QJsonDocument& data) override; - - private: - class Private; - Private* d; - }; -} // namespace QMatrixClient diff --git a/jobs/mediathumbnailjob.cpp b/jobs/mediathumbnailjob.cpp deleted file mode 100644 index dda1cdb4..00000000 --- a/jobs/mediathumbnailjob.cpp +++ /dev/null @@ -1,63 +0,0 @@ -/****************************************************************************** - * Copyright (C) 2016 Felix Rohrbach - * - * 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 - */ - -#include "mediathumbnailjob.h" - -using namespace QMatrixClient; - -QUrl MediaThumbnailJob::makeRequestUrl(QUrl baseUrl, - const QUrl& mxcUri, QSize requestedSize) -{ - return makeRequestUrl(baseUrl, mxcUri.authority(), mxcUri.path().mid(1), - requestedSize.width(), requestedSize.height()); -} - -MediaThumbnailJob::MediaThumbnailJob(const QString& serverName, - const QString& mediaId, QSize requestedSize) - : GetContentThumbnailJob(serverName, mediaId, - requestedSize.width(), requestedSize.height()) -{ } - -MediaThumbnailJob::MediaThumbnailJob(const QUrl& mxcUri, QSize requestedSize) - : GetContentThumbnailJob(mxcUri.authority(), - mxcUri.path().mid(1), // sans leading '/' - requestedSize.width(), requestedSize.height()) -{ } - -QImage MediaThumbnailJob::thumbnail() const -{ - return _thumbnail; -} - -QImage MediaThumbnailJob::scaledThumbnail(QSize toSize) const -{ - return _thumbnail.scaled(toSize, - Qt::KeepAspectRatio, Qt::SmoothTransformation); -} - -BaseJob::Status MediaThumbnailJob::parseReply(QNetworkReply* reply) -{ - auto result = GetContentThumbnailJob::parseReply(reply); - if (!result.good()) - return result; - - if( _thumbnail.loadFromData(content()->readAll()) ) - return Success; - - return { IncorrectResponseError, "Could not read image data" }; -} diff --git a/jobs/mediathumbnailjob.h b/jobs/mediathumbnailjob.h deleted file mode 100644 index 6e0b94f3..00000000 --- a/jobs/mediathumbnailjob.h +++ /dev/null @@ -1,47 +0,0 @@ -/****************************************************************************** - * Copyright (C) 2016 Felix Rohrbach - * - * 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 - */ - -#pragma once - -#include "generated/content-repo.h" - -#include - -namespace QMatrixClient -{ - class MediaThumbnailJob: public GetContentThumbnailJob - { - public: - using GetContentThumbnailJob::makeRequestUrl; - static QUrl makeRequestUrl(QUrl baseUrl, - const QUrl& mxcUri, QSize requestedSize); - - MediaThumbnailJob(const QString& serverName, const QString& mediaId, - QSize requestedSize); - MediaThumbnailJob(const QUrl& mxcUri, QSize requestedSize); - - QImage thumbnail() const; - QImage scaledThumbnail(QSize toSize) const; - - protected: - Status parseReply(QNetworkReply* reply) override; - - private: - QImage _thumbnail; - }; -} // namespace QMatrixClient diff --git a/jobs/passwordlogin.cpp b/jobs/passwordlogin.cpp deleted file mode 100644 index 8abfe66a..00000000 --- a/jobs/passwordlogin.cpp +++ /dev/null @@ -1,74 +0,0 @@ -/****************************************************************************** - * Copyright (C) 2015 Felix Rohrbach - * - * 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 - */ - -#include "passwordlogin.h" - -using namespace QMatrixClient; - -class PasswordLogin::Private -{ - public: - QString returned_id; - QString returned_server; - QString returned_token; -}; - -PasswordLogin::PasswordLogin(QString user, QString password) - : BaseJob(HttpVerb::Post, "PasswordLogin", - "_matrix/client/r0/login", Query(), Data(), false) - , d(new Private) -{ - QJsonObject _data; - _data.insert("type", QStringLiteral("m.login.password")); - _data.insert("user", user); - _data.insert("password", password); - setRequestData(_data); -} - -PasswordLogin::~PasswordLogin() -{ - delete d; -} - -QString PasswordLogin::token() const -{ - return d->returned_token; -} - -QString PasswordLogin::id() const -{ - return d->returned_id; -} - -QString PasswordLogin::server() const -{ - return d->returned_server; -} - -BaseJob::Status PasswordLogin::parseJson(const QJsonDocument& data) -{ - QJsonObject json = data.object(); - if( !json.contains("access_token") || !json.contains("home_server") || !json.contains("user_id") ) - { - return { UserDefinedError, "No expected data" }; - } - d->returned_token = json.value("access_token").toString(); - d->returned_server = json.value("home_server").toString(); - d->returned_id = json.value("user_id").toString(); - return Success; -} diff --git a/jobs/passwordlogin.h b/jobs/passwordlogin.h deleted file mode 100644 index fb8777a3..00000000 --- a/jobs/passwordlogin.h +++ /dev/null @@ -1,42 +0,0 @@ -/****************************************************************************** - * Copyright (C) 2015 Felix Rohrbach - * - * 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 - */ - -#pragma once - -#include "basejob.h" - -namespace QMatrixClient -{ - class PasswordLogin : public BaseJob - { - public: - PasswordLogin(QString user, QString password); - virtual ~PasswordLogin(); - - QString token() const; - QString id() const; - QString server() const; - - protected: - Status parseJson(const QJsonDocument& data) override; - - private: - class Private; - Private* d; - }; -} // namespace QMatrixClient diff --git a/jobs/postreadmarkersjob.h b/jobs/postreadmarkersjob.h deleted file mode 100644 index d0198821..00000000 --- a/jobs/postreadmarkersjob.h +++ /dev/null @@ -1,37 +0,0 @@ -/****************************************************************************** - * Copyright (C) 2017 Kitsune Ral - * - * 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 - */ - -#pragma once - -#include "basejob.h" - -using namespace QMatrixClient; - -class PostReadMarkersJob : public BaseJob -{ - public: - explicit PostReadMarkersJob(const QString& roomId, - const QString& readUpToEventId) - : BaseJob(HttpVerb::Post, "PostReadMarkersJob", - QStringLiteral("_matrix/client/r0/rooms/%1/read_markers") - .arg(roomId)) - { - setRequestData(QJsonObject {{ - QStringLiteral("m.fully_read"), readUpToEventId }}); - } -}; diff --git a/jobs/postreceiptjob.cpp b/jobs/postreceiptjob.cpp deleted file mode 100644 index 4572d74c..00000000 --- a/jobs/postreceiptjob.cpp +++ /dev/null @@ -1,27 +0,0 @@ -/****************************************************************************** - * Copyright (C) 2016 Felix Rohrbach - * - * 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 - */ - -#include "postreceiptjob.h" - -using namespace QMatrixClient; - -PostReceiptJob::PostReceiptJob(const QString& roomId, const QString& eventId) - : BaseJob(HttpVerb::Post, "PostReceiptJob", - QStringLiteral("/_matrix/client/r0/rooms/%1/receipt/m.read/%2") - .arg(roomId, eventId)) -{ } diff --git a/jobs/postreceiptjob.h b/jobs/postreceiptjob.h deleted file mode 100644 index 23df7c05..00000000 --- a/jobs/postreceiptjob.h +++ /dev/null @@ -1,30 +0,0 @@ -/****************************************************************************** - * Copyright (C) 2016 Felix Rohrbach - * - * 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 - */ - -#pragma once - -#include "basejob.h" - -namespace QMatrixClient -{ - class PostReceiptJob: public BaseJob - { - public: - PostReceiptJob(const QString& roomId, const QString& eventId); - }; -} diff --git a/jobs/requestdata.cpp b/jobs/requestdata.cpp deleted file mode 100644 index 5cb62221..00000000 --- a/jobs/requestdata.cpp +++ /dev/null @@ -1,38 +0,0 @@ -#include "requestdata.h" - -#include -#include -#include -#include -#include - -using namespace QMatrixClient; - -auto fromData(const QByteArray& data) -{ - auto source = std::make_unique(); - source->open(QIODevice::WriteOnly); - source->write(data); - source->close(); - return source; -} - -template -inline auto fromJson(const JsonDataT& jdata) -{ - return fromData(QJsonDocument(jdata).toJson(QJsonDocument::Compact)); -} - -RequestData::RequestData(const QByteArray& a) - : _source(fromData(a)) -{ } - -RequestData::RequestData(const QJsonObject& jo) - : _source(fromJson(jo)) -{ } - -RequestData::RequestData(const QJsonArray& ja) - : _source(fromJson(ja)) -{ } - -RequestData::~RequestData() = default; diff --git a/jobs/requestdata.h b/jobs/requestdata.h deleted file mode 100644 index aa03b744..00000000 --- a/jobs/requestdata.h +++ /dev/null @@ -1,59 +0,0 @@ -/****************************************************************************** - * Copyright (C) 2018 Kitsune Ral - * - * 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 - */ - -#pragma once - -#include - -class QByteArray; -class QJsonObject; -class QJsonArray; -class QJsonDocument; -class QIODevice; - -namespace QMatrixClient -{ - /** - * A simple wrapper that represents the request body. - * Provides a unified interface to dump an unstructured byte stream - * as well as JSON (and possibly other structures in the future) to - * a QByteArray consumed by QNetworkAccessManager request methods. - */ - class RequestData - { - public: - RequestData() = default; - RequestData(const QByteArray& a); - RequestData(const QJsonObject& jo); - RequestData(const QJsonArray& ja); - RequestData(QIODevice* source) - : _source(std::unique_ptr(source)) - { } - RequestData(RequestData&&) = default; - RequestData& operator=(RequestData&&) = default; - ~RequestData(); - - QIODevice* source() const - { - return _source.get(); - } - - private: - std::unique_ptr _source; - }; -} // namespace QMatrixClient diff --git a/jobs/roommessagesjob.cpp b/jobs/roommessagesjob.cpp deleted file mode 100644 index e5568f17..00000000 --- a/jobs/roommessagesjob.cpp +++ /dev/null @@ -1,65 +0,0 @@ -/****************************************************************************** - * Copyright (C) 2016 Felix Rohrbach - * - * 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 - */ - -#include "roommessagesjob.h" - -using namespace QMatrixClient; - -class RoomMessagesJob::Private -{ - public: - RoomEvents events; - QString end; -}; - -RoomMessagesJob::RoomMessagesJob(const QString& roomId, const QString& from, - int limit, FetchDirection dir) - : BaseJob(HttpVerb::Get, "RoomMessagesJob", - QStringLiteral("/_matrix/client/r0/rooms/%1/messages").arg(roomId), - Query( - { { "from", from } - , { "dir", dir == FetchDirection::Backward ? "b" : "f" } - , { "limit", QString::number(limit) } - })) - , d(new Private) -{ - qCDebug(JOBS) << "Room messages query:" << query().toString(QUrl::PrettyDecoded); -} - -RoomMessagesJob::~RoomMessagesJob() -{ - delete d; -} - -RoomEvents&& RoomMessagesJob::releaseEvents() -{ - return move(d->events); -} - -QString RoomMessagesJob::end() const -{ - return d->end; -} - -BaseJob::Status RoomMessagesJob::parseJson(const QJsonDocument& data) -{ - const auto obj = data.object(); - d->events.fromJson(obj, "chunk"); - d->end = obj.value("end").toString(); - return Success; -} diff --git a/jobs/roommessagesjob.h b/jobs/roommessagesjob.h deleted file mode 100644 index 7b3fd9c9..00000000 --- a/jobs/roommessagesjob.h +++ /dev/null @@ -1,47 +0,0 @@ -/****************************************************************************** - * Copyright (C) 2016 Felix Rohrbach - * - * 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 - */ - -#pragma once - -#include "basejob.h" - -#include "../events/event.h" - -namespace QMatrixClient -{ - enum class FetchDirection { Backward, Forward }; - - class RoomMessagesJob: public BaseJob - { - public: - RoomMessagesJob(const QString& roomId, const QString& from, - int limit = 10, - FetchDirection dir = FetchDirection::Backward); - virtual ~RoomMessagesJob(); - - RoomEvents&& releaseEvents(); - QString end() const; - - protected: - Status parseJson(const QJsonDocument& data) override; - - private: - class Private; - Private* d; - }; -} // namespace QMatrixClient diff --git a/jobs/sendeventjob.cpp b/jobs/sendeventjob.cpp deleted file mode 100644 index f5190d4b..00000000 --- a/jobs/sendeventjob.cpp +++ /dev/null @@ -1,45 +0,0 @@ -/****************************************************************************** - * Copyright (C) 2015 Felix Rohrbach - * - * 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 - */ - -#include "sendeventjob.h" - -#include "events/roommessageevent.h" - -using namespace QMatrixClient; - -SendEventJob::SendEventJob(const QString& roomId, const QString& type, - const QString& plainText) - : SendEventJob(roomId, RoomMessageEvent(plainText, type)) -{ } - -void SendEventJob::beforeStart(const ConnectionData* connData) -{ - BaseJob::beforeStart(connData); - setApiEndpoint(apiEndpoint() + connData->generateTxnId()); -} - -BaseJob::Status SendEventJob::parseJson(const QJsonDocument& data) -{ - _eventId = data.object().value("event_id").toString(); - if (!_eventId.isEmpty()) - return Success; - - qCDebug(JOBS) << data; - return { UserDefinedError, "No event_id in the JSON response" }; -} - diff --git a/jobs/sendeventjob.h b/jobs/sendeventjob.h deleted file mode 100644 index 3a11eb6a..00000000 --- a/jobs/sendeventjob.h +++ /dev/null @@ -1,57 +0,0 @@ -/****************************************************************************** - * Copyright (C) 2015 Felix Rohrbach - * - * 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 - */ - -#pragma once - -#include "basejob.h" - -#include "connectiondata.h" - -namespace QMatrixClient -{ - class SendEventJob: public BaseJob - { - public: - /** Constructs a job that sends an arbitrary room event */ - template - SendEventJob(const QString& roomId, const EvT& event) - : BaseJob(HttpVerb::Put, QStringLiteral("SendEventJob"), - QStringLiteral("_matrix/client/r0/rooms/%1/send/%2/") - .arg(roomId, EvT::TypeId), // See also beforeStart() - Query(), - Data(event.toJson())) - { } - - /** - * Constructs a plain text message job (for compatibility with - * the old PostMessageJob API). - */ - SendEventJob(const QString& roomId, const QString& type, - const QString& plainText); - - QString eventId() const { return _eventId; } - - protected: - Status parseJson(const QJsonDocument& data) override; - - private: - QString _eventId; - - void beforeStart(const ConnectionData* connData) override; - }; -} // namespace QMatrixClient diff --git a/jobs/setroomstatejob.cpp b/jobs/setroomstatejob.cpp deleted file mode 100644 index c2beb87b..00000000 --- a/jobs/setroomstatejob.cpp +++ /dev/null @@ -1,32 +0,0 @@ -/****************************************************************************** - * Copyright (C) 2015 Felix Rohrbach - * - * 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 - */ - -#include "setroomstatejob.h" - -using namespace QMatrixClient; - -BaseJob::Status SetRoomStateJob::parseJson(const QJsonDocument& data) -{ - _eventId = data.object().value("event_id").toString(); - if (!_eventId.isEmpty()) - return Success; - - qCDebug(JOBS) << data; - return { UserDefinedError, "No event_id in the JSON response" }; -} - diff --git a/jobs/setroomstatejob.h b/jobs/setroomstatejob.h deleted file mode 100644 index b7e6d4a1..00000000 --- a/jobs/setroomstatejob.h +++ /dev/null @@ -1,64 +0,0 @@ -/****************************************************************************** - * Copyright (C) 2015 Felix Rohrbach - * - * 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 - */ - -#pragma once - -#include "basejob.h" - -#include "connectiondata.h" - -namespace QMatrixClient -{ - class SetRoomStateJob: public BaseJob - { - public: - /** - * Constructs a job that sets a state using an arbitrary room event - * with a state key. - */ - template - SetRoomStateJob(const QString& roomId, const QString& stateKey, - const EvT& event) - : BaseJob(HttpVerb::Put, "SetRoomStateJob", - QStringLiteral("_matrix/client/r0/rooms/%1/state/%2/%3") - .arg(roomId, EvT::TypeId, stateKey), - Query(), - Data(event.toJson())) - { } - /** - * Constructs a job that sets a state using an arbitrary room event - * without a state key. - */ - template - SetRoomStateJob(const QString& roomId, const EvT& event) - : BaseJob(HttpVerb::Put, "SetRoomStateJob", - QStringLiteral("_matrix/client/r0/rooms/%1/state/%2") - .arg(roomId, EvT::TypeId), - Query(), - Data(event.toJson())) - { } - - QString eventId() const { return _eventId; } - - protected: - Status parseJson(const QJsonDocument& data) override; - - private: - QString _eventId; - }; -} // namespace QMatrixClient diff --git a/jobs/syncjob.cpp b/jobs/syncjob.cpp deleted file mode 100644 index 435dfd0e..00000000 --- a/jobs/syncjob.cpp +++ /dev/null @@ -1,133 +0,0 @@ -/****************************************************************************** - * Copyright (C) 2016 Felix Rohrbach - * - * 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 - */ - -#include "syncjob.h" - -#include - -using namespace QMatrixClient; - -static size_t jobId = 0; - -SyncJob::SyncJob(const QString& since, const QString& filter, int timeout, - const QString& presence) - : BaseJob(HttpVerb::Get, QStringLiteral("SyncJob-%1").arg(++jobId), - QStringLiteral("_matrix/client/r0/sync")) -{ - setLoggingCategory(SYNCJOB); - QUrlQuery query; - if( !filter.isEmpty() ) - query.addQueryItem("filter", filter); - if( !presence.isEmpty() ) - query.addQueryItem("set_presence", presence); - if( timeout >= 0 ) - query.addQueryItem("timeout", QString::number(timeout)); - if( !since.isEmpty() ) - query.addQueryItem("since", since); - setRequestQuery(query); - - setMaxRetries(std::numeric_limits::max()); -} - -QString SyncData::nextBatch() const -{ - return nextBatch_; -} - -SyncDataList&& SyncData::takeRoomData() -{ - return std::move(roomData); -} - -SyncBatch&& SyncData::takeAccountData() -{ - return std::move(accountData); -} - -BaseJob::Status SyncJob::parseJson(const QJsonDocument& data) -{ - return d.parseJson(data); -} - -BaseJob::Status SyncData::parseJson(const QJsonDocument &data) -{ - QElapsedTimer et; et.start(); - - auto json = data.object(); - nextBatch_ = json.value("next_batch").toString(); - // TODO: presence - accountData.fromJson(json); - - QJsonObject rooms = json.value("rooms").toObject(); - JoinStates::Int ii = 1; // ii is used to make a JoinState value - for (size_t i = 0; i < JoinStateStrings.size(); ++i, ii <<= 1) - { - const auto rs = rooms.value(JoinStateStrings[i]).toObject(); - // We have a Qt container on the right and an STL one on the left - roomData.reserve(static_cast(rs.size())); - for(auto roomIt = rs.begin(); roomIt != rs.end(); ++roomIt) - roomData.emplace_back(roomIt.key(), JoinState(ii), - roomIt.value().toObject()); - } - qCDebug(PROFILER) << "*** SyncData::parseJson(): batch with" - << rooms.size() << "room(s) in" << et; - return BaseJob::Success; -} - -const QString SyncRoomData::UnreadCountKey = - QStringLiteral("x-qmatrixclient.unread_count"); - -SyncRoomData::SyncRoomData(const QString& roomId_, JoinState joinState_, - const QJsonObject& room_) - : roomId(roomId_) - , joinState(joinState_) - , state(joinState == JoinState::Invite ? "invite_state" : "state") - , timeline("timeline") - , ephemeral("ephemeral") - , accountData("account_data") -{ - switch (joinState) { - case JoinState::Invite: - state.fromJson(room_); - break; - case JoinState::Join: - state.fromJson(room_); - timeline.fromJson(room_); - ephemeral.fromJson(room_); - accountData.fromJson(room_); - break; - case JoinState::Leave: - state.fromJson(room_); - timeline.fromJson(room_); - break; - default: - qCWarning(SYNCJOB) << "SyncRoomData: Unknown JoinState value, ignoring:" << int(joinState); - } - - auto timelineJson = room_.value("timeline").toObject(); - timelineLimited = timelineJson.value("limited").toBool(); - timelinePrevBatch = timelineJson.value("prev_batch").toString(); - - auto unreadJson = room_.value("unread_notifications").toObject(); - unreadCount = unreadJson.value(UnreadCountKey).toInt(-2); - highlightCount = unreadJson.value("highlight_count").toInt(); - notificationCount = unreadJson.value("notification_count").toInt(); - if (highlightCount > 0 || notificationCount > 0) - qCDebug(SYNCJOB) << "Highlights: " << highlightCount - << " Notifications:" << notificationCount; -} diff --git a/jobs/syncjob.h b/jobs/syncjob.h deleted file mode 100644 index 919060be..00000000 --- a/jobs/syncjob.h +++ /dev/null @@ -1,99 +0,0 @@ -/****************************************************************************** - * Copyright (C) 2016 Felix Rohrbach - * - * 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 - */ - -#pragma once - -#include "basejob.h" - -#include "joinstate.h" -#include "events/event.h" -#include "util.h" - -namespace QMatrixClient -{ - template - class SyncBatch : public EventsBatch - { - public: - explicit SyncBatch(QString k) : jsonKey(std::move(k)) { } - void fromJson(const QJsonObject& roomContents) - { - EventsBatch::fromJson( - roomContents[jsonKey].toObject(), "events"); - } - - private: - QString jsonKey; - }; - - class SyncRoomData - { - public: - QString roomId; - JoinState joinState; - SyncBatch state; - SyncBatch timeline; - SyncBatch ephemeral; - SyncBatch accountData; - - bool timelineLimited; - QString timelinePrevBatch; - int unreadCount; - int highlightCount; - int notificationCount; - - SyncRoomData(const QString& roomId, JoinState joinState_, - const QJsonObject& room_); - SyncRoomData(SyncRoomData&&) = default; - SyncRoomData& operator=(SyncRoomData&&) = default; - - static const QString UnreadCountKey; - }; - // QVector cannot work with non-copiable objects, std::vector can. - using SyncDataList = std::vector; - - class SyncData - { - public: - BaseJob::Status parseJson(const QJsonDocument &data); - SyncBatch&& takeAccountData(); - SyncDataList&& takeRoomData(); - QString nextBatch() const; - - private: - QString nextBatch_; - SyncBatch accountData { "account_data" }; - SyncDataList roomData; - }; - - class SyncJob: public BaseJob - { - public: - explicit SyncJob(const QString& since = {}, - const QString& filter = {}, - int timeout = -1, const QString& presence = {}); - - SyncData &&takeData() { return std::move(d); } - - protected: - Status parseJson(const QJsonDocument& data) override; - - private: - SyncData d; - }; -} // namespace QMatrixClient diff --git a/joinstate.h b/joinstate.h deleted file mode 100644 index 42613895..00000000 --- a/joinstate.h +++ /dev/null @@ -1,48 +0,0 @@ -/****************************************************************************** - * Copyright (C) 2015 Felix Rohrbach - * - * 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 - */ - -#pragma once - -#include - -#include - -namespace QMatrixClient -{ - enum class JoinState - { - Join = 0x1, - Invite = 0x2, - Leave = 0x4 - }; - - Q_DECLARE_FLAGS(JoinStates, JoinState) - - // We cannot use REGISTER_ENUM outside of a Q_OBJECT and besides, we want - // to use strings that match respective JSON keys. - static const std::array JoinStateStrings - { { "join", "invite", "leave" } }; - - inline const char* toCString(JoinState js) - { - size_t state = size_t(js), index = 0; - while (state >>= 1) ++index; - return JoinStateStrings[index]; - } -} // namespace QMatrixClient -Q_DECLARE_OPERATORS_FOR_FLAGS(QMatrixClient::JoinStates) diff --git a/lib/avatar.cpp b/lib/avatar.cpp new file mode 100644 index 00000000..1ff2aae1 --- /dev/null +++ b/lib/avatar.cpp @@ -0,0 +1,187 @@ +/****************************************************************************** + * Copyright (C) 2017 Kitsune Ral + * + * 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 + */ + +#include "avatar.h" + +#include "jobs/mediathumbnailjob.h" +#include "events/eventcontent.h" +#include "connection.h" + +#include +#include + +using namespace QMatrixClient; + +class Avatar::Private +{ + public: + explicit Private(QIcon di, QUrl url = {}) + : _defaultIcon(di), _url(url) + { } + QImage get(Connection* connection, QSize size, + get_callback_t callback) const; + bool upload(UploadContentJob* job, upload_callback_t callback); + + bool checkUrl(QUrl url) const; + + const QIcon _defaultIcon; + QUrl _url; + + // The below are related to image caching, hence mutable + mutable QImage _originalImage; + mutable std::vector> _scaledImages; + mutable QSize _requestedSize; + mutable bool _bannedUrl = false; + mutable bool _fetched = false; + mutable QPointer _thumbnailRequest = nullptr; + mutable QPointer _uploadRequest = nullptr; + mutable std::vector callbacks; + mutable get_callback_t uploadCallback; +}; + +Avatar::Avatar(QIcon defaultIcon) + : d(std::make_unique(std::move(defaultIcon))) +{ } + +Avatar::Avatar(QUrl url, QIcon defaultIcon) + : d(std::make_unique(std::move(defaultIcon), std::move(url))) +{ } + +Avatar::Avatar(Avatar&&) = default; + +Avatar::~Avatar() = default; + +Avatar& Avatar::operator=(Avatar&&) = default; + +QImage Avatar::get(Connection* connection, int dimension, + get_callback_t callback) const +{ + return d->get(connection, {dimension, dimension}, callback); +} + +QImage Avatar::get(Connection* connection, int width, int height, + get_callback_t callback) const +{ + return d->get(connection, {width, height}, callback); +} + +bool Avatar::upload(Connection* connection, const QString& fileName, + upload_callback_t callback) const +{ + if (isJobRunning(d->_uploadRequest)) + return false; + return d->upload(connection->uploadFile(fileName), callback); +} + +bool Avatar::upload(Connection* connection, QIODevice* source, + upload_callback_t callback) const +{ + if (isJobRunning(d->_uploadRequest) || !source->isReadable()) + return false; + return d->upload(connection->uploadContent(source), callback); +} + +QString Avatar::mediaId() const +{ + return d->_url.authority() + d->_url.path(); +} + +QImage Avatar::Private::get(Connection* connection, QSize size, + get_callback_t callback) const +{ + // FIXME: Alternating between longer-width and longer-height requests + // is a sure way to trick the below code into constantly getting another + // image from the server because the existing one is alleged unsatisfactory. + // This is plain abuse by the client, though; so not critical for now. + if( ( !(_fetched || _thumbnailRequest) + || size.width() > _requestedSize.width() + || size.height() > _requestedSize.height() ) && checkUrl(_url) ) + { + qCDebug(MAIN) << "Getting avatar from" << _url.toString(); + _requestedSize = size; + if (isJobRunning(_thumbnailRequest)) + _thumbnailRequest->abandon(); + callbacks.emplace_back(std::move(callback)); + _thumbnailRequest = connection->getThumbnail(_url, size); + QObject::connect( _thumbnailRequest, &MediaThumbnailJob::success, [this] + { + _fetched = true; + _originalImage = _thumbnailRequest->scaledThumbnail(_requestedSize); + _scaledImages.clear(); + for (auto n: callbacks) + n(); + }); + } + + if( _originalImage.isNull() ) + { + if (_defaultIcon.isNull()) + return _originalImage; + + QPainter p { &_originalImage }; + _defaultIcon.paint(&p, { QPoint(), _defaultIcon.actualSize(size) }); + } + + for (auto p: _scaledImages) + if (p.first == size) + return p.second; + auto result = _originalImage.scaled(size, + Qt::KeepAspectRatio, Qt::SmoothTransformation); + _scaledImages.emplace_back(size, result); + return result; +} + +bool Avatar::Private::upload(UploadContentJob* job, upload_callback_t callback) +{ + _uploadRequest = job; + if (!isJobRunning(_uploadRequest)) + return false; + _uploadRequest->connect(_uploadRequest, &BaseJob::success, + [job,callback] { callback(job->contentUri()); }); + return true; +} + +bool Avatar::Private::checkUrl(QUrl url) const +{ + if (_bannedUrl || url.isEmpty()) + return false; + + // FIXME: Make "mxc" a library-wide constant and maybe even make + // the URL checker a Connection(?) method. + _bannedUrl = !(url.isValid() && + url.scheme() == "mxc" && url.path().count('/') == 1); + if (_bannedUrl) + qCWarning(MAIN) << "Avatar URL is invalid or not mxc-based:" + << url.toDisplayString(); + return !_bannedUrl; +} + +QUrl Avatar::url() const { return d->_url; } + +bool Avatar::updateUrl(const QUrl& newUrl) +{ + if (newUrl == d->_url) + return false; + + d->_url = newUrl; + d->_fetched = false; + if (isJobRunning(d->_thumbnailRequest)) + d->_thumbnailRequest->abandon(); + return true; +} + diff --git a/lib/avatar.h b/lib/avatar.h new file mode 100644 index 00000000..0166ae9e --- /dev/null +++ b/lib/avatar.h @@ -0,0 +1,61 @@ +/****************************************************************************** + * Copyright (C) 2017 Kitsune Ral + * + * 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 + */ + +#pragma once + +#include +#include + +#include +#include + +namespace QMatrixClient +{ + class Connection; + + class Avatar + { + public: + explicit Avatar(QIcon defaultIcon = {}); + explicit Avatar(QUrl url, QIcon defaultIcon = {}); + Avatar(Avatar&&); + ~Avatar(); + Avatar& operator=(Avatar&&); + + using get_callback_t = std::function; + using upload_callback_t = std::function; + + QImage get(Connection* connection, int dimension, + get_callback_t callback) const; + QImage get(Connection* connection, int w, int h, + get_callback_t callback) const; + + bool upload(Connection* connection, const QString& fileName, + upload_callback_t callback) const; + bool upload(Connection* connection, QIODevice* source, + upload_callback_t callback) const; + + QString mediaId() const; + QUrl url() const; + bool updateUrl(const QUrl& newUrl); + + private: + class Private; + std::unique_ptr d; + }; +} // namespace QMatrixClient diff --git a/lib/connection.cpp b/lib/connection.cpp new file mode 100644 index 00000000..2d7235b9 --- /dev/null +++ b/lib/connection.cpp @@ -0,0 +1,943 @@ +/****************************************************************************** + * Copyright (C) 2015 Felix Rohrbach + * + * 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 + */ + +#include "connection.h" +#include "connectiondata.h" +#include "user.h" +#include "events/event.h" +#include "events/directchatevent.h" +#include "room.h" +#include "settings.h" +#include "jobs/generated/login.h" +#include "jobs/generated/logout.h" +#include "jobs/generated/receipts.h" +#include "jobs/generated/leaving.h" +#include "jobs/generated/account-data.h" +#include "jobs/sendeventjob.h" +#include "jobs/joinroomjob.h" +#include "jobs/roommessagesjob.h" +#include "jobs/syncjob.h" +#include "jobs/mediathumbnailjob.h" +#include "jobs/downloadfilejob.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +using namespace QMatrixClient; + +using DirectChatsMap = QMultiHash; + +class Connection::Private +{ + public: + explicit Private(std::unique_ptr&& connection) + : data(move(connection)) + { } + Q_DISABLE_COPY(Private) + Private(Private&&) = delete; + Private operator=(Private&&) = delete; + + Connection* q = nullptr; + std::unique_ptr data; + // A complex key below is a pair of room name and whether its + // state is Invited. The spec mandates to keep Invited room state + // separately so we should, e.g., keep objects for Invite and + // Leave state of the same room. + QHash, Room*> roomMap; + QVector roomIdsToForget; + QMap userMap; + DirectChatsMap directChats; + QHash accountData; + QString userId; + + SyncJob* syncJob = nullptr; + + bool cacheState = true; + bool cacheToBinary = SettingsGroup("libqmatrixclient") + .value("cache_type").toString() != "json"; + + void connectWithToken(const QString& user, const QString& accessToken, + const QString& deviceId); + void broadcastDirectChatUpdates(); +}; + +Connection::Connection(const QUrl& server, QObject* parent) + : QObject(parent) + , d(std::make_unique(std::make_unique(server))) +{ + d->q = this; // All d initialization should occur before this line +} + +Connection::Connection(QObject* parent) + : Connection({}, parent) +{ } + +Connection::~Connection() +{ + qCDebug(MAIN) << "deconstructing connection object for" << d->userId; + stopSync(); +} + +void Connection::resolveServer(const QString& mxidOrDomain) +{ + // At this point we may have something as complex as + // @username:[IPv6:address]:port, or as simple as a plain domain name. + + // Try to parse as an FQID; if there's no @ part, assume it's a domain name. + QRegularExpression parser( + "^(@.+?:)?" // Optional username (allow everything for compatibility) + "(\\[[^]]+\\]|[^:@]+)" // Either IPv6 address or hostname/IPv4 address + "(:\\d{1,5})?$", // Optional port + QRegularExpression::UseUnicodePropertiesOption); // Because asian digits + auto match = parser.match(mxidOrDomain); + + QUrl maybeBaseUrl = QUrl::fromUserInput(match.captured(2)); + maybeBaseUrl.setScheme("https"); // Instead of the Qt-default "http" + if (!match.hasMatch() || !maybeBaseUrl.isValid()) + { + emit resolveError( + tr("%1 is not a valid homeserver address") + .arg(maybeBaseUrl.toString())); + return; + } + + setHomeserver(maybeBaseUrl); + emit resolved(); + return; + + // FIXME, #178: The below code is incorrect and is no more executed. The + // correct server resolution should be done from .well-known/matrix/client + auto domain = maybeBaseUrl.host(); + qCDebug(MAIN) << "Finding the server" << domain; + // Check if the Matrix server has a dedicated service record. + QDnsLookup* dns = new QDnsLookup(); + dns->setType(QDnsLookup::SRV); + dns->setName("_matrix._tcp." + domain); + + connect(dns, &QDnsLookup::finished, [this,dns,maybeBaseUrl]() { + QUrl baseUrl { maybeBaseUrl }; + if (dns->error() == QDnsLookup::NoError && + dns->serviceRecords().isEmpty()) + { + auto record = dns->serviceRecords().front(); + baseUrl.setHost(record.target()); + baseUrl.setPort(record.port()); + qCDebug(MAIN) << "SRV record for" << maybeBaseUrl.host() + << "is" << baseUrl.authority(); + } else { + qCDebug(MAIN) << baseUrl.host() << "doesn't have SRV record" + << dns->name() << "- using the hostname as is"; + } + setHomeserver(baseUrl); + emit resolved(); + dns->deleteLater(); + }); + dns->lookup(); +} + +void Connection::connectToServer(const QString& user, const QString& password, + const QString& initialDeviceName, + const QString& deviceId) +{ + checkAndConnect(user, + [=] { + doConnectToServer(user, password, initialDeviceName, deviceId); + }); +} +void Connection::doConnectToServer(const QString& user, const QString& password, + const QString& initialDeviceName, + const QString& deviceId) +{ + auto loginJob = callApi(QStringLiteral("m.login.password"), + user, /*medium*/ "", /*address*/ "", password, /*token*/ "", + deviceId, initialDeviceName); + connect(loginJob, &BaseJob::success, this, + [this, loginJob] { + d->connectWithToken(loginJob->userId(), loginJob->accessToken(), + loginJob->deviceId()); + }); + connect(loginJob, &BaseJob::failure, this, + [this, loginJob] { + emit loginError(loginJob->errorString()); + }); +} + +void Connection::connectWithToken(const QString& userId, + const QString& accessToken, + const QString& deviceId) +{ + checkAndConnect(userId, + [=] { d->connectWithToken(userId, accessToken, deviceId); }); +} + +void Connection::Private::connectWithToken(const QString& user, + const QString& accessToken, + const QString& deviceId) +{ + userId = user; + data->setToken(accessToken.toLatin1()); + data->setDeviceId(deviceId); + qCDebug(MAIN) << "Using server" << data->baseUrl().toDisplayString() + << "by user" << userId << "from device" << deviceId; + emit q->connected(); + +} + +void Connection::checkAndConnect(const QString& userId, + std::function connectFn) +{ + if (d->data->baseUrl().isValid()) + { + connectFn(); + return; + } + // Not good to go, try to fix the homeserver URL. + if (userId.startsWith('@') && userId.indexOf(':') != -1) + { + // The below construct makes a single-shot connection that triggers + // on the signal and then self-disconnects. + // NB: doResolveServer can emit resolveError, so this is a part of + // checkAndConnect function contract. + QMetaObject::Connection connection; + connection = connect(this, &Connection::homeserverChanged, + this, [=] { connectFn(); disconnect(connection); }); + resolveServer(userId); + } else + emit resolveError( + tr("%1 is an invalid homeserver URL") + .arg(d->data->baseUrl().toString())); +} + +void Connection::logout() +{ + auto job = callApi(); + connect( job, &LogoutJob::success, this, [this] { + stopSync(); + emit loggedOut(); + }); +} + +void Connection::sync(int timeout) +{ + if (d->syncJob) + return; + + // Raw string: http://en.cppreference.com/w/cpp/language/string_literal + const QString filter { R"({"room": { "timeline": { "limit": 100 } } })" }; + auto job = d->syncJob = + callApi(d->data->lastEvent(), filter, timeout); + connect( job, &SyncJob::success, [this, job] { + onSyncSuccess(job->takeData()); + d->syncJob = nullptr; + emit syncDone(); + }); + connect( job, &SyncJob::retryScheduled, this, &Connection::networkError); + connect( job, &SyncJob::failure, [this, job] { + d->syncJob = nullptr; + if (job->error() == BaseJob::ContentAccessError) + emit loginError(job->errorString()); + else + emit syncError(job->errorString()); + }); +} + +void Connection::onSyncSuccess(SyncData &&data) { + d->data->setLastEvent(data.nextBatch()); + for (auto&& roomData: data.takeRoomData()) + { + const auto forgetIdx = d->roomIdsToForget.indexOf(roomData.roomId); + if (forgetIdx != -1) + { + d->roomIdsToForget.removeAt(forgetIdx); + if (roomData.joinState == JoinState::Leave) + { + qDebug(MAIN) << "Room" << roomData.roomId + << "has been forgotten, ignoring /sync response for it"; + continue; + } + qWarning(MAIN) << "Room" << roomData.roomId + << "has just been forgotten but /sync returned it in" + << toCString(roomData.joinState) + << "state - suspiciously fast turnaround"; + } + if ( auto* r = provideRoom(roomData.roomId, roomData.joinState) ) + r->updateData(std::move(roomData)); + QCoreApplication::processEvents(); + } + for (auto&& accountEvent: data.takeAccountData()) + { + if (accountEvent->type() == EventType::DirectChat) + { + DirectChatsMap newDirectChats; + const auto* event = static_cast(accountEvent.get()); + auto usersToDCs = event->usersToDirectChats(); + for (auto it = usersToDCs.begin(); it != usersToDCs.end(); ++it) + { + newDirectChats.insert(user(it.key()), it.value()); + qCDebug(MAIN) << "Marked room" << it.value() + << "as a direct chat with" << it.key(); + } + if (newDirectChats != d->directChats) + { + d->directChats = newDirectChats; + emit directChatsListChanged(); + } + continue; + } + d->accountData[accountEvent->jsonType()] = + accountEvent->contentJson().toVariantHash(); + } +} + +void Connection::stopSync() +{ + if (d->syncJob) + { + d->syncJob->abandon(); + d->syncJob = nullptr; + } +} + +void Connection::postMessage(Room* room, const QString& type, const QString& message) const +{ + callApi(room->id(), type, message); +} + +PostReceiptJob* Connection::postReceipt(Room* room, RoomEvent* event) const +{ + return callApi(room->id(), "m.read", event->id()); +} + +JoinRoomJob* Connection::joinRoom(const QString& roomAlias) +{ + auto job = callApi(roomAlias); + connect(job, &JoinRoomJob::success, + this, [this, job] { provideRoom(job->roomId(), JoinState::Join); }); + return job; +} + +void Connection::leaveRoom(Room* room) +{ + callApi(room->id()); +} + +RoomMessagesJob* Connection::getMessages(Room* room, const QString& from) const +{ + return callApi(room->id(), from); +} + +inline auto splitMediaId(const QString& mediaId) +{ + auto idParts = mediaId.split('/'); + Q_ASSERT_X(idParts.size() == 2, __FUNCTION__, + ("'" + mediaId + + "' doesn't look like 'serverName/localMediaId'").toLatin1()); + return idParts; +} + +MediaThumbnailJob* Connection::getThumbnail(const QString& mediaId, QSize requestedSize) const +{ + auto idParts = splitMediaId(mediaId); + return callApi(idParts.front(), idParts.back(), + requestedSize); +} + +MediaThumbnailJob* Connection::getThumbnail(const QUrl& url, QSize requestedSize) const +{ + return getThumbnail(url.authority() + url.path(), requestedSize); +} + +MediaThumbnailJob* Connection::getThumbnail(const QUrl& url, int requestedWidth, + int requestedHeight) const +{ + return getThumbnail(url, QSize(requestedWidth, requestedHeight)); +} + +UploadContentJob* Connection::uploadContent(QIODevice* contentSource, + const QString& filename, const QString& contentType) const +{ + return callApi(contentSource, filename, contentType); +} + +UploadContentJob* Connection::uploadFile(const QString& fileName, + const QString& contentType) +{ + auto sourceFile = new QFile(fileName); + if (sourceFile->open(QIODevice::ReadOnly)) + { + qCWarning(MAIN) << "Couldn't open" << sourceFile->fileName() + << "for reading"; + return nullptr; + } + return uploadContent(sourceFile, QFileInfo(*sourceFile).fileName(), + contentType); +} + +GetContentJob* Connection::getContent(const QString& mediaId) const +{ + auto idParts = splitMediaId(mediaId); + return callApi(idParts.front(), idParts.back()); +} + +GetContentJob* Connection::getContent(const QUrl& url) const +{ + return getContent(url.authority() + url.path()); +} + +DownloadFileJob* Connection::downloadFile(const QUrl& url, + const QString& localFilename) const +{ + auto mediaId = url.authority() + url.path(); + auto idParts = splitMediaId(mediaId); + auto* job = callApi(idParts.front(), idParts.back(), + localFilename); + return job; +} + +CreateRoomJob* Connection::createRoom(RoomVisibility visibility, + const QString& alias, const QString& name, const QString& topic, + const QVector& invites, const QString& presetName, + bool isDirect, bool guestsCanJoin, + const QVector& initialState, + const QVector& invite3pids, + const QJsonObject creationContent) +{ + auto job = callApi( + visibility == PublishRoom ? "public" : "private", alias, name, + topic, invites, invite3pids, creationContent, initialState, + presetName, isDirect, guestsCanJoin); + connect(job, &BaseJob::success, this, [this,job] { + emit createdRoom(provideRoom(job->roomId(), JoinState::Join)); + }); + return job; +} + +void Connection::requestDirectChat(const QString& userId) +{ + doInDirectChat(userId, [this] (Room* r) { emit directChatAvailable(r); }); +} + +void Connection::doInDirectChat(const QString& userId, + std::function operation) +{ + // There can be more than one DC; find the first valid, and delete invalid + // (left/forgotten) ones along the way. + for (auto roomId: d->directChats.values(user(userId))) + { + if (auto r = room(roomId, JoinState::Join)) + { + Q_ASSERT(r->id() == roomId); + qCDebug(MAIN) << "Requested direct chat with" << userId + << "is already available as" << r->id(); + operation(r); + return; + } + if (auto ir = invitation(roomId)) + { + Q_ASSERT(ir->id() == roomId); + auto j = joinRoom(ir->id()); + connect(j, &BaseJob::success, this, [this,roomId,userId,operation] { + qCDebug(MAIN) << "Joined the already invited direct chat with" + << userId << "as" << roomId; + operation(room(roomId, JoinState::Join)); + }); + } + qCWarning(MAIN) << "Direct chat with" << userId << "known as room" + << roomId << "is not valid, discarding it"; + removeFromDirectChats(roomId); + } + + auto j = createDirectChat(userId); + connect(j, &BaseJob::success, this, [this,j,userId,operation] { + qCDebug(MAIN) << "Direct chat with" << userId + << "has been created as" << j->roomId(); + operation(room(j->roomId(), JoinState::Join)); + }); +} + +CreateRoomJob* Connection::createDirectChat(const QString& userId, + const QString& topic, const QString& name) +{ + return createRoom(UnpublishRoom, "", name, topic, {userId}, + "trusted_private_chat", true); +} + +ForgetRoomJob* Connection::forgetRoom(const QString& id) +{ + // To forget is hard :) First we should ensure the local user is not + // in the room (by leaving it, if necessary); once it's done, the /forget + // endpoint can be called; and once this is through, the local Room object + // (if any existed) is deleted. At the same time, we still have to + // (basically immediately) return a pointer to ForgetRoomJob. Therefore + // a ForgetRoomJob is created in advance and can be returned in a probably + // not-yet-started state (it will start once /leave completes). + auto forgetJob = new ForgetRoomJob(id); + auto room = d->roomMap.value({id, false}); + if (!room) + room = d->roomMap.value({id, true}); + if (room && room->joinState() != JoinState::Leave) + { + auto leaveJob = room->leaveRoom(); + connect(leaveJob, &BaseJob::success, this, [this, forgetJob, room] { + forgetJob->start(connectionData()); + // If the matching /sync response hasn't arrived yet, mark the room + // for explicit deletion + if (room->joinState() != JoinState::Leave) + d->roomIdsToForget.push_back(room->id()); + }); + connect(leaveJob, &BaseJob::failure, forgetJob, &BaseJob::abandon); + } + else + forgetJob->start(connectionData()); + connect(forgetJob, &BaseJob::success, this, [this, id] + { + // If the room is in the map (possibly in both forms), delete all forms. + for (auto f: {false, true}) + if (auto r = d->roomMap.take({ id, f })) + { + emit aboutToDeleteRoom(r); + qCDebug(MAIN) << "Room" << id + << "in join state" << toCString(r->joinState()) + << "will be deleted"; + r->deleteLater(); + } + }); + return forgetJob; +} + +QUrl Connection::homeserver() const +{ + return d->data->baseUrl(); +} + +Room* Connection::room(const QString& roomId, JoinStates states) const +{ + Room* room = d->roomMap.value({roomId, false}, nullptr); + if (states.testFlag(JoinState::Join) && + room && room->joinState() == JoinState::Join) + return room; + + if (states.testFlag(JoinState::Invite)) + if (Room* invRoom = invitation(roomId)) + return invRoom; + + if (states.testFlag(JoinState::Leave) && + room && room->joinState() == JoinState::Leave) + return room; + + return nullptr; +} + +Room* Connection::invitation(const QString& roomId) const +{ + return d->roomMap.value({roomId, true}, nullptr); +} + +User* Connection::user(const QString& userId) +{ + if( d->userMap.contains(userId) ) + return d->userMap.value(userId); + auto* user = userFactory(this, userId); + d->userMap.insert(userId, user); + emit newUser(user); + return user; +} + +const User* Connection::user() const +{ + return d->userId.isEmpty() ? nullptr : d->userMap.value(d->userId, nullptr); +} + +User* Connection::user() +{ + return d->userId.isEmpty() ? nullptr : user(d->userId); +} + +QString Connection::userId() const +{ + return d->userId; +} + +QString Connection::deviceId() const +{ + return d->data->deviceId(); +} + +QString Connection::token() const +{ + return accessToken(); +} + +QByteArray Connection::accessToken() const +{ + return d->data->accessToken(); +} + +SyncJob* Connection::syncJob() const +{ + return d->syncJob; +} + +int Connection::millisToReconnect() const +{ + return d->syncJob ? d->syncJob->millisToRetry() : 0; +} + +QHash< QPair, Room* > Connection::roomMap() const +{ + // Copy-on-write-and-remove-elements is faster than copying elements one by one. + QHash< QPair, Room* > roomMap = d->roomMap; + for (auto it = roomMap.begin(); it != roomMap.end(); ) + { + if (it.value()->joinState() == JoinState::Leave) + it = roomMap.erase(it); + else + ++it; + } + return roomMap; +} + +QHash> Connection::tagsToRooms() const +{ + QHash> result; + for (auto* r: d->roomMap) + { + for (const auto& tagName: r->tagNames()) + result[tagName].push_back(r); + } + for (auto it = result.begin(); it != result.end(); ++it) + std::sort(it->begin(), it->end(), + [t=it.key()] (Room* r1, Room* r2) { + return r1->tags().value(t).order < r2->tags().value(t).order; + }); + return result; +} + +QStringList Connection::tagNames() const +{ + QStringList tags ({FavouriteTag}); + for (auto* r: d->roomMap) + for (const auto& tag: r->tagNames()) + if (tag != LowPriorityTag && !tags.contains(tag)) + tags.push_back(tag); + tags.push_back(LowPriorityTag); + return tags; +} + +QVector Connection::roomsWithTag(const QString& tagName) const +{ + QVector rooms; + std::copy_if(d->roomMap.begin(), d->roomMap.end(), std::back_inserter(rooms), + [&tagName] (Room* r) { return r->tags().contains(tagName); }); + return rooms; +} + +QJsonObject toJson(const DirectChatsMap& directChats) +{ + QJsonObject json; + for (auto it = directChats.keyBegin(); it != directChats.keyEnd(); ++it) + json.insert((*it)->id(), toJson(directChats.values(*it))); + return json; +} + +void Connection::Private::broadcastDirectChatUpdates() +{ + q->callApi(userId, QStringLiteral("m.direct"), + toJson(directChats)); + emit q->directChatsListChanged(); +} + +void Connection::addToDirectChats(const Room* room, const User* user) +{ + Q_ASSERT(room != nullptr && user != nullptr); + if (d->directChats.contains(user, room->id())) + return; + d->directChats.insert(user, room->id()); + d->broadcastDirectChatUpdates(); +} + +void Connection::removeFromDirectChats(const QString& roomId, const User* user) +{ + Q_ASSERT(!roomId.isEmpty()); + if ((user != nullptr && !d->directChats.contains(user, roomId)) || + d->directChats.key(roomId) == nullptr) + return; + if (user != nullptr) + d->directChats.remove(user, roomId); + else + for (auto it = d->directChats.begin(); it != d->directChats.end();) + { + if (it.value() == roomId) + it = d->directChats.erase(it); + else + ++it; + } + d->broadcastDirectChatUpdates(); +} + +bool Connection::isDirectChat(const QString& roomId) const +{ + return d->directChats.key(roomId) != nullptr; +} + +QList Connection::directChatUsers(const Room* room) const +{ + Q_ASSERT(room != nullptr); + return d->directChats.keys(room->id()); +} + +QMap Connection::users() const +{ + return d->userMap; +} + +const ConnectionData* Connection::connectionData() const +{ + return d->data.get(); +} + +Room* Connection::provideRoom(const QString& id, JoinState joinState) +{ + // TODO: This whole function is a strong case for a RoomManager class. + Q_ASSERT_X(!id.isEmpty(), __FUNCTION__, "Empty room id"); + + const auto roomKey = qMakePair(id, joinState == JoinState::Invite); + auto* room = d->roomMap.value(roomKey, nullptr); + if (room) + { + // Leave is a special case because in transition (5a) (see the .h file) + // joinState == room->joinState but we still have to preempt the Invite + // and emit a signal. For Invite and Join, there's no such problem. + if (room->joinState() == joinState && joinState != JoinState::Leave) + return room; + } + else + { + room = roomFactory(this, id, joinState); + if (!room) + { + qCCritical(MAIN) << "Failed to create a room" << id; + return nullptr; + } + d->roomMap.insert(roomKey, room); + emit newRoom(room); + } + if (joinState == JoinState::Invite) + { + // prev is either Leave or nullptr + auto* prev = d->roomMap.value({id, false}, nullptr); + emit invitedRoom(room, prev); + } + else + { + room->setJoinState(joinState); + // Preempt the Invite room (if any) with a room in Join/Leave state. + auto* prevInvite = d->roomMap.take({id, true}); + if (joinState == JoinState::Join) + emit joinedRoom(room, prevInvite); + else if (joinState == JoinState::Leave) + emit leftRoom(room, prevInvite); + if (prevInvite) + { + qCDebug(MAIN) << "Deleting Invite state for room" << prevInvite->id(); + emit aboutToDeleteRoom(prevInvite); + prevInvite->deleteLater(); + } + } + + return room; +} + +Connection::room_factory_t Connection::roomFactory = + [](Connection* c, const QString& id, JoinState joinState) + { return new Room(c, id, joinState); }; + +Connection::user_factory_t Connection::userFactory = + [](Connection* c, const QString& id) { return new User(id, c); }; + +QByteArray Connection::generateTxnId() +{ + return d->data->generateTxnId(); +} + +void Connection::setHomeserver(const QUrl& url) +{ + if (homeserver() == url) + return; + + d->data->setBaseUrl(url); + emit homeserverChanged(homeserver()); +} + +static constexpr int CACHE_VERSION_MAJOR = 7; +static constexpr int CACHE_VERSION_MINOR = 0; + +void Connection::saveState(const QUrl &toFile) const +{ + if (!d->cacheState) + return; + + QElapsedTimer et; et.start(); + + QFileInfo stateFile { + toFile.isEmpty() ? stateCachePath() : toFile.toLocalFile() + }; + if (!stateFile.dir().exists()) + stateFile.dir().mkpath("."); + + QFile outfile { stateFile.absoluteFilePath() }; + if (!outfile.open(QFile::WriteOnly)) + { + qCWarning(MAIN) << "Error opening" << stateFile.absoluteFilePath() + << ":" << outfile.errorString(); + qCWarning(MAIN) << "Caching the rooms state disabled"; + d->cacheState = false; + return; + } + + QJsonObject rootObj; + { + QJsonObject rooms; + QJsonObject inviteRooms; + for (const auto* i : roomMap()) // Pass on rooms in Leave state + { + if (i->joinState() == JoinState::Invite) + inviteRooms.insert(i->id(), i->toJson()); + else + rooms.insert(i->id(), i->toJson()); + QElapsedTimer et1; et1.start(); + QCoreApplication::processEvents(); + if (et1.elapsed() > 1) + qCDebug(PROFILER) << "processEvents() borrowed" << et1; + } + + QJsonObject roomObj; + if (!rooms.isEmpty()) + roomObj.insert("join", rooms); + if (!inviteRooms.isEmpty()) + roomObj.insert("invite", inviteRooms); + + rootObj.insert("next_batch", d->data->lastEvent()); + rootObj.insert("rooms", roomObj); + } + { + QJsonArray accountDataEvents { + QJsonObject { + { QStringLiteral("type"), QStringLiteral("m.direct") }, + { QStringLiteral("content"), toJson(d->directChats) } + } + }; + + for (auto it = d->accountData.begin(); it != d->accountData.end(); ++it) + accountDataEvents.append(QJsonObject { + {"type", it.key()}, + {"content", QJsonObject::fromVariantHash(it.value())} + }); + rootObj.insert("account_data", + QJsonObject {{ QStringLiteral("events"), accountDataEvents }}); + } + + QJsonObject versionObj; + versionObj.insert("major", CACHE_VERSION_MAJOR); + versionObj.insert("minor", CACHE_VERSION_MINOR); + rootObj.insert("cache_version", versionObj); + + QJsonDocument json { rootObj }; + auto data = d->cacheToBinary ? json.toBinaryData() : + json.toJson(QJsonDocument::Compact); + qCDebug(PROFILER) << "Cache for" << userId() << "generated in" << et; + + outfile.write(data.data(), data.size()); + qCDebug(MAIN) << "State cache saved to" << outfile.fileName(); +} + +void Connection::loadState(const QUrl &fromFile) +{ + if (!d->cacheState) + return; + + QElapsedTimer et; et.start(); + QFile file { + fromFile.isEmpty() ? stateCachePath() : fromFile.toLocalFile() + }; + if (!file.exists()) + { + qCDebug(MAIN) << "No state cache file found"; + return; + } + if(!file.open(QFile::ReadOnly)) + { + qCWarning(MAIN) << "file " << file.fileName() << "failed to open for read"; + return; + } + QByteArray data = file.readAll(); + + auto jsonDoc = d->cacheToBinary ? QJsonDocument::fromBinaryData(data) : + QJsonDocument::fromJson(data); + if (jsonDoc.isNull()) + { + qCWarning(MAIN) << "Cache file broken, discarding"; + return; + } + auto actualCacheVersionMajor = + jsonDoc.object() + .value("cache_version").toObject() + .value("major").toInt(); + if (actualCacheVersionMajor < CACHE_VERSION_MAJOR) + { + qCWarning(MAIN) + << "Major version of the cache file is" << actualCacheVersionMajor + << "but" << CACHE_VERSION_MAJOR << "required; discarding the cache"; + return; + } + + SyncData sync; + sync.parseJson(jsonDoc); + onSyncSuccess(std::move(sync)); + qCDebug(PROFILER) << "*** Cached state for" << userId() << "loaded in" << et; +} + +QString Connection::stateCachePath() const +{ + auto safeUserId = userId(); + safeUserId.replace(':', '_'); + return QStandardPaths::writableLocation(QStandardPaths::CacheLocation) + % '/' % safeUserId % "_state.json"; +} + +bool Connection::cacheState() const +{ + return d->cacheState; +} + +void Connection::setCacheState(bool newValue) +{ + if (d->cacheState != newValue) + { + d->cacheState = newValue; + emit cacheStateChanged(); + } +} + diff --git a/lib/connection.h b/lib/connection.h new file mode 100644 index 00000000..c6d543ec --- /dev/null +++ b/lib/connection.h @@ -0,0 +1,475 @@ +/****************************************************************************** + * Copyright (C) 2015 Felix Rohrbach + * + * 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 + */ + +#pragma once + +#include "jobs/generated/create_room.h" +#include "joinstate.h" + +#include +#include +#include + +#include +#include + +namespace QMatrixClient +{ + class Room; + class User; + class RoomEvent; + class ConnectionData; + + class SyncJob; + class SyncData; + class RoomMessagesJob; + class PostReceiptJob; + class ForgetRoomJob; + class MediaThumbnailJob; + class JoinRoomJob; + class UploadContentJob; + class GetContentJob; + class DownloadFileJob; + + class Connection: public QObject { + Q_OBJECT + + /** Whether or not the rooms state should be cached locally + * \sa loadState(), saveState() + */ + Q_PROPERTY(User* localUser READ user CONSTANT) + Q_PROPERTY(QString localUserId READ userId CONSTANT) + Q_PROPERTY(QString deviceId READ deviceId CONSTANT) + Q_PROPERTY(QByteArray accessToken READ accessToken CONSTANT) + Q_PROPERTY(QUrl homeserver READ homeserver WRITE setHomeserver NOTIFY homeserverChanged) + Q_PROPERTY(bool cacheState READ cacheState WRITE setCacheState NOTIFY cacheStateChanged) + public: + using room_factory_t = + std::function; + using user_factory_t = + std::function; + + enum RoomVisibility { PublishRoom, UnpublishRoom }; // FIXME: Should go inside CreateRoomJob + + explicit Connection(QObject* parent = nullptr); + explicit Connection(const QUrl& server, QObject* parent = nullptr); + virtual ~Connection(); + + /** Get all Invited and Joined rooms + * \return a hashmap from a composite key - room name and whether + * it's an Invite rather than Join - to room pointers + */ + QHash, Room*> roomMap() const; + + /** Get all Invited and Joined rooms grouped by tag + * \return a hashmap from tag name to a vector of room pointers, + * sorted by their order in the tag - details are at + * https://matrix.org/speculator/spec/drafts%2Fe2e/client_server/unstable.html#id95 + */ + QHash> tagsToRooms() const; + + /** Get all room tags known on this connection */ + QStringList tagNames() const; + + /** Get the list of rooms with the specified tag */ + QVector roomsWithTag(const QString& tagName) const; + + /** Mark the room as a direct chat with the user + * This function marks \p room as a direct chat with \p user. + * Emits the signal synchronously, without waiting to complete + * synchronisation with the server. + * + * \sa directChatsListChanged + */ + void addToDirectChats(const Room* room, const User* user); + + /** Unmark the room from direct chats + * This function removes the room id from direct chats either for + * a specific \p user or for all users if \p user in nullptr. + * The room id is used to allow removal of, e.g., ids of forgotten + * rooms; a Room object need not exist. Emits the signal + * immediately, without waiting to complete synchronisation with + * the server. + * + * \sa directChatsListChanged + */ + void removeFromDirectChats(const QString& roomId, + const User* user = nullptr); + + /** Check whether the room id corresponds to a direct chat */ + bool isDirectChat(const QString& roomId) const; + + /** Retrieve the list of users the room is a direct chat with + * @return The list of users for which this room is marked as + * a direct chat; an empty list if the room is not a direct chat + */ + QList directChatUsers(const Room* room) const; + + QMap users() const; + + // FIXME: Convert Q_INVOKABLEs to Q_PROPERTIES + // (breaks back-compatibility) + QUrl homeserver() const; + Q_INVOKABLE Room* room(const QString& roomId, + JoinStates states = JoinState::Invite|JoinState::Join) const; + Q_INVOKABLE Room* invitation(const QString& roomId) const; + Q_INVOKABLE User* user(const QString& userId); + const User* user() const; + User* user(); + QString userId() const; + QString deviceId() const; + /** @deprecated Use accessToken() instead. */ + Q_INVOKABLE QString token() const; + QByteArray accessToken() const; + Q_INVOKABLE SyncJob* syncJob() const; + Q_INVOKABLE int millisToReconnect() const; + + /** + * Call this before first sync to load from previously saved file. + * + * \param fromFile A local path to read the state from. Uses QUrl + * to be QML-friendly. Empty parameter means using a path + * defined by stateCachePath(). + */ + Q_INVOKABLE void loadState(const QUrl &fromFile = {}); + /** + * This method saves the current state of rooms (but not messages + * in them) to a local cache file, so that it could be loaded by + * loadState() on a next run of the client. + * + * \param toFile A local path to save the state to. Uses QUrl to be + * QML-friendly. Empty parameter means using a path defined by + * stateCachePath(). + */ + Q_INVOKABLE void saveState(const QUrl &toFile = {}) const; + + /** + * The default path to store the cached room state, defined as + * follows: + * QStandardPaths::writeableLocation(QStandardPaths::CacheLocation) + _safeUserId + "_state.json" + * where `_safeUserId` is userId() with `:` (colon) replaced with + * `_` (underscore) + * /see loadState(), saveState() + */ + Q_INVOKABLE QString stateCachePath() const; + + bool cacheState() const; + void setCacheState(bool newValue); + + /** + * This is a universal method to start a job of a type passed + * as a template parameter. Arguments to callApi() are arguments + * to the job constructor _except_ the first ConnectionData* + * argument - callApi() will pass it automatically. + */ + template + JobT* callApi(JobArgTs&&... jobArgs) const + { + auto job = new JobT(std::forward(jobArgs)...); + job->start(connectionData()); + return job; + } + + /** Generates a new transaction id. Transaction id's are unique within + * a single Connection object + */ + Q_INVOKABLE QByteArray generateTxnId(); + + template + static void setRoomType() + { + roomFactory = + [](Connection* c, const QString& id, JoinState joinState) + { return new T(c, id, joinState); }; + } + + template + static void setUserType() + { + userFactory = + [](Connection* c, const QString& id) { return new T(id, c); }; + } + + public slots: + /** Set the homeserver base URL */ + void setHomeserver(const QUrl& baseUrl); + + /** Determine and set the homeserver from domain or MXID */ + void resolveServer(const QString& mxidOrDomain); + + void connectToServer(const QString& user, const QString& password, + const QString& initialDeviceName, + const QString& deviceId = {}); + void connectWithToken(const QString& userId, const QString& accessToken, + const QString& deviceId); + + /** @deprecated Use stopSync() instead */ + void disconnectFromServer() { stopSync(); } + void logout(); + + void sync(int timeout = -1); + void stopSync(); + + virtual MediaThumbnailJob* getThumbnail(const QString& mediaId, + QSize requestedSize) const; + MediaThumbnailJob* getThumbnail(const QUrl& url, + QSize requestedSize) const; + MediaThumbnailJob* getThumbnail(const QUrl& url, + int requestedWidth, + int requestedHeight) const; + + // QIODevice* should already be open + UploadContentJob* uploadContent(QIODevice* contentSource, + const QString& filename = {}, + const QString& contentType = {}) const; + UploadContentJob* uploadFile(const QString& fileName, + const QString& contentType = {}); + GetContentJob* getContent(const QString& mediaId) const; + GetContentJob* getContent(const QUrl& url) const; + // If localFilename is empty, a temporary file will be created + DownloadFileJob* downloadFile(const QUrl& url, + const QString& localFilename = {}) const; + + /** + * \brief Create a room (generic method) + * This method allows to customize room entirely to your liking, + * providing all the attributes the original CS API provides. + */ + CreateRoomJob* createRoom(RoomVisibility visibility, + const QString& alias, const QString& name, const QString& topic, + const QVector& invites, const QString& presetName = {}, bool isDirect = false, + bool guestsCanJoin = false, + const QVector& initialState = {}, + const QVector& invite3pids = {}, + const QJsonObject creationContent = {}); + + /** Get a direct chat with a single user + * This method may return synchronously or asynchoronously depending + * on whether a direct chat room with the respective person exists + * already. + * + * \sa directChatAvailable + */ + Q_INVOKABLE void requestDirectChat(const QString& userId); + + /** Run an operation in a direct chat with the user + * This method may return synchronously or asynchoronously depending + * on whether a direct chat room with the respective person exists + * already. Instead of emitting a signal it executes the passed + * function object with the direct chat room as its parameter. + */ + Q_INVOKABLE void doInDirectChat(const QString& userId, + std::function operation); + + /** Create a direct chat with a single user, optional name and topic + * A room will always be created, unlike in requestDirectChat. + * It is advised to use requestDirectChat as a default way of getting + * one-on-one with a person, and only use createDirectChat when + * a new creation is explicitly desired. + */ + CreateRoomJob* createDirectChat(const QString& userId, + const QString& topic = {}, const QString& name = {}); + + virtual JoinRoomJob* joinRoom(const QString& roomAlias); + + /** Sends /forget to the server and also deletes room locally. + * This method is in Connection, not in Room, since it's a + * room lifecycle operation, and Connection is an acting room manager. + * It ensures that the local user is not a member of a room (running /leave, + * if necessary) then issues a /forget request and if that one doesn't fail + * deletion of the local Room object is ensured. + * \param id - the room id to forget + * \return - the ongoing /forget request to the server; note that the + * success() signal of this request is connected to deleteLater() + * of a respective room so by the moment this finishes, there might be no + * Room object anymore. + */ + ForgetRoomJob* forgetRoom(const QString& id); + + // Old API that will be abolished any time soon. DO NOT USE. + + /** @deprecated Use callApi() or Room::postMessage() instead */ + virtual void postMessage(Room* room, const QString& type, + const QString& message) const; + /** @deprecated Use callApi() or Room::postReceipt() instead */ + virtual PostReceiptJob* postReceipt(Room* room, + RoomEvent* event) const; + /** @deprecated Use callApi() or Room::leaveRoom() instead */ + virtual void leaveRoom( Room* room ); + /** @deprecated User callApi() or Room::getPreviousContent() instead */ + virtual RoomMessagesJob* getMessages(Room* room, + const QString& from) const; + signals: + /** + * @deprecated + * This was a signal resulting from a successful resolveServer(). + * Since Connection now provides setHomeserver(), the HS URL + * may change even without resolveServer() invocation. Use + * homeserverChanged() instead of resolved(). You can also use + * connectToServer and connectWithToken without the HS URL set in + * advance (i.e. without calling resolveServer), as they now trigger + * server name resolution from MXID if the server URL is not valid. + */ + void resolved(); + void resolveError(QString error); + + void homeserverChanged(QUrl baseUrl); + + void connected(); + void reconnected(); //< Unused; use connected() instead + void loggedOut(); + void loginError(QString error); + void networkError(int retriesTaken, int inMilliseconds); + + void syncDone(); + void syncError(QString error); + + void newUser(User* user); + + /** + * \group Signals emitted on room transitions + * + * Note: Rooms in Invite state are always stored separately from + * rooms in Join/Leave state, because of special treatment of + * invite_state in Matrix CS API (see The Spec on /sync for details). + * Therefore, objects below are: r - room in Join/Leave state; + * i - room in Invite state + * + * 1. none -> Invite: newRoom(r), invitedRoom(r,nullptr) + * 2. none -> Join: newRoom(r), joinedRoom(r,nullptr) + * 3. none -> Leave: newRoom(r), leftRoom(r,nullptr) + * 4. Invite -> Join: + * newRoom(r), joinedRoom(r,i), aboutToDeleteRoom(i) + * 4a. Leave and Invite -> Join: + * joinedRoom(r,i), aboutToDeleteRoom(i) + * 5. Invite -> Leave: + * newRoom(r), leftRoom(r,i), aboutToDeleteRoom(i) + * 5a. Leave and Invite -> Leave: + * leftRoom(r,i), aboutToDeleteRoom(i) + * 6. Join -> Leave: leftRoom(r) + * 7. Leave -> Invite: newRoom(i), invitedRoom(i,r) + * 8. Leave -> Join: joinedRoom(r) + * The following transitions are only possible via forgetRoom() + * so far; if a room gets forgotten externally, sync won't tell + * about it: + * 9. any -> none: as any -> Leave, then aboutToDeleteRoom(r) + */ + + /** A new room object has been created */ + void newRoom(Room* room); + + /** Invitation to a room received + * + * If the same room is in Left state, it's passed in prev. + */ + void invitedRoom(Room* room, Room* prev); + + /** A room has just been joined + * + * It's not the same as receiving a room in "join" section of sync + * response (rooms will be there even after joining). If this room + * was in Invite state before, the respective object is passed in + * prev (and it will be deleted shortly afterwards). + */ + void joinedRoom(Room* room, Room* prev); + + /** A room has just been left + * + * If this room has been in Invite state (as in case of rejecting + * an invitation), the respective object will be passed in prev + * (and will be deleted shortly afterwards). + */ + void leftRoom(Room* room, Room* prev); + + /** The room object is about to be deleted */ + void aboutToDeleteRoom(Room* room); + + /** The room has just been created by createRoom or requestDirectChat + * + * This signal is not emitted in usual room state transitions, + * only as an outcome of room creation operations invoked by + * the client. + * \note requestDirectChat doesn't necessarily create a new chat; + * use directChatAvailable signal if you just need to obtain + * a direct chat room. + */ + void createdRoom(Room* room); + + /** The direct chat room is ready for using + * This signal is emitted upon any successful outcome from + * requestDirectChat. + */ + void directChatAvailable(Room* directChat); + + /** The list of direct chats has changed + * This signal is emitted every time when the mapping of users + * to direct chat rooms is changed (because of either local updates + * or a different list arrived from the server). + */ + void directChatsListChanged(); + + void cacheStateChanged(); + + protected: + /** + * @brief Access the underlying ConnectionData class + */ + const ConnectionData* connectionData() const; + + /** + * @brief Find a (possibly new) Room object for the specified id + * Use this method whenever you need to find a Room object in + * the local list of rooms. Note that this does not interact with + * the server; in particular, does not automatically create rooms + * on the server. + * @return a pointer to a Room object with the specified id; nullptr + * if roomId is empty if roomFactory() failed to create a Room object. + */ + Room* provideRoom(const QString& roomId, JoinState joinState); + + /** + * Completes loading sync data. + */ + void onSyncSuccess(SyncData &&data); + + private: + class Private; + std::unique_ptr d; + + /** + * A single entry for functions that need to check whether the + * homeserver is valid before running. May either execute connectFn + * synchronously or asynchronously (if tryResolve is true and + * a DNS lookup is initiated); in case of errors, emits resolveError + * if the homeserver URL is not valid and cannot be resolved from + * userId. + * + * @param userId - fully-qualified MXID to resolve HS from + * @param connectFn - a function to execute once the HS URL is good + */ + void checkAndConnect(const QString& userId, + std::function connectFn); + void doConnectToServer(const QString& user, const QString& password, + const QString& initialDeviceName, + const QString& deviceId = {}); + + static room_factory_t roomFactory; + static user_factory_t userFactory; + }; +} // namespace QMatrixClient +Q_DECLARE_METATYPE(QMatrixClient::Connection*) diff --git a/lib/connectiondata.cpp b/lib/connectiondata.cpp new file mode 100644 index 00000000..4e9bc77e --- /dev/null +++ b/lib/connectiondata.cpp @@ -0,0 +1,108 @@ +/****************************************************************************** + * Copyright (C) 2015 Felix Rohrbach + * + * 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 + */ + +#include "connectiondata.h" + +#include "networkaccessmanager.h" +#include "logging.h" + +using namespace QMatrixClient; + +struct ConnectionData::Private +{ + explicit Private(const QUrl& url) : baseUrl(url) { } + + QUrl baseUrl; + QByteArray accessToken; + QString lastEvent; + QString deviceId; + + mutable unsigned int txnCounter = 0; + const qint64 id = QDateTime::currentMSecsSinceEpoch(); +}; + +ConnectionData::ConnectionData(QUrl baseUrl) + : d(std::make_unique(baseUrl)) +{ } + +ConnectionData::~ConnectionData() = default; + +QByteArray ConnectionData::accessToken() const +{ + return d->accessToken; +} + +QUrl ConnectionData::baseUrl() const +{ + return d->baseUrl; +} + +QNetworkAccessManager* ConnectionData::nam() const +{ + return NetworkAccessManager::instance(); +} + +void ConnectionData::setBaseUrl(QUrl baseUrl) +{ + d->baseUrl = baseUrl; + qCDebug(MAIN) << "updated baseUrl to" << d->baseUrl; +} + +void ConnectionData::setToken(QByteArray token) +{ + d->accessToken = token; +} + +void ConnectionData::setHost(QString host) +{ + d->baseUrl.setHost(host); + qCDebug(MAIN) << "updated baseUrl to" << d->baseUrl; +} + +void ConnectionData::setPort(int port) +{ + d->baseUrl.setPort(port); + qCDebug(MAIN) << "updated baseUrl to" << d->baseUrl; +} + +const QString& ConnectionData::deviceId() const +{ + return d->deviceId; +} + +void ConnectionData::setDeviceId(const QString& deviceId) +{ + d->deviceId = deviceId; + qCDebug(MAIN) << "updated deviceId to" << d->deviceId; +} + +QString ConnectionData::lastEvent() const +{ + return d->lastEvent; +} + +void ConnectionData::setLastEvent(QString identifier) +{ + d->lastEvent = identifier; +} + +QByteArray ConnectionData::generateTxnId() const +{ + return QByteArray::number(d->id) + 'q' + + QByteArray::number(++d->txnCounter); +} diff --git a/lib/connectiondata.h b/lib/connectiondata.h new file mode 100644 index 00000000..7a2f2e90 --- /dev/null +++ b/lib/connectiondata.h @@ -0,0 +1,55 @@ +/****************************************************************************** + * Copyright (C) 2015 Felix Rohrbach + * + * 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 + */ + +#pragma once + +#include + +#include + +class QNetworkAccessManager; + +namespace QMatrixClient +{ + class ConnectionData + { + public: + explicit ConnectionData(QUrl baseUrl); + virtual ~ConnectionData(); + + QByteArray accessToken() const; + QUrl baseUrl() const; + const QString& deviceId() const; + + QNetworkAccessManager* nam() const; + void setBaseUrl(QUrl baseUrl); + void setToken(QByteArray accessToken); + void setHost( QString host ); + void setPort( int port ); + void setDeviceId(const QString& deviceId); + + QString lastEvent() const; + void setLastEvent( QString identifier ); + + QByteArray generateTxnId() const; + + private: + struct Private; + std::unique_ptr d; + }; +} // namespace QMatrixClient diff --git a/lib/converters.h b/lib/converters.h new file mode 100644 index 00000000..bba298e0 --- /dev/null +++ b/lib/converters.h @@ -0,0 +1,177 @@ +/****************************************************************************** +* Copyright (C) 2017 Kitsune Ral +* +* 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 +*/ + +#pragma once + +#include +#include // Includes +#include + +namespace QMatrixClient +{ + // This catches anything implicitly convertible to QJsonValue/Object/Array + inline QJsonValue toJson(const QJsonValue& val) { return val; } + inline QJsonObject toJson(const QJsonObject& o) { return o; } + inline QJsonArray toJson(const QJsonArray& arr) { return arr; } +#ifdef _MSC_VER // MSVC gets lost and doesn't know which overload to use + inline QJsonValue toJson(const QString& s) { return s; } +#endif + + template + inline QJsonArray toJson(const QVector& vals) + { + QJsonArray ar; + for (const auto& v: vals) + ar.push_back(toJson(v)); + return ar; + } + + inline QJsonArray toJson(const QStringList& strings) + { + return QJsonArray::fromStringList(strings); + } + + inline QJsonValue toJson(const QByteArray& bytes) + { + return QJsonValue(bytes.constData()); + } + + template + inline QJsonObject toJson(const QHash& hashMap) + { + QJsonObject json; + for (auto it = hashMap.begin(); it != hashMap.end(); ++it) + json.insert(it.key(), toJson(it.value())); + return json; + } + + template + struct FromJson + { + T operator()(const QJsonValue& jv) const { return static_cast(jv); } + }; + + template + inline T fromJson(const QJsonValue& jv) + { + return FromJson()(jv); + } + + template <> struct FromJson + { + bool operator()(const QJsonValue& jv) const { return jv.toBool(); } + }; + + template <> struct FromJson + { + int operator()(const QJsonValue& jv) const { return jv.toInt(); } + }; + + template <> struct FromJson + { + double operator()(const QJsonValue& jv) const { return jv.toDouble(); } + }; + + template <> struct FromJson + { + qint64 operator()(const QJsonValue& jv) const { return qint64(jv.toDouble()); } + }; + + template <> struct FromJson + { + QString operator()(const QJsonValue& jv) const { return jv.toString(); } + }; + + template <> struct FromJson + { + QDateTime operator()(const QJsonValue& jv) const + { + return QDateTime::fromMSecsSinceEpoch(fromJson(jv), Qt::UTC); + } + }; + + template <> struct FromJson + { + QDate operator()(const QJsonValue& jv) const + { + return fromJson(jv).date(); + } + }; + + template <> struct FromJson + { + QJsonObject operator()(const QJsonValue& jv) const + { + return jv.toObject(); + } + }; + + template <> struct FromJson + { + QJsonArray operator()(const QJsonValue& jv) const + { + return jv.toArray(); + } + }; + + template struct FromJson> + { + QVector operator()(const QJsonValue& jv) const + { + const auto jsonArray = jv.toArray(); + QVector vect; vect.resize(jsonArray.size()); + std::transform(jsonArray.begin(), jsonArray.end(), + vect.begin(), FromJson()); + return vect; + } + }; + + template struct FromJson> + { + QList operator()(const QJsonValue& jv) const + { + const auto jsonArray = jv.toArray(); + QList sl; sl.reserve(jsonArray.size()); + std::transform(jsonArray.begin(), jsonArray.end(), + std::back_inserter(sl), FromJson()); + return sl; + } + }; + + template <> struct FromJson : FromJson> { }; + + template <> struct FromJson + { + inline QByteArray operator()(const QJsonValue& jv) const + { + return fromJson(jv).toLatin1(); + } + }; + + template struct FromJson> + { + QHash operator()(const QJsonValue& jv) const + { + const auto json = jv.toObject(); + QHash h; h.reserve(json.size()); + for (auto it = json.begin(); it != json.end(); ++it) + h.insert(it.key(), fromJson(it.value())); + return h; + } + }; +} // namespace QMatrixClient diff --git a/lib/events/accountdataevents.h b/lib/events/accountdataevents.h new file mode 100644 index 00000000..f3ba27bb --- /dev/null +++ b/lib/events/accountdataevents.h @@ -0,0 +1,78 @@ +#include + +/****************************************************************************** + * Copyright (C) 2018 Kitsune Ral + * + * 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 + */ + +#pragma once + +#include "event.h" +#include "eventcontent.h" + +namespace QMatrixClient +{ + static constexpr const char* FavouriteTag = "m.favourite"; + static constexpr const char* LowPriorityTag = "m.lowpriority"; + + struct TagRecord + { + TagRecord (QString order = {}) : order(std::move(order)) { } + explicit TagRecord(const QJsonValue& jv) + : order(jv.toObject().value("order").toString()) + { } + + QString order; + + bool operator==(const TagRecord& other) const + { return order == other.order; } + bool operator!=(const TagRecord& other) const + { return !operator==(other); } + }; + + inline QJsonValue toJson(const TagRecord& rec) + { + return QJsonObject {{ QStringLiteral("order"), rec.order }}; + } + + using TagsMap = QHash; + +#define DEFINE_SIMPLE_EVENT(_Name, _TypeId, _EnumType, _ContentType, _ContentKey) \ + class _Name : public Event \ + { \ + public: \ + static constexpr const char* TypeId = _TypeId; \ + static const char* typeId() { return TypeId; } \ + explicit _Name(const QJsonObject& obj) \ + : Event((_EnumType), obj) \ + , _content(contentJson(), QStringLiteral(#_ContentKey)) \ + { } \ + template \ + explicit _Name(Ts&&... contentArgs) \ + : Event(_EnumType) \ + , _content(QStringLiteral(#_ContentKey), \ + std::forward(contentArgs)...) \ + { } \ + const _ContentType& _ContentKey() const { return _content.value; } \ + QJsonObject toJson() const { return _content.toJson(); } \ + protected: \ + EventContent::SimpleContent<_ContentType> _content; \ + }; + + DEFINE_SIMPLE_EVENT(TagEvent, "m.tag", EventType::Tag, TagsMap, tags) + DEFINE_SIMPLE_EVENT(ReadMarkerEvent, "m.fully_read", EventType::ReadMarker, + QString, event_id) +} diff --git a/lib/events/directchatevent.cpp b/lib/events/directchatevent.cpp new file mode 100644 index 00000000..7049d967 --- /dev/null +++ b/lib/events/directchatevent.cpp @@ -0,0 +1,36 @@ +/****************************************************************************** + * Copyright (C) 2018 Kitsune Ral + * + * 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 + */ + +#include "directchatevent.h" + +#include "converters.h" + +using namespace QMatrixClient; + +DirectChatEvent::DirectChatEvent(const QJsonObject& obj) + : Event(Type::DirectChat, obj) +{ } + +QMultiHash DirectChatEvent::usersToDirectChats() const +{ + QMultiHash result; + for (auto it = contentJson().begin(); it != contentJson().end(); ++it) + for (auto roomIdValue: it.value().toArray()) + result.insert(it.key(), roomIdValue.toString()); + return result; +} diff --git a/lib/events/directchatevent.h b/lib/events/directchatevent.h new file mode 100644 index 00000000..2b0ad0a0 --- /dev/null +++ b/lib/events/directchatevent.h @@ -0,0 +1,34 @@ +/****************************************************************************** + * Copyright (C) 2018 Kitsune Ral + * + * 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 + */ + +#pragma once + +#include "event.h" + +namespace QMatrixClient +{ + class DirectChatEvent : public Event + { + public: + explicit DirectChatEvent(const QJsonObject& obj); + + QMultiHash usersToDirectChats() const; + + static constexpr const char * TypeId = "m.direct"; + }; +} diff --git a/lib/events/event.cpp b/lib/events/event.cpp new file mode 100644 index 00000000..8ddf3945 --- /dev/null +++ b/lib/events/event.cpp @@ -0,0 +1,182 @@ +/****************************************************************************** + * Copyright (C) 2015 Felix Rohrbach + * + * 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 + */ + +#include "event.h" + +#include "roommessageevent.h" +#include "simplestateevents.h" +#include "roommemberevent.h" +#include "roomavatarevent.h" +#include "typingevent.h" +#include "receiptevent.h" +#include "accountdataevents.h" +#include "directchatevent.h" +#include "redactionevent.h" +#include "logging.h" + +#include + +using namespace QMatrixClient; + +Event::Event(Type type, const QJsonObject& rep) + : _type(type), _originalJson(rep) +{ + if (!rep.contains("content") && + !rep.value("unsigned").toObject().contains("redacted_because")) + { + qCWarning(EVENTS) << "Event without 'content' node"; + qCWarning(EVENTS) << formatJson << rep; + } +} + +Event::~Event() = default; + +QString Event::jsonType() const +{ + return originalJsonObject().value("type").toString(); +} + +QByteArray Event::originalJson() const +{ + return QJsonDocument(_originalJson).toJson(); +} + +QJsonObject Event::originalJsonObject() const +{ + return _originalJson; +} + +const QJsonObject Event::contentJson() const +{ + return _originalJson["content"].toObject(); +} + +template +inline BaseEventT* makeIfMatches(const QJsonObject&, const QString&) +{ + return nullptr; +} + +template +inline BaseEventT* makeIfMatches(const QJsonObject& o, const QString& selector) +{ + if (selector == EventT::TypeId) + return new EventT(o); + + return makeIfMatches(o, selector); +} + +template <> +EventPtr _impl::doMakeEvent(const QJsonObject& obj) +{ + // Check more specific event types first + if (auto e = doMakeEvent(obj)) + return EventPtr(move(e)); + + return EventPtr { makeIfMatches( + obj, obj["type"].toString()) }; +} + +RoomEvent::RoomEvent(Event::Type type) : Event(type) { } + +RoomEvent::RoomEvent(Type type, const QJsonObject& rep) + : Event(type, rep) + , _id(rep["event_id"].toString()) +// , _roomId(rep["room_id"].toString()) +// , _senderId(rep["sender"].toString()) +// , _serverTimestamp( +// QMatrixClient::fromJson(rep["origin_server_ts"])) +{ +// if (_id.isEmpty()) +// { +// qCWarning(EVENTS) << "Can't find event_id in a room event"; +// qCWarning(EVENTS) << formatJson << rep; +// } +// if (!rep.contains("origin_server_ts")) +// { +// qCWarning(EVENTS) << "Can't find server timestamp in a room event"; +// qCWarning(EVENTS) << formatJson << rep; +// } +// if (_senderId.isEmpty()) +// { +// qCWarning(EVENTS) << "Can't find sender in a room event"; +// qCWarning(EVENTS) << formatJson << rep; +// } + auto unsignedData = rep["unsigned"].toObject(); + auto redaction = unsignedData.value("redacted_because"); + if (redaction.isObject()) + { + _redactedBecause = + std::make_unique(redaction.toObject()); + return; + } + + _txnId = unsignedData.value("transactionId").toString(); + if (!_txnId.isEmpty()) + qCDebug(EVENTS) << "Event transactionId:" << _txnId; +} + +RoomEvent::~RoomEvent() = default; // Let the smart pointer do its job + +QDateTime RoomEvent::timestamp() const +{ + return QMatrixClient::fromJson( + originalJsonObject().value("origin_server_ts")); +} + +QString RoomEvent::roomId() const +{ + return originalJsonObject().value("room_id").toString(); +} + +QString RoomEvent::senderId() const +{ + return originalJsonObject().value("sender").toString(); +} + +QString RoomEvent::redactionReason() const +{ + return isRedacted() ? _redactedBecause->reason() : QString{}; +} + +void RoomEvent::addId(const QString& id) +{ + Q_ASSERT(_id.isEmpty()); Q_ASSERT(!id.isEmpty()); + _id = id; +} + +template <> +RoomEventPtr _impl::doMakeEvent(const QJsonObject& obj) +{ + return RoomEventPtr { makeIfMatches + (obj, obj["type"].toString()) }; +} + +StateEventBase::~StateEventBase() = default; + +bool StateEventBase::repeatsState() const +{ + auto contentJson = originalJsonObject().value("content"); + auto prevContentJson = originalJsonObject().value("unsigned") + .toObject().value("prev_content"); + return contentJson == prevContentJson; +} diff --git a/lib/events/event.h b/lib/events/event.h new file mode 100644 index 00000000..eccfec41 --- /dev/null +++ b/lib/events/event.h @@ -0,0 +1,314 @@ +/****************************************************************************** + * Copyright (C) 2015 Felix Rohrbach + * + * 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 + */ + +#pragma once + +#include +#include +#include +#include + +#include "util.h" + +#include + +namespace QMatrixClient +{ + template + using event_ptr_tt = std::unique_ptr; + + namespace _impl + { + template + event_ptr_tt doMakeEvent(const QJsonObject& obj); + } + + class Event + { + Q_GADGET + public: + enum class Type : quint16 + { + Unknown = 0, + Typing, Receipt, Tag, DirectChat, ReadMarker, + RoomEventBase = 0x1000, + RoomMessage = RoomEventBase + 1, + RoomEncryptedMessage, Redaction, + RoomStateEventBase = 0x1800, + RoomName = RoomStateEventBase + 1, + RoomAliases, RoomCanonicalAlias, RoomMember, RoomTopic, + RoomAvatar, RoomEncryption, RoomCreate, RoomJoinRules, + RoomPowerLevels, + Reserved = 0x2000 + }; + + explicit Event(Type type) : _type(type) { } + Event(Type type, const QJsonObject& rep); + Event(const Event&) = delete; + virtual ~Event(); + + Type type() const { return _type; } + QString jsonType() const; + bool isStateEvent() const + { + return (quint16(_type) & 0x1800) == 0x1800; + } + QByteArray originalJson() const; + QJsonObject originalJsonObject() const; + + // According to the CS API spec, every event also has + // a "content" object; but since its structure is different for + // different types, we're implementing it per-event type + // (and in most cases it will be a combination of other fields + // instead of "content" field). + + const QJsonObject contentJson() const; + + private: + Type _type; + QJsonObject _originalJson; + + REGISTER_ENUM(Type) + Q_PROPERTY(Type type READ type CONSTANT) + Q_PROPERTY(QJsonObject contentJson READ contentJson CONSTANT) + }; + using EventType = Event::Type; + using EventPtr = event_ptr_tt; + + /** Create an event with proper type from a JSON object + * Use this factory template to detect the type from the JSON object + * contents (the detected event type should derive from the template + * parameter type) and create an event object of that type. + */ + template + event_ptr_tt makeEvent(const QJsonObject& obj) + { + auto e = _impl::doMakeEvent(obj); + if (!e) + e = std::make_unique(EventType::Unknown, obj); + return e; + } + + namespace _impl + { + template <> + EventPtr doMakeEvent(const QJsonObject& obj); + } + + /** + * \brief A vector of pointers to events with deserialisation capabilities + * + * This is a simple wrapper over a generic vector type that adds + * a convenience method to deserialise events from QJsonArray. + * \tparam EventT base type of all events in the vector + */ + template + class EventsBatch : public std::vector> + { + public: + /** + * \brief Deserialise events from an array + * + * Given the following JSON construct, creates events from + * the array stored at key "node": + * \code + * "container": { + * "node": [ { "event_id": "!evt1:srv.org", ... }, ... ] + * } + * \endcode + * \param container - the wrapping JSON object + * \param node - the key in container that holds the array of events + */ + void fromJson(const QJsonObject& container, const QString& node) + { + const auto objs = container.value(node).toArray(); + using size_type = typename std::vector>::size_type; + // The below line accommodates the difference in size types of + // STL and Qt containers. + this->reserve(static_cast(objs.size())); + for (auto objValue: objs) + this->emplace_back(makeEvent(objValue.toObject())); + } + }; + using Events = EventsBatch; + + class RedactionEvent; + + /** This class corresponds to m.room.* events */ + class RoomEvent : public Event + { + Q_GADGET + Q_PROPERTY(QString id READ id) + Q_PROPERTY(QDateTime timestamp READ timestamp CONSTANT) + Q_PROPERTY(QString roomId READ roomId CONSTANT) + Q_PROPERTY(QString senderId READ senderId CONSTANT) + Q_PROPERTY(QString redactionReason READ redactionReason) + Q_PROPERTY(bool isRedacted READ isRedacted) + Q_PROPERTY(QString transactionId READ transactionId) + public: + // RedactionEvent is an incomplete type here so we cannot inline + // constructors and destructors + explicit RoomEvent(Type type); + RoomEvent(Type type, const QJsonObject& rep); + ~RoomEvent(); + + QString id() const { return _id; } + QDateTime timestamp() const; + QString roomId() const; + QString senderId() const; + bool isRedacted() const { return bool(_redactedBecause); } + const RedactionEvent* redactedBecause() const + { + return _redactedBecause.get(); + } + QString redactionReason() const; + const QString& transactionId() const { return _txnId; } + + /** + * Sets the transaction id for locally created events. This should be + * done before the event is exposed to any code using the respective + * Q_PROPERTY. + * + * \param txnId - transaction id, normally obtained from + * Connection::generateTxnId() + */ + void setTransactionId(const QString& txnId) { _txnId = txnId; } + + /** + * Sets event id for locally created events + * + * When a new event is created locally, it has no server id yet. + * This function allows to add the id once the confirmation from + * the server is received. There should be no id set previously + * in the event. It's the responsibility of the code calling addId() + * to notify clients that use Q_PROPERTY(id) about its change + */ + void addId(const QString& id); + + private: + QString _id; +// QString _roomId; +// QString _senderId; +// QDateTime _serverTimestamp; + event_ptr_tt _redactedBecause; + QString _txnId; + }; + using RoomEvents = EventsBatch; + using RoomEventPtr = event_ptr_tt; + + namespace _impl + { + template <> + RoomEventPtr doMakeEvent(const QJsonObject& obj); + } + + /** + * Conceptually similar to QStringView (but much more primitive), it's a + * simple abstraction over a pair of RoomEvents::const_iterator values + * referring to the beginning and the end of a range in a RoomEvents + * container. + */ + struct RoomEventsRange + { + RoomEvents::iterator from; + RoomEvents::iterator to; + + RoomEvents::size_type size() const + { + Q_ASSERT(std::distance(from, to) >= 0); + return RoomEvents::size_type(std::distance(from, to)); + } + bool empty() const { return from == to; } + RoomEvents::const_iterator begin() const { return from; } + RoomEvents::const_iterator end() const { return to; } + RoomEvents::iterator begin() { return from; } + RoomEvents::iterator end() { return to; } + }; + + class StateEventBase: public RoomEvent + { + public: + explicit StateEventBase(Type type, const QJsonObject& obj) + : RoomEvent(obj.contains("state_key") ? type : Type::Unknown, + obj) + { } + explicit StateEventBase(Type type) + : RoomEvent(type) + { } + ~StateEventBase() override = 0; + + virtual bool repeatsState() const; + }; + + template + struct Prev + { + template + explicit Prev(const QJsonObject& unsignedJson, + ContentParamTs&&... contentParams) + : senderId(unsignedJson.value("prev_sender").toString()) + , content(unsignedJson.value("prev_content").toObject(), + std::forward(contentParams)...) + { } + + QString senderId; + ContentT content; + }; + + template + class StateEvent: public StateEventBase + { + public: + using content_type = ContentT; + + template + explicit StateEvent(Type type, const QJsonObject& obj, + ContentParamTs&&... contentParams) + : StateEventBase(type, obj) + , _content(contentJson(), + std::forward(contentParams)...) + { + auto unsignedData = obj.value("unsigned").toObject(); + if (unsignedData.contains("prev_content")) + _prev = std::make_unique>(unsignedData, + std::forward(contentParams)...); + } + template + explicit StateEvent(Type type, ContentParamTs&&... contentParams) + : StateEventBase(type) + , _content(std::forward(contentParams)...) + { } + + QJsonObject toJson() const { return _content.toJson(); } + + const ContentT& content() const { return _content; } + /** @deprecated Use prevContent instead */ + const ContentT* prev_content() const { return prevContent(); } + const ContentT* prevContent() const + { return _prev ? &_prev->content : nullptr; } + QString prevSenderId() const { return _prev ? _prev->senderId : ""; } + + protected: + ContentT _content; + std::unique_ptr> _prev; + }; +} // namespace QMatrixClient +Q_DECLARE_METATYPE(QMatrixClient::Event*) +Q_DECLARE_METATYPE(QMatrixClient::RoomEvent*) +Q_DECLARE_METATYPE(const QMatrixClient::Event*) +Q_DECLARE_METATYPE(const QMatrixClient::RoomEvent*) diff --git a/lib/events/eventcontent.cpp b/lib/events/eventcontent.cpp new file mode 100644 index 00000000..f5974b46 --- /dev/null +++ b/lib/events/eventcontent.cpp @@ -0,0 +1,85 @@ +/****************************************************************************** + * Copyright (C) 2017 Kitsune Ral + * + * 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 + */ + +#include "eventcontent.h" + +#include +#include + +using namespace QMatrixClient::EventContent; + +QJsonObject Base::toJson() const +{ + QJsonObject o; + fillJson(&o); + return o; +} + +FileInfo::FileInfo(const QUrl& u, int payloadSize, const QMimeType& mimeType, + const QString& originalFilename) + : mimeType(mimeType), url(u), payloadSize(payloadSize) + , originalName(originalFilename) +{ } + +FileInfo::FileInfo(const QUrl& u, const QJsonObject& infoJson, + const QString& originalFilename) + : originalInfoJson(infoJson) + , mimeType(QMimeDatabase().mimeTypeForName(infoJson["mimetype"].toString())) + , url(u) + , payloadSize(infoJson["size"].toInt()) + , originalName(originalFilename) +{ + if (!mimeType.isValid()) + mimeType = QMimeDatabase().mimeTypeForData(QByteArray()); +} + +void FileInfo::fillInfoJson(QJsonObject* infoJson) const +{ + Q_ASSERT(infoJson); + infoJson->insert("size", payloadSize); + infoJson->insert("mimetype", mimeType.name()); +} + +ImageInfo::ImageInfo(const QUrl& u, int fileSize, QMimeType mimeType, + const QSize& imageSize) + : FileInfo(u, fileSize, mimeType), imageSize(imageSize) +{ } + +ImageInfo::ImageInfo(const QUrl& u, const QJsonObject& infoJson, + const QString& originalFilename) + : FileInfo(u, infoJson, originalFilename) + , imageSize(infoJson["w"].toInt(), infoJson["h"].toInt()) +{ } + +void ImageInfo::fillInfoJson(QJsonObject* infoJson) const +{ + FileInfo::fillInfoJson(infoJson); + infoJson->insert("w", imageSize.width()); + infoJson->insert("h", imageSize.height()); +} + +Thumbnail::Thumbnail(const QJsonObject& infoJson) + : ImageInfo(infoJson["thumbnail_url"].toString(), + infoJson["thumbnail_info"].toObject()) +{ } + +void Thumbnail::fillInfoJson(QJsonObject* infoJson) const +{ + infoJson->insert("thumbnail_url", url.toString()); + infoJson->insert("thumbnail_info", toInfoJson(*this)); +} diff --git a/lib/events/eventcontent.h b/lib/events/eventcontent.h new file mode 100644 index 00000000..9d44aec0 --- /dev/null +++ b/lib/events/eventcontent.h @@ -0,0 +1,314 @@ +/****************************************************************************** + * Copyright (C) 2017 Kitsune Ral + * + * 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 + */ + +#pragma once + +// This file contains generic event content definitions, applicable to room +// message events as well as other events (e.g., avatars). + +#include "converters.h" + +#include +#include +#include + +#include + +namespace QMatrixClient +{ + namespace EventContent + { + /** + * A base class for all content types that can be stored + * in a RoomMessageEvent + * + * Each content type class should have a constructor taking + * a QJsonObject and override fillJson() with an implementation + * that will fill the target QJsonObject with stored values. It is + * assumed but not required that a content object can also be created + * from plain data. + */ + class Base + { + public: + explicit Base (const QJsonObject& o = {}) : originalJson(o) { } + virtual ~Base() = default; + + QJsonObject toJson() const; + + public: + QJsonObject originalJson; + + protected: + virtual void fillJson(QJsonObject* o) const = 0; + }; + + template + class SimpleContent: public Base + { + public: + using value_type = T; + + // The constructor is templated to enable perfect forwarding + template + SimpleContent(QString keyName, TT&& value) + : value(std::forward(value)), key(std::move(keyName)) + { } + SimpleContent(const QJsonObject& json, QString keyName) + : Base(json) + , value(QMatrixClient::fromJson(json[keyName])) + , key(std::move(keyName)) + { } + + public: + T value; + + protected: + QString key; + + private: + void fillJson(QJsonObject* json) const override + { + Q_ASSERT(json); + json->insert(key, QMatrixClient::toJson(value)); + } + }; + + // The below structures fairly follow CS spec 11.2.1.6. The overall + // set of attributes for each content types is a superset of the spec + // but specific aggregation structure is altered. See doc comments to + // each type for the list of available attributes. + + // A quick classes inheritance structure follows: + // FileInfo + // FileContent : UrlBasedContent + // AudioContent : UrlBasedContent + // ImageInfo : FileInfo + imageSize attribute + // ImageContent : UrlBasedContent + // VideoContent : UrlBasedContent + + /** + * A base/mixin class for structures representing an "info" object for + * some content types. These include most attachment types currently in + * the CS API spec. + * + * In order to use it in a content class, derive both from TypedBase + * (or Base) and from FileInfo (or its derivative, such as \p ImageInfo) + * and call fillInfoJson() to fill the "info" subobject. Make sure + * to pass an "info" part of JSON to FileInfo constructor, not the whole + * JSON content, as well as contents of "url" (or a similar key) and + * optionally "filename" node from the main JSON content. Assuming you + * don't do unusual things, you should use \p UrlBasedContent<> instead + * of doing multiple inheritance and overriding Base::fillJson() by hand. + * + * This class is not polymorphic. + */ + class FileInfo + { + public: + explicit FileInfo(const QUrl& u, int payloadSize = -1, + const QMimeType& mimeType = {}, + const QString& originalFilename = {}); + FileInfo(const QUrl& u, const QJsonObject& infoJson, + const QString& originalFilename = {}); + + void fillInfoJson(QJsonObject* infoJson) const; + + /** + * \brief Extract media id from the URL + * + * This can be used, e.g., to construct a QML-facing image:// + * URI as follows: + * \code "image://provider/" + info.mediaId() \endcode + */ + QString mediaId() const { return url.authority() + url.path(); } + + public: + QJsonObject originalInfoJson; + QMimeType mimeType; + QUrl url; + int payloadSize; + QString originalName; + }; + + template + QJsonObject toInfoJson(const InfoT& info) + { + QJsonObject infoJson; + info.fillInfoJson(&infoJson); + return infoJson; + } + + /** + * A content info class for image content types: image, thumbnail, video + */ + class ImageInfo : public FileInfo + { + public: + explicit ImageInfo(const QUrl& u, int fileSize = -1, + QMimeType mimeType = {}, + const QSize& imageSize = {}); + ImageInfo(const QUrl& u, const QJsonObject& infoJson, + const QString& originalFilename = {}); + + void fillInfoJson(QJsonObject* infoJson) const; + + public: + QSize imageSize; + }; + + /** + * An auxiliary class for an info type that carries a thumbnail + * + * This class saves/loads a thumbnail to/from "info" subobject of + * the JSON representation of event content; namely, + * "info/thumbnail_url" and "info/thumbnail_info" fields are used. + */ + class Thumbnail : public ImageInfo + { + public: + Thumbnail(const QJsonObject& infoJson); + Thumbnail(const ImageInfo& info) + : ImageInfo(info) + { } + + /** + * Writes thumbnail information to "thumbnail_info" subobject + * and thumbnail URL to "thumbnail_url" node inside "info". + */ + void fillInfoJson(QJsonObject* infoJson) const; + }; + + class TypedBase: public Base + { + public: + explicit TypedBase(const QJsonObject& o = {}) : Base(o) { } + virtual QMimeType type() const = 0; + virtual const FileInfo* fileInfo() const { return nullptr; } + virtual const Thumbnail* thumbnailInfo() const { return nullptr; } + }; + + /** + * A base class for content types that have a URL and additional info + * + * Types that derive from this class template take "url" and, + * optionally, "filename" values from the top-level JSON object and + * the rest of information from the "info" subobject, as defined by + * the parameter type. + * + * \tparam InfoT base info class + */ + template + class UrlBasedContent : public TypedBase, public InfoT + { + public: + UrlBasedContent(QUrl url, InfoT&& info, QString filename = {}) + : InfoT(url, std::forward(info), filename) + { } + explicit UrlBasedContent(const QJsonObject& json) + : TypedBase(json) + , InfoT(json["url"].toString(), json["info"].toObject(), + json["filename"].toString()) + { + // A small hack to facilitate links creation in QML. + originalJson.insert("mediaId", InfoT::mediaId()); + } + + QMimeType type() const override { return InfoT::mimeType; } + const FileInfo* fileInfo() const override { return this; } + + protected: + void fillJson(QJsonObject* json) const override + { + Q_ASSERT(json); + json->insert("url", InfoT::url.toString()); + if (!InfoT::originalName.isEmpty()) + json->insert("filename", InfoT::originalName); + json->insert("info", toInfoJson(*this)); + } + }; + + template + class UrlWithThumbnailContent : public UrlBasedContent + { + public: + // TODO: POD constructor + explicit UrlWithThumbnailContent(const QJsonObject& json) + : UrlBasedContent(json) + , thumbnail(InfoT::originalInfoJson) + { + // Another small hack, to simplify making a thumbnail link + UrlBasedContent::originalJson.insert( + "thumbnailMediaId", thumbnail.mediaId()); + } + + const Thumbnail* thumbnailInfo() const override + { return &thumbnail; } + + public: + Thumbnail thumbnail; + + protected: + void fillJson(QJsonObject* json) const override + { + UrlBasedContent::fillJson(json); + auto infoJson = json->take("info").toObject(); + thumbnail.fillInfoJson(&infoJson); + json->insert("info", infoJson); + } + }; + + /** + * Content class for m.image + * + * Available fields: + * - corresponding to the top-level JSON: + * - url + * - filename (extension to the spec) + * - corresponding to the "info" subobject: + * - payloadSize ("size" in JSON) + * - mimeType ("mimetype" in JSON) + * - imageSize (QSize for a combination of "h" and "w" in JSON) + * - thumbnail.url ("thumbnail_url" in JSON) + * - corresponding to the "info/thumbnail_info" subobject: contents of + * thumbnail field, in the same vein as for the main image: + * - payloadSize + * - mimeType + * - imageSize + */ + using ImageContent = UrlWithThumbnailContent; + + /** + * Content class for m.file + * + * Available fields: + * - corresponding to the top-level JSON: + * - url + * - filename + * - corresponding to the "info" subobject: + * - payloadSize ("size" in JSON) + * - mimeType ("mimetype" in JSON) + * - thumbnail.url ("thumbnail_url" in JSON) + * - corresponding to the "info/thumbnail_info" subobject: + * - thumbnail.payloadSize + * - thumbnail.mimeType + * - thumbnail.imageSize (QSize for "h" and "w" in JSON) + */ + using FileContent = UrlWithThumbnailContent; + } // namespace EventContent +} // namespace QMatrixClient diff --git a/lib/events/receiptevent.cpp b/lib/events/receiptevent.cpp new file mode 100644 index 00000000..7555db82 --- /dev/null +++ b/lib/events/receiptevent.cpp @@ -0,0 +1,70 @@ +/****************************************************************************** + * Copyright (C) 2016 Felix Rohrbach + * + * 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 + */ + +/* +Example of a Receipt Event: +{ + "content": { + "$1435641916114394fHBLK:matrix.org": { + "m.read": { + "@rikj:jki.re": { + "ts": 1436451550453 + } + } + } + }, + "room_id": "!KpjVgQyZpzBwvMBsnT:matrix.org", + "type": "m.receipt" +} +*/ + +#include "receiptevent.h" + +#include "converters.h" +#include "logging.h" + +using namespace QMatrixClient; + +ReceiptEvent::ReceiptEvent(const QJsonObject& obj) + : Event(Type::Receipt, obj) +{ + Q_ASSERT(obj["type"].toString() == TypeId); + + const QJsonObject contents = contentJson(); + _eventsWithReceipts.reserve(contents.size()); + for( auto eventIt = contents.begin(); eventIt != contents.end(); ++eventIt ) + { + if (eventIt.key().isEmpty()) + { + qCWarning(EPHEMERAL) << "ReceiptEvent has an empty event id, skipping"; + qCDebug(EPHEMERAL) << "ReceiptEvent content follows:\n" << contents; + continue; + } + const QJsonObject reads = eventIt.value().toObject().value("m.read").toObject(); + QVector receipts; + receipts.reserve(reads.size()); + for( auto userIt = reads.begin(); userIt != reads.end(); ++userIt ) + { + const QJsonObject user = userIt.value().toObject(); + receipts.push_back({userIt.key(), + QMatrixClient::fromJson(user["ts"])}); + } + _eventsWithReceipts.push_back({eventIt.key(), std::move(receipts)}); + } +} + diff --git a/lib/events/receiptevent.h b/lib/events/receiptevent.h new file mode 100644 index 00000000..5b99ae3f --- /dev/null +++ b/lib/events/receiptevent.h @@ -0,0 +1,50 @@ +/****************************************************************************** + * Copyright (C) 2016 Felix Rohrbach + * + * 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 + */ + +#pragma once + +#include "event.h" + +namespace QMatrixClient +{ + struct Receipt + { + QString userId; + QDateTime timestamp; + }; + struct ReceiptsForEvent + { + QString evtId; + QVector receipts; + }; + using EventsWithReceipts = QVector; + + class ReceiptEvent: public Event + { + public: + explicit ReceiptEvent(const QJsonObject& obj); + + EventsWithReceipts eventsWithReceipts() const + { return _eventsWithReceipts; } + + static constexpr const char* const TypeId = "m.receipt"; + + private: + EventsWithReceipts _eventsWithReceipts; + }; +} // namespace QMatrixClient diff --git a/lib/events/redactionevent.cpp b/lib/events/redactionevent.cpp new file mode 100644 index 00000000..bf467718 --- /dev/null +++ b/lib/events/redactionevent.cpp @@ -0,0 +1 @@ +#include "redactionevent.h" diff --git a/lib/events/redactionevent.h b/lib/events/redactionevent.h new file mode 100644 index 00000000..fa6902ab --- /dev/null +++ b/lib/events/redactionevent.h @@ -0,0 +1,43 @@ +/****************************************************************************** + * Copyright (C) 2017 Kitsune Ral + * + * 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 + */ + +#pragma once + +#include "event.h" + +namespace QMatrixClient +{ + class RedactionEvent : public RoomEvent + { + public: + static constexpr const char* const TypeId = "m.room.redaction"; + + RedactionEvent(const QJsonObject& obj) + : RoomEvent(Type::Redaction, obj) + , _redactedEvent(obj.value("redacts").toString()) + , _reason(contentJson().value("reason").toString()) + { } + + const QString& redactedEvent() const { return _redactedEvent; } + const QString& reason() const { return _reason; } + + private: + QString _redactedEvent; + QString _reason; + }; +} // namespace QMatrixClient diff --git a/lib/events/roomavatarevent.cpp b/lib/events/roomavatarevent.cpp new file mode 100644 index 00000000..7a5f82a1 --- /dev/null +++ b/lib/events/roomavatarevent.cpp @@ -0,0 +1,23 @@ +/****************************************************************************** + * Copyright (C) 2017 Kitsune Ral + * + * 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 + */ + +#include "roomavatarevent.h" + +using namespace QMatrixClient; + + diff --git a/lib/events/roomavatarevent.h b/lib/events/roomavatarevent.h new file mode 100644 index 00000000..ccfe8fbf --- /dev/null +++ b/lib/events/roomavatarevent.h @@ -0,0 +1,43 @@ +/****************************************************************************** + * Copyright (C) 2017 Kitsune Ral + * + * 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 + */ + +#pragma once + +#include "event.h" + +#include + +#include "eventcontent.h" + +namespace QMatrixClient +{ + class RoomAvatarEvent: public StateEvent + { + // It's a bit of an overkill to use a full-fledged ImageContent + // because in reality m.room.avatar usually only has a single URL, + // without a thumbnail. But The Spec says there be thumbnails, and + // we follow The Spec. + public: + explicit RoomAvatarEvent(const QJsonObject& obj) + : StateEvent(Type::RoomAvatar, obj) + { } + + static constexpr const char* TypeId = "m.room.avatar"; + }; + +} // namespace QMatrixClient diff --git a/lib/events/roommemberevent.cpp b/lib/events/roommemberevent.cpp new file mode 100644 index 00000000..76b003c2 --- /dev/null +++ b/lib/events/roommemberevent.cpp @@ -0,0 +1,69 @@ +/****************************************************************************** + * Copyright (C) 2015 Felix Rohrbach + * + * 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 + */ + +#include "roommemberevent.h" + +#include "logging.h" + +#include + +using namespace QMatrixClient; + +static const std::array membershipStrings = { { + QStringLiteral("invite"), QStringLiteral("join"), + QStringLiteral("knock"), QStringLiteral("leave"), + QStringLiteral("ban") +} }; + +namespace QMatrixClient +{ + template <> + struct FromJson + { + MembershipType operator()(const QJsonValue& jv) const + { + const auto& membershipString = jv.toString(); + for (auto it = membershipStrings.begin(); + it != membershipStrings.end(); ++it) + if (membershipString == *it) + return MembershipType(it - membershipStrings.begin()); + + qCWarning(EVENTS) << "Unknown MembershipType: " << membershipString; + return MembershipType::Undefined; + } + }; +} + +MemberEventContent::MemberEventContent(const QJsonObject& json) + : membership(fromJson(json["membership"])) + , isDirect(json["is_direct"].toBool()) + , displayName(json["displayname"].toString()) + , avatarUrl(json["avatar_url"].toString()) +{ } + +void MemberEventContent::fillJson(QJsonObject* o) const +{ + Q_ASSERT(o); + Q_ASSERT_X(membership != MembershipType::Undefined, __FUNCTION__, + "The key 'membership' must be explicit in MemberEventContent"); + if (membership != MembershipType::Undefined) + o->insert("membership", membershipStrings[membership]); + o->insert("displayname", displayName); + if (avatarUrl.isValid()) + o->insert("avatar_url", avatarUrl.toString()); +} diff --git a/lib/events/roommemberevent.h b/lib/events/roommemberevent.h new file mode 100644 index 00000000..89b970c9 --- /dev/null +++ b/lib/events/roommemberevent.h @@ -0,0 +1,78 @@ +/****************************************************************************** + * Copyright (C) 2015 Felix Rohrbach + * + * 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 + */ + +#pragma once + +#include "event.h" + +#include "eventcontent.h" + +#include + +namespace QMatrixClient +{ + class MemberEventContent: public EventContent::Base + { + public: + enum MembershipType : size_t { Invite = 0, Join, Knock, Leave, Ban, + Undefined }; + + explicit MemberEventContent(MembershipType mt = MembershipType::Join) + : membership(mt) + { } + explicit MemberEventContent(const QJsonObject& json); + + MembershipType membership; + bool isDirect = false; + QString displayName; + QUrl avatarUrl; + + protected: + void fillJson(QJsonObject* o) const override; + }; + + using MembershipType = MemberEventContent::MembershipType; + + class RoomMemberEvent: public StateEvent + { + Q_GADGET + public: + static constexpr const char* TypeId = "m.room.member"; + + using MembershipType = MemberEventContent::MembershipType; + + RoomMemberEvent(MemberEventContent&& c) + : StateEvent(Type::RoomMember, c) + { } + explicit RoomMemberEvent(const QJsonObject& obj) + : StateEvent(Type::RoomMember, obj) +// , _userId(obj["state_key"].toString()) + { } + + MembershipType membership() const { return content().membership; } + QString userId() const + { return originalJsonObject().value("state_key").toString(); } + bool isDirect() const { return content().isDirect; } + QString displayName() const { return content().displayName; } + QUrl avatarUrl() const { return content().avatarUrl; } + + private: +// QString _userId; + REGISTER_ENUM(MembershipType) + }; +} // namespace QMatrixClient diff --git a/lib/events/roommessageevent.cpp b/lib/events/roommessageevent.cpp new file mode 100644 index 00000000..dec0ca50 --- /dev/null +++ b/lib/events/roommessageevent.cpp @@ -0,0 +1,193 @@ +/****************************************************************************** + * Copyright (C) 2015 Felix Rohrbach + * + * 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 + */ + +#include "roommessageevent.h" + +#include "logging.h" + +#include + +using namespace QMatrixClient; +using namespace EventContent; + +using MsgType = RoomMessageEvent::MsgType; + +template +TypedBase* make(const QJsonObject& json) +{ + return new ContentT(json); +} + +struct MsgTypeDesc +{ + QString jsonType; + MsgType enumType; + TypedBase* (*maker)(const QJsonObject&); +}; + +const std::vector msgTypes = + { { QStringLiteral("m.text"), MsgType::Text, make } + , { QStringLiteral("m.emote"), MsgType::Emote, make } + , { QStringLiteral("m.notice"), MsgType::Notice, make } + , { QStringLiteral("m.image"), MsgType::Image, make } + , { QStringLiteral("m.file"), MsgType::File, make } + , { QStringLiteral("m.location"), MsgType::Location, make } + , { QStringLiteral("m.video"), MsgType::Video, make } + , { QStringLiteral("m.audio"), MsgType::Audio, make } + }; + +QString msgTypeToJson(MsgType enumType) +{ + auto it = std::find_if(msgTypes.begin(), msgTypes.end(), + [=](const MsgTypeDesc& mtd) { return mtd.enumType == enumType; }); + if (it != msgTypes.end()) + return it->jsonType; + + return {}; +} + +MsgType jsonToMsgType(const QString& jsonType) +{ + auto it = std::find_if(msgTypes.begin(), msgTypes.end(), + [=](const MsgTypeDesc& mtd) { return mtd.jsonType == jsonType; }); + if (it != msgTypes.end()) + return it->enumType; + + return MsgType::Unknown; +} + +RoomMessageEvent::RoomMessageEvent(const QString& plainBody, + MsgType msgType, TypedBase* content) + : RoomMessageEvent(plainBody, msgTypeToJson(msgType), content) +{ } + +RoomMessageEvent::RoomMessageEvent(const QJsonObject& obj) + : RoomEvent(Type::RoomMessage, obj), _content(nullptr) +{ + if (isRedacted()) + return; + const QJsonObject content = contentJson(); + if ( content.contains("msgtype") && content.contains("body") ) + { + _plainBody = content["body"].toString(); + + _msgtype = content["msgtype"].toString(); + for (auto mt: msgTypes) + if (mt.jsonType == _msgtype) + _content.reset(mt.maker(content)); + + if (!_content) + { + qCWarning(EVENTS) << "RoomMessageEvent: couldn't load content," + << " full content dump follows"; + qCWarning(EVENTS) << formatJson << content; + } + } + else + { + qCWarning(EVENTS) << "No body or msgtype in room message event"; + qCWarning(EVENTS) << formatJson << obj; + } +} + +RoomMessageEvent::MsgType RoomMessageEvent::msgtype() const +{ + return jsonToMsgType(_msgtype); +} + +QMimeType RoomMessageEvent::mimeType() const +{ + return _content ? _content->type() : + QMimeDatabase().mimeTypeForName("text/plain"); +} + +bool RoomMessageEvent::hasTextContent() const +{ + return content() && + (msgtype() == MsgType::Text || msgtype() == MsgType::Emote || + msgtype() == MsgType::Notice); // FIXME: Unbind from specific msgtypes +} + +bool RoomMessageEvent::hasFileContent() const +{ + return content() && content()->fileInfo(); +} + +bool RoomMessageEvent::hasThumbnail() const +{ + return content() && content()->thumbnailInfo(); +} + +QJsonObject RoomMessageEvent::toJson() const +{ + QJsonObject obj = _content ? _content->toJson() : QJsonObject(); + obj.insert("msgtype", msgTypeToJson(msgtype())); + obj.insert("body", plainBody()); + return obj; +} + +TextContent::TextContent(const QString& text, const QString& contentType) + : mimeType(QMimeDatabase().mimeTypeForName(contentType)), body(text) +{ } + +TextContent::TextContent(const QJsonObject& json) +{ + QMimeDatabase db; + + // Special-casing the custom matrix.org's (actually, Riot's) way + // of sending HTML messages. + if (json["format"].toString() == "org.matrix.custom.html") + { + mimeType = db.mimeTypeForName("text/html"); + body = json["formatted_body"].toString(); + } else { + // Falling back to plain text, as there's no standard way to describe + // rich text in messages. + mimeType = db.mimeTypeForName("text/plain"); + body = json["body"].toString(); + } +} + +void TextContent::fillJson(QJsonObject* json) const +{ + Q_ASSERT(json); + json->insert("format", QStringLiteral("org.matrix.custom.html")); + json->insert("formatted_body", body); +} + +LocationContent::LocationContent(const QString& geoUri, const ImageInfo& thumbnail) + : geoUri(geoUri), thumbnail(thumbnail) +{ } + +LocationContent::LocationContent(const QJsonObject& json) + : TypedBase(json) + , geoUri(json["geo_uri"].toString()) + , thumbnail(json["info"].toObject()) +{ } + +QMimeType LocationContent::type() const +{ + return QMimeDatabase().mimeTypeForData(geoUri.toLatin1()); +} + +void LocationContent::fillJson(QJsonObject* o) const +{ + Q_ASSERT(o); + o->insert("geo_uri", geoUri); + o->insert("info", toInfoJson(thumbnail)); +} diff --git a/lib/events/roommessageevent.h b/lib/events/roommessageevent.h new file mode 100644 index 00000000..a55564ed --- /dev/null +++ b/lib/events/roommessageevent.h @@ -0,0 +1,194 @@ +/****************************************************************************** + * Copyright (C) 2015 Felix Rohrbach + * + * 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 + */ + +#pragma once + +#include "event.h" + +#include "eventcontent.h" + +namespace QMatrixClient +{ + namespace MessageEventContent = EventContent; // Back-compatibility + + /** + * The event class corresponding to m.room.message events + */ + class RoomMessageEvent: public RoomEvent + { + Q_GADGET + Q_PROPERTY(QString msgType READ rawMsgtype CONSTANT) + Q_PROPERTY(QString plainBody READ plainBody CONSTANT) + Q_PROPERTY(QMimeType mimeType READ mimeType STORED false CONSTANT) + Q_PROPERTY(EventContent::TypedBase* content READ content CONSTANT) + public: + enum class MsgType + { + Text, Emote, Notice, Image, File, Location, Video, Audio, Unknown + }; + + RoomMessageEvent(const QString& plainBody, + const QString& jsonMsgType, + EventContent::TypedBase* content = nullptr) + : RoomEvent(Type::RoomMessage) + , _msgtype(jsonMsgType), _plainBody(plainBody), _content(content) + { } + explicit RoomMessageEvent(const QString& plainBody, + MsgType msgType = MsgType::Text, + EventContent::TypedBase* content = nullptr); + explicit RoomMessageEvent(const QJsonObject& obj); + + MsgType msgtype() const; + QString rawMsgtype() const { return _msgtype; } + const QString& plainBody() const { return _plainBody; } + EventContent::TypedBase* content() const + { return _content.data(); } + QMimeType mimeType() const; + bool hasTextContent() const; + bool hasFileContent() const; + bool hasThumbnail() const; + + QJsonObject toJson() const; + + static constexpr const char* TypeId = "m.room.message"; + + private: + QString _msgtype; + QString _plainBody; + QScopedPointer _content; + + REGISTER_ENUM(MsgType) + }; + using MessageEventType = RoomMessageEvent::MsgType; + + namespace EventContent + { + // Additional event content types + + /** + * Rich text content for m.text, m.emote, m.notice + * + * Available fields: mimeType, body. The body can be either rich text + * or plain text, depending on what mimeType specifies. + */ + class TextContent: public TypedBase + { + public: + TextContent(const QString& text, const QString& contentType); + explicit TextContent(const QJsonObject& json); + + QMimeType type() const override { return mimeType; } + + QMimeType mimeType; + QString body; + + protected: + void fillJson(QJsonObject* json) const override; + }; + + /** + * Content class for m.location + * + * Available fields: + * - corresponding to the top-level JSON: + * - geoUri ("geo_uri" in JSON) + * - corresponding to the "info" subobject: + * - thumbnail.url ("thumbnail_url" in JSON) + * - corresponding to the "info/thumbnail_info" subobject: + * - thumbnail.payloadSize + * - thumbnail.mimeType + * - thumbnail.imageSize + */ + class LocationContent: public TypedBase + { + public: + LocationContent(const QString& geoUri, + const ImageInfo& thumbnail); + explicit LocationContent(const QJsonObject& json); + + QMimeType type() const override; + + public: + QString geoUri; + Thumbnail thumbnail; + + protected: + void fillJson(QJsonObject* o) const override; + }; + + /** + * A base class for info types that include duration: audio and video + */ + template + class PlayableContent : public ContentT + { + public: + PlayableContent(const QJsonObject& json) + : ContentT(json) + , duration(ContentT::originalInfoJson["duration"].toInt()) + { } + + protected: + void fillJson(QJsonObject* json) const override + { + ContentT::fillJson(json); + auto infoJson = json->take("info").toObject(); + infoJson.insert("duration", duration); + json->insert("info", infoJson); + } + + public: + int duration; + }; + + /** + * Content class for m.video + * + * Available fields: + * - corresponding to the top-level JSON: + * - url + * - filename (extension to the CS API spec) + * - corresponding to the "info" subobject: + * - payloadSize ("size" in JSON) + * - mimeType ("mimetype" in JSON) + * - duration + * - imageSize (QSize for a combination of "h" and "w" in JSON) + * - thumbnail.url ("thumbnail_url" in JSON) + * - corresponding to the "info/thumbnail_info" subobject: contents of + * thumbnail field, in the same vein as for "info": + * - payloadSize + * - mimeType + * - imageSize + */ + using VideoContent = PlayableContent>; + + /** + * Content class for m.audio + * + * Available fields: + * - corresponding to the top-level JSON: + * - url + * - filename (extension to the CS API spec) + * - corresponding to the "info" subobject: + * - payloadSize ("size" in JSON) + * - mimeType ("mimetype" in JSON) + * - duration + */ + using AudioContent = PlayableContent>; + } // namespace EventContent +} // namespace QMatrixClient diff --git a/lib/events/simplestateevents.h b/lib/events/simplestateevents.h new file mode 100644 index 00000000..6b0cd51a --- /dev/null +++ b/lib/events/simplestateevents.h @@ -0,0 +1,53 @@ +/****************************************************************************** + * Copyright (C) 2017 Kitsune Ral + * + * 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 + */ + +#pragma once + +#include "event.h" +#include "eventcontent.h" + +namespace QMatrixClient +{ +#define DEFINE_SIMPLE_STATE_EVENT(_Name, _TypeId, _EnumType, _ContentType, _ContentKey) \ + class _Name \ + : public StateEvent> \ + { \ + public: \ + static constexpr const char* TypeId = _TypeId; \ + explicit _Name(const QJsonObject& obj) \ + : StateEvent(_EnumType, obj, QStringLiteral(#_ContentKey)) \ + { } \ + template \ + explicit _Name(T&& value) \ + : StateEvent(_EnumType, QStringLiteral(#_ContentKey), \ + std::forward(value)) \ + { } \ + const _ContentType& _ContentKey() const { return content().value; } \ + }; + + DEFINE_SIMPLE_STATE_EVENT(RoomNameEvent, "m.room.name", + Event::Type::RoomName, QString, name) + DEFINE_SIMPLE_STATE_EVENT(RoomAliasesEvent, "m.room.aliases", + Event::Type::RoomAliases, QStringList, aliases) + DEFINE_SIMPLE_STATE_EVENT(RoomCanonicalAliasEvent, "m.room.canonical_alias", + Event::Type::RoomCanonicalAlias, QString, alias) + DEFINE_SIMPLE_STATE_EVENT(RoomTopicEvent, "m.room.topic", + Event::Type::RoomTopic, QString, topic) + DEFINE_SIMPLE_STATE_EVENT(EncryptionEvent, "m.room.encryption", + Event::Type::RoomEncryption, QString, algorithm) +} // namespace QMatrixClient diff --git a/lib/events/typingevent.cpp b/lib/events/typingevent.cpp new file mode 100644 index 00000000..a4d3bae4 --- /dev/null +++ b/lib/events/typingevent.cpp @@ -0,0 +1,32 @@ +/****************************************************************************** + * Copyright (C) 2015 Felix Rohrbach + * + * 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 + */ + +#include "typingevent.h" + +using namespace QMatrixClient; + +TypingEvent::TypingEvent(const QJsonObject& obj) + : Event(Type::Typing, obj) +{ + QJsonValue result; + result= contentJson()["user_ids"]; + QJsonArray array = result.toArray(); + for( const QJsonValue& user: array ) + _users.push_back(user.toString()); +} + diff --git a/lib/events/typingevent.h b/lib/events/typingevent.h new file mode 100644 index 00000000..8c9551a4 --- /dev/null +++ b/lib/events/typingevent.h @@ -0,0 +1,39 @@ +/****************************************************************************** + * Copyright (C) 2015 Felix Rohrbach + * + * 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 + */ + +#pragma once + +#include "event.h" + +#include + +namespace QMatrixClient +{ + class TypingEvent: public Event + { + public: + static constexpr const char* const TypeId = "m.typing"; + + TypingEvent(const QJsonObject& obj); + + QStringList users() const { return _users; } + + private: + QStringList _users; + }; +} // namespace QMatrixClient diff --git a/lib/jobs/basejob.cpp b/lib/jobs/basejob.cpp new file mode 100644 index 00000000..3cde7c50 --- /dev/null +++ b/lib/jobs/basejob.cpp @@ -0,0 +1,508 @@ +/****************************************************************************** + * Copyright (C) 2015 Felix Rohrbach + * + * 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 + */ + +#include "basejob.h" + +#include "connectiondata.h" + +#include +#include +#include +#include +#include +//#include + +#include + +using namespace QMatrixClient; + +struct NetworkReplyDeleter : public QScopedPointerDeleteLater +{ + static inline void cleanup(QNetworkReply* reply) + { + if (reply && reply->isRunning()) + reply->abort(); + QScopedPointerDeleteLater::cleanup(reply); + } +}; + +class BaseJob::Private +{ + public: + // Using an idiom from clang-tidy: + // http://clang.llvm.org/extra/clang-tidy/checks/modernize-pass-by-value.html + Private(HttpVerb v, QString endpoint, QUrlQuery q, Data&& data, bool nt) + : verb(v), apiEndpoint(std::move(endpoint)) + , requestQuery(std::move(q)), requestData(std::move(data)) + , needsToken(nt) + { } + + void sendRequest(); + const JobTimeoutConfig& getCurrentTimeoutConfig() const; + + const ConnectionData* connection = nullptr; + + // Contents for the network request + HttpVerb verb; + QString apiEndpoint; + QHash requestHeaders; + QUrlQuery requestQuery; + Data requestData; + bool needsToken; + + // There's no use of QMimeType here because we don't want to match + // content types against the known MIME type hierarchy; and at the same + // type QMimeType is of little help with MIME type globs (`text/*` etc.) + QByteArrayList expectedContentTypes; + + QScopedPointer reply; + Status status = Pending; + + QTimer timer; + QTimer retryTimer; + + QVector errorStrategy = + { { 90, 5 }, { 90, 10 }, { 120, 30 } }; + int maxRetries = errorStrategy.size(); + int retriesTaken = 0; + + LoggingCategory logCat = JOBS; +}; + +BaseJob::BaseJob(HttpVerb verb, const QString& name, const QString& endpoint, bool needsToken) + : BaseJob(verb, name, endpoint, Query { }, Data { }, needsToken) +{ } + +BaseJob::BaseJob(HttpVerb verb, const QString& name, const QString& endpoint, + const Query& query, Data&& data, bool needsToken) + : d(new Private(verb, endpoint, query, std::move(data), needsToken)) +{ + setObjectName(name); + setExpectedContentTypes({ "application/json" }); + d->timer.setSingleShot(true); + connect (&d->timer, &QTimer::timeout, this, &BaseJob::timeout); + d->retryTimer.setSingleShot(true); + connect (&d->retryTimer, &QTimer::timeout, this, &BaseJob::sendRequest); +} + +BaseJob::~BaseJob() +{ + stop(); + qCDebug(d->logCat) << this << "destroyed"; +} + +const QString& BaseJob::apiEndpoint() const +{ + return d->apiEndpoint; +} + +void BaseJob::setApiEndpoint(const QString& apiEndpoint) +{ + d->apiEndpoint = apiEndpoint; +} + +const BaseJob::headers_t&BaseJob::requestHeaders() const +{ + return d->requestHeaders; +} + +void BaseJob::setRequestHeader(const headers_t::key_type& headerName, + const headers_t::mapped_type& headerValue) +{ + d->requestHeaders[headerName] = headerValue; +} + +void BaseJob::setRequestHeaders(const BaseJob::headers_t& headers) +{ + d->requestHeaders = headers; +} + +const QUrlQuery& BaseJob::query() const +{ + return d->requestQuery; +} + +void BaseJob::setRequestQuery(const QUrlQuery& query) +{ + d->requestQuery = query; +} + +const BaseJob::Data& BaseJob::requestData() const +{ + return d->requestData; +} + +void BaseJob::setRequestData(Data&& data) +{ + std::swap(d->requestData, data); +} + +const QByteArrayList& BaseJob::expectedContentTypes() const +{ + return d->expectedContentTypes; +} + +void BaseJob::addExpectedContentType(const QByteArray& contentType) +{ + d->expectedContentTypes << contentType; +} + +void BaseJob::setExpectedContentTypes(const QByteArrayList& contentTypes) +{ + d->expectedContentTypes = contentTypes; +} + +QUrl BaseJob::makeRequestUrl(QUrl baseUrl, + const QString& path, const QUrlQuery& query) +{ + auto pathBase = baseUrl.path(); + if (!pathBase.endsWith('/') && !path.startsWith('/')) + pathBase.push_back('/'); + + baseUrl.setPath( pathBase + path ); + baseUrl.setQuery(query); + return baseUrl; +} + +void BaseJob::Private::sendRequest() +{ + QNetworkRequest req + { makeRequestUrl(connection->baseUrl(), apiEndpoint, requestQuery) }; + if (!requestHeaders.contains("Content-Type")) + req.setHeader(QNetworkRequest::ContentTypeHeader, "application/json"); + req.setRawHeader(QByteArray("Authorization"), + QByteArray("Bearer ") + connection->accessToken()); +#if (QT_VERSION >= QT_VERSION_CHECK(5, 6, 0)) + req.setAttribute(QNetworkRequest::FollowRedirectsAttribute, true); + req.setMaximumRedirectsAllowed(10); +#endif + for (auto it = requestHeaders.cbegin(); it != requestHeaders.cend(); ++it) + req.setRawHeader(it.key(), it.value()); + switch( verb ) + { + case HttpVerb::Get: + reply.reset( connection->nam()->get(req) ); + break; + case HttpVerb::Post: + reply.reset( connection->nam()->post(req, requestData.source()) ); + break; + case HttpVerb::Put: + reply.reset( connection->nam()->put(req, requestData.source()) ); + break; + case HttpVerb::Delete: + reply.reset( connection->nam()->deleteResource(req) ); + break; + } +} + +void BaseJob::beforeStart(const ConnectionData*) +{ } + +void BaseJob::afterStart(const ConnectionData*, QNetworkReply*) +{ } + +void BaseJob::beforeAbandon(QNetworkReply*) +{ } + +void BaseJob::start(const ConnectionData* connData) +{ + d->connection = connData; + beforeStart(connData); + if (status().good()) + sendRequest(); + if (status().good()) + afterStart(connData, d->reply.data()); + if (!status().good()) + QTimer::singleShot(0, this, &BaseJob::finishJob); +} + +void BaseJob::sendRequest() +{ + emit aboutToStart(); + d->retryTimer.stop(); // In case we were counting down at the moment + qCDebug(d->logCat) << this << "sending request to" << d->apiEndpoint; + if (!d->requestQuery.isEmpty()) + qCDebug(d->logCat) << " query:" << d->requestQuery.toString(); + d->sendRequest(); + connect( d->reply.data(), &QNetworkReply::finished, this, &BaseJob::gotReply ); + if (d->reply->isRunning()) + { + connect( d->reply.data(), &QNetworkReply::uploadProgress, + this, &BaseJob::uploadProgress); + connect( d->reply.data(), &QNetworkReply::downloadProgress, + this, &BaseJob::downloadProgress); + d->timer.start(getCurrentTimeout()); + qCDebug(d->logCat) << this << "request has been sent"; + emit started(); + } + else + qCWarning(d->logCat) << this << "request could not start"; +} + +void BaseJob::gotReply() +{ + setStatus(checkReply(d->reply.data())); + if (status().good()) + setStatus(parseReply(d->reply.data())); + else + { + const auto body = d->reply->readAll(); + if (!body.isEmpty()) + { + qCDebug(d->logCat).noquote() << "Error body:" << body; + auto json = QJsonDocument::fromJson(body).object(); + if (json.isEmpty()) + setStatus(IncorrectRequestError, body); + else { + if (error() == TooManyRequestsError || + json.value("errcode").toString() == "M_LIMIT_EXCEEDED") + { + QString msg = tr("Too many requests"); + auto retryInterval = json.value("retry_after_ms").toInt(-1); + if (retryInterval != -1) + msg += tr(", next retry advised after %1 ms") + .arg(retryInterval); + else // We still have to figure some reasonable interval + retryInterval = getNextRetryInterval(); + + setStatus(TooManyRequestsError, msg); + + // Shortcut to retry instead of executing finishJob() + stop(); + qCWarning(d->logCat) + << this << "will retry in" << retryInterval; + d->retryTimer.start(retryInterval); + emit retryScheduled(d->retriesTaken, retryInterval); + return; + } + setStatus(IncorrectRequestError, json.value("error").toString()); + } + } + } + + finishJob(); +} + +bool checkContentType(const QByteArray& type, const QByteArrayList& patterns) +{ + if (patterns.isEmpty()) + return true; + + // ignore possible appendixes of the content type + const auto ctype = type.split(';').front(); + + for (const auto& pattern: patterns) + { + if (pattern.startsWith('*') || ctype == pattern) // Fast lane + return true; + + auto patternParts = pattern.split('/'); + Q_ASSERT_X(patternParts.size() <= 2, __FUNCTION__, + "BaseJob: Expected content type should have up to two" + " /-separated parts; violating pattern: " + pattern); + + if (ctype.split('/').front() == patternParts.front() && + patternParts.back() == "*") + return true; // Exact match already went on fast lane + } + + return false; +} + +BaseJob::Status BaseJob::checkReply(QNetworkReply* reply) const +{ + const auto httpCode = + reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt(); + qCDebug(d->logCat).nospace().noquote() << this << " returned HTTP code " + << httpCode << ": " + << (reply->error() == QNetworkReply::NoError ? + "Success" : reply->errorString()) + << " (URL: " << reply->url().toDisplayString() << ")"; + + if (httpCode == 429) // Qt doesn't know about it yet + return { TooManyRequestsError, tr("Too many requests") }; + + // Should we check httpCode instead? Maybe even use it in BaseJob::Status? + // That would make codes in logs slightly more readable. + switch( reply->error() ) + { + case QNetworkReply::NoError: + if (checkContentType(reply->rawHeader("Content-Type"), + d->expectedContentTypes)) + return NoError; + else // A warning in the logs might be more proper instead + return { IncorrectResponseError, + "Incorrect content type of the response" }; + + case QNetworkReply::AuthenticationRequiredError: + case QNetworkReply::ContentAccessDenied: + case QNetworkReply::ContentOperationNotPermittedError: + return { ContentAccessError, reply->errorString() }; + + case QNetworkReply::ProtocolInvalidOperationError: + case QNetworkReply::UnknownContentError: + return { IncorrectRequestError, reply->errorString() }; + + case QNetworkReply::ContentNotFoundError: + return { NotFoundError, reply->errorString() }; + + default: + return { NetworkError, reply->errorString() }; + } +} + +BaseJob::Status BaseJob::parseReply(QNetworkReply* reply) +{ + QJsonParseError error; + QJsonDocument json = QJsonDocument::fromJson(reply->readAll(), &error); + if( error.error == QJsonParseError::NoError ) + return parseJson(json); + else + return { JsonParseError, error.errorString() }; +} + +BaseJob::Status BaseJob::parseJson(const QJsonDocument&) +{ + return Success; +} + +void BaseJob::stop() +{ + d->timer.stop(); + if (d->reply) + { + d->reply->disconnect(this); // Ignore whatever comes from the reply + if (d->reply->isRunning()) + { + qCWarning(d->logCat) << this << "stopped without ready network reply"; + d->reply->abort(); + } + } + else + qCWarning(d->logCat) << this << "stopped with empty network reply"; +} + +void BaseJob::finishJob() +{ + stop(); + if ((error() == NetworkError || error() == TimeoutError) + && d->retriesTaken < d->maxRetries) + { + // TODO: The whole retrying thing should be put to ConnectionManager + // otherwise independently retrying jobs make a bit of notification + // storm towards the UI. + const auto retryInterval = + error() == TimeoutError ? 0 : getNextRetryInterval(); + ++d->retriesTaken; + qCWarning(d->logCat) << this << "will retry" << d->retriesTaken + << "in" << retryInterval/1000 << "s"; + d->retryTimer.start(retryInterval); + emit retryScheduled(d->retriesTaken, retryInterval); + return; + } + + // Notify those interested in any completion of the job (including killing) + emit finished(this); + + emit result(this); + if (error()) + emit failure(this); + else + emit success(this); + + deleteLater(); +} + +const JobTimeoutConfig& BaseJob::Private::getCurrentTimeoutConfig() const +{ + return errorStrategy[std::min(retriesTaken, errorStrategy.size() - 1)]; +} + +BaseJob::duration_t BaseJob::getCurrentTimeout() const +{ + return d->getCurrentTimeoutConfig().jobTimeout * 1000; +} + +BaseJob::duration_t BaseJob::getNextRetryInterval() const +{ + return d->getCurrentTimeoutConfig().nextRetryInterval * 1000; +} + +BaseJob::duration_t BaseJob::millisToRetry() const +{ + return d->retryTimer.isActive() ? d->retryTimer.remainingTime() : 0; +} + +int BaseJob::maxRetries() const +{ + return d->maxRetries; +} + +void BaseJob::setMaxRetries(int newMaxRetries) +{ + d->maxRetries = newMaxRetries; +} + +BaseJob::Status BaseJob::status() const +{ + return d->status; +} + +int BaseJob::error() const +{ + return d->status.code; +} + +QString BaseJob::errorString() const +{ + return d->status.message; +} + +void BaseJob::setStatus(Status s) +{ + d->status = s; + if (!s.good()) + qCWarning(d->logCat) << this << "status" << s; +} + +void BaseJob::setStatus(int code, QString message) +{ + message.replace(d->connection->accessToken(), "(REDACTED)"); + setStatus({ code, message }); +} + +void BaseJob::abandon() +{ + beforeAbandon(d->reply.data()); + setStatus(Abandoned); + this->disconnect(); + if (d->reply) + d->reply->disconnect(this); + deleteLater(); +} + +void BaseJob::timeout() +{ + setStatus( TimeoutError, "The job has timed out" ); + finishJob(); +} + +void BaseJob::setLoggingCategory(LoggingCategory lcf) +{ + d->logCat = lcf; +} diff --git a/lib/jobs/basejob.h b/lib/jobs/basejob.h new file mode 100644 index 00000000..ed630a67 --- /dev/null +++ b/lib/jobs/basejob.h @@ -0,0 +1,303 @@ +/****************************************************************************** + * Copyright (C) 2015 Felix Rohrbach + * + * 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 + */ + +#pragma once + +#include "../logging.h" +#include "requestdata.h" + +#include +#include + +// Any job that parses the response will need the below two. +#include +#include + +class QNetworkReply; +class QSslError; + +namespace QMatrixClient +{ + class ConnectionData; + + enum class HttpVerb { Get, Put, Post, Delete }; + + struct JobTimeoutConfig + { + int jobTimeout; + int nextRetryInterval; + }; + + class BaseJob: public QObject + { + Q_OBJECT + Q_PROPERTY(int maxRetries READ maxRetries WRITE setMaxRetries) + public: + /* Just in case, the values are compatible with KJob + * (which BaseJob used to inherit from). */ + enum StatusCode { NoError = 0 // To be compatible with Qt conventions + , Success = 0 + , Pending = 1 + , Abandoned = 50 //< A very brief period between abandoning and object deletion + , ErrorLevel = 100 //< Errors have codes starting from this + , NetworkError = 100 + , JsonParseError + , TimeoutError + , ContentAccessError + , NotFoundError + , IncorrectRequestError + , IncorrectResponseError + , TooManyRequestsError + , UserDefinedError = 200 + }; + + /** + * A simple wrapper around QUrlQuery that allows its creation from + * a list of string pairs + */ + class Query : public QUrlQuery + { + public: + using QUrlQuery::QUrlQuery; + Query() = default; + Query(const std::initializer_list< QPair >& l) + { + setQueryItems(l); + } + }; + + using Data = RequestData; + + /** + * This structure stores the status of a server call job. The status consists + * of a code, that is described (but not delimited) by the respective enum, + * and a freeform message. + * + * To extend the list of error codes, define an (anonymous) enum + * along the lines of StatusCode, with additional values + * starting at UserDefinedError + */ + class Status + { + public: + Status(StatusCode c) : code(c) { } + Status(int c, QString m) : code(c), message(std::move(m)) { } + + bool good() const { return code < ErrorLevel; } + friend QDebug operator<<(QDebug dbg, const Status& s) + { + QDebug(dbg).noquote().nospace() + << s.code << ": " << s.message; + return dbg; + } + + int code; + QString message; + }; + + using duration_t = int; // milliseconds + + public: + BaseJob(HttpVerb verb, const QString& name, const QString& endpoint, + bool needsToken = true); + BaseJob(HttpVerb verb, const QString& name, const QString& endpoint, + const Query& query, Data&& data = {}, + bool needsToken = true); + + Status status() const; + int error() const; + virtual QString errorString() const; + + int maxRetries() const; + void setMaxRetries(int newMaxRetries); + + Q_INVOKABLE duration_t getCurrentTimeout() const; + Q_INVOKABLE duration_t getNextRetryInterval() const; + Q_INVOKABLE duration_t millisToRetry() const; + + friend QDebug operator<<(QDebug dbg, const BaseJob* j) + { + return dbg << j->objectName(); + } + + public slots: + void start(const ConnectionData* connData); + + /** + * Abandons the result of this job, arrived or unarrived. + * + * This aborts waiting for a reply from the server (if there was + * any pending) and deletes the job object. It is always done quietly + * (as opposed to KJob::kill() that can trigger emitting the result). + */ + void abandon(); + + signals: + /** The job is about to send a network request */ + void aboutToStart(); + + /** The job has sent a network request */ + void started(); + + /** + * The previous network request has failed; the next attempt will + * be done in the specified time + * @param nextAttempt the 1-based number of attempt (will always be more than 1) + * @param inMilliseconds the interval after which the next attempt will be taken + */ + void retryScheduled(int nextAttempt, int inMilliseconds); + + /** + * Emitted when the job is finished, in any case. It is used to notify + * observers that the job is terminated and that progress can be hidden. + * + * This should not be emitted directly by subclasses; + * use finishJob() instead. + * + * In general, to be notified of a job's completion, client code + * should connect to success() and failure() + * rather than finished(), so that kill() is indeed quiet. + * However if you store a list of jobs and they might get killed + * silently, then you must connect to this instead of result(), + * to avoid dangling pointers in your list. + * + * @param job the job that emitted this signal + * + * @see success, failure + */ + void finished(BaseJob* job); + + /** + * Emitted when the job is finished (except when killed). + * + * Use error() to know if the job was finished with error. + * + * @param job the job that emitted this signal + * + * @see success, failure + */ + void result(BaseJob* job); + + /** + * Emitted together with result() but only if there's no error. + */ + void success(BaseJob*); + + /** + * Emitted together with result() if there's an error. + * Same as result(), this won't be emitted in case of kill(). + */ + void failure(BaseJob*); + + void downloadProgress(qint64 bytesReceived, qint64 bytesTotal); + void uploadProgress(qint64 bytesSent, qint64 bytesTotal); + + protected: + using headers_t = QHash; + + const QString& apiEndpoint() const; + void setApiEndpoint(const QString& apiEndpoint); + const headers_t& requestHeaders() const; + void setRequestHeader(const headers_t::key_type& headerName, + const headers_t::mapped_type& headerValue); + void setRequestHeaders(const headers_t& headers); + const QUrlQuery& query() const; + void setRequestQuery(const QUrlQuery& query); + const Data& requestData() const; + void setRequestData(Data&& data); + const QByteArrayList& expectedContentTypes() const; + void addExpectedContentType(const QByteArray& contentType); + void setExpectedContentTypes(const QByteArrayList& contentTypes); + + /** Construct a URL out of baseUrl, path and query + * The function automatically adds '/' between baseUrl's path and + * \p path if necessary. The query component of \p baseUrl + * is ignored. + */ + static QUrl makeRequestUrl(QUrl baseUrl, const QString& path, + const QUrlQuery& query = {}); + + virtual void beforeStart(const ConnectionData* connData); + virtual void afterStart(const ConnectionData* connData, + QNetworkReply* reply); + virtual void beforeAbandon(QNetworkReply*); + + /** + * Used by gotReply() to check the received reply for general + * issues such as network errors or access denial. + * Returning anything except NoError/Success prevents + * further parseReply()/parseJson() invocation. + * + * @param reply the reply received from the server + * @return the result of checking the reply + * + * @see gotReply + */ + virtual Status checkReply(QNetworkReply* reply) const; + + /** + * Processes the reply. By default, parses the reply into + * a QJsonDocument and calls parseJson() if it's a valid JSON. + * + * @param reply raw contents of a HTTP reply from the server (without headers) + * + * @see gotReply, parseJson + */ + virtual Status parseReply(QNetworkReply* reply); + + /** + * Processes the JSON document received from the Matrix server. + * By default returns succesful status without analysing the JSON. + * + * @param json valid JSON document received from the server + * + * @see parseReply + */ + virtual Status parseJson(const QJsonDocument&); + + void setStatus(Status s); + void setStatus(int code, QString message); + + // Q_DECLARE_LOGGING_CATEGORY return different function types + // in different versions + using LoggingCategory = decltype(JOBS)*; + void setLoggingCategory(LoggingCategory lcf); + + // Job objects should only be deleted via QObject::deleteLater + ~BaseJob() override; + + protected slots: + void timeout(); + + private slots: + void sendRequest(); + void gotReply(); + + private: + void stop(); + void finishJob(); + + class Private; + QScopedPointer d; + }; + + inline bool isJobRunning(BaseJob* job) + { + return job && job->error() == BaseJob::Pending; + } +} // namespace QMatrixClient diff --git a/lib/jobs/checkauthmethods.cpp b/lib/jobs/checkauthmethods.cpp new file mode 100644 index 00000000..117def89 --- /dev/null +++ b/lib/jobs/checkauthmethods.cpp @@ -0,0 +1,53 @@ +/****************************************************************************** + * Copyright (C) 2015 Felix Rohrbach + * + * 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 + */ + +#include "checkauthmethods.h" + +#include +#include + +using namespace QMatrixClient; + +class CheckAuthMethods::Private +{ + public: + QString session; +}; + +CheckAuthMethods::CheckAuthMethods() + : BaseJob(HttpVerb::Get, "CheckAuthMethods", + QStringLiteral("_matrix/client/r0/login"), Query(), Data(), false) + , d(new Private) +{ +} + +CheckAuthMethods::~CheckAuthMethods() +{ + delete d; +} + +QString CheckAuthMethods::session() +{ + return d->session; +} + +BaseJob::Status CheckAuthMethods::parseJson(const QJsonDocument& data) +{ + // TODO + return { BaseJob::StatusCode::UserDefinedError, "Not implemented" }; +} diff --git a/lib/jobs/checkauthmethods.h b/lib/jobs/checkauthmethods.h new file mode 100644 index 00000000..647f3db6 --- /dev/null +++ b/lib/jobs/checkauthmethods.h @@ -0,0 +1,40 @@ +/****************************************************************************** + * Copyright (C) 2015 Felix Rohrbach + * + * 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 + */ + +#pragma once + +#include "basejob.h" + +namespace QMatrixClient +{ + class CheckAuthMethods : public BaseJob + { + public: + CheckAuthMethods(); + virtual ~CheckAuthMethods(); + + QString session(); + + protected: + Status parseJson(const QJsonDocument& data) override; + + private: + class Private; + Private* d; + }; +} diff --git a/lib/jobs/downloadfilejob.cpp b/lib/jobs/downloadfilejob.cpp new file mode 100644 index 00000000..6a3d8483 --- /dev/null +++ b/lib/jobs/downloadfilejob.cpp @@ -0,0 +1,120 @@ +#include "downloadfilejob.h" + +#include +#include +#include + +using namespace QMatrixClient; + +class DownloadFileJob::Private +{ + public: + Private() : tempFile(new QTemporaryFile()) { } + + explicit Private(const QString& localFilename) + : targetFile(new QFile(localFilename)) + , tempFile(new QFile(targetFile->fileName() + ".qmcdownload")) + { } + + QScopedPointer targetFile; + QScopedPointer tempFile; +}; + +QUrl DownloadFileJob::makeRequestUrl(QUrl baseUrl, const QUrl& mxcUri) +{ + return makeRequestUrl(baseUrl, mxcUri.authority(), mxcUri.path().mid(1)); +} + +DownloadFileJob::DownloadFileJob(const QString& serverName, + const QString& mediaId, + const QString& localFilename) + : GetContentJob(serverName, mediaId) + , d(localFilename.isEmpty() ? new Private : new Private(localFilename)) +{ + setObjectName("DownloadFileJob"); +} + +QString DownloadFileJob::targetFileName() const +{ + return (d->targetFile ? d->targetFile : d->tempFile)->fileName(); +} + +void DownloadFileJob::beforeStart(const ConnectionData*) +{ + if (d->targetFile && !d->targetFile->isReadable() && + !d->targetFile->open(QIODevice::WriteOnly)) + { + qCWarning(JOBS) << "Couldn't open the file" + << d->targetFile->fileName() << "for writing"; + setStatus(FileError, "Could not open the target file for writing"); + return; + } + if (!d->tempFile->isReadable() && !d->tempFile->open(QIODevice::WriteOnly)) + { + qCWarning(JOBS) << "Couldn't open the temporary file" + << d->tempFile->fileName() << "for writing"; + setStatus(FileError, "Could not open the temporary download file"); + return; + } + qCDebug(JOBS) << "Downloading to" << d->tempFile->fileName(); +} + +void DownloadFileJob::afterStart(const ConnectionData*, QNetworkReply* reply) +{ + connect(reply, &QNetworkReply::metaDataChanged, this, [this,reply] { + auto sizeHeader = reply->header(QNetworkRequest::ContentLengthHeader); + if (sizeHeader.isValid()) + { + auto targetSize = sizeHeader.value(); + if (targetSize != -1) + if (!d->tempFile->resize(targetSize)) + { + qCWarning(JOBS) << "Failed to allocate" << targetSize + << "bytes for" << d->tempFile->fileName(); + setStatus(FileError, + "Could not reserve disk space for download"); + } + } + }); + connect(reply, &QIODevice::readyRead, this, [this,reply] { + auto bytes = reply->read(reply->bytesAvailable()); + if (bytes.isEmpty()) + { + qCWarning(JOBS) + << "Unexpected empty chunk when downloading from" + << reply->url() << "to" << d->tempFile->fileName(); + } else { + d->tempFile->write(bytes); + } + }); +} + +void DownloadFileJob::beforeAbandon(QNetworkReply*) +{ + if (d->targetFile) + d->targetFile->remove(); + d->tempFile->remove(); +} + +BaseJob::Status DownloadFileJob::parseReply(QNetworkReply*) +{ + if (d->targetFile) + { + d->targetFile->close(); + if (!d->targetFile->remove()) + { + qCWarning(JOBS) << "Failed to remove the target file placeholder"; + return { FileError, "Couldn't finalise the download" }; + } + if (!d->tempFile->rename(d->targetFile->fileName())) + { + qCWarning(JOBS) << "Failed to rename" << d->tempFile->fileName() + << "to" << d->targetFile->fileName(); + return { FileError, "Couldn't finalise the download" }; + } + } + else + d->tempFile->close(); + qCDebug(JOBS) << "Saved a file as" << targetFileName(); + return Success; +} diff --git a/lib/jobs/downloadfilejob.h b/lib/jobs/downloadfilejob.h new file mode 100644 index 00000000..1815a7c8 --- /dev/null +++ b/lib/jobs/downloadfilejob.h @@ -0,0 +1,30 @@ +#pragma once + +#include "generated/content-repo.h" + +namespace QMatrixClient +{ + class DownloadFileJob : public GetContentJob + { + public: + enum { FileError = BaseJob::UserDefinedError + 1 }; + + using GetContentJob::makeRequestUrl; + static QUrl makeRequestUrl(QUrl baseUrl, const QUrl& mxcUri); + + DownloadFileJob(const QString& serverName, const QString& mediaId, + const QString& localFilename = {}); + + QString targetFileName() const; + + private: + class Private; + QScopedPointer d; + + void beforeStart(const ConnectionData*) override; + void afterStart(const ConnectionData*, + QNetworkReply* reply) override; + void beforeAbandon(QNetworkReply*) override; + Status parseReply(QNetworkReply*) override; + }; +} diff --git a/lib/jobs/generated/account-data.cpp b/lib/jobs/generated/account-data.cpp new file mode 100644 index 00000000..35ee94c0 --- /dev/null +++ b/lib/jobs/generated/account-data.cpp @@ -0,0 +1,28 @@ +/****************************************************************************** + * THIS FILE IS GENERATED - ANY EDITS WILL BE OVERWRITTEN + */ + +#include "account-data.h" + +#include "converters.h" + +#include + +using namespace QMatrixClient; + +static const auto basePath = QStringLiteral("/_matrix/client/r0"); + +SetAccountDataJob::SetAccountDataJob(const QString& userId, const QString& type, const QJsonObject& content) + : BaseJob(HttpVerb::Put, "SetAccountDataJob", + basePath % "/user/" % userId % "/account_data/" % type) +{ + setRequestData(Data(content)); +} + +SetAccountDataPerRoomJob::SetAccountDataPerRoomJob(const QString& userId, const QString& roomId, const QString& type, const QJsonObject& content) + : BaseJob(HttpVerb::Put, "SetAccountDataPerRoomJob", + basePath % "/user/" % userId % "/rooms/" % roomId % "/account_data/" % type) +{ + setRequestData(Data(content)); +} + diff --git a/lib/jobs/generated/account-data.h b/lib/jobs/generated/account-data.h new file mode 100644 index 00000000..69ad9fb4 --- /dev/null +++ b/lib/jobs/generated/account-data.h @@ -0,0 +1,27 @@ +/****************************************************************************** + * THIS FILE IS GENERATED - ANY EDITS WILL BE OVERWRITTEN + */ + +#pragma once + +#include "../basejob.h" + +#include + + +namespace QMatrixClient +{ + // Operations + + class SetAccountDataJob : public BaseJob + { + public: + explicit SetAccountDataJob(const QString& userId, const QString& type, const QJsonObject& content = {}); + }; + + class SetAccountDataPerRoomJob : public BaseJob + { + public: + explicit SetAccountDataPerRoomJob(const QString& userId, const QString& roomId, const QString& type, const QJsonObject& content = {}); + }; +} // namespace QMatrixClient diff --git a/lib/jobs/generated/administrative_contact.cpp b/lib/jobs/generated/administrative_contact.cpp new file mode 100644 index 00000000..1af57941 --- /dev/null +++ b/lib/jobs/generated/administrative_contact.cpp @@ -0,0 +1,122 @@ +/****************************************************************************** + * THIS FILE IS GENERATED - ANY EDITS WILL BE OVERWRITTEN + */ + +#include "administrative_contact.h" + +#include + +using namespace QMatrixClient; + +static const auto basePath = QStringLiteral("/_matrix/client/r0"); + +GetAccount3PIDsJob::ThirdPartyIdentifier::operator QJsonObject() const +{ + QJsonObject o; + o.insert("medium", toJson(medium)); + o.insert("address", toJson(address)); + + return o; +} +namespace QMatrixClient +{ + template <> struct FromJson + { + GetAccount3PIDsJob::ThirdPartyIdentifier operator()(QJsonValue jv) + { + QJsonObject o = jv.toObject(); + GetAccount3PIDsJob::ThirdPartyIdentifier result; + result.medium = + fromJson(o.value("medium")); + result.address = + fromJson(o.value("address")); + + return result; + } + }; +} // namespace QMatrixClient + +class GetAccount3PIDsJob::Private +{ + public: + QVector threepids; +}; + +QUrl GetAccount3PIDsJob::makeRequestUrl(QUrl baseUrl) +{ + return BaseJob::makeRequestUrl(baseUrl, + basePath % "/account/3pid"); +} + +GetAccount3PIDsJob::GetAccount3PIDsJob() + : BaseJob(HttpVerb::Get, "GetAccount3PIDsJob", + basePath % "/account/3pid") + , d(new Private) +{ +} + +GetAccount3PIDsJob::~GetAccount3PIDsJob() = default; + +const QVector& GetAccount3PIDsJob::threepids() const +{ + return d->threepids; +} + +BaseJob::Status GetAccount3PIDsJob::parseJson(const QJsonDocument& data) +{ + auto json = data.object(); + d->threepids = fromJson>(json.value("threepids")); + return Success; +} + +Post3PIDsJob::ThreePidCredentials::operator QJsonObject() const +{ + QJsonObject o; + o.insert("client_secret", toJson(clientSecret)); + o.insert("id_server", toJson(idServer)); + o.insert("sid", toJson(sid)); + + return o; +} +namespace QMatrixClient +{ + template <> struct FromJson + { + Post3PIDsJob::ThreePidCredentials operator()(QJsonValue jv) + { + QJsonObject o = jv.toObject(); + Post3PIDsJob::ThreePidCredentials result; + result.clientSecret = + fromJson(o.value("client_secret")); + result.idServer = + fromJson(o.value("id_server")); + result.sid = + fromJson(o.value("sid")); + + return result; + } + }; +} // namespace QMatrixClient + +Post3PIDsJob::Post3PIDsJob(const ThreePidCredentials& threePidCreds, bool bind) + : BaseJob(HttpVerb::Post, "Post3PIDsJob", + basePath % "/account/3pid") +{ + QJsonObject _data; + _data.insert("three_pid_creds", toJson(threePidCreds)); + _data.insert("bind", toJson(bind)); + setRequestData(_data); +} + +QUrl RequestTokenTo3PIDJob::makeRequestUrl(QUrl baseUrl) +{ + return BaseJob::makeRequestUrl(baseUrl, + basePath % "/account/3pid/email/requestToken"); +} + +RequestTokenTo3PIDJob::RequestTokenTo3PIDJob() + : BaseJob(HttpVerb::Post, "RequestTokenTo3PIDJob", + basePath % "/account/3pid/email/requestToken", false) +{ +} + diff --git a/lib/jobs/generated/administrative_contact.h b/lib/jobs/generated/administrative_contact.h new file mode 100644 index 00000000..c8429d39 --- /dev/null +++ b/lib/jobs/generated/administrative_contact.h @@ -0,0 +1,83 @@ +/****************************************************************************** + * THIS FILE IS GENERATED - ANY EDITS WILL BE OVERWRITTEN + */ + +#pragma once + +#include "../basejob.h" + +#include + +#include "converters.h" + +namespace QMatrixClient +{ + // Operations + + class GetAccount3PIDsJob : public BaseJob + { + public: + // Inner data structures + + struct ThirdPartyIdentifier + { + QString medium; + QString address; + + operator QJsonObject() const; + }; + + // End of inner data structures + + /** Construct a URL out of baseUrl and usual parameters passed to + * GetAccount3PIDsJob. This function can be used when + * a URL for GetAccount3PIDsJob is necessary but the job + * itself isn't. + */ + static QUrl makeRequestUrl(QUrl baseUrl); + + explicit GetAccount3PIDsJob(); + ~GetAccount3PIDsJob() override; + + const QVector& threepids() const; + + protected: + Status parseJson(const QJsonDocument& data) override; + + private: + class Private; + QScopedPointer d; + }; + + class Post3PIDsJob : public BaseJob + { + public: + // Inner data structures + + struct ThreePidCredentials + { + QString clientSecret; + QString idServer; + QString sid; + + operator QJsonObject() const; + }; + + // End of inner data structures + + explicit Post3PIDsJob(const ThreePidCredentials& threePidCreds, bool bind = {}); + }; + + class RequestTokenTo3PIDJob : public BaseJob + { + public: + /** Construct a URL out of baseUrl and usual parameters passed to + * RequestTokenTo3PIDJob. This function can be used when + * a URL for RequestTokenTo3PIDJob is necessary but the job + * itself isn't. + */ + static QUrl makeRequestUrl(QUrl baseUrl); + + explicit RequestTokenTo3PIDJob(); + }; +} // namespace QMatrixClient diff --git a/lib/jobs/generated/banning.cpp b/lib/jobs/generated/banning.cpp new file mode 100644 index 00000000..f66b27b6 --- /dev/null +++ b/lib/jobs/generated/banning.cpp @@ -0,0 +1,34 @@ +/****************************************************************************** + * THIS FILE IS GENERATED - ANY EDITS WILL BE OVERWRITTEN + */ + +#include "banning.h" + +#include "converters.h" + +#include + +using namespace QMatrixClient; + +static const auto basePath = QStringLiteral("/_matrix/client/r0"); + +BanJob::BanJob(const QString& roomId, const QString& userId, const QString& reason) + : BaseJob(HttpVerb::Post, "BanJob", + basePath % "/rooms/" % roomId % "/ban") +{ + QJsonObject _data; + _data.insert("user_id", toJson(userId)); + if (!reason.isEmpty()) + _data.insert("reason", toJson(reason)); + setRequestData(_data); +} + +UnbanJob::UnbanJob(const QString& roomId, const QString& userId) + : BaseJob(HttpVerb::Post, "UnbanJob", + basePath % "/rooms/" % roomId % "/unban") +{ + QJsonObject _data; + _data.insert("user_id", toJson(userId)); + setRequestData(_data); +} + diff --git a/lib/jobs/generated/banning.h b/lib/jobs/generated/banning.h new file mode 100644 index 00000000..2d6fbd9b --- /dev/null +++ b/lib/jobs/generated/banning.h @@ -0,0 +1,26 @@ +/****************************************************************************** + * THIS FILE IS GENERATED - ANY EDITS WILL BE OVERWRITTEN + */ + +#pragma once + +#include "../basejob.h" + + + +namespace QMatrixClient +{ + // Operations + + class BanJob : public BaseJob + { + public: + explicit BanJob(const QString& roomId, const QString& userId, const QString& reason = {}); + }; + + class UnbanJob : public BaseJob + { + public: + explicit UnbanJob(const QString& roomId, const QString& userId); + }; +} // namespace QMatrixClient diff --git a/lib/jobs/generated/content-repo.cpp b/lib/jobs/generated/content-repo.cpp new file mode 100644 index 00000000..51011251 --- /dev/null +++ b/lib/jobs/generated/content-repo.cpp @@ -0,0 +1,254 @@ +/****************************************************************************** + * THIS FILE IS GENERATED - ANY EDITS WILL BE OVERWRITTEN + */ + +#include "content-repo.h" + +#include "converters.h" + +#include +#include + +using namespace QMatrixClient; + +static const auto basePath = QStringLiteral("/_matrix/media/r0"); + +class UploadContentJob::Private +{ + public: + QString contentUri; +}; + +BaseJob::Query queryToUploadContent(const QString& filename) +{ + BaseJob::Query _q; + if (!filename.isEmpty()) + _q.addQueryItem("filename", filename); + return _q; +} + +UploadContentJob::UploadContentJob(QIODevice* content, const QString& filename, const QString& contentType) + : BaseJob(HttpVerb::Post, "UploadContentJob", + basePath % "/upload", + queryToUploadContent(filename)) + , d(new Private) +{ + setRequestHeader("Content-Type", contentType.toLatin1()); + + setRequestData(Data(content)); +} + +UploadContentJob::~UploadContentJob() = default; + +const QString& UploadContentJob::contentUri() const +{ + return d->contentUri; +} + +BaseJob::Status UploadContentJob::parseJson(const QJsonDocument& data) +{ + auto json = data.object(); + if (!json.contains("content_uri")) + return { JsonParseError, + "The key 'content_uri' not found in the response" }; + d->contentUri = fromJson(json.value("content_uri")); + return Success; +} + +class GetContentJob::Private +{ + public: + QString contentType; + QString contentDisposition; + QIODevice* content; +}; + +QUrl GetContentJob::makeRequestUrl(QUrl baseUrl, const QString& serverName, const QString& mediaId) +{ + return BaseJob::makeRequestUrl(baseUrl, + basePath % "/download/" % serverName % "/" % mediaId); +} + +GetContentJob::GetContentJob(const QString& serverName, const QString& mediaId) + : BaseJob(HttpVerb::Get, "GetContentJob", + basePath % "/download/" % serverName % "/" % mediaId, false) + , d(new Private) +{ + setExpectedContentTypes({ "*/*" }); +} + +GetContentJob::~GetContentJob() = default; + +const QString& GetContentJob::contentType() const +{ + return d->contentType; +} + +const QString& GetContentJob::contentDisposition() const +{ + return d->contentDisposition; +} + +QIODevice* GetContentJob::content() const +{ + return d->content; +} + +BaseJob::Status GetContentJob::parseReply(QNetworkReply* reply) +{ + d->contentType = reply->rawHeader("Content-Type"); + d->contentDisposition = reply->rawHeader("Content-Disposition"); + d->content = reply; + return Success; +} + +class GetContentOverrideNameJob::Private +{ + public: + QString contentType; + QString contentDisposition; + QIODevice* content; +}; + +QUrl GetContentOverrideNameJob::makeRequestUrl(QUrl baseUrl, const QString& serverName, const QString& mediaId, const QString& fileName) +{ + return BaseJob::makeRequestUrl(baseUrl, + basePath % "/download/" % serverName % "/" % mediaId % "/" % fileName); +} + +GetContentOverrideNameJob::GetContentOverrideNameJob(const QString& serverName, const QString& mediaId, const QString& fileName) + : BaseJob(HttpVerb::Get, "GetContentOverrideNameJob", + basePath % "/download/" % serverName % "/" % mediaId % "/" % fileName, false) + , d(new Private) +{ + setExpectedContentTypes({ "*/*" }); +} + +GetContentOverrideNameJob::~GetContentOverrideNameJob() = default; + +const QString& GetContentOverrideNameJob::contentType() const +{ + return d->contentType; +} + +const QString& GetContentOverrideNameJob::contentDisposition() const +{ + return d->contentDisposition; +} + +QIODevice* GetContentOverrideNameJob::content() const +{ + return d->content; +} + +BaseJob::Status GetContentOverrideNameJob::parseReply(QNetworkReply* reply) +{ + d->contentType = reply->rawHeader("Content-Type"); + d->contentDisposition = reply->rawHeader("Content-Disposition"); + d->content = reply; + return Success; +} + +class GetContentThumbnailJob::Private +{ + public: + QString contentType; + QIODevice* content; +}; + +BaseJob::Query queryToGetContentThumbnail(int width, int height, const QString& method) +{ + BaseJob::Query _q; + _q.addQueryItem("width", QString("%1").arg(width)); + _q.addQueryItem("height", QString("%1").arg(height)); + if (!method.isEmpty()) + _q.addQueryItem("method", method); + return _q; +} + +QUrl GetContentThumbnailJob::makeRequestUrl(QUrl baseUrl, const QString& serverName, const QString& mediaId, int width, int height, const QString& method) +{ + return BaseJob::makeRequestUrl(baseUrl, + basePath % "/thumbnail/" % serverName % "/" % mediaId, + queryToGetContentThumbnail(width, height, method)); +} + +GetContentThumbnailJob::GetContentThumbnailJob(const QString& serverName, const QString& mediaId, int width, int height, const QString& method) + : BaseJob(HttpVerb::Get, "GetContentThumbnailJob", + basePath % "/thumbnail/" % serverName % "/" % mediaId, + queryToGetContentThumbnail(width, height, method), + {}, false) + , d(new Private) +{ + setExpectedContentTypes({ "image/jpeg", "image/png" }); +} + +GetContentThumbnailJob::~GetContentThumbnailJob() = default; + +const QString& GetContentThumbnailJob::contentType() const +{ + return d->contentType; +} + +QIODevice* GetContentThumbnailJob::content() const +{ + return d->content; +} + +BaseJob::Status GetContentThumbnailJob::parseReply(QNetworkReply* reply) +{ + d->contentType = reply->rawHeader("Content-Type"); + d->content = reply; + return Success; +} + +class GetUrlPreviewJob::Private +{ + public: + double matrixImageSize; + QString ogImage; +}; + +BaseJob::Query queryToGetUrlPreview(const QString& url, double ts) +{ + BaseJob::Query _q; + _q.addQueryItem("url", url); + _q.addQueryItem("ts", QString("%1").arg(ts)); + return _q; +} + +QUrl GetUrlPreviewJob::makeRequestUrl(QUrl baseUrl, const QString& url, double ts) +{ + return BaseJob::makeRequestUrl(baseUrl, + basePath % "/preview_url", + queryToGetUrlPreview(url, ts)); +} + +GetUrlPreviewJob::GetUrlPreviewJob(const QString& url, double ts) + : BaseJob(HttpVerb::Get, "GetUrlPreviewJob", + basePath % "/preview_url", + queryToGetUrlPreview(url, ts)) + , d(new Private) +{ +} + +GetUrlPreviewJob::~GetUrlPreviewJob() = default; + +double GetUrlPreviewJob::matrixImageSize() const +{ + return d->matrixImageSize; +} + +const QString& GetUrlPreviewJob::ogImage() const +{ + return d->ogImage; +} + +BaseJob::Status GetUrlPreviewJob::parseJson(const QJsonDocument& data) +{ + auto json = data.object(); + d->matrixImageSize = fromJson(json.value("matrix:image:size")); + d->ogImage = fromJson(json.value("og:image")); + return Success; +} + diff --git a/lib/jobs/generated/content-repo.h b/lib/jobs/generated/content-repo.h new file mode 100644 index 00000000..b4ea562f --- /dev/null +++ b/lib/jobs/generated/content-repo.h @@ -0,0 +1,129 @@ +/****************************************************************************** + * THIS FILE IS GENERATED - ANY EDITS WILL BE OVERWRITTEN + */ + +#pragma once + +#include "../basejob.h" + +#include + + +namespace QMatrixClient +{ + // Operations + + class UploadContentJob : public BaseJob + { + public: + explicit UploadContentJob(QIODevice* content, const QString& filename = {}, const QString& contentType = {}); + ~UploadContentJob() override; + + const QString& contentUri() const; + + protected: + Status parseJson(const QJsonDocument& data) override; + + private: + class Private; + QScopedPointer d; + }; + + class GetContentJob : public BaseJob + { + public: + /** Construct a URL out of baseUrl and usual parameters passed to + * GetContentJob. This function can be used when + * a URL for GetContentJob is necessary but the job + * itself isn't. + */ + static QUrl makeRequestUrl(QUrl baseUrl, const QString& serverName, const QString& mediaId); + + explicit GetContentJob(const QString& serverName, const QString& mediaId); + ~GetContentJob() override; + + const QString& contentType() const; + const QString& contentDisposition() const; + QIODevice* content() const; + + protected: + Status parseReply(QNetworkReply* reply) override; + + private: + class Private; + QScopedPointer d; + }; + + class GetContentOverrideNameJob : public BaseJob + { + public: + /** Construct a URL out of baseUrl and usual parameters passed to + * GetContentOverrideNameJob. This function can be used when + * a URL for GetContentOverrideNameJob is necessary but the job + * itself isn't. + */ + static QUrl makeRequestUrl(QUrl baseUrl, const QString& serverName, const QString& mediaId, const QString& fileName); + + explicit GetContentOverrideNameJob(const QString& serverName, const QString& mediaId, const QString& fileName); + ~GetContentOverrideNameJob() override; + + const QString& contentType() const; + const QString& contentDisposition() const; + QIODevice* content() const; + + protected: + Status parseReply(QNetworkReply* reply) override; + + private: + class Private; + QScopedPointer d; + }; + + class GetContentThumbnailJob : public BaseJob + { + public: + /** Construct a URL out of baseUrl and usual parameters passed to + * GetContentThumbnailJob. This function can be used when + * a URL for GetContentThumbnailJob is necessary but the job + * itself isn't. + */ + static QUrl makeRequestUrl(QUrl baseUrl, const QString& serverName, const QString& mediaId, int width = {}, int height = {}, const QString& method = {}); + + explicit GetContentThumbnailJob(const QString& serverName, const QString& mediaId, int width = {}, int height = {}, const QString& method = {}); + ~GetContentThumbnailJob() override; + + const QString& contentType() const; + QIODevice* content() const; + + protected: + Status parseReply(QNetworkReply* reply) override; + + private: + class Private; + QScopedPointer d; + }; + + class GetUrlPreviewJob : public BaseJob + { + public: + /** Construct a URL out of baseUrl and usual parameters passed to + * GetUrlPreviewJob. This function can be used when + * a URL for GetUrlPreviewJob is necessary but the job + * itself isn't. + */ + static QUrl makeRequestUrl(QUrl baseUrl, const QString& url, double ts = {}); + + explicit GetUrlPreviewJob(const QString& url, double ts = {}); + ~GetUrlPreviewJob() override; + + double matrixImageSize() const; + const QString& ogImage() const; + + protected: + Status parseJson(const QJsonDocument& data) override; + + private: + class Private; + QScopedPointer d; + }; +} // namespace QMatrixClient diff --git a/lib/jobs/generated/create_room.cpp b/lib/jobs/generated/create_room.cpp new file mode 100644 index 00000000..de7807b5 --- /dev/null +++ b/lib/jobs/generated/create_room.cpp @@ -0,0 +1,115 @@ +/****************************************************************************** + * THIS FILE IS GENERATED - ANY EDITS WILL BE OVERWRITTEN + */ + +#include "create_room.h" + +#include + +using namespace QMatrixClient; + +static const auto basePath = QStringLiteral("/_matrix/client/r0"); + +CreateRoomJob::Invite3pid::operator QJsonObject() const +{ + QJsonObject o; + o.insert("id_server", toJson(idServer)); + o.insert("medium", toJson(medium)); + o.insert("address", toJson(address)); + + return o; +} +namespace QMatrixClient +{ + template <> struct FromJson + { + CreateRoomJob::Invite3pid operator()(QJsonValue jv) + { + QJsonObject o = jv.toObject(); + CreateRoomJob::Invite3pid result; + result.idServer = + fromJson(o.value("id_server")); + result.medium = + fromJson(o.value("medium")); + result.address = + fromJson(o.value("address")); + + return result; + } + }; +} // namespace QMatrixClient + +CreateRoomJob::StateEvent::operator QJsonObject() const +{ + QJsonObject o; + o.insert("type", toJson(type)); + o.insert("state_key", toJson(stateKey)); + o.insert("content", toJson(content)); + + return o; +} +namespace QMatrixClient +{ + template <> struct FromJson + { + CreateRoomJob::StateEvent operator()(QJsonValue jv) + { + QJsonObject o = jv.toObject(); + CreateRoomJob::StateEvent result; + result.type = + fromJson(o.value("type")); + result.stateKey = + fromJson(o.value("state_key")); + result.content = + fromJson(o.value("content")); + + return result; + } + }; +} // namespace QMatrixClient + +class CreateRoomJob::Private +{ + public: + QString roomId; +}; + +CreateRoomJob::CreateRoomJob(const QString& visibility, const QString& roomAliasName, const QString& name, const QString& topic, const QVector& invite, const QVector& invite3pid, const QJsonObject& creationContent, const QVector& initialState, const QString& preset, bool isDirect, bool guestCanJoin) + : BaseJob(HttpVerb::Post, "CreateRoomJob", + basePath % "/createRoom") + , d(new Private) +{ + QJsonObject _data; + if (!visibility.isEmpty()) + _data.insert("visibility", toJson(visibility)); + if (!roomAliasName.isEmpty()) + _data.insert("room_alias_name", toJson(roomAliasName)); + if (!name.isEmpty()) + _data.insert("name", toJson(name)); + if (!topic.isEmpty()) + _data.insert("topic", toJson(topic)); + _data.insert("invite", toJson(invite)); + _data.insert("invite_3pid", toJson(invite3pid)); + _data.insert("creation_content", toJson(creationContent)); + _data.insert("initial_state", toJson(initialState)); + if (!preset.isEmpty()) + _data.insert("preset", toJson(preset)); + _data.insert("is_direct", toJson(isDirect)); + _data.insert("guest_can_join", toJson(guestCanJoin)); + setRequestData(_data); +} + +CreateRoomJob::~CreateRoomJob() = default; + +const QString& CreateRoomJob::roomId() const +{ + return d->roomId; +} + +BaseJob::Status CreateRoomJob::parseJson(const QJsonDocument& data) +{ + auto json = data.object(); + d->roomId = fromJson(json.value("room_id")); + return Success; +} + diff --git a/lib/jobs/generated/create_room.h b/lib/jobs/generated/create_room.h new file mode 100644 index 00000000..b479615a --- /dev/null +++ b/lib/jobs/generated/create_room.h @@ -0,0 +1,55 @@ +/****************************************************************************** + * THIS FILE IS GENERATED - ANY EDITS WILL BE OVERWRITTEN + */ + +#pragma once + +#include "../basejob.h" + +#include +#include + +#include "converters.h" + +namespace QMatrixClient +{ + // Operations + + class CreateRoomJob : public BaseJob + { + public: + // Inner data structures + + struct Invite3pid + { + QString idServer; + QString medium; + QString address; + + operator QJsonObject() const; + }; + + struct StateEvent + { + QString type; + QString stateKey; + QJsonObject content; + + operator QJsonObject() const; + }; + + // End of inner data structures + + explicit CreateRoomJob(const QString& visibility = {}, const QString& roomAliasName = {}, const QString& name = {}, const QString& topic = {}, const QVector& invite = {}, const QVector& invite3pid = {}, const QJsonObject& creationContent = {}, const QVector& initialState = {}, const QString& preset = {}, bool isDirect = {}, bool guestCanJoin = {}); + ~CreateRoomJob() override; + + const QString& roomId() const; + + protected: + Status parseJson(const QJsonDocument& data) override; + + private: + class Private; + QScopedPointer d; + }; +} // namespace QMatrixClient diff --git a/lib/jobs/generated/directory.cpp b/lib/jobs/generated/directory.cpp new file mode 100644 index 00000000..9428dcee --- /dev/null +++ b/lib/jobs/generated/directory.cpp @@ -0,0 +1,76 @@ +/****************************************************************************** + * THIS FILE IS GENERATED - ANY EDITS WILL BE OVERWRITTEN + */ + +#include "directory.h" + +#include "converters.h" + +#include + +using namespace QMatrixClient; + +static const auto basePath = QStringLiteral("/_matrix/client/r0/directory"); + +SetRoomAliasJob::SetRoomAliasJob(const QString& roomAlias, const QString& roomId) + : BaseJob(HttpVerb::Put, "SetRoomAliasJob", + basePath % "/room/" % roomAlias) +{ + QJsonObject _data; + if (!roomId.isEmpty()) + _data.insert("room_id", toJson(roomId)); + setRequestData(_data); +} + +class GetRoomIdByAliasJob::Private +{ + public: + QString roomId; + QVector servers; +}; + +QUrl GetRoomIdByAliasJob::makeRequestUrl(QUrl baseUrl, const QString& roomAlias) +{ + return BaseJob::makeRequestUrl(baseUrl, + basePath % "/room/" % roomAlias); +} + +GetRoomIdByAliasJob::GetRoomIdByAliasJob(const QString& roomAlias) + : BaseJob(HttpVerb::Get, "GetRoomIdByAliasJob", + basePath % "/room/" % roomAlias, false) + , d(new Private) +{ +} + +GetRoomIdByAliasJob::~GetRoomIdByAliasJob() = default; + +const QString& GetRoomIdByAliasJob::roomId() const +{ + return d->roomId; +} + +const QVector& GetRoomIdByAliasJob::servers() const +{ + return d->servers; +} + +BaseJob::Status GetRoomIdByAliasJob::parseJson(const QJsonDocument& data) +{ + auto json = data.object(); + d->roomId = fromJson(json.value("room_id")); + d->servers = fromJson>(json.value("servers")); + return Success; +} + +QUrl DeleteRoomAliasJob::makeRequestUrl(QUrl baseUrl, const QString& roomAlias) +{ + return BaseJob::makeRequestUrl(baseUrl, + basePath % "/room/" % roomAlias); +} + +DeleteRoomAliasJob::DeleteRoomAliasJob(const QString& roomAlias) + : BaseJob(HttpVerb::Delete, "DeleteRoomAliasJob", + basePath % "/room/" % roomAlias) +{ +} + diff --git a/lib/jobs/generated/directory.h b/lib/jobs/generated/directory.h new file mode 100644 index 00000000..87591240 --- /dev/null +++ b/lib/jobs/generated/directory.h @@ -0,0 +1,58 @@ +/****************************************************************************** + * THIS FILE IS GENERATED - ANY EDITS WILL BE OVERWRITTEN + */ + +#pragma once + +#include "../basejob.h" + +#include + + +namespace QMatrixClient +{ + // Operations + + class SetRoomAliasJob : public BaseJob + { + public: + explicit SetRoomAliasJob(const QString& roomAlias, const QString& roomId = {}); + }; + + class GetRoomIdByAliasJob : public BaseJob + { + public: + /** Construct a URL out of baseUrl and usual parameters passed to + * GetRoomIdByAliasJob. This function can be used when + * a URL for GetRoomIdByAliasJob is necessary but the job + * itself isn't. + */ + static QUrl makeRequestUrl(QUrl baseUrl, const QString& roomAlias); + + explicit GetRoomIdByAliasJob(const QString& roomAlias); + ~GetRoomIdByAliasJob() override; + + const QString& roomId() const; + const QVector& servers() const; + + protected: + Status parseJson(const QJsonDocument& data) override; + + private: + class Private; + QScopedPointer d; + }; + + class DeleteRoomAliasJob : public BaseJob + { + public: + /** Construct a URL out of baseUrl and usual parameters passed to + * DeleteRoomAliasJob. This function can be used when + * a URL for DeleteRoomAliasJob is necessary but the job + * itself isn't. + */ + static QUrl makeRequestUrl(QUrl baseUrl, const QString& roomAlias); + + explicit DeleteRoomAliasJob(const QString& roomAlias); + }; +} // namespace QMatrixClient diff --git a/lib/jobs/generated/inviting.cpp b/lib/jobs/generated/inviting.cpp new file mode 100644 index 00000000..d2ee2107 --- /dev/null +++ b/lib/jobs/generated/inviting.cpp @@ -0,0 +1,23 @@ +/****************************************************************************** + * THIS FILE IS GENERATED - ANY EDITS WILL BE OVERWRITTEN + */ + +#include "inviting.h" + +#include "converters.h" + +#include + +using namespace QMatrixClient; + +static const auto basePath = QStringLiteral("/_matrix/client/r0"); + +InviteUserJob::InviteUserJob(const QString& roomId, const QString& userId) + : BaseJob(HttpVerb::Post, "InviteUserJob", + basePath % "/rooms/" % roomId % "/invite") +{ + QJsonObject _data; + _data.insert("user_id", toJson(userId)); + setRequestData(_data); +} + diff --git a/lib/jobs/generated/inviting.h b/lib/jobs/generated/inviting.h new file mode 100644 index 00000000..eaa884df --- /dev/null +++ b/lib/jobs/generated/inviting.h @@ -0,0 +1,20 @@ +/****************************************************************************** + * THIS FILE IS GENERATED - ANY EDITS WILL BE OVERWRITTEN + */ + +#pragma once + +#include "../basejob.h" + + + +namespace QMatrixClient +{ + // Operations + + class InviteUserJob : public BaseJob + { + public: + explicit InviteUserJob(const QString& roomId, const QString& userId); + }; +} // namespace QMatrixClient diff --git a/lib/jobs/generated/kicking.cpp b/lib/jobs/generated/kicking.cpp new file mode 100644 index 00000000..bf2490b7 --- /dev/null +++ b/lib/jobs/generated/kicking.cpp @@ -0,0 +1,25 @@ +/****************************************************************************** + * THIS FILE IS GENERATED - ANY EDITS WILL BE OVERWRITTEN + */ + +#include "kicking.h" + +#include "converters.h" + +#include + +using namespace QMatrixClient; + +static const auto basePath = QStringLiteral("/_matrix/client/r0"); + +KickJob::KickJob(const QString& roomId, const QString& userId, const QString& reason) + : BaseJob(HttpVerb::Post, "KickJob", + basePath % "/rooms/" % roomId % "/kick") +{ + QJsonObject _data; + _data.insert("user_id", toJson(userId)); + if (!reason.isEmpty()) + _data.insert("reason", toJson(reason)); + setRequestData(_data); +} + diff --git a/lib/jobs/generated/kicking.h b/lib/jobs/generated/kicking.h new file mode 100644 index 00000000..3814bef7 --- /dev/null +++ b/lib/jobs/generated/kicking.h @@ -0,0 +1,20 @@ +/****************************************************************************** + * THIS FILE IS GENERATED - ANY EDITS WILL BE OVERWRITTEN + */ + +#pragma once + +#include "../basejob.h" + + + +namespace QMatrixClient +{ + // Operations + + class KickJob : public BaseJob + { + public: + explicit KickJob(const QString& roomId, const QString& userId, const QString& reason = {}); + }; +} // namespace QMatrixClient diff --git a/lib/jobs/generated/leaving.cpp b/lib/jobs/generated/leaving.cpp new file mode 100644 index 00000000..fbc40d11 --- /dev/null +++ b/lib/jobs/generated/leaving.cpp @@ -0,0 +1,38 @@ +/****************************************************************************** + * THIS FILE IS GENERATED - ANY EDITS WILL BE OVERWRITTEN + */ + +#include "leaving.h" + +#include "converters.h" + +#include + +using namespace QMatrixClient; + +static const auto basePath = QStringLiteral("/_matrix/client/r0"); + +QUrl LeaveRoomJob::makeRequestUrl(QUrl baseUrl, const QString& roomId) +{ + return BaseJob::makeRequestUrl(baseUrl, + basePath % "/rooms/" % roomId % "/leave"); +} + +LeaveRoomJob::LeaveRoomJob(const QString& roomId) + : BaseJob(HttpVerb::Post, "LeaveRoomJob", + basePath % "/rooms/" % roomId % "/leave") +{ +} + +QUrl ForgetRoomJob::makeRequestUrl(QUrl baseUrl, const QString& roomId) +{ + return BaseJob::makeRequestUrl(baseUrl, + basePath % "/rooms/" % roomId % "/forget"); +} + +ForgetRoomJob::ForgetRoomJob(const QString& roomId) + : BaseJob(HttpVerb::Post, "ForgetRoomJob", + basePath % "/rooms/" % roomId % "/forget") +{ +} + diff --git a/lib/jobs/generated/leaving.h b/lib/jobs/generated/leaving.h new file mode 100644 index 00000000..9bae2363 --- /dev/null +++ b/lib/jobs/generated/leaving.h @@ -0,0 +1,40 @@ +/****************************************************************************** + * THIS FILE IS GENERATED - ANY EDITS WILL BE OVERWRITTEN + */ + +#pragma once + +#include "../basejob.h" + + + +namespace QMatrixClient +{ + // Operations + + class LeaveRoomJob : public BaseJob + { + public: + /** Construct a URL out of baseUrl and usual parameters passed to + * LeaveRoomJob. This function can be used when + * a URL for LeaveRoomJob is necessary but the job + * itself isn't. + */ + static QUrl makeRequestUrl(QUrl baseUrl, const QString& roomId); + + explicit LeaveRoomJob(const QString& roomId); + }; + + class ForgetRoomJob : public BaseJob + { + public: + /** Construct a URL out of baseUrl and usual parameters passed to + * ForgetRoomJob. This function can be used when + * a URL for ForgetRoomJob is necessary but the job + * itself isn't. + */ + static QUrl makeRequestUrl(QUrl baseUrl, const QString& roomId); + + explicit ForgetRoomJob(const QString& roomId); + }; +} // namespace QMatrixClient diff --git a/lib/jobs/generated/list_public_rooms.cpp b/lib/jobs/generated/list_public_rooms.cpp new file mode 100644 index 00000000..39653300 --- /dev/null +++ b/lib/jobs/generated/list_public_rooms.cpp @@ -0,0 +1,266 @@ +/****************************************************************************** + * THIS FILE IS GENERATED - ANY EDITS WILL BE OVERWRITTEN + */ + +#include "list_public_rooms.h" + +#include + +using namespace QMatrixClient; + +static const auto basePath = QStringLiteral("/_matrix/client/r0"); + +GetPublicRoomsJob::PublicRoomsChunk::operator QJsonObject() const +{ + QJsonObject o; + o.insert("aliases", toJson(aliases)); + o.insert("canonical_alias", toJson(canonicalAlias)); + o.insert("name", toJson(name)); + o.insert("num_joined_members", toJson(numJoinedMembers)); + o.insert("room_id", toJson(roomId)); + o.insert("topic", toJson(topic)); + o.insert("world_readable", toJson(worldReadable)); + o.insert("guest_can_join", toJson(guestCanJoin)); + o.insert("avatar_url", toJson(avatarUrl)); + + return o; +} +namespace QMatrixClient +{ + template <> struct FromJson + { + GetPublicRoomsJob::PublicRoomsChunk operator()(QJsonValue jv) + { + QJsonObject o = jv.toObject(); + GetPublicRoomsJob::PublicRoomsChunk result; + result.aliases = + fromJson>(o.value("aliases")); + result.canonicalAlias = + fromJson(o.value("canonical_alias")); + result.name = + fromJson(o.value("name")); + result.numJoinedMembers = + fromJson(o.value("num_joined_members")); + result.roomId = + fromJson(o.value("room_id")); + result.topic = + fromJson(o.value("topic")); + result.worldReadable = + fromJson(o.value("world_readable")); + result.guestCanJoin = + fromJson(o.value("guest_can_join")); + result.avatarUrl = + fromJson(o.value("avatar_url")); + + return result; + } + }; +} // namespace QMatrixClient + +class GetPublicRoomsJob::Private +{ + public: + QVector chunk; + QString nextBatch; + QString prevBatch; + double totalRoomCountEstimate; +}; + +BaseJob::Query queryToGetPublicRooms(double limit, const QString& since, const QString& server) +{ + BaseJob::Query _q; + _q.addQueryItem("limit", QString("%1").arg(limit)); + if (!since.isEmpty()) + _q.addQueryItem("since", since); + if (!server.isEmpty()) + _q.addQueryItem("server", server); + return _q; +} + +QUrl GetPublicRoomsJob::makeRequestUrl(QUrl baseUrl, double limit, const QString& since, const QString& server) +{ + return BaseJob::makeRequestUrl(baseUrl, + basePath % "/publicRooms", + queryToGetPublicRooms(limit, since, server)); +} + +GetPublicRoomsJob::GetPublicRoomsJob(double limit, const QString& since, const QString& server) + : BaseJob(HttpVerb::Get, "GetPublicRoomsJob", + basePath % "/publicRooms", + queryToGetPublicRooms(limit, since, server), + {}, false) + , d(new Private) +{ +} + +GetPublicRoomsJob::~GetPublicRoomsJob() = default; + +const QVector& GetPublicRoomsJob::chunk() const +{ + return d->chunk; +} + +const QString& GetPublicRoomsJob::nextBatch() const +{ + return d->nextBatch; +} + +const QString& GetPublicRoomsJob::prevBatch() const +{ + return d->prevBatch; +} + +double GetPublicRoomsJob::totalRoomCountEstimate() const +{ + return d->totalRoomCountEstimate; +} + +BaseJob::Status GetPublicRoomsJob::parseJson(const QJsonDocument& data) +{ + auto json = data.object(); + if (!json.contains("chunk")) + return { JsonParseError, + "The key 'chunk' not found in the response" }; + d->chunk = fromJson>(json.value("chunk")); + d->nextBatch = fromJson(json.value("next_batch")); + d->prevBatch = fromJson(json.value("prev_batch")); + d->totalRoomCountEstimate = fromJson(json.value("total_room_count_estimate")); + return Success; +} + +QueryPublicRoomsJob::Filter::operator QJsonObject() const +{ + QJsonObject o; + o.insert("generic_search_term", toJson(genericSearchTerm)); + + return o; +} +namespace QMatrixClient +{ + template <> struct FromJson + { + QueryPublicRoomsJob::Filter operator()(QJsonValue jv) + { + QJsonObject o = jv.toObject(); + QueryPublicRoomsJob::Filter result; + result.genericSearchTerm = + fromJson(o.value("generic_search_term")); + + return result; + } + }; +} // namespace QMatrixClient + +QueryPublicRoomsJob::PublicRoomsChunk::operator QJsonObject() const +{ + QJsonObject o; + o.insert("aliases", toJson(aliases)); + o.insert("canonical_alias", toJson(canonicalAlias)); + o.insert("name", toJson(name)); + o.insert("num_joined_members", toJson(numJoinedMembers)); + o.insert("room_id", toJson(roomId)); + o.insert("topic", toJson(topic)); + o.insert("world_readable", toJson(worldReadable)); + o.insert("guest_can_join", toJson(guestCanJoin)); + o.insert("avatar_url", toJson(avatarUrl)); + + return o; +} +namespace QMatrixClient +{ + template <> struct FromJson + { + QueryPublicRoomsJob::PublicRoomsChunk operator()(QJsonValue jv) + { + QJsonObject o = jv.toObject(); + QueryPublicRoomsJob::PublicRoomsChunk result; + result.aliases = + fromJson>(o.value("aliases")); + result.canonicalAlias = + fromJson(o.value("canonical_alias")); + result.name = + fromJson(o.value("name")); + result.numJoinedMembers = + fromJson(o.value("num_joined_members")); + result.roomId = + fromJson(o.value("room_id")); + result.topic = + fromJson(o.value("topic")); + result.worldReadable = + fromJson(o.value("world_readable")); + result.guestCanJoin = + fromJson(o.value("guest_can_join")); + result.avatarUrl = + fromJson(o.value("avatar_url")); + + return result; + } + }; +} // namespace QMatrixClient + +class QueryPublicRoomsJob::Private +{ + public: + QVector chunk; + QString nextBatch; + QString prevBatch; + double totalRoomCountEstimate; +}; + +BaseJob::Query queryToQueryPublicRooms(const QString& server) +{ + BaseJob::Query _q; + if (!server.isEmpty()) + _q.addQueryItem("server", server); + return _q; +} + +QueryPublicRoomsJob::QueryPublicRoomsJob(const QString& server, double limit, const QString& since, const Filter& filter) + : BaseJob(HttpVerb::Post, "QueryPublicRoomsJob", + basePath % "/publicRooms", + queryToQueryPublicRooms(server)) + , d(new Private) +{ + QJsonObject _data; + _data.insert("limit", toJson(limit)); + if (!since.isEmpty()) + _data.insert("since", toJson(since)); + _data.insert("filter", toJson(filter)); + setRequestData(_data); +} + +QueryPublicRoomsJob::~QueryPublicRoomsJob() = default; + +const QVector& QueryPublicRoomsJob::chunk() const +{ + return d->chunk; +} + +const QString& QueryPublicRoomsJob::nextBatch() const +{ + return d->nextBatch; +} + +const QString& QueryPublicRoomsJob::prevBatch() const +{ + return d->prevBatch; +} + +double QueryPublicRoomsJob::totalRoomCountEstimate() const +{ + return d->totalRoomCountEstimate; +} + +BaseJob::Status QueryPublicRoomsJob::parseJson(const QJsonDocument& data) +{ + auto json = data.object(); + if (!json.contains("chunk")) + return { JsonParseError, + "The key 'chunk' not found in the response" }; + d->chunk = fromJson>(json.value("chunk")); + d->nextBatch = fromJson(json.value("next_batch")); + d->prevBatch = fromJson(json.value("prev_batch")); + d->totalRoomCountEstimate = fromJson(json.value("total_room_count_estimate")); + return Success; +} + diff --git a/lib/jobs/generated/list_public_rooms.h b/lib/jobs/generated/list_public_rooms.h new file mode 100644 index 00000000..5c281de3 --- /dev/null +++ b/lib/jobs/generated/list_public_rooms.h @@ -0,0 +1,106 @@ +/****************************************************************************** + * THIS FILE IS GENERATED - ANY EDITS WILL BE OVERWRITTEN + */ + +#pragma once + +#include "../basejob.h" + +#include + +#include "converters.h" + +namespace QMatrixClient +{ + // Operations + + class GetPublicRoomsJob : public BaseJob + { + public: + // Inner data structures + + struct PublicRoomsChunk + { + QVector aliases; + QString canonicalAlias; + QString name; + double numJoinedMembers; + QString roomId; + QString topic; + bool worldReadable; + bool guestCanJoin; + QString avatarUrl; + + operator QJsonObject() const; + }; + + // End of inner data structures + + /** Construct a URL out of baseUrl and usual parameters passed to + * GetPublicRoomsJob. This function can be used when + * a URL for GetPublicRoomsJob is necessary but the job + * itself isn't. + */ + static QUrl makeRequestUrl(QUrl baseUrl, double limit = {}, const QString& since = {}, const QString& server = {}); + + explicit GetPublicRoomsJob(double limit = {}, const QString& since = {}, const QString& server = {}); + ~GetPublicRoomsJob() override; + + const QVector& chunk() const; + const QString& nextBatch() const; + const QString& prevBatch() const; + double totalRoomCountEstimate() const; + + protected: + Status parseJson(const QJsonDocument& data) override; + + private: + class Private; + QScopedPointer d; + }; + + class QueryPublicRoomsJob : public BaseJob + { + public: + // Inner data structures + + struct Filter + { + QString genericSearchTerm; + + operator QJsonObject() const; + }; + + struct PublicRoomsChunk + { + QVector aliases; + QString canonicalAlias; + QString name; + double numJoinedMembers; + QString roomId; + QString topic; + bool worldReadable; + bool guestCanJoin; + QString avatarUrl; + + operator QJsonObject() const; + }; + + // End of inner data structures + + explicit QueryPublicRoomsJob(const QString& server = {}, double limit = {}, const QString& since = {}, const Filter& filter = {}); + ~QueryPublicRoomsJob() override; + + const QVector& chunk() const; + const QString& nextBatch() const; + const QString& prevBatch() const; + double totalRoomCountEstimate() const; + + protected: + Status parseJson(const QJsonDocument& data) override; + + private: + class Private; + QScopedPointer d; + }; +} // namespace QMatrixClient diff --git a/lib/jobs/generated/login.cpp b/lib/jobs/generated/login.cpp new file mode 100644 index 00000000..a4dab428 --- /dev/null +++ b/lib/jobs/generated/login.cpp @@ -0,0 +1,79 @@ +/****************************************************************************** + * THIS FILE IS GENERATED - ANY EDITS WILL BE OVERWRITTEN + */ + +#include "login.h" + +#include "converters.h" + +#include + +using namespace QMatrixClient; + +static const auto basePath = QStringLiteral("/_matrix/client/r0"); + +class LoginJob::Private +{ + public: + QString userId; + QString accessToken; + QString homeServer; + QString deviceId; +}; + +LoginJob::LoginJob(const QString& type, const QString& user, const QString& medium, const QString& address, const QString& password, const QString& token, const QString& deviceId, const QString& initialDeviceDisplayName) + : BaseJob(HttpVerb::Post, "LoginJob", + basePath % "/login", false) + , d(new Private) +{ + QJsonObject _data; + _data.insert("type", toJson(type)); + if (!user.isEmpty()) + _data.insert("user", toJson(user)); + if (!medium.isEmpty()) + _data.insert("medium", toJson(medium)); + if (!address.isEmpty()) + _data.insert("address", toJson(address)); + if (!password.isEmpty()) + _data.insert("password", toJson(password)); + if (!token.isEmpty()) + _data.insert("token", toJson(token)); + if (!deviceId.isEmpty()) + _data.insert("device_id", toJson(deviceId)); + if (!initialDeviceDisplayName.isEmpty()) + _data.insert("initial_device_display_name", toJson(initialDeviceDisplayName)); + setRequestData(_data); +} + +LoginJob::~LoginJob() = default; + +const QString& LoginJob::userId() const +{ + return d->userId; +} + +const QString& LoginJob::accessToken() const +{ + return d->accessToken; +} + +const QString& LoginJob::homeServer() const +{ + return d->homeServer; +} + +const QString& LoginJob::deviceId() const +{ + return d->deviceId; +} + +BaseJob::Status LoginJob::parseJson(const QJsonDocument& data) +{ + auto json = data.object(); + d->userId = fromJson(json.value("user_id")); + d->accessToken = fromJson(json.value("access_token")); + d->homeServer = fromJson(json.value("home_server")); + d->deviceId = fromJson(json.value("device_id")); + return Success; +} + diff --git a/lib/jobs/generated/login.h b/lib/jobs/generated/login.h new file mode 100644 index 00000000..3ac955d4 --- /dev/null +++ b/lib/jobs/generated/login.h @@ -0,0 +1,33 @@ +/****************************************************************************** + * THIS FILE IS GENERATED - ANY EDITS WILL BE OVERWRITTEN + */ + +#pragma once + +#include "../basejob.h" + + + +namespace QMatrixClient +{ + // Operations + + class LoginJob : public BaseJob + { + public: + explicit LoginJob(const QString& type, const QString& user = {}, const QString& medium = {}, const QString& address = {}, const QString& password = {}, const QString& token = {}, const QString& deviceId = {}, const QString& initialDeviceDisplayName = {}); + ~LoginJob() override; + + const QString& userId() const; + const QString& accessToken() const; + const QString& homeServer() const; + const QString& deviceId() const; + + protected: + Status parseJson(const QJsonDocument& data) override; + + private: + class Private; + QScopedPointer d; + }; +} // namespace QMatrixClient diff --git a/lib/jobs/generated/logout.cpp b/lib/jobs/generated/logout.cpp new file mode 100644 index 00000000..83139842 --- /dev/null +++ b/lib/jobs/generated/logout.cpp @@ -0,0 +1,26 @@ +/****************************************************************************** + * THIS FILE IS GENERATED - ANY EDITS WILL BE OVERWRITTEN + */ + +#include "logout.h" + +#include "converters.h" + +#include + +using namespace QMatrixClient; + +static const auto basePath = QStringLiteral("/_matrix/client/r0"); + +QUrl LogoutJob::makeRequestUrl(QUrl baseUrl) +{ + return BaseJob::makeRequestUrl(baseUrl, + basePath % "/logout"); +} + +LogoutJob::LogoutJob() + : BaseJob(HttpVerb::Post, "LogoutJob", + basePath % "/logout") +{ +} + diff --git a/lib/jobs/generated/logout.h b/lib/jobs/generated/logout.h new file mode 100644 index 00000000..7640ba55 --- /dev/null +++ b/lib/jobs/generated/logout.h @@ -0,0 +1,27 @@ +/****************************************************************************** + * THIS FILE IS GENERATED - ANY EDITS WILL BE OVERWRITTEN + */ + +#pragma once + +#include "../basejob.h" + + + +namespace QMatrixClient +{ + // Operations + + class LogoutJob : public BaseJob + { + public: + /** Construct a URL out of baseUrl and usual parameters passed to + * LogoutJob. This function can be used when + * a URL for LogoutJob is necessary but the job + * itself isn't. + */ + static QUrl makeRequestUrl(QUrl baseUrl); + + explicit LogoutJob(); + }; +} // namespace QMatrixClient diff --git a/lib/jobs/generated/profile.cpp b/lib/jobs/generated/profile.cpp new file mode 100644 index 00000000..1f7092d7 --- /dev/null +++ b/lib/jobs/generated/profile.cpp @@ -0,0 +1,140 @@ +/****************************************************************************** + * THIS FILE IS GENERATED - ANY EDITS WILL BE OVERWRITTEN + */ + +#include "profile.h" + +#include "converters.h" + +#include + +using namespace QMatrixClient; + +static const auto basePath = QStringLiteral("/_matrix/client/r0"); + +SetDisplayNameJob::SetDisplayNameJob(const QString& userId, const QString& displayname) + : BaseJob(HttpVerb::Put, "SetDisplayNameJob", + basePath % "/profile/" % userId % "/displayname") +{ + QJsonObject _data; + if (!displayname.isEmpty()) + _data.insert("displayname", toJson(displayname)); + setRequestData(_data); +} + +class GetDisplayNameJob::Private +{ + public: + QString displayname; +}; + +QUrl GetDisplayNameJob::makeRequestUrl(QUrl baseUrl, const QString& userId) +{ + return BaseJob::makeRequestUrl(baseUrl, + basePath % "/profile/" % userId % "/displayname"); +} + +GetDisplayNameJob::GetDisplayNameJob(const QString& userId) + : BaseJob(HttpVerb::Get, "GetDisplayNameJob", + basePath % "/profile/" % userId % "/displayname", false) + , d(new Private) +{ +} + +GetDisplayNameJob::~GetDisplayNameJob() = default; + +const QString& GetDisplayNameJob::displayname() const +{ + return d->displayname; +} + +BaseJob::Status GetDisplayNameJob::parseJson(const QJsonDocument& data) +{ + auto json = data.object(); + d->displayname = fromJson(json.value("displayname")); + return Success; +} + +SetAvatarUrlJob::SetAvatarUrlJob(const QString& userId, const QString& avatarUrl) + : BaseJob(HttpVerb::Put, "SetAvatarUrlJob", + basePath % "/profile/" % userId % "/avatar_url") +{ + QJsonObject _data; + if (!avatarUrl.isEmpty()) + _data.insert("avatar_url", toJson(avatarUrl)); + setRequestData(_data); +} + +class GetAvatarUrlJob::Private +{ + public: + QString avatarUrl; +}; + +QUrl GetAvatarUrlJob::makeRequestUrl(QUrl baseUrl, const QString& userId) +{ + return BaseJob::makeRequestUrl(baseUrl, + basePath % "/profile/" % userId % "/avatar_url"); +} + +GetAvatarUrlJob::GetAvatarUrlJob(const QString& userId) + : BaseJob(HttpVerb::Get, "GetAvatarUrlJob", + basePath % "/profile/" % userId % "/avatar_url", false) + , d(new Private) +{ +} + +GetAvatarUrlJob::~GetAvatarUrlJob() = default; + +const QString& GetAvatarUrlJob::avatarUrl() const +{ + return d->avatarUrl; +} + +BaseJob::Status GetAvatarUrlJob::parseJson(const QJsonDocument& data) +{ + auto json = data.object(); + d->avatarUrl = fromJson(json.value("avatar_url")); + return Success; +} + +class GetUserProfileJob::Private +{ + public: + QString avatarUrl; + QString displayname; +}; + +QUrl GetUserProfileJob::makeRequestUrl(QUrl baseUrl, const QString& userId) +{ + return BaseJob::makeRequestUrl(baseUrl, + basePath % "/profile/" % userId); +} + +GetUserProfileJob::GetUserProfileJob(const QString& userId) + : BaseJob(HttpVerb::Get, "GetUserProfileJob", + basePath % "/profile/" % userId, false) + , d(new Private) +{ +} + +GetUserProfileJob::~GetUserProfileJob() = default; + +const QString& GetUserProfileJob::avatarUrl() const +{ + return d->avatarUrl; +} + +const QString& GetUserProfileJob::displayname() const +{ + return d->displayname; +} + +BaseJob::Status GetUserProfileJob::parseJson(const QJsonDocument& data) +{ + auto json = data.object(); + d->avatarUrl = fromJson(json.value("avatar_url")); + d->displayname = fromJson(json.value("displayname")); + return Success; +} + diff --git a/lib/jobs/generated/profile.h b/lib/jobs/generated/profile.h new file mode 100644 index 00000000..024130f5 --- /dev/null +++ b/lib/jobs/generated/profile.h @@ -0,0 +1,96 @@ +/****************************************************************************** + * THIS FILE IS GENERATED - ANY EDITS WILL BE OVERWRITTEN + */ + +#pragma once + +#include "../basejob.h" + + + +namespace QMatrixClient +{ + // Operations + + class SetDisplayNameJob : public BaseJob + { + public: + explicit SetDisplayNameJob(const QString& userId, const QString& displayname = {}); + }; + + class GetDisplayNameJob : public BaseJob + { + public: + /** Construct a URL out of baseUrl and usual parameters passed to + * GetDisplayNameJob. This function can be used when + * a URL for GetDisplayNameJob is necessary but the job + * itself isn't. + */ + static QUrl makeRequestUrl(QUrl baseUrl, const QString& userId); + + explicit GetDisplayNameJob(const QString& userId); + ~GetDisplayNameJob() override; + + const QString& displayname() const; + + protected: + Status parseJson(const QJsonDocument& data) override; + + private: + class Private; + QScopedPointer d; + }; + + class SetAvatarUrlJob : public BaseJob + { + public: + explicit SetAvatarUrlJob(const QString& userId, const QString& avatarUrl = {}); + }; + + class GetAvatarUrlJob : public BaseJob + { + public: + /** Construct a URL out of baseUrl and usual parameters passed to + * GetAvatarUrlJob. This function can be used when + * a URL for GetAvatarUrlJob is necessary but the job + * itself isn't. + */ + static QUrl makeRequestUrl(QUrl baseUrl, const QString& userId); + + explicit GetAvatarUrlJob(const QString& userId); + ~GetAvatarUrlJob() override; + + const QString& avatarUrl() const; + + protected: + Status parseJson(const QJsonDocument& data) override; + + private: + class Private; + QScopedPointer d; + }; + + class GetUserProfileJob : public BaseJob + { + public: + /** Construct a URL out of baseUrl and usual parameters passed to + * GetUserProfileJob. This function can be used when + * a URL for GetUserProfileJob is necessary but the job + * itself isn't. + */ + static QUrl makeRequestUrl(QUrl baseUrl, const QString& userId); + + explicit GetUserProfileJob(const QString& userId); + ~GetUserProfileJob() override; + + const QString& avatarUrl() const; + const QString& displayname() const; + + protected: + Status parseJson(const QJsonDocument& data) override; + + private: + class Private; + QScopedPointer d; + }; +} // namespace QMatrixClient diff --git a/lib/jobs/generated/receipts.cpp b/lib/jobs/generated/receipts.cpp new file mode 100644 index 00000000..83c38b6f --- /dev/null +++ b/lib/jobs/generated/receipts.cpp @@ -0,0 +1,21 @@ +/****************************************************************************** + * THIS FILE IS GENERATED - ANY EDITS WILL BE OVERWRITTEN + */ + +#include "receipts.h" + +#include "converters.h" + +#include + +using namespace QMatrixClient; + +static const auto basePath = QStringLiteral("/_matrix/client/r0"); + +PostReceiptJob::PostReceiptJob(const QString& roomId, const QString& receiptType, const QString& eventId, const QJsonObject& receipt) + : BaseJob(HttpVerb::Post, "PostReceiptJob", + basePath % "/rooms/" % roomId % "/receipt/" % receiptType % "/" % eventId) +{ + setRequestData(Data(receipt)); +} + diff --git a/lib/jobs/generated/receipts.h b/lib/jobs/generated/receipts.h new file mode 100644 index 00000000..9eb7a489 --- /dev/null +++ b/lib/jobs/generated/receipts.h @@ -0,0 +1,21 @@ +/****************************************************************************** + * THIS FILE IS GENERATED - ANY EDITS WILL BE OVERWRITTEN + */ + +#pragma once + +#include "../basejob.h" + +#include + + +namespace QMatrixClient +{ + // Operations + + class PostReceiptJob : public BaseJob + { + public: + explicit PostReceiptJob(const QString& roomId, const QString& receiptType, const QString& eventId, const QJsonObject& receipt = {}); + }; +} // namespace QMatrixClient diff --git a/lib/jobs/generated/redaction.cpp b/lib/jobs/generated/redaction.cpp new file mode 100644 index 00000000..0da35dfc --- /dev/null +++ b/lib/jobs/generated/redaction.cpp @@ -0,0 +1,45 @@ +/****************************************************************************** + * THIS FILE IS GENERATED - ANY EDITS WILL BE OVERWRITTEN + */ + +#include "redaction.h" + +#include "converters.h" + +#include + +using namespace QMatrixClient; + +static const auto basePath = QStringLiteral("/_matrix/client/r0"); + +class RedactEventJob::Private +{ + public: + QString eventId; +}; + +RedactEventJob::RedactEventJob(const QString& roomId, const QString& eventId, const QString& txnId, const QString& reason) + : BaseJob(HttpVerb::Put, "RedactEventJob", + basePath % "/rooms/" % roomId % "/redact/" % eventId % "/" % txnId) + , d(new Private) +{ + QJsonObject _data; + if (!reason.isEmpty()) + _data.insert("reason", toJson(reason)); + setRequestData(_data); +} + +RedactEventJob::~RedactEventJob() = default; + +const QString& RedactEventJob::eventId() const +{ + return d->eventId; +} + +BaseJob::Status RedactEventJob::parseJson(const QJsonDocument& data) +{ + auto json = data.object(); + d->eventId = fromJson(json.value("event_id")); + return Success; +} + diff --git a/lib/jobs/generated/redaction.h b/lib/jobs/generated/redaction.h new file mode 100644 index 00000000..e3b3ff4f --- /dev/null +++ b/lib/jobs/generated/redaction.h @@ -0,0 +1,30 @@ +/****************************************************************************** + * THIS FILE IS GENERATED - ANY EDITS WILL BE OVERWRITTEN + */ + +#pragma once + +#include "../basejob.h" + + + +namespace QMatrixClient +{ + // Operations + + class RedactEventJob : public BaseJob + { + public: + explicit RedactEventJob(const QString& roomId, const QString& eventId, const QString& txnId, const QString& reason = {}); + ~RedactEventJob() override; + + const QString& eventId() const; + + protected: + Status parseJson(const QJsonDocument& data) override; + + private: + class Private; + QScopedPointer d; + }; +} // namespace QMatrixClient diff --git a/lib/jobs/generated/third_party_membership.cpp b/lib/jobs/generated/third_party_membership.cpp new file mode 100644 index 00000000..b637d481 --- /dev/null +++ b/lib/jobs/generated/third_party_membership.cpp @@ -0,0 +1,25 @@ +/****************************************************************************** + * THIS FILE IS GENERATED - ANY EDITS WILL BE OVERWRITTEN + */ + +#include "third_party_membership.h" + +#include "converters.h" + +#include + +using namespace QMatrixClient; + +static const auto basePath = QStringLiteral("/_matrix/client/r0"); + +InviteBy3PIDJob::InviteBy3PIDJob(const QString& roomId, const QString& idServer, const QString& medium, const QString& address) + : BaseJob(HttpVerb::Post, "InviteBy3PIDJob", + basePath % "/rooms/" % roomId % "/invite") +{ + QJsonObject _data; + _data.insert("id_server", toJson(idServer)); + _data.insert("medium", toJson(medium)); + _data.insert("address", toJson(address)); + setRequestData(_data); +} + diff --git a/lib/jobs/generated/third_party_membership.h b/lib/jobs/generated/third_party_membership.h new file mode 100644 index 00000000..c7b5214e --- /dev/null +++ b/lib/jobs/generated/third_party_membership.h @@ -0,0 +1,20 @@ +/****************************************************************************** + * THIS FILE IS GENERATED - ANY EDITS WILL BE OVERWRITTEN + */ + +#pragma once + +#include "../basejob.h" + + + +namespace QMatrixClient +{ + // Operations + + class InviteBy3PIDJob : public BaseJob + { + public: + explicit InviteBy3PIDJob(const QString& roomId, const QString& idServer, const QString& medium, const QString& address); + }; +} // namespace QMatrixClient diff --git a/lib/jobs/generated/typing.cpp b/lib/jobs/generated/typing.cpp new file mode 100644 index 00000000..fa700290 --- /dev/null +++ b/lib/jobs/generated/typing.cpp @@ -0,0 +1,24 @@ +/****************************************************************************** + * THIS FILE IS GENERATED - ANY EDITS WILL BE OVERWRITTEN + */ + +#include "typing.h" + +#include "converters.h" + +#include + +using namespace QMatrixClient; + +static const auto basePath = QStringLiteral("/_matrix/client/r0"); + +SetTypingJob::SetTypingJob(const QString& userId, const QString& roomId, bool typing, int timeout) + : BaseJob(HttpVerb::Put, "SetTypingJob", + basePath % "/rooms/" % roomId % "/typing/" % userId) +{ + QJsonObject _data; + _data.insert("typing", toJson(typing)); + _data.insert("timeout", toJson(timeout)); + setRequestData(_data); +} + diff --git a/lib/jobs/generated/typing.h b/lib/jobs/generated/typing.h new file mode 100644 index 00000000..0495ed0a --- /dev/null +++ b/lib/jobs/generated/typing.h @@ -0,0 +1,20 @@ +/****************************************************************************** + * THIS FILE IS GENERATED - ANY EDITS WILL BE OVERWRITTEN + */ + +#pragma once + +#include "../basejob.h" + + + +namespace QMatrixClient +{ + // Operations + + class SetTypingJob : public BaseJob + { + public: + explicit SetTypingJob(const QString& userId, const QString& roomId, bool typing, int timeout = {}); + }; +} // namespace QMatrixClient diff --git a/lib/jobs/generated/versions.cpp b/lib/jobs/generated/versions.cpp new file mode 100644 index 00000000..b12594ca --- /dev/null +++ b/lib/jobs/generated/versions.cpp @@ -0,0 +1,47 @@ +/****************************************************************************** + * THIS FILE IS GENERATED - ANY EDITS WILL BE OVERWRITTEN + */ + +#include "versions.h" + +#include "converters.h" + +#include + +using namespace QMatrixClient; + +static const auto basePath = QStringLiteral("/_matrix/client"); + +class GetVersionsJob::Private +{ + public: + QVector versions; +}; + +QUrl GetVersionsJob::makeRequestUrl(QUrl baseUrl) +{ + return BaseJob::makeRequestUrl(baseUrl, + basePath % "/versions"); +} + +GetVersionsJob::GetVersionsJob() + : BaseJob(HttpVerb::Get, "GetVersionsJob", + basePath % "/versions", false) + , d(new Private) +{ +} + +GetVersionsJob::~GetVersionsJob() = default; + +const QVector& GetVersionsJob::versions() const +{ + return d->versions; +} + +BaseJob::Status GetVersionsJob::parseJson(const QJsonDocument& data) +{ + auto json = data.object(); + d->versions = fromJson>(json.value("versions")); + return Success; +} + diff --git a/lib/jobs/generated/versions.h b/lib/jobs/generated/versions.h new file mode 100644 index 00000000..18f6bb44 --- /dev/null +++ b/lib/jobs/generated/versions.h @@ -0,0 +1,38 @@ +/****************************************************************************** + * THIS FILE IS GENERATED - ANY EDITS WILL BE OVERWRITTEN + */ + +#pragma once + +#include "../basejob.h" + +#include + + +namespace QMatrixClient +{ + // Operations + + class GetVersionsJob : public BaseJob + { + public: + /** Construct a URL out of baseUrl and usual parameters passed to + * GetVersionsJob. This function can be used when + * a URL for GetVersionsJob is necessary but the job + * itself isn't. + */ + static QUrl makeRequestUrl(QUrl baseUrl); + + explicit GetVersionsJob(); + ~GetVersionsJob() override; + + const QVector& versions() const; + + protected: + Status parseJson(const QJsonDocument& data) override; + + private: + class Private; + QScopedPointer d; + }; +} // namespace QMatrixClient diff --git a/lib/jobs/generated/whoami.cpp b/lib/jobs/generated/whoami.cpp new file mode 100644 index 00000000..cc38fa4d --- /dev/null +++ b/lib/jobs/generated/whoami.cpp @@ -0,0 +1,50 @@ +/****************************************************************************** + * THIS FILE IS GENERATED - ANY EDITS WILL BE OVERWRITTEN + */ + +#include "whoami.h" + +#include "converters.h" + +#include + +using namespace QMatrixClient; + +static const auto basePath = QStringLiteral("/_matrix/client/r0"); + +class GetTokenOwnerJob::Private +{ + public: + QString userId; +}; + +QUrl GetTokenOwnerJob::makeRequestUrl(QUrl baseUrl) +{ + return BaseJob::makeRequestUrl(baseUrl, + basePath % "/account/whoami"); +} + +GetTokenOwnerJob::GetTokenOwnerJob() + : BaseJob(HttpVerb::Get, "GetTokenOwnerJob", + basePath % "/account/whoami") + , d(new Private) +{ +} + +GetTokenOwnerJob::~GetTokenOwnerJob() = default; + +const QString& GetTokenOwnerJob::userId() const +{ + return d->userId; +} + +BaseJob::Status GetTokenOwnerJob::parseJson(const QJsonDocument& data) +{ + auto json = data.object(); + if (!json.contains("user_id")) + return { JsonParseError, + "The key 'user_id' not found in the response" }; + d->userId = fromJson(json.value("user_id")); + return Success; +} + diff --git a/lib/jobs/generated/whoami.h b/lib/jobs/generated/whoami.h new file mode 100644 index 00000000..835232ee --- /dev/null +++ b/lib/jobs/generated/whoami.h @@ -0,0 +1,37 @@ +/****************************************************************************** + * THIS FILE IS GENERATED - ANY EDITS WILL BE OVERWRITTEN + */ + +#pragma once + +#include "../basejob.h" + + + +namespace QMatrixClient +{ + // Operations + + class GetTokenOwnerJob : public BaseJob + { + public: + /** Construct a URL out of baseUrl and usual parameters passed to + * GetTokenOwnerJob. This function can be used when + * a URL for GetTokenOwnerJob is necessary but the job + * itself isn't. + */ + static QUrl makeRequestUrl(QUrl baseUrl); + + explicit GetTokenOwnerJob(); + ~GetTokenOwnerJob() override; + + const QString& userId() const; + + protected: + Status parseJson(const QJsonDocument& data) override; + + private: + class Private; + QScopedPointer d; + }; +} // namespace QMatrixClient diff --git a/lib/jobs/joinroomjob.cpp b/lib/jobs/joinroomjob.cpp new file mode 100644 index 00000000..66a75089 --- /dev/null +++ b/lib/jobs/joinroomjob.cpp @@ -0,0 +1,58 @@ +/****************************************************************************** + * Copyright (C) 2015 Felix Rohrbach + * + * 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 + */ + +#include "joinroomjob.h" +#include "util.h" + +using namespace QMatrixClient; + +class JoinRoomJob::Private +{ + public: + QString roomId; +}; + +JoinRoomJob::JoinRoomJob(const QString& roomAlias) + : BaseJob(HttpVerb::Post, "JoinRoomJob", + QStringLiteral("_matrix/client/r0/join/%1").arg(roomAlias)) + , d(new Private) +{ +} + +JoinRoomJob::~JoinRoomJob() +{ + delete d; +} + +QString JoinRoomJob::roomId() +{ + return d->roomId; +} + +BaseJob::Status JoinRoomJob::parseJson(const QJsonDocument& data) +{ + QJsonObject json = data.object(); + if( json.contains("room_id") ) + { + d->roomId = json.value("room_id").toString(); + return Success; + } + + qCDebug(JOBS) << data; + return { UserDefinedError, "No room_id in the JSON response" }; +} diff --git a/lib/jobs/joinroomjob.h b/lib/jobs/joinroomjob.h new file mode 100644 index 00000000..f3ba216f --- /dev/null +++ b/lib/jobs/joinroomjob.h @@ -0,0 +1,40 @@ +/****************************************************************************** + * Copyright (C) 2015 Felix Rohrbach + * + * 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 + */ + +#pragma once + +#include "basejob.h" + +namespace QMatrixClient +{ + class JoinRoomJob: public BaseJob + { + public: + explicit JoinRoomJob(const QString& roomAlias); + virtual ~JoinRoomJob(); + + QString roomId(); + + protected: + Status parseJson(const QJsonDocument& data) override; + + private: + class Private; + Private* d; + }; +} // namespace QMatrixClient diff --git a/lib/jobs/mediathumbnailjob.cpp b/lib/jobs/mediathumbnailjob.cpp new file mode 100644 index 00000000..dda1cdb4 --- /dev/null +++ b/lib/jobs/mediathumbnailjob.cpp @@ -0,0 +1,63 @@ +/****************************************************************************** + * Copyright (C) 2016 Felix Rohrbach + * + * 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 + */ + +#include "mediathumbnailjob.h" + +using namespace QMatrixClient; + +QUrl MediaThumbnailJob::makeRequestUrl(QUrl baseUrl, + const QUrl& mxcUri, QSize requestedSize) +{ + return makeRequestUrl(baseUrl, mxcUri.authority(), mxcUri.path().mid(1), + requestedSize.width(), requestedSize.height()); +} + +MediaThumbnailJob::MediaThumbnailJob(const QString& serverName, + const QString& mediaId, QSize requestedSize) + : GetContentThumbnailJob(serverName, mediaId, + requestedSize.width(), requestedSize.height()) +{ } + +MediaThumbnailJob::MediaThumbnailJob(const QUrl& mxcUri, QSize requestedSize) + : GetContentThumbnailJob(mxcUri.authority(), + mxcUri.path().mid(1), // sans leading '/' + requestedSize.width(), requestedSize.height()) +{ } + +QImage MediaThumbnailJob::thumbnail() const +{ + return _thumbnail; +} + +QImage MediaThumbnailJob::scaledThumbnail(QSize toSize) const +{ + return _thumbnail.scaled(toSize, + Qt::KeepAspectRatio, Qt::SmoothTransformation); +} + +BaseJob::Status MediaThumbnailJob::parseReply(QNetworkReply* reply) +{ + auto result = GetContentThumbnailJob::parseReply(reply); + if (!result.good()) + return result; + + if( _thumbnail.loadFromData(content()->readAll()) ) + return Success; + + return { IncorrectResponseError, "Could not read image data" }; +} diff --git a/lib/jobs/mediathumbnailjob.h b/lib/jobs/mediathumbnailjob.h new file mode 100644 index 00000000..6e0b94f3 --- /dev/null +++ b/lib/jobs/mediathumbnailjob.h @@ -0,0 +1,47 @@ +/****************************************************************************** + * Copyright (C) 2016 Felix Rohrbach + * + * 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 + */ + +#pragma once + +#include "generated/content-repo.h" + +#include + +namespace QMatrixClient +{ + class MediaThumbnailJob: public GetContentThumbnailJob + { + public: + using GetContentThumbnailJob::makeRequestUrl; + static QUrl makeRequestUrl(QUrl baseUrl, + const QUrl& mxcUri, QSize requestedSize); + + MediaThumbnailJob(const QString& serverName, const QString& mediaId, + QSize requestedSize); + MediaThumbnailJob(const QUrl& mxcUri, QSize requestedSize); + + QImage thumbnail() const; + QImage scaledThumbnail(QSize toSize) const; + + protected: + Status parseReply(QNetworkReply* reply) override; + + private: + QImage _thumbnail; + }; +} // namespace QMatrixClient diff --git a/lib/jobs/passwordlogin.cpp b/lib/jobs/passwordlogin.cpp new file mode 100644 index 00000000..8abfe66a --- /dev/null +++ b/lib/jobs/passwordlogin.cpp @@ -0,0 +1,74 @@ +/****************************************************************************** + * Copyright (C) 2015 Felix Rohrbach + * + * 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 + */ + +#include "passwordlogin.h" + +using namespace QMatrixClient; + +class PasswordLogin::Private +{ + public: + QString returned_id; + QString returned_server; + QString returned_token; +}; + +PasswordLogin::PasswordLogin(QString user, QString password) + : BaseJob(HttpVerb::Post, "PasswordLogin", + "_matrix/client/r0/login", Query(), Data(), false) + , d(new Private) +{ + QJsonObject _data; + _data.insert("type", QStringLiteral("m.login.password")); + _data.insert("user", user); + _data.insert("password", password); + setRequestData(_data); +} + +PasswordLogin::~PasswordLogin() +{ + delete d; +} + +QString PasswordLogin::token() const +{ + return d->returned_token; +} + +QString PasswordLogin::id() const +{ + return d->returned_id; +} + +QString PasswordLogin::server() const +{ + return d->returned_server; +} + +BaseJob::Status PasswordLogin::parseJson(const QJsonDocument& data) +{ + QJsonObject json = data.object(); + if( !json.contains("access_token") || !json.contains("home_server") || !json.contains("user_id") ) + { + return { UserDefinedError, "No expected data" }; + } + d->returned_token = json.value("access_token").toString(); + d->returned_server = json.value("home_server").toString(); + d->returned_id = json.value("user_id").toString(); + return Success; +} diff --git a/lib/jobs/passwordlogin.h b/lib/jobs/passwordlogin.h new file mode 100644 index 00000000..fb8777a3 --- /dev/null +++ b/lib/jobs/passwordlogin.h @@ -0,0 +1,42 @@ +/****************************************************************************** + * Copyright (C) 2015 Felix Rohrbach + * + * 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 + */ + +#pragma once + +#include "basejob.h" + +namespace QMatrixClient +{ + class PasswordLogin : public BaseJob + { + public: + PasswordLogin(QString user, QString password); + virtual ~PasswordLogin(); + + QString token() const; + QString id() const; + QString server() const; + + protected: + Status parseJson(const QJsonDocument& data) override; + + private: + class Private; + Private* d; + }; +} // namespace QMatrixClient diff --git a/lib/jobs/postreadmarkersjob.h b/lib/jobs/postreadmarkersjob.h new file mode 100644 index 00000000..d0198821 --- /dev/null +++ b/lib/jobs/postreadmarkersjob.h @@ -0,0 +1,37 @@ +/****************************************************************************** + * Copyright (C) 2017 Kitsune Ral + * + * 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 + */ + +#pragma once + +#include "basejob.h" + +using namespace QMatrixClient; + +class PostReadMarkersJob : public BaseJob +{ + public: + explicit PostReadMarkersJob(const QString& roomId, + const QString& readUpToEventId) + : BaseJob(HttpVerb::Post, "PostReadMarkersJob", + QStringLiteral("_matrix/client/r0/rooms/%1/read_markers") + .arg(roomId)) + { + setRequestData(QJsonObject {{ + QStringLiteral("m.fully_read"), readUpToEventId }}); + } +}; diff --git a/lib/jobs/postreceiptjob.cpp b/lib/jobs/postreceiptjob.cpp new file mode 100644 index 00000000..4572d74c --- /dev/null +++ b/lib/jobs/postreceiptjob.cpp @@ -0,0 +1,27 @@ +/****************************************************************************** + * Copyright (C) 2016 Felix Rohrbach + * + * 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 + */ + +#include "postreceiptjob.h" + +using namespace QMatrixClient; + +PostReceiptJob::PostReceiptJob(const QString& roomId, const QString& eventId) + : BaseJob(HttpVerb::Post, "PostReceiptJob", + QStringLiteral("/_matrix/client/r0/rooms/%1/receipt/m.read/%2") + .arg(roomId, eventId)) +{ } diff --git a/lib/jobs/postreceiptjob.h b/lib/jobs/postreceiptjob.h new file mode 100644 index 00000000..23df7c05 --- /dev/null +++ b/lib/jobs/postreceiptjob.h @@ -0,0 +1,30 @@ +/****************************************************************************** + * Copyright (C) 2016 Felix Rohrbach + * + * 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 + */ + +#pragma once + +#include "basejob.h" + +namespace QMatrixClient +{ + class PostReceiptJob: public BaseJob + { + public: + PostReceiptJob(const QString& roomId, const QString& eventId); + }; +} diff --git a/lib/jobs/requestdata.cpp b/lib/jobs/requestdata.cpp new file mode 100644 index 00000000..5cb62221 --- /dev/null +++ b/lib/jobs/requestdata.cpp @@ -0,0 +1,38 @@ +#include "requestdata.h" + +#include +#include +#include +#include +#include + +using namespace QMatrixClient; + +auto fromData(const QByteArray& data) +{ + auto source = std::make_unique(); + source->open(QIODevice::WriteOnly); + source->write(data); + source->close(); + return source; +} + +template +inline auto fromJson(const JsonDataT& jdata) +{ + return fromData(QJsonDocument(jdata).toJson(QJsonDocument::Compact)); +} + +RequestData::RequestData(const QByteArray& a) + : _source(fromData(a)) +{ } + +RequestData::RequestData(const QJsonObject& jo) + : _source(fromJson(jo)) +{ } + +RequestData::RequestData(const QJsonArray& ja) + : _source(fromJson(ja)) +{ } + +RequestData::~RequestData() = default; diff --git a/lib/jobs/requestdata.h b/lib/jobs/requestdata.h new file mode 100644 index 00000000..aa03b744 --- /dev/null +++ b/lib/jobs/requestdata.h @@ -0,0 +1,59 @@ +/****************************************************************************** + * Copyright (C) 2018 Kitsune Ral + * + * 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 + */ + +#pragma once + +#include + +class QByteArray; +class QJsonObject; +class QJsonArray; +class QJsonDocument; +class QIODevice; + +namespace QMatrixClient +{ + /** + * A simple wrapper that represents the request body. + * Provides a unified interface to dump an unstructured byte stream + * as well as JSON (and possibly other structures in the future) to + * a QByteArray consumed by QNetworkAccessManager request methods. + */ + class RequestData + { + public: + RequestData() = default; + RequestData(const QByteArray& a); + RequestData(const QJsonObject& jo); + RequestData(const QJsonArray& ja); + RequestData(QIODevice* source) + : _source(std::unique_ptr(source)) + { } + RequestData(RequestData&&) = default; + RequestData& operator=(RequestData&&) = default; + ~RequestData(); + + QIODevice* source() const + { + return _source.get(); + } + + private: + std::unique_ptr _source; + }; +} // namespace QMatrixClient diff --git a/lib/jobs/roommessagesjob.cpp b/lib/jobs/roommessagesjob.cpp new file mode 100644 index 00000000..e5568f17 --- /dev/null +++ b/lib/jobs/roommessagesjob.cpp @@ -0,0 +1,65 @@ +/****************************************************************************** + * Copyright (C) 2016 Felix Rohrbach + * + * 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 + */ + +#include "roommessagesjob.h" + +using namespace QMatrixClient; + +class RoomMessagesJob::Private +{ + public: + RoomEvents events; + QString end; +}; + +RoomMessagesJob::RoomMessagesJob(const QString& roomId, const QString& from, + int limit, FetchDirection dir) + : BaseJob(HttpVerb::Get, "RoomMessagesJob", + QStringLiteral("/_matrix/client/r0/rooms/%1/messages").arg(roomId), + Query( + { { "from", from } + , { "dir", dir == FetchDirection::Backward ? "b" : "f" } + , { "limit", QString::number(limit) } + })) + , d(new Private) +{ + qCDebug(JOBS) << "Room messages query:" << query().toString(QUrl::PrettyDecoded); +} + +RoomMessagesJob::~RoomMessagesJob() +{ + delete d; +} + +RoomEvents&& RoomMessagesJob::releaseEvents() +{ + return move(d->events); +} + +QString RoomMessagesJob::end() const +{ + return d->end; +} + +BaseJob::Status RoomMessagesJob::parseJson(const QJsonDocument& data) +{ + const auto obj = data.object(); + d->events.fromJson(obj, "chunk"); + d->end = obj.value("end").toString(); + return Success; +} diff --git a/lib/jobs/roommessagesjob.h b/lib/jobs/roommessagesjob.h new file mode 100644 index 00000000..7b3fd9c9 --- /dev/null +++ b/lib/jobs/roommessagesjob.h @@ -0,0 +1,47 @@ +/****************************************************************************** + * Copyright (C) 2016 Felix Rohrbach + * + * 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 + */ + +#pragma once + +#include "basejob.h" + +#include "../events/event.h" + +namespace QMatrixClient +{ + enum class FetchDirection { Backward, Forward }; + + class RoomMessagesJob: public BaseJob + { + public: + RoomMessagesJob(const QString& roomId, const QString& from, + int limit = 10, + FetchDirection dir = FetchDirection::Backward); + virtual ~RoomMessagesJob(); + + RoomEvents&& releaseEvents(); + QString end() const; + + protected: + Status parseJson(const QJsonDocument& data) override; + + private: + class Private; + Private* d; + }; +} // namespace QMatrixClient diff --git a/lib/jobs/sendeventjob.cpp b/lib/jobs/sendeventjob.cpp new file mode 100644 index 00000000..f5190d4b --- /dev/null +++ b/lib/jobs/sendeventjob.cpp @@ -0,0 +1,45 @@ +/****************************************************************************** + * Copyright (C) 2015 Felix Rohrbach + * + * 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 + */ + +#include "sendeventjob.h" + +#include "events/roommessageevent.h" + +using namespace QMatrixClient; + +SendEventJob::SendEventJob(const QString& roomId, const QString& type, + const QString& plainText) + : SendEventJob(roomId, RoomMessageEvent(plainText, type)) +{ } + +void SendEventJob::beforeStart(const ConnectionData* connData) +{ + BaseJob::beforeStart(connData); + setApiEndpoint(apiEndpoint() + connData->generateTxnId()); +} + +BaseJob::Status SendEventJob::parseJson(const QJsonDocument& data) +{ + _eventId = data.object().value("event_id").toString(); + if (!_eventId.isEmpty()) + return Success; + + qCDebug(JOBS) << data; + return { UserDefinedError, "No event_id in the JSON response" }; +} + diff --git a/lib/jobs/sendeventjob.h b/lib/jobs/sendeventjob.h new file mode 100644 index 00000000..3a11eb6a --- /dev/null +++ b/lib/jobs/sendeventjob.h @@ -0,0 +1,57 @@ +/****************************************************************************** + * Copyright (C) 2015 Felix Rohrbach + * + * 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 + */ + +#pragma once + +#include "basejob.h" + +#include "connectiondata.h" + +namespace QMatrixClient +{ + class SendEventJob: public BaseJob + { + public: + /** Constructs a job that sends an arbitrary room event */ + template + SendEventJob(const QString& roomId, const EvT& event) + : BaseJob(HttpVerb::Put, QStringLiteral("SendEventJob"), + QStringLiteral("_matrix/client/r0/rooms/%1/send/%2/") + .arg(roomId, EvT::TypeId), // See also beforeStart() + Query(), + Data(event.toJson())) + { } + + /** + * Constructs a plain text message job (for compatibility with + * the old PostMessageJob API). + */ + SendEventJob(const QString& roomId, const QString& type, + const QString& plainText); + + QString eventId() const { return _eventId; } + + protected: + Status parseJson(const QJsonDocument& data) override; + + private: + QString _eventId; + + void beforeStart(const ConnectionData* connData) override; + }; +} // namespace QMatrixClient diff --git a/lib/jobs/setroomstatejob.cpp b/lib/jobs/setroomstatejob.cpp new file mode 100644 index 00000000..c2beb87b --- /dev/null +++ b/lib/jobs/setroomstatejob.cpp @@ -0,0 +1,32 @@ +/****************************************************************************** + * Copyright (C) 2015 Felix Rohrbach + * + * 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 + */ + +#include "setroomstatejob.h" + +using namespace QMatrixClient; + +BaseJob::Status SetRoomStateJob::parseJson(const QJsonDocument& data) +{ + _eventId = data.object().value("event_id").toString(); + if (!_eventId.isEmpty()) + return Success; + + qCDebug(JOBS) << data; + return { UserDefinedError, "No event_id in the JSON response" }; +} + diff --git a/lib/jobs/setroomstatejob.h b/lib/jobs/setroomstatejob.h new file mode 100644 index 00000000..b7e6d4a1 --- /dev/null +++ b/lib/jobs/setroomstatejob.h @@ -0,0 +1,64 @@ +/****************************************************************************** + * Copyright (C) 2015 Felix Rohrbach + * + * 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 + */ + +#pragma once + +#include "basejob.h" + +#include "connectiondata.h" + +namespace QMatrixClient +{ + class SetRoomStateJob: public BaseJob + { + public: + /** + * Constructs a job that sets a state using an arbitrary room event + * with a state key. + */ + template + SetRoomStateJob(const QString& roomId, const QString& stateKey, + const EvT& event) + : BaseJob(HttpVerb::Put, "SetRoomStateJob", + QStringLiteral("_matrix/client/r0/rooms/%1/state/%2/%3") + .arg(roomId, EvT::TypeId, stateKey), + Query(), + Data(event.toJson())) + { } + /** + * Constructs a job that sets a state using an arbitrary room event + * without a state key. + */ + template + SetRoomStateJob(const QString& roomId, const EvT& event) + : BaseJob(HttpVerb::Put, "SetRoomStateJob", + QStringLiteral("_matrix/client/r0/rooms/%1/state/%2") + .arg(roomId, EvT::TypeId), + Query(), + Data(event.toJson())) + { } + + QString eventId() const { return _eventId; } + + protected: + Status parseJson(const QJsonDocument& data) override; + + private: + QString _eventId; + }; +} // namespace QMatrixClient diff --git a/lib/jobs/syncjob.cpp b/lib/jobs/syncjob.cpp new file mode 100644 index 00000000..435dfd0e --- /dev/null +++ b/lib/jobs/syncjob.cpp @@ -0,0 +1,133 @@ +/****************************************************************************** + * Copyright (C) 2016 Felix Rohrbach + * + * 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 + */ + +#include "syncjob.h" + +#include + +using namespace QMatrixClient; + +static size_t jobId = 0; + +SyncJob::SyncJob(const QString& since, const QString& filter, int timeout, + const QString& presence) + : BaseJob(HttpVerb::Get, QStringLiteral("SyncJob-%1").arg(++jobId), + QStringLiteral("_matrix/client/r0/sync")) +{ + setLoggingCategory(SYNCJOB); + QUrlQuery query; + if( !filter.isEmpty() ) + query.addQueryItem("filter", filter); + if( !presence.isEmpty() ) + query.addQueryItem("set_presence", presence); + if( timeout >= 0 ) + query.addQueryItem("timeout", QString::number(timeout)); + if( !since.isEmpty() ) + query.addQueryItem("since", since); + setRequestQuery(query); + + setMaxRetries(std::numeric_limits::max()); +} + +QString SyncData::nextBatch() const +{ + return nextBatch_; +} + +SyncDataList&& SyncData::takeRoomData() +{ + return std::move(roomData); +} + +SyncBatch&& SyncData::takeAccountData() +{ + return std::move(accountData); +} + +BaseJob::Status SyncJob::parseJson(const QJsonDocument& data) +{ + return d.parseJson(data); +} + +BaseJob::Status SyncData::parseJson(const QJsonDocument &data) +{ + QElapsedTimer et; et.start(); + + auto json = data.object(); + nextBatch_ = json.value("next_batch").toString(); + // TODO: presence + accountData.fromJson(json); + + QJsonObject rooms = json.value("rooms").toObject(); + JoinStates::Int ii = 1; // ii is used to make a JoinState value + for (size_t i = 0; i < JoinStateStrings.size(); ++i, ii <<= 1) + { + const auto rs = rooms.value(JoinStateStrings[i]).toObject(); + // We have a Qt container on the right and an STL one on the left + roomData.reserve(static_cast(rs.size())); + for(auto roomIt = rs.begin(); roomIt != rs.end(); ++roomIt) + roomData.emplace_back(roomIt.key(), JoinState(ii), + roomIt.value().toObject()); + } + qCDebug(PROFILER) << "*** SyncData::parseJson(): batch with" + << rooms.size() << "room(s) in" << et; + return BaseJob::Success; +} + +const QString SyncRoomData::UnreadCountKey = + QStringLiteral("x-qmatrixclient.unread_count"); + +SyncRoomData::SyncRoomData(const QString& roomId_, JoinState joinState_, + const QJsonObject& room_) + : roomId(roomId_) + , joinState(joinState_) + , state(joinState == JoinState::Invite ? "invite_state" : "state") + , timeline("timeline") + , ephemeral("ephemeral") + , accountData("account_data") +{ + switch (joinState) { + case JoinState::Invite: + state.fromJson(room_); + break; + case JoinState::Join: + state.fromJson(room_); + timeline.fromJson(room_); + ephemeral.fromJson(room_); + accountData.fromJson(room_); + break; + case JoinState::Leave: + state.fromJson(room_); + timeline.fromJson(room_); + break; + default: + qCWarning(SYNCJOB) << "SyncRoomData: Unknown JoinState value, ignoring:" << int(joinState); + } + + auto timelineJson = room_.value("timeline").toObject(); + timelineLimited = timelineJson.value("limited").toBool(); + timelinePrevBatch = timelineJson.value("prev_batch").toString(); + + auto unreadJson = room_.value("unread_notifications").toObject(); + unreadCount = unreadJson.value(UnreadCountKey).toInt(-2); + highlightCount = unreadJson.value("highlight_count").toInt(); + notificationCount = unreadJson.value("notification_count").toInt(); + if (highlightCount > 0 || notificationCount > 0) + qCDebug(SYNCJOB) << "Highlights: " << highlightCount + << " Notifications:" << notificationCount; +} diff --git a/lib/jobs/syncjob.h b/lib/jobs/syncjob.h new file mode 100644 index 00000000..919060be --- /dev/null +++ b/lib/jobs/syncjob.h @@ -0,0 +1,99 @@ +/****************************************************************************** + * Copyright (C) 2016 Felix Rohrbach + * + * 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 + */ + +#pragma once + +#include "basejob.h" + +#include "joinstate.h" +#include "events/event.h" +#include "util.h" + +namespace QMatrixClient +{ + template + class SyncBatch : public EventsBatch + { + public: + explicit SyncBatch(QString k) : jsonKey(std::move(k)) { } + void fromJson(const QJsonObject& roomContents) + { + EventsBatch::fromJson( + roomContents[jsonKey].toObject(), "events"); + } + + private: + QString jsonKey; + }; + + class SyncRoomData + { + public: + QString roomId; + JoinState joinState; + SyncBatch state; + SyncBatch timeline; + SyncBatch ephemeral; + SyncBatch accountData; + + bool timelineLimited; + QString timelinePrevBatch; + int unreadCount; + int highlightCount; + int notificationCount; + + SyncRoomData(const QString& roomId, JoinState joinState_, + const QJsonObject& room_); + SyncRoomData(SyncRoomData&&) = default; + SyncRoomData& operator=(SyncRoomData&&) = default; + + static const QString UnreadCountKey; + }; + // QVector cannot work with non-copiable objects, std::vector can. + using SyncDataList = std::vector; + + class SyncData + { + public: + BaseJob::Status parseJson(const QJsonDocument &data); + SyncBatch&& takeAccountData(); + SyncDataList&& takeRoomData(); + QString nextBatch() const; + + private: + QString nextBatch_; + SyncBatch accountData { "account_data" }; + SyncDataList roomData; + }; + + class SyncJob: public BaseJob + { + public: + explicit SyncJob(const QString& since = {}, + const QString& filter = {}, + int timeout = -1, const QString& presence = {}); + + SyncData &&takeData() { return std::move(d); } + + protected: + Status parseJson(const QJsonDocument& data) override; + + private: + SyncData d; + }; +} // namespace QMatrixClient diff --git a/lib/joinstate.h b/lib/joinstate.h new file mode 100644 index 00000000..42613895 --- /dev/null +++ b/lib/joinstate.h @@ -0,0 +1,48 @@ +/****************************************************************************** + * Copyright (C) 2015 Felix Rohrbach + * + * 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 + */ + +#pragma once + +#include + +#include + +namespace QMatrixClient +{ + enum class JoinState + { + Join = 0x1, + Invite = 0x2, + Leave = 0x4 + }; + + Q_DECLARE_FLAGS(JoinStates, JoinState) + + // We cannot use REGISTER_ENUM outside of a Q_OBJECT and besides, we want + // to use strings that match respective JSON keys. + static const std::array JoinStateStrings + { { "join", "invite", "leave" } }; + + inline const char* toCString(JoinState js) + { + size_t state = size_t(js), index = 0; + while (state >>= 1) ++index; + return JoinStateStrings[index]; + } +} // namespace QMatrixClient +Q_DECLARE_OPERATORS_FOR_FLAGS(QMatrixClient::JoinStates) diff --git a/lib/logging.cpp b/lib/logging.cpp new file mode 100644 index 00000000..7476781f --- /dev/null +++ b/lib/logging.cpp @@ -0,0 +1,33 @@ +/****************************************************************************** + * Copyright (C) 2017 Elvis Angelaccio + * + * 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 + */ + +#include "logging.h" + +#if QT_VERSION >= QT_VERSION_CHECK(5, 5, 0) +#define LOGGING_CATEGORY(Name, Id) Q_LOGGING_CATEGORY((Name), (Id), QtInfoMsg) +#else +#define LOGGING_CATEGORY(Name, Id) Q_LOGGING_CATEGORY((Name), (Id)) +#endif + +// Use LOGGING_CATEGORY instead of Q_LOGGING_CATEGORY in the rest of the code +LOGGING_CATEGORY(MAIN, "libqmatrixclient.main") +LOGGING_CATEGORY(PROFILER, "libqmatrixclient.profiler") +LOGGING_CATEGORY(EVENTS, "libqmatrixclient.events") +LOGGING_CATEGORY(EPHEMERAL, "libqmatrixclient.events.ephemeral") +LOGGING_CATEGORY(JOBS, "libqmatrixclient.jobs") +LOGGING_CATEGORY(SYNCJOB, "libqmatrixclient.jobs.sync") diff --git a/lib/logging.h b/lib/logging.h new file mode 100644 index 00000000..8dbfdf30 --- /dev/null +++ b/lib/logging.h @@ -0,0 +1,78 @@ +/****************************************************************************** + * Copyright (C) 2017 Kitsune Ral + * + * 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 + */ + +#pragma once + +#include +#include + +Q_DECLARE_LOGGING_CATEGORY(MAIN) +Q_DECLARE_LOGGING_CATEGORY(PROFILER) +Q_DECLARE_LOGGING_CATEGORY(EVENTS) +Q_DECLARE_LOGGING_CATEGORY(EPHEMERAL) +Q_DECLARE_LOGGING_CATEGORY(JOBS) +Q_DECLARE_LOGGING_CATEGORY(SYNCJOB) + +namespace QMatrixClient +{ + // QDebug manipulators + + using QDebugManip = QDebug (*)(QDebug); + + /** + * @brief QDebug manipulator to setup the stream for JSON output + * + * Originally made to encapsulate the change in QDebug behavior in Qt 5.4 + * and the respective addition of QDebug::noquote(). + * Together with the operator<<() helper, the proposed usage is + * (similar to std:: I/O manipulators): + * + * @example qCDebug() << formatJson << json_object; // (QJsonObject, etc.) + */ + inline QDebug formatJson(QDebug debug_object) + { +#if QT_VERSION < QT_VERSION_CHECK(5, 4, 0) + return debug_object; +#else + return debug_object.noquote(); +#endif + } + + /** + * @brief A helper operator to facilitate usage of formatJson (and possibly + * other manipulators) + * + * @param debug_object to output the json to + * @param qdm a QDebug manipulator + * @return a copy of debug_object that has its mode altered by qdm + */ + inline QDebug operator<< (QDebug debug_object, QDebugManip qdm) + { + return qdm(debug_object); + } +} + +inline QDebug operator<< (QDebug debug_object, const QElapsedTimer& et) +{ + auto val = et.nsecsElapsed() / 1000; + if (val < 1000) + debug_object << val << "µs"; + else + debug_object << val / 1000 << "ms"; + return debug_object; +} diff --git a/lib/networkaccessmanager.cpp b/lib/networkaccessmanager.cpp new file mode 100644 index 00000000..89967a8a --- /dev/null +++ b/lib/networkaccessmanager.cpp @@ -0,0 +1,75 @@ +/****************************************************************************** + * Copyright (C) 2018 Kitsune Ral + * + * 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 + */ + +#include "networkaccessmanager.h" + +#include +#include + +using namespace QMatrixClient; + +class NetworkAccessManager::Private +{ + public: + QList ignoredSslErrors; +}; + +NetworkAccessManager::NetworkAccessManager(QObject* parent) : d(std::make_unique()) +{ } + +QList NetworkAccessManager::ignoredSslErrors() const +{ + return d->ignoredSslErrors; +} + +void NetworkAccessManager::addIgnoredSslError(const QSslError& error) +{ + d->ignoredSslErrors << error; +} + +void NetworkAccessManager::clearIgnoredSslErrors() +{ + d->ignoredSslErrors.clear(); +} + +static NetworkAccessManager* createNam() +{ + auto nam = new NetworkAccessManager(QCoreApplication::instance()); + // See #109. Once Qt bearer management gets better, this workaround + // should become unnecessary. + nam->connect(nam, &QNetworkAccessManager::networkAccessibleChanged, + [nam] { nam->setNetworkAccessible(QNetworkAccessManager::Accessible); }); + return nam; +} + +NetworkAccessManager* NetworkAccessManager::instance() +{ + static auto* nam = createNam(); + return nam; +} + +NetworkAccessManager::~NetworkAccessManager() = default; + +QNetworkReply* NetworkAccessManager::createRequest(Operation op, + const QNetworkRequest& request, QIODevice* outgoingData) +{ + auto reply = + QNetworkAccessManager::createRequest(op, request, outgoingData); + reply->ignoreSslErrors(d->ignoredSslErrors); + return reply; +} diff --git a/lib/networkaccessmanager.h b/lib/networkaccessmanager.h new file mode 100644 index 00000000..ae847582 --- /dev/null +++ b/lib/networkaccessmanager.h @@ -0,0 +1,49 @@ +/****************************************************************************** + * Copyright (C) 2018 Kitsune Ral + * + * 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 + */ + +#pragma once + +#include + +#include + +namespace QMatrixClient +{ + class NetworkAccessManager : public QNetworkAccessManager + { + Q_OBJECT + public: + NetworkAccessManager(QObject* parent = nullptr); + ~NetworkAccessManager() override; + + QList ignoredSslErrors() const; + void addIgnoredSslError(const QSslError& error); + void clearIgnoredSslErrors(); + + /** Get a pointer to the singleton */ + static NetworkAccessManager* instance(); + + private: + QNetworkReply * createRequest(Operation op, + const QNetworkRequest &request, + QIODevice *outgoingData = Q_NULLPTR) override; + + class Private; + std::unique_ptr d; + }; +} // namespace QMatrixClient diff --git a/lib/networksettings.cpp b/lib/networksettings.cpp new file mode 100644 index 00000000..48bd09f3 --- /dev/null +++ b/lib/networksettings.cpp @@ -0,0 +1,31 @@ +/****************************************************************************** + * Copyright (C) 2017 Kitsune Ral + * + * 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 + */ + +#include "networksettings.h" + +using namespace QMatrixClient; + +void NetworkSettings::setupApplicationProxy() const +{ + QNetworkProxy::setApplicationProxy( + { proxyType(), proxyHostName(), proxyPort() }); +} + +QMC_DEFINE_SETTING(NetworkSettings, QNetworkProxy::ProxyType, proxyType, "proxy_type", QNetworkProxy::DefaultProxy, setProxyType) +QMC_DEFINE_SETTING(NetworkSettings, QString, proxyHostName, "proxy_hostname", "", setProxyHostName) +QMC_DEFINE_SETTING(NetworkSettings, quint16, proxyPort, "proxy_port", -1, setProxyPort) diff --git a/lib/networksettings.h b/lib/networksettings.h new file mode 100644 index 00000000..83613060 --- /dev/null +++ b/lib/networksettings.h @@ -0,0 +1,44 @@ +/****************************************************************************** + * Copyright (C) 2017 Kitsune Ral + * + * 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 + */ + +#pragma once + +#include "settings.h" + +#include + +Q_DECLARE_METATYPE(QNetworkProxy::ProxyType) + +namespace QMatrixClient { + class NetworkSettings: public SettingsGroup + { + Q_OBJECT + QMC_DECLARE_SETTING(QNetworkProxy::ProxyType, proxyType, setProxyType) + QMC_DECLARE_SETTING(QString, proxyHostName, setProxyHostName) + QMC_DECLARE_SETTING(quint16, proxyPort, setProxyPort) + Q_PROPERTY(QString proxyHost READ proxyHostName WRITE setProxyHostName) + public: + template + explicit NetworkSettings(ArgTs... qsettingsArgs) + : SettingsGroup(QStringLiteral("Network"), qsettingsArgs...) + { } + ~NetworkSettings() override = default; + + Q_INVOKABLE void setupApplicationProxy() const; + }; +} diff --git a/lib/room.cpp b/lib/room.cpp new file mode 100644 index 00000000..25669889 --- /dev/null +++ b/lib/room.cpp @@ -0,0 +1,1851 @@ +/****************************************************************************** + * Copyright (C) 2015 Felix Rohrbach + * + * 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 + */ + +#include "room.h" + +#include "jobs/generated/kicking.h" +#include "jobs/generated/inviting.h" +#include "jobs/generated/banning.h" +#include "jobs/generated/leaving.h" +#include "jobs/generated/receipts.h" +#include "jobs/generated/redaction.h" +#include "jobs/generated/account-data.h" +#include "jobs/setroomstatejob.h" +#include "events/simplestateevents.h" +#include "events/roomavatarevent.h" +#include "events/roommemberevent.h" +#include "events/typingevent.h" +#include "events/receiptevent.h" +#include "events/redactionevent.h" +#include "jobs/sendeventjob.h" +#include "jobs/roommessagesjob.h" +#include "jobs/mediathumbnailjob.h" +#include "jobs/downloadfilejob.h" +#include "jobs/postreadmarkersjob.h" +#include "avatar.h" +#include "connection.h" +#include "user.h" + +#include +#include // for efficient string concats (operator%) +#include +#include +#include +#include +#include + +#include +#include +#include + +using namespace QMatrixClient; +using namespace std::placeholders; +#if !(defined __GLIBCXX__ && __GLIBCXX__ <= 20150123) +using std::llround; +#endif + +enum EventsPlacement : int { Older = -1, Newer = 1 }; + +// A workaround for MSVC 2015 that fails with "error C2440: 'return': +// cannot convert from 'initializer list' to 'QMatrixClient::FileTransferInfo'" +#if (defined(_MSC_VER) && _MSC_VER < 1910) || (defined(__GNUC__) && __GNUC__ <= 4) +# define WORKAROUND_EXTENDED_INITIALIZER_LIST +#endif + +class Room::Private +{ + public: + /** Map of user names to users. User names potentially duplicate, hence a multi-hashmap. */ + typedef QMultiHash members_map_t; + + Private(Connection* c, QString id_, JoinState initialJoinState) + : q(nullptr), connection(c), id(std::move(id_)) + , joinState(initialJoinState) + { } + + Room* q; + + // This updates the room displayname field (which is the way a room + // should be shown in the room list) It should be called whenever the + // list of members or the room name (m.room.name) or canonical alias change. + void updateDisplayname(); + + Connection* connection; + Timeline timeline; + QHash eventsIndex; + QString id; + QStringList aliases; + QString canonicalAlias; + QString name; + QString displayname; + QString topic; + QString encryptionAlgorithm; + Avatar avatar; + JoinState joinState; + int highlightCount = 0; + int notificationCount = 0; + members_map_t membersMap; + QList usersTyping; + QList membersLeft; + int unreadMessages = 0; + bool displayed = false; + QString firstDisplayedEventId; + QString lastDisplayedEventId; + QHash lastReadEventIds; + QString serverReadMarker; + TagsMap tags; + QHash accountData; + QString prevBatch; + QPointer roomMessagesJob; + + struct FileTransferPrivateInfo + { +#ifdef WORKAROUND_EXTENDED_INITIALIZER_LIST + FileTransferPrivateInfo() = default; + FileTransferPrivateInfo(BaseJob* j, QString fileName) + : job(j), localFileInfo(fileName) + { } +#endif + QPointer job = nullptr; + QFileInfo localFileInfo { }; + FileTransferInfo::Status status = FileTransferInfo::Started; + qint64 progress = 0; + qint64 total = -1; + + void update(qint64 p, qint64 t) + { + if (t == 0) + { + t = -1; + if (p == 0) + p = -1; + } + if (p != -1) + qCDebug(PROFILER) << "Transfer progress:" << p << "/" << t + << "=" << llround(double(p) / t * 100) << "%"; + progress = p; total = t; + } + }; + void failedTransfer(const QString& tid, const QString& errorMessage = {}) + { + qCWarning(MAIN) << "File transfer failed for id" << tid; + if (!errorMessage.isEmpty()) + qCWarning(MAIN) << "Message:" << errorMessage; + fileTransfers[tid].status = FileTransferInfo::Failed; + emit q->fileTransferFailed(tid, errorMessage); + } + // A map from event/txn ids to information about the long operation; + // used for both download and upload operations + QHash fileTransfers; + + const RoomMessageEvent* getEventWithFile(const QString& eventId) const; + QString fileNameToDownload(const RoomMessageEvent* event) const; + + //void inviteUser(User* u); // We might get it at some point in time. + void insertMemberIntoMap(User* u); + void renameMember(User* u, QString oldName); + void removeMemberFromMap(const QString& username, User* u); + + void getPreviousContent(int limit = 10); + + bool isEventNotable(const TimelineItem& ti) const + { + return !ti->isRedacted() && + ti->senderId() != connection->userId() && + ti->type() == EventType::RoomMessage; + } + + void addNewMessageEvents(RoomEvents&& events); + void addHistoricalMessageEvents(RoomEvents&& events); + + /** + * @brief Move events into the timeline + * + * Insert events into the timeline, either new or historical. + * Pointers in the original container become empty, the ownership + * is passed to the timeline container. + * @param events - the range of events to be inserted + * @param placement - position and direction of insertion: Older for + * historical messages, Newer for new ones + */ + Timeline::size_type insertEvents(RoomEventsRange&& events, + EventsPlacement placement); + + /** + * Removes events from the passed container that are already in the timeline + */ + void dropDuplicateEvents(RoomEvents* events) const; + + void setLastReadEvent(User* u, const QString& eventId); + void updateUnreadCount(rev_iter_t from, rev_iter_t to); + void promoteReadMarker(User* u, rev_iter_t newMarker, + bool force = false); + + void markMessagesAsRead(rev_iter_t upToMarker); + + /** + * @brief Apply redaction to the timeline + * + * Tries to find an event in the timeline and redact it; deletes the + * redaction event whether the redacted event was found or not. + */ + void processRedaction(RoomEventPtr redactionEvent); + + void broadcastTagUpdates() + { + connection->callApi( + connection->userId(), id, TagEvent::typeId(), + TagEvent(tags).toJson()); + emit q->tagsChanged(); + } + + QJsonObject toJson() const; + + private: + QString calculateDisplayname() const; + QString roomNameFromMemberNames(const QList& userlist) const; + + bool isLocalUser(const User* u) const + { + return u == q->localUser(); + } +}; + +RoomEventPtr TimelineItem::replaceEvent(RoomEventPtr&& other) +{ + return std::exchange(evt, std::move(other)); +} + +Room::Room(Connection* connection, QString id, JoinState initialJoinState) + : QObject(connection), d(new Private(connection, id, initialJoinState)) +{ + setObjectName(id); + // See "Accessing the Public Class" section in + // https://marcmutz.wordpress.com/translated-articles/pimp-my-pimpl-%E2%80%94-reloaded/ + d->q = this; + connect(this, &Room::userAdded, this, &Room::memberListChanged); + connect(this, &Room::userRemoved, this, &Room::memberListChanged); + connect(this, &Room::memberRenamed, this, &Room::memberListChanged); + qCDebug(MAIN) << "New" << toCString(initialJoinState) << "Room:" << id; +} + +Room::~Room() +{ + delete d; +} + +const QString& Room::id() const +{ + return d->id; +} + +const Room::Timeline& Room::messageEvents() const +{ + return d->timeline; +} + +QString Room::name() const +{ + return d->name; +} + +QStringList Room::aliases() const +{ + return d->aliases; +} + +QString Room::canonicalAlias() const +{ + return d->canonicalAlias; +} + +QString Room::displayName() const +{ + return d->displayname; +} + +QString Room::topic() const +{ + return d->topic; +} + +QString Room::avatarMediaId() const +{ + return d->avatar.mediaId(); +} + +QUrl Room::avatarUrl() const +{ + return d->avatar.url(); +} + +QImage Room::avatar(int dimension) +{ + return avatar(dimension, dimension); +} + +QImage Room::avatar(int width, int height) +{ + if (!d->avatar.url().isEmpty()) + return d->avatar.get(connection(), width, height, [=] { emit avatarChanged(); }); + + // Use the other side's avatar for 1:1's + if (d->membersMap.size() == 2) + { + auto theOtherOneIt = d->membersMap.begin(); + if (theOtherOneIt.value() == localUser()) + ++theOtherOneIt; + return (*theOtherOneIt)->avatar(width, height, this, + [=] { emit avatarChanged(); }); + } + return {}; +} + +User* Room::user(const QString& userId) const +{ + return connection()->user(userId); +} + +JoinState Room::memberJoinState(User* user) const +{ + return + d->membersMap.contains(user->name(this), user) ? JoinState::Join : + JoinState::Leave; +} + +JoinState Room::joinState() const +{ + return d->joinState; +} + +void Room::setJoinState(JoinState state) +{ + JoinState oldState = d->joinState; + if( state == oldState ) + return; + d->joinState = state; + qCDebug(MAIN) << "Room" << id() << "changed state: " + << int(oldState) << "->" << int(state); + emit joinStateChanged(oldState, state); +} + +void Room::Private::setLastReadEvent(User* u, const QString& eventId) +{ + auto& storedId = lastReadEventIds[u]; + if (storedId == eventId) + return; + storedId = eventId; + emit q->lastReadEventChanged(u); + if (isLocalUser(u)) + { + if (eventId != serverReadMarker) + connection->callApi(id, eventId); + emit q->readMarkerMoved(); + } +} + +void Room::Private::updateUnreadCount(rev_iter_t from, rev_iter_t to) +{ + Q_ASSERT(from >= timeline.crbegin() && from <= timeline.crend()); + Q_ASSERT(to >= from && to <= timeline.crend()); + + // Catch a special case when the last read event id refers to an event + // that has just arrived. In this case we should recalculate + // unreadMessages and might need to promote the read marker further + // over local-origin messages. + const auto readMarker = q->readMarker(); + if (readMarker >= from && readMarker < to) + { + qCDebug(MAIN) << "Discovered last read event in room" << displayname; + promoteReadMarker(q->localUser(), readMarker, true); + return; + } + + Q_ASSERT(to <= readMarker); + + QElapsedTimer et; et.start(); + const auto newUnreadMessages = count_if(from, to, + std::bind(&Room::Private::isEventNotable, this, _1)); + if (et.nsecsElapsed() > 10000) + qCDebug(PROFILER) << "Counting gained unread messages took" << et; + + if(newUnreadMessages > 0) + { + // See https://github.com/QMatrixClient/libqmatrixclient/wiki/unread_count + if (unreadMessages < 0) + unreadMessages = 0; + + unreadMessages += newUnreadMessages; + qCDebug(MAIN) << "Room" << displayname << "has gained" + << newUnreadMessages << "unread message(s)," + << (q->readMarker() == timeline.crend() ? + "in total at least" : "in total") + << unreadMessages << "unread message(s)"; + emit q->unreadMessagesChanged(q); + } +} + +void Room::Private::promoteReadMarker(User* u, rev_iter_t newMarker, bool force) +{ + Q_ASSERT_X(u, __FUNCTION__, "User* should not be nullptr"); + Q_ASSERT(newMarker >= timeline.crbegin() && newMarker <= timeline.crend()); + + const auto prevMarker = q->readMarker(u); + if (!force && prevMarker <= newMarker) // Remember, we deal with reverse iterators + return; + + Q_ASSERT(newMarker < timeline.crend()); + + // Try to auto-promote the read marker over the user's own messages + // (switch to direct iterators for that). + auto eagerMarker = find_if(newMarker.base(), timeline.cend(), + [=](const TimelineItem& ti) { return ti->senderId() != u->id(); }); + + setLastReadEvent(u, (*(eagerMarker - 1))->id()); + if (isLocalUser(u)) + { + const auto oldUnreadCount = unreadMessages; + QElapsedTimer et; et.start(); + unreadMessages = count_if(eagerMarker, timeline.cend(), + std::bind(&Room::Private::isEventNotable, this, _1)); + if (et.nsecsElapsed() > 10000) + qCDebug(PROFILER) << "Recounting unread messages took" << et; + + // See https://github.com/QMatrixClient/libqmatrixclient/wiki/unread_count + if (unreadMessages == 0) + unreadMessages = -1; + + if (force || unreadMessages != oldUnreadCount) + { + if (unreadMessages == -1) + { + qCDebug(MAIN) << "Room" << displayname + << "has no more unread messages"; + } else + qCDebug(MAIN) << "Room" << displayname << "still has" + << unreadMessages << "unread message(s)"; + emit q->unreadMessagesChanged(q); + } + } +} + +void Room::Private::markMessagesAsRead(rev_iter_t upToMarker) +{ + const auto prevMarker = q->readMarker(); + promoteReadMarker(q->localUser(), upToMarker); + if (prevMarker != upToMarker) + qCDebug(MAIN) << "Marked messages as read until" << *q->readMarker(); + + // We shouldn't send read receipts for the local user's own messages - so + // search earlier messages for the latest message not from the local user + // until the previous last-read message, whichever comes first. + for (; upToMarker < prevMarker; ++upToMarker) + { + if ((*upToMarker)->senderId() != q->localUser()->id()) + { + connection->callApi(id, "m.read", + (*upToMarker)->id()); + break; + } + } +} + +void Room::markMessagesAsRead(QString uptoEventId) +{ + d->markMessagesAsRead(findInTimeline(uptoEventId)); +} + +void Room::markAllMessagesAsRead() +{ + if (!d->timeline.empty()) + d->markMessagesAsRead(d->timeline.crbegin()); +} + +bool Room::hasUnreadMessages() const +{ + return unreadCount() >= 0; +} + +int Room::unreadCount() const +{ + return d->unreadMessages; +} + +Room::rev_iter_t Room::timelineEdge() const +{ + return d->timeline.crend(); +} + +TimelineItem::index_t Room::minTimelineIndex() const +{ + return d->timeline.empty() ? 0 : d->timeline.front().index(); +} + +TimelineItem::index_t Room::maxTimelineIndex() const +{ + return d->timeline.empty() ? 0 : d->timeline.back().index(); +} + +bool Room::isValidIndex(TimelineItem::index_t timelineIndex) const +{ + return !d->timeline.empty() && + timelineIndex >= minTimelineIndex() && + timelineIndex <= maxTimelineIndex(); +} + +Room::rev_iter_t Room::findInTimeline(TimelineItem::index_t index) const +{ + return timelineEdge() - + (isValidIndex(index) ? index - minTimelineIndex() + 1 : 0); +} + +Room::rev_iter_t Room::findInTimeline(const QString& evtId) const +{ + if (!d->timeline.empty() && d->eventsIndex.contains(evtId)) + return findInTimeline(d->eventsIndex.value(evtId)); + return timelineEdge(); +} + +bool Room::displayed() const +{ + return d->displayed; +} + +void Room::setDisplayed(bool displayed) +{ + if (d->displayed == displayed) + return; + + d->displayed = displayed; + emit displayedChanged(displayed); + if( displayed ) + { + resetHighlightCount(); + resetNotificationCount(); + } +} + +QString Room::firstDisplayedEventId() const +{ + return d->firstDisplayedEventId; +} + +Room::rev_iter_t Room::firstDisplayedMarker() const +{ + return findInTimeline(firstDisplayedEventId()); +} + +void Room::setFirstDisplayedEventId(const QString& eventId) +{ + if (d->firstDisplayedEventId == eventId) + return; + + d->firstDisplayedEventId = eventId; + emit firstDisplayedEventChanged(); +} + +void Room::setFirstDisplayedEvent(TimelineItem::index_t index) +{ + Q_ASSERT(isValidIndex(index)); + setFirstDisplayedEventId(findInTimeline(index)->event()->id()); +} + +QString Room::lastDisplayedEventId() const +{ + return d->lastDisplayedEventId; +} + +Room::rev_iter_t Room::lastDisplayedMarker() const +{ + return findInTimeline(lastDisplayedEventId()); +} + +void Room::setLastDisplayedEventId(const QString& eventId) +{ + if (d->lastDisplayedEventId == eventId) + return; + + d->lastDisplayedEventId = eventId; + emit lastDisplayedEventChanged(); +} + +void Room::setLastDisplayedEvent(TimelineItem::index_t index) +{ + Q_ASSERT(isValidIndex(index)); + setLastDisplayedEventId(findInTimeline(index)->event()->id()); +} + +Room::rev_iter_t Room::readMarker(const User* user) const +{ + Q_ASSERT(user); + return findInTimeline(d->lastReadEventIds.value(user)); +} + +Room::rev_iter_t Room::readMarker() const +{ + return readMarker(localUser()); +} + +QString Room::readMarkerEventId() const +{ + return d->lastReadEventIds.value(localUser()); +} + +int Room::notificationCount() const +{ + return d->notificationCount; +} + +void Room::resetNotificationCount() +{ + if( d->notificationCount == 0 ) + return; + d->notificationCount = 0; + emit notificationCountChanged(this); +} + +int Room::highlightCount() const +{ + return d->highlightCount; +} + +void Room::resetHighlightCount() +{ + if( d->highlightCount == 0 ) + return; + d->highlightCount = 0; + emit highlightCountChanged(this); +} + +QStringList Room::tagNames() const +{ + return d->tags.keys(); +} + +TagsMap Room::tags() const +{ + return d->tags; +} + +TagRecord Room::tag(const QString& name) const +{ + return d->tags.value(name); +} + +void Room::addTag(const QString& name, const TagRecord& record) +{ + if (d->tags.contains(name)) + return; + + d->tags.insert(name, record); + d->broadcastTagUpdates(); +} + +void Room::removeTag(const QString& name) +{ + if (!d->tags.contains(name)) + return; + + d->tags.remove(name); + d->broadcastTagUpdates(); +} + +void Room::setTags(const TagsMap& newTags) +{ + if (newTags == d->tags) + return; + d->tags = newTags; + d->broadcastTagUpdates(); +} + +bool Room::isFavourite() const +{ + return d->tags.contains(FavouriteTag); +} + +bool Room::isLowPriority() const +{ + return d->tags.contains(LowPriorityTag); +} + +bool Room::isDirectChat() const +{ + return connection()->isDirectChat(id()); +} + +QList Room::directChatUsers() const +{ + return connection()->directChatUsers(this); +} + +const RoomMessageEvent* +Room::Private::getEventWithFile(const QString& eventId) const +{ + auto evtIt = q->findInTimeline(eventId); + if (evtIt != timeline.rend() && + evtIt->event()->type() == EventType::RoomMessage) + { + auto* event = static_cast(evtIt->event()); + if (event->hasFileContent()) + return event; + } + qWarning() << "No files to download in event" << eventId; + return nullptr; +} + +QString Room::Private::fileNameToDownload(const RoomMessageEvent* event) const +{ + Q_ASSERT(event->hasFileContent()); + const auto* fileInfo = event->content()->fileInfo(); + QString fileName; + if (!fileInfo->originalName.isEmpty()) + { + fileName = QFileInfo(fileInfo->originalName).fileName(); + } + else if (!event->plainBody().isEmpty()) + { + // Having no better options, assume that the body has + // the original file URL or at least the file name. + QUrl u { event->plainBody() }; + if (u.isValid()) + fileName = QFileInfo(u.path()).fileName(); + } + // Check the file name for sanity + if (fileName.isEmpty() || !QTemporaryFile(fileName).open()) + return "file." % fileInfo->mimeType.preferredSuffix(); + + if (QSysInfo::productType() == "windows") + { + const auto& suffixes = fileInfo->mimeType.suffixes(); + if (!suffixes.isEmpty() && + std::none_of(suffixes.begin(), suffixes.end(), + [&fileName] (const QString& s) { + return fileName.endsWith(s); })) + return fileName % '.' % fileInfo->mimeType.preferredSuffix(); + } + return fileName; +} + +QUrl Room::urlToThumbnail(const QString& eventId) +{ + if (auto* event = d->getEventWithFile(eventId)) + if (event->hasThumbnail()) + { + auto* thumbnail = event->content()->thumbnailInfo(); + Q_ASSERT(thumbnail != nullptr); + return MediaThumbnailJob::makeRequestUrl(connection()->homeserver(), + thumbnail->url, thumbnail->imageSize); + } + qDebug() << "Event" << eventId << "has no thumbnail"; + return {}; +} + +QUrl Room::urlToDownload(const QString& eventId) +{ + if (auto* event = d->getEventWithFile(eventId)) + { + auto* fileInfo = event->content()->fileInfo(); + Q_ASSERT(fileInfo != nullptr); + return DownloadFileJob::makeRequestUrl(connection()->homeserver(), + fileInfo->url); + } + return {}; +} + +QString Room::fileNameToDownload(const QString& eventId) +{ + if (auto* event = d->getEventWithFile(eventId)) + return d->fileNameToDownload(event); + return {}; +} + +FileTransferInfo Room::fileTransferInfo(const QString& id) const +{ + auto infoIt = d->fileTransfers.find(id); + if (infoIt == d->fileTransfers.end()) + return {}; + + // FIXME: Add lib tests to make sure FileTransferInfo::status stays + // consistent with FileTransferInfo::job + + qint64 progress = infoIt->progress; + qint64 total = infoIt->total; + if (total > INT_MAX) + { + // JavaScript doesn't deal with 64-bit integers; scale down if necessary + progress = llround(double(progress) / total * INT_MAX); + total = INT_MAX; + } + +#ifdef WORKAROUND_EXTENDED_INITIALIZER_LIST + FileTransferInfo fti; + fti.status = infoIt->status; + fti.progress = int(progress); + fti.total = int(total); + fti.localDir = QUrl::fromLocalFile(infoIt->localFileInfo.absolutePath()); + fti.localPath = QUrl::fromLocalFile(infoIt->localFileInfo.absoluteFilePath()); + return fti; +#else + return { infoIt->status, int(progress), int(total), + QUrl::fromLocalFile(infoIt->localFileInfo.absolutePath()), + QUrl::fromLocalFile(infoIt->localFileInfo.absoluteFilePath()) + }; +#endif +} + +static const auto RegExpOptions = + QRegularExpression::CaseInsensitiveOption + | QRegularExpression::OptimizeOnFirstUsageOption + | QRegularExpression::UseUnicodePropertiesOption; + +// regexp is originally taken from Konsole (https://github.com/KDE/konsole) +// full url: +// protocolname:// or www. followed by anything other than whitespaces, +// <, >, ' or ", and ends before whitespaces, <, >, ', ", ], !, ), :, +// comma or dot +// Note: outer parentheses are a part of C++ raw string delimiters, not of +// the regex (see http://en.cppreference.com/w/cpp/language/string_literal). +static const QRegularExpression FullUrlRegExp(QStringLiteral( + R"(((www\.(?!\.)|[a-z][a-z0-9+.-]*://)(&(?![lg]t;)|[^&\s<>'"])+(&(?![lg]t;)|[^&!,.\s<>'"\]):])))" + ), RegExpOptions); +// email address: +// [word chars, dots or dashes]@[word chars, dots or dashes].[word chars] +static const QRegularExpression EmailAddressRegExp(QStringLiteral( + R"((mailto:)?(\b(\w|\.|-)+@(\w|\.|-)+\.\w+\b))" + ), RegExpOptions); + +/** Converts all that looks like a URL into HTML links */ +static void linkifyUrls(QString& htmlEscapedText) +{ + // NOTE: htmlEscapedText is already HTML-escaped (no literal <,>,&)! + + htmlEscapedText.replace(EmailAddressRegExp, + QStringLiteral(R"(\1\2)")); + htmlEscapedText.replace(FullUrlRegExp, + QStringLiteral(R"(\1)")); +} + +QString Room::prettyPrint(const QString& plainText) const +{ + auto pt = QStringLiteral("") + + plainText.toHtmlEscaped() + QStringLiteral(""); + pt.replace('\n', "
"); + + linkifyUrls(pt); + return pt; +} + +QList< User* > Room::usersTyping() const +{ + return d->usersTyping; +} + +QList< User* > Room::membersLeft() const +{ + return d->membersLeft; +} + +QList< User* > Room::users() const +{ + return d->membersMap.values(); +} + +QStringList Room::memberNames() const +{ + QStringList res; + for (auto u : d->membersMap) + res.append( roomMembername(u) ); + + return res; +} + +int Room::memberCount() const +{ + return d->membersMap.size(); +} + +int Room::timelineSize() const +{ + return int(d->timeline.size()); +} + +bool Room::usesEncryption() const +{ + return !d->encryptionAlgorithm.isEmpty(); +} + +void Room::Private::insertMemberIntoMap(User *u) +{ + const auto userName = u->name(q); + // If there is exactly one namesake of the added user, signal member renaming + // for that other one because the two should be disambiguated now. + auto namesakes = membersMap.values(userName); + if (namesakes.size() == 1) + emit q->memberAboutToRename(namesakes.front(), + namesakes.front()->fullName(q)); + membersMap.insert(userName, u); + if (namesakes.size() == 1) + emit q->memberRenamed(namesakes.front()); +} + +void Room::Private::renameMember(User* u, QString oldName) +{ + if (u->name(q) == oldName) + { + qCWarning(MAIN) << "Room::Private::renameMember(): the user " + << u->fullName(q) + << "is already known in the room under a new name."; + } + else if (membersMap.contains(oldName, u)) + { + removeMemberFromMap(oldName, u); + insertMemberIntoMap(u); + } + emit q->memberRenamed(u); +} + +void Room::Private::removeMemberFromMap(const QString& username, User* u) +{ + User* namesake = nullptr; + auto namesakes = membersMap.values(username); + if (namesakes.size() == 2) + { + namesake = namesakes.front() == u ? namesakes.back() : namesakes.front(); + Q_ASSERT_X(namesake != u, __FUNCTION__, "Room members list is broken"); + emit q->memberAboutToRename(namesake, username); + } + membersMap.remove(username, u); + // If there was one namesake besides the removed user, signal member renaming + // for it because it doesn't need to be disambiguated anymore. + // TODO: Think about left users. + if (namesake) + emit q->memberRenamed(namesake); +} + +inline auto makeErrorStr(const Event& e, QByteArray msg) +{ + return msg.append("; event dump follows:\n").append(e.originalJson()); +} + +Room::Timeline::size_type Room::Private::insertEvents(RoomEventsRange&& events, + EventsPlacement placement) +{ + // Historical messages arrive in newest-to-oldest order, so the process for + // them is symmetric to the one for new messages. + auto index = timeline.empty() ? -int(placement) : + placement == Older ? timeline.front().index() : + timeline.back().index(); + auto baseIndex = index; + for (auto&& e: events) + { + const auto eId = e->id(); + Q_ASSERT_X(e, __FUNCTION__, "Attempt to add nullptr to timeline"); + Q_ASSERT_X(!eId.isEmpty(), __FUNCTION__, + makeErrorStr(*e, + "Event with empty id cannot be in the timeline")); + Q_ASSERT_X(!eventsIndex.contains(eId), __FUNCTION__, + makeErrorStr(*e, "Event is already in the timeline; " + "incoming events were not properly deduplicated")); + if (placement == Older) + timeline.emplace_front(move(e), --index); + else + timeline.emplace_back(move(e), ++index); + eventsIndex.insert(eId, index); + Q_ASSERT(q->findInTimeline(eId)->event()->id() == eId); + } + // Pointers in "events" are empty now, but events.size() didn't change + Q_ASSERT(int(events.size()) == (index - baseIndex) * int(placement)); + return events.size(); +} + +QString Room::roomMembername(const User* u) const +{ + // See the CS spec, section 11.2.2.3 + + const auto username = u->name(this); + if (username.isEmpty()) + return u->id(); + + // Get the list of users with the same display name. Most likely, + // there'll be one, but there's a chance there are more. + if (d->membersMap.count(username) == 1) + return username; + + // We expect a user to be a member of the room - but technically it is + // possible to invoke roomMemberName() even for non-members. In such case + // we return the name _with_ id, to stay on a safe side. + // XXX: Causes a storm of false alarms when scrolling through older events + // with left users; commented out until we have a proper backtracking of + // room state ("room time machine"). +// if ( !namesakes.contains(u) ) +// { +// qCWarning() +// << "Room::roomMemberName(): user" << u->id() +// << "is not a member of the room" << id(); +// } + + // In case of more than one namesake, use the full name to disambiguate + return u->fullName(this); +} + +QString Room::roomMembername(const QString& userId) const +{ + return roomMembername(user(userId)); +} + +void Room::updateData(SyncRoomData&& data) +{ + if( d->prevBatch.isEmpty() ) + d->prevBatch = data.timelinePrevBatch; + setJoinState(data.joinState); + + QElapsedTimer et; et.start(); + for (auto&& event: data.accountData) + processAccountDataEvent(move(event)); + + if (!data.state.empty()) + { + et.restart(); + processStateEvents(data.state); + qCDebug(PROFILER) << "*** Room::processStateEvents(state):" + << data.state.size() << "event(s)," << et; + } + if (!data.timeline.empty()) + { + et.restart(); + // State changes can arrive in a timeline event; so check those. + processStateEvents(data.timeline); + qCDebug(PROFILER) << "*** Room::processStateEvents(timeline):" + << data.timeline.size() << "event(s)," << et; + + et.restart(); + d->addNewMessageEvents(move(data.timeline)); + qCDebug(PROFILER) << "*** Room::addNewMessageEvents():" << et; + } + for( auto&& ephemeralEvent: data.ephemeral ) + processEphemeralEvent(move(ephemeralEvent)); + + // See https://github.com/QMatrixClient/libqmatrixclient/wiki/unread_count + if (data.unreadCount != -2 && data.unreadCount != d->unreadMessages) + { + qCDebug(MAIN) << "Setting unread_count to" << data.unreadCount; + d->unreadMessages = data.unreadCount; + emit unreadMessagesChanged(this); + } + + if( data.highlightCount != d->highlightCount ) + { + d->highlightCount = data.highlightCount; + emit highlightCountChanged(this); + } + if( data.notificationCount != d->notificationCount ) + { + d->notificationCount = data.notificationCount; + emit notificationCountChanged(this); + } +} + +void Room::postMessage(const QString& type, const QString& plainText) +{ + postMessage(RoomMessageEvent { plainText, type }); +} + +void Room::postMessage(const QString& plainText, MessageEventType type) +{ + postMessage(RoomMessageEvent { plainText, type }); +} + +void Room::postMessage(const RoomMessageEvent& event) +{ + if (usesEncryption()) + { + qCCritical(MAIN) << "Room" << displayName() + << "enforces encryption; sending encrypted messages is not supported yet"; + } + connection()->callApi(id(), event); +} + +void Room::setName(const QString& newName) +{ + connection()->callApi(id(), RoomNameEvent(newName)); +} + +void Room::setCanonicalAlias(const QString& newAlias) +{ + connection()->callApi(id(), + RoomCanonicalAliasEvent(newAlias)); +} + +void Room::setTopic(const QString& newTopic) +{ + RoomTopicEvent evt(newTopic); + connection()->callApi(id(), evt); +} + +void Room::getPreviousContent(int limit) +{ + d->getPreviousContent(limit); +} + +void Room::Private::getPreviousContent(int limit) +{ + if( !isJobRunning(roomMessagesJob) ) + { + roomMessagesJob = + connection->callApi(id, prevBatch, limit); + connect( roomMessagesJob, &RoomMessagesJob::success, [=] { + prevBatch = roomMessagesJob->end(); + addHistoricalMessageEvents(roomMessagesJob->releaseEvents()); + }); + } +} + +void Room::inviteToRoom(const QString& memberId) +{ + connection()->callApi(id(), memberId); +} + +LeaveRoomJob* Room::leaveRoom() +{ + return connection()->callApi(id()); +} + +void Room::kickMember(const QString& memberId, const QString& reason) +{ + connection()->callApi(id(), memberId, reason); +} + +void Room::ban(const QString& userId, const QString& reason) +{ + connection()->callApi(id(), userId, reason); +} + +void Room::unban(const QString& userId) +{ + connection()->callApi(id(), userId); +} + +void Room::redactEvent(const QString& eventId, const QString& reason) +{ + connection()->callApi( + id(), eventId, connection()->generateTxnId(), reason); +} + +void Room::uploadFile(const QString& id, const QUrl& localFilename, + const QString& overrideContentType) +{ + Q_ASSERT_X(localFilename.isLocalFile(), __FUNCTION__, + "localFilename should point at a local file"); + auto fileName = localFilename.toLocalFile(); + auto job = connection()->uploadFile(fileName, overrideContentType); + if (isJobRunning(job)) + { + d->fileTransfers.insert(id, { job, fileName }); + connect(job, &BaseJob::uploadProgress, this, + [this,id] (qint64 sent, qint64 total) { + d->fileTransfers[id].update(sent, total); + emit fileTransferProgress(id, sent, total); + }); + connect(job, &BaseJob::success, this, [this,id,localFilename,job] { + d->fileTransfers[id].status = FileTransferInfo::Completed; + emit fileTransferCompleted(id, localFilename, job->contentUri()); + }); + connect(job, &BaseJob::failure, this, + std::bind(&Private::failedTransfer, d, id, job->errorString())); + emit newFileTransfer(id, localFilename); + } else + d->failedTransfer(id); +} + +void Room::downloadFile(const QString& eventId, const QUrl& localFilename) +{ + auto ongoingTransfer = d->fileTransfers.find(eventId); + if (ongoingTransfer != d->fileTransfers.end() && + ongoingTransfer->status == FileTransferInfo::Started) + { + qCWarning(MAIN) << "Download for" << eventId + << "already started; to restart, cancel it first"; + return; + } + + Q_ASSERT_X(localFilename.isEmpty() || localFilename.isLocalFile(), + __FUNCTION__, "localFilename should point at a local file"); + const auto* event = d->getEventWithFile(eventId); + if (!event) + { + qCCritical(MAIN) + << eventId << "is not in the local timeline or has no file content"; + Q_ASSERT(false); + return; + } + const auto fileUrl = event->content()->fileInfo()->url; + auto filePath = localFilename.toLocalFile(); + if (filePath.isEmpty()) + { + // Build our own file path, starting with temp directory and eventId. + filePath = eventId; + filePath = QDir::tempPath() % '/' % filePath.replace(':', '_') % + '#' % d->fileNameToDownload(event); + } + auto job = connection()->downloadFile(fileUrl, filePath); + if (isJobRunning(job)) + { + // If there was a previous transfer (completed or failed), remove it. + d->fileTransfers.remove(eventId); + d->fileTransfers.insert(eventId, { job, job->targetFileName() }); + connect(job, &BaseJob::downloadProgress, this, + [this,eventId] (qint64 received, qint64 total) { + d->fileTransfers[eventId].update(received, total); + emit fileTransferProgress(eventId, received, total); + }); + connect(job, &BaseJob::success, this, [this,eventId,fileUrl,job] { + d->fileTransfers[eventId].status = FileTransferInfo::Completed; + emit fileTransferCompleted(eventId, fileUrl, + QUrl::fromLocalFile(job->targetFileName())); + }); + connect(job, &BaseJob::failure, this, + std::bind(&Private::failedTransfer, d, + eventId, job->errorString())); + } else + d->failedTransfer(eventId); +} + +void Room::cancelFileTransfer(const QString& id) +{ + auto it = d->fileTransfers.find(id); + if (it == d->fileTransfers.end()) + { + qCWarning(MAIN) << "No information on file transfer" << id + << "in room" << d->id; + return; + } + if (isJobRunning(it->job)) + it->job->abandon(); + d->fileTransfers.remove(id); + emit fileTransferCancelled(id); +} + +void Room::Private::dropDuplicateEvents(RoomEvents* events) const +{ + if (events->empty()) + return; + + // Multiple-remove (by different criteria), single-erase + // 1. Check for duplicates against the timeline. + auto dupsBegin = remove_if(events->begin(), events->end(), + [&] (const RoomEventPtr& e) + { return eventsIndex.contains(e->id()); }); + + // 2. Check for duplicates within the batch if there are still events. + for (auto eIt = events->begin(); distance(eIt, dupsBegin) > 1; ++eIt) + dupsBegin = remove_if(eIt + 1, dupsBegin, + [&] (const RoomEventPtr& e) + { return e->id() == (*eIt)->id(); }); + if (dupsBegin == events->end()) + return; + + qCDebug(EVENTS) << "Dropping" << distance(dupsBegin, events->end()) + << "duplicate event(s)"; + events->erase(dupsBegin, events->end()); +} + +inline bool isRedaction(const RoomEventPtr& e) +{ + return e->type() == EventType::Redaction; +} + +void Room::Private::processRedaction(RoomEventPtr redactionEvent) +{ + Q_ASSERT(redactionEvent && isRedaction(redactionEvent)); + const auto& redaction = + static_cast(redactionEvent.get()); + + const auto pIdx = eventsIndex.find(redaction->redactedEvent()); + if (pIdx == eventsIndex.end()) + { + qCDebug(MAIN) << "Redaction" << redaction->id() + << "ignored: target event not found"; + return; // If the target events comes later, it comes already redacted. + } + Q_ASSERT(q->isValidIndex(*pIdx)); + + auto& ti = timeline[Timeline::size_type(*pIdx - q->minTimelineIndex())]; + + // Apply the redaction procedure from chapter 6.5 of The Spec + auto originalJson = ti->originalJsonObject(); + if (originalJson.value("unsigned").toObject() + .value("redacted_because").toObject() + .value("event_id") == redaction->id()) + { + qCDebug(MAIN) << "Redaction" << redaction->id() + << "of event" << ti.event()->id() << "already done, skipping"; + return; + } + static const QStringList keepKeys = + { "event_id", "type", "room_id", "sender", "state_key", + "prev_content", "content", "origin_server_ts" }; + static const + std::vector> keepContentKeysMap + { { Event::Type::RoomMember, { "membership" } } + , { Event::Type::RoomCreate, { "creator" } } + , { Event::Type::RoomJoinRules, { "join_rule" } } + , { Event::Type::RoomPowerLevels, + { "ban", "events", "events_default", "kick", "redact", + "state_default", "users", "users_default" } } + , { Event::Type::RoomAliases, { "alias" } } + }; + for (auto it = originalJson.begin(); it != originalJson.end();) + { + if (!keepKeys.contains(it.key())) + it = originalJson.erase(it); // TODO: shred the value + else + ++it; + } + auto keepContentKeys = + find_if(keepContentKeysMap.begin(), keepContentKeysMap.end(), + [&ti](const auto& t) { return ti->type() == t.first; } ); + if (keepContentKeys == keepContentKeysMap.end()) + { + originalJson.remove("content"); + originalJson.remove("prev_content"); + } else { + auto content = originalJson.take("content").toObject(); + for (auto it = content.begin(); it != content.end(); ) + { + if (!keepContentKeys->second.contains(it.key())) + it = content.erase(it); + else + ++it; + } + originalJson.insert("content", content); + } + auto unsignedData = originalJson.take("unsigned").toObject(); + unsignedData["redacted_because"] = redaction->originalJsonObject(); + originalJson.insert("unsigned", unsignedData); + + // Make a new event from the redacted JSON, exchange events, + // notify everyone and delete the old event + RoomEventPtr oldEvent + { ti.replaceEvent(makeEvent(originalJson)) }; + q->onRedaction(oldEvent.get(), ti.event()); + qCDebug(MAIN) << "Redacted" << oldEvent->id() << "with" << redaction->id(); + emit q->replacedEvent(ti.event(), oldEvent.get()); +} + +Connection* Room::connection() const +{ + Q_ASSERT(d->connection); + return d->connection; +} + +User* Room::localUser() const +{ + return connection()->user(); +} + +void Room::Private::addNewMessageEvents(RoomEvents&& events) +{ + auto timelineSize = timeline.size(); + + dropDuplicateEvents(&events); + // We want to process redactions in the order of arrival (covering the + // case of one redaction superseding another one), hence stable partition. + const auto normalsBegin = + stable_partition(events.begin(), events.end(), isRedaction); + RoomEventsRange redactions { events.begin(), normalsBegin }, + normalEvents { normalsBegin, events.end() }; + + if (!normalEvents.empty()) + emit q->aboutToAddNewMessages(normalEvents); + const auto insertedSize = insertEvents(std::move(normalEvents), Newer); + const auto from = timeline.cend() - insertedSize; + if (insertedSize > 0) + { + qCDebug(MAIN) + << "Room" << displayname << "received" << insertedSize + << "new events; the last event is now" << timeline.back(); + q->onAddNewTimelineEvents(from); + } + for (auto&& r: redactions) + processRedaction(move(r)); + if (insertedSize > 0) + { + emit q->addedMessages(); + + // The first event in the just-added batch (referred to by `from`) + // defines whose read marker can possibly be promoted any further over + // the same author's events newly arrived. Others will need explicit + // read receipts from the server (or, for the local user, + // markMessagesAsRead() invocation) to promote their read markers over + // the new message events. + auto firstWriter = q->user((*from)->senderId()); + if (q->readMarker(firstWriter) != timeline.crend()) + { + promoteReadMarker(firstWriter, rev_iter_t(from) - 1); + qCDebug(MAIN) << "Auto-promoted read marker for" << firstWriter->id() + << "to" << *q->readMarker(firstWriter); + } + + updateUnreadCount(timeline.crbegin(), rev_iter_t(from)); + } + + Q_ASSERT(timeline.size() == timelineSize + insertedSize); +} + +void Room::Private::addHistoricalMessageEvents(RoomEvents&& events) +{ + const auto timelineSize = timeline.size(); + + dropDuplicateEvents(&events); + const auto redactionsBegin = + remove_if(events.begin(), events.end(), isRedaction); + RoomEventsRange normalEvents { events.begin(), redactionsBegin }; + if (normalEvents.empty()) + return; + + emit q->aboutToAddHistoricalMessages(normalEvents); + const auto insertedSize = insertEvents(std::move(normalEvents), Older); + const auto from = timeline.crend() - insertedSize; + + qCDebug(MAIN) << "Room" << displayname << "received" << insertedSize + << "past events; the oldest event is now" << timeline.front(); + q->onAddHistoricalTimelineEvents(from); + emit q->addedMessages(); + + if (from <= q->readMarker()) + updateUnreadCount(from, timeline.crend()); + + Q_ASSERT(timeline.size() == timelineSize + insertedSize); +} + +void Room::processStateEvents(const RoomEvents& events) +{ + bool emitNamesChanged = false; + for (const auto& e: events) + { + auto* event = e.get(); + switch (event->type()) + { + case EventType::RoomName: { + auto nameEvent = static_cast(event); + d->name = nameEvent->name(); + qCDebug(MAIN) << "Room name updated:" << d->name; + emitNamesChanged = true; + break; + } + case EventType::RoomAliases: { + auto aliasesEvent = static_cast(event); + d->aliases = aliasesEvent->aliases(); + qCDebug(MAIN) << "Room aliases updated:" << d->aliases; + emitNamesChanged = true; + break; + } + case EventType::RoomCanonicalAlias: { + auto aliasEvent = static_cast(event); + d->canonicalAlias = aliasEvent->alias(); + setObjectName(d->canonicalAlias); + qCDebug(MAIN) << "Room canonical alias updated:" << d->canonicalAlias; + emitNamesChanged = true; + break; + } + case EventType::RoomTopic: { + auto topicEvent = static_cast(event); + d->topic = topicEvent->topic(); + qCDebug(MAIN) << "Room topic updated:" << d->topic; + emit topicChanged(); + break; + } + case EventType::RoomAvatar: { + const auto& avatarEventContent = + static_cast(event)->content(); + if (d->avatar.updateUrl(avatarEventContent.url)) + { + qCDebug(MAIN) << "Room avatar URL updated:" + << avatarEventContent.url.toString(); + emit avatarChanged(); + } + break; + } + case EventType::RoomMember: { + auto memberEvent = static_cast(event); + auto u = user(memberEvent->userId()); + u->processEvent(memberEvent, this); + if (u == localUser() && memberJoinState(u) == JoinState::Invite + && memberEvent->isDirect()) + connection()->addToDirectChats(this, + user(memberEvent->senderId())); + + if( memberEvent->membership() == MembershipType::Join ) + { + if (memberJoinState(u) != JoinState::Join) + { + d->insertMemberIntoMap(u); + connect(u, &User::nameAboutToChange, this, + [=] (QString newName, QString, const Room* context) { + if (context == this) + emit memberAboutToRename(u, newName); + }); + connect(u, &User::nameChanged, this, + [=] (QString, QString oldName, const Room* context) { + if (context == this) + d->renameMember(u, oldName); + }); + emit userAdded(u); + } + } + else if( memberEvent->membership() == MembershipType::Leave ) + { + if (memberJoinState(u) == JoinState::Join) + { + if (!d->membersLeft.contains(u)) + d->membersLeft.append(u); + d->removeMemberFromMap(u->name(this), u); + emit userRemoved(u); + } + } + break; + } + case EventType::RoomEncryption: + { + d->encryptionAlgorithm = + static_cast(event)->algorithm(); + qCDebug(MAIN) << "Encryption switched on in" << displayName(); + emit encryption(); + break; + } + default: /* Ignore events of other types */; + } + } + if (emitNamesChanged) { + emit namesChanged(this); + } + d->updateDisplayname(); +} + +void Room::processEphemeralEvent(EventPtr event) +{ + QElapsedTimer et; et.start(); + switch (event->type()) + { + case EventType::Typing: { + auto typingEvent = static_cast(event.get()); + d->usersTyping.clear(); + for( const QString& userId: typingEvent->users() ) + { + auto u = user(userId); + if (memberJoinState(u) == JoinState::Join) + d->usersTyping.append(u); + } + if (!typingEvent->users().isEmpty()) + qCDebug(PROFILER) << "*** Room::processEphemeralEvent(typing):" + << typingEvent->users().size() << "users," << et; + emit typingChanged(); + break; + } + case EventType::Receipt: { + auto receiptEvent = static_cast(event.get()); + for( const auto &p: receiptEvent->eventsWithReceipts() ) + { + { + if (p.receipts.size() == 1) + qCDebug(EPHEMERAL) << "Marking" << p.evtId + << "as read for" << p.receipts[0].userId; + else + qCDebug(EPHEMERAL) << "Marking" << p.evtId + << "as read for" + << p.receipts.size() << "users"; + } + const auto newMarker = findInTimeline(p.evtId); + if (newMarker != timelineEdge()) + { + for( const Receipt& r: p.receipts ) + { + if (r.userId == connection()->userId()) + continue; // FIXME, #185 + auto u = user(r.userId); + if (memberJoinState(u) == JoinState::Join) + d->promoteReadMarker(u, newMarker); + } + } else + { + qCDebug(EPHEMERAL) << "Event" << p.evtId + << "not found; saving read receipts anyway"; + // If the event is not found (most likely, because it's too old + // and hasn't been fetched from the server yet), but there is + // a previous marker for a user, keep the previous marker. + // Otherwise, blindly store the event id for this user. + for( const Receipt& r: p.receipts ) + { + if (r.userId == connection()->userId()) + continue; // FIXME, #185 + auto u = user(r.userId); + if (memberJoinState(u) == JoinState::Join && + readMarker(u) == timelineEdge()) + d->setLastReadEvent(u, p.evtId); + } + } + } + if (!receiptEvent->eventsWithReceipts().isEmpty()) + qCDebug(PROFILER) << "*** Room::processEphemeralEvent(receipts):" + << receiptEvent->eventsWithReceipts().size() + << "events with receipts," << et; + break; + } + default: + qCWarning(EPHEMERAL) << "Unexpected event type in 'ephemeral' batch:" + << event->jsonType(); + } +} + +void Room::processAccountDataEvent(EventPtr event) +{ + switch (event->type()) + { + case EventType::Tag: + { + auto newTags = static_cast(event.get())->tags(); + if (newTags == d->tags) + break; + d->tags = newTags; + qCDebug(MAIN) << "Room" << id() << "is tagged with:" + << tagNames().join(", "); + emit tagsChanged(); + break; + } + case EventType::ReadMarker: + { + const auto* rmEvent = static_cast(event.get()); + const auto& readEventId = rmEvent->event_id(); + qCDebug(MAIN) << "Server-side read marker at" << readEventId; + d->serverReadMarker = readEventId; + const auto newMarker = findInTimeline(readEventId); + if (newMarker != timelineEdge()) + d->markMessagesAsRead(newMarker); + else { + d->setLastReadEvent(localUser(), readEventId); + } + break; + } + default: + d->accountData[event->jsonType()] = + event->contentJson().toVariantHash(); + } +} + +QString Room::Private::roomNameFromMemberNames(const QList &userlist) const +{ + // This is part 3(i,ii,iii) in the room displayname algorithm described + // in the CS spec (see also Room::Private::updateDisplayname() ). + // The spec requires to sort users lexicographically by state_key (user id) + // and use disambiguated display names of two topmost users excluding + // the current one to render the name of the room. + + // std::array is the leanest C++ container + std::array first_two = { {nullptr, nullptr} }; + std::partial_sort_copy( + userlist.begin(), userlist.end(), + first_two.begin(), first_two.end(), + [this](const User* u1, const User* u2) { + // Filter out the "me" user so that it never hits the room name + return isLocalUser(u2) || (!isLocalUser(u1) && u1->id() < u2->id()); + } + ); + + // Spec extension. A single person in the chat but not the local user + // (the local user is apparently invited). + if (userlist.size() == 1 && !isLocalUser(first_two.front())) + return tr("Invitation from %1") + .arg(q->roomMembername(first_two.front())); + + // i. One-on-one chat. first_two[1] == localUser() in this case. + if (userlist.size() == 2) + return q->roomMembername(first_two[0]); + + // ii. Two users besides the current one. + if (userlist.size() == 3) + return tr("%1 and %2") + .arg(q->roomMembername(first_two[0])) + .arg(q->roomMembername(first_two[1])); + + // iii. More users. + if (userlist.size() > 3) + return tr("%1 and %L2 others") + .arg(q->roomMembername(first_two[0])) + .arg(userlist.size() - 3); + + // userlist.size() < 2 - apparently, there's only current user in the room + return QString(); +} + +QString Room::Private::calculateDisplayname() const +{ + // CS spec, section 11.2.2.5 Calculating the display name for a room + // Numbers below refer to respective parts in the spec. + + // 1. Name (from m.room.name) + if (!name.isEmpty()) { + return name; + } + + // 2. Canonical alias + if (!canonicalAlias.isEmpty()) + return canonicalAlias; + + // 3. Room members + QString topMemberNames = roomNameFromMemberNames(membersMap.values()); + if (!topMemberNames.isEmpty()) + return topMemberNames; + + // 4. Users that previously left the room + topMemberNames = roomNameFromMemberNames(membersLeft); + if (!topMemberNames.isEmpty()) + return tr("Empty room (was: %1)").arg(topMemberNames); + + // 5. Fail miserably + return tr("Empty room (%1)").arg(id); + + // Using m.room.aliases is explicitly discouraged by the spec + //if (!aliases.empty() && !aliases.at(0).isEmpty()) + // displayname = aliases.at(0); +} + +void Room::Private::updateDisplayname() +{ + const QString old_name = displayname; + displayname = calculateDisplayname(); + if (old_name != displayname) + emit q->displaynameChanged(q); +} + +void appendStateEvent(QJsonArray& events, const QString& type, + const QJsonObject& content, const QString& stateKey = {}) +{ + if (!content.isEmpty() || !stateKey.isEmpty()) + events.append(QJsonObject + { { QStringLiteral("type"), type } + , { QStringLiteral("content"), content } + , { QStringLiteral("state_key"), stateKey } + }); +} + +#define ADD_STATE_EVENT(events, type, name, content) \ + appendStateEvent((events), QStringLiteral(type), \ + {{ QStringLiteral(name), content }}); + +void appendEvent(QJsonArray& events, const QString& type, + const QJsonObject& content) +{ + if (!content.isEmpty()) + events.append(QJsonObject + { { QStringLiteral("type"), type } + , { QStringLiteral("content"), content } + }); +} + +template +void appendEvent(QJsonArray& events, const EvtT& event) +{ + appendEvent(events, EvtT::TypeId, event.toJson()); +} + +QJsonObject Room::Private::toJson() const +{ + QElapsedTimer et; et.start(); + QJsonObject result; + { + QJsonArray stateEvents; + + ADD_STATE_EVENT(stateEvents, "m.room.name", "name", name); + ADD_STATE_EVENT(stateEvents, "m.room.topic", "topic", topic); + ADD_STATE_EVENT(stateEvents, "m.room.avatar", "url", + avatar.url().toString()); + ADD_STATE_EVENT(stateEvents, "m.room.aliases", "aliases", + QJsonArray::fromStringList(aliases)); + ADD_STATE_EVENT(stateEvents, "m.room.canonical_alias", "alias", + canonicalAlias); + ADD_STATE_EVENT(stateEvents, "m.room.encryption", "algorithm", + encryptionAlgorithm); + + for (const auto *m : membersMap) + appendStateEvent(stateEvents, QStringLiteral("m.room.member"), + { { QStringLiteral("membership"), QStringLiteral("join") } + , { QStringLiteral("displayname"), m->name(q) } + , { QStringLiteral("avatar_url"), m->avatarUrl(q).toString() } + }, m->id()); + + const auto stateObjName = joinState == JoinState::Invite ? + QStringLiteral("invite_state") : QStringLiteral("state"); + result.insert(stateObjName, + QJsonObject {{ QStringLiteral("events"), stateEvents }}); + } + + QJsonArray accountDataEvents; + if (!tags.empty()) + appendEvent(accountDataEvents, TagEvent(tags)); + + if (!serverReadMarker.isEmpty()) + appendEvent(accountDataEvents, ReadMarkerEvent(serverReadMarker)); + + if (!accountData.empty()) + { + for (auto it = accountData.begin(); it != accountData.end(); ++it) + appendEvent(accountDataEvents, it.key(), + QJsonObject::fromVariantHash(it.value())); + } + result.insert("account_data", QJsonObject {{ "events", accountDataEvents }}); + + QJsonObject unreadNotificationsObj; + + unreadNotificationsObj.insert(SyncRoomData::UnreadCountKey, unreadMessages); + if (highlightCount > 0) + unreadNotificationsObj.insert("highlight_count", highlightCount); + if (notificationCount > 0) + unreadNotificationsObj.insert("notification_count", notificationCount); + + result.insert("unread_notifications", unreadNotificationsObj); + + if (et.elapsed() > 50) + qCDebug(PROFILER) << "Room::toJson() for" << displayname << "took" << et; + + return result; +} + +QJsonObject Room::toJson() const +{ + return d->toJson(); +} + +MemberSorter Room::memberSorter() const +{ + return MemberSorter(this); +} + +bool MemberSorter::operator()(User *u1, User *u2) const +{ + return operator()(u1, room->roomMembername(u2)); +} + +bool MemberSorter::operator ()(User* u1, const QString& u2name) const +{ + auto n1 = room->roomMembername(u1); + if (n1.startsWith('@')) + n1.remove(0, 1); + auto n2 = u2name.midRef(u2name.startsWith('@') ? 1 : 0); + + return n1.localeAwareCompare(n2) < 0; +} diff --git a/lib/room.h b/lib/room.h new file mode 100644 index 00000000..bdef04ee --- /dev/null +++ b/lib/room.h @@ -0,0 +1,424 @@ +/****************************************************************************** + * Copyright (C) 2015 Felix Rohrbach + * + * 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 + */ + +#pragma once + +#include "jobs/syncjob.h" +#include "events/roommessageevent.h" +#include "events/accountdataevents.h" +#include "joinstate.h" + +#include + +#include +#include +#include + +namespace QMatrixClient +{ + class Event; + class Connection; + class User; + class MemberSorter; + class LeaveRoomJob; + class RedactEventJob; + + class TimelineItem + { + public: + // For compatibility with Qt containers, even though we use + // a std:: container now for the room timeline + using index_t = int; + + TimelineItem(RoomEventPtr&& e, index_t number) + : evt(move(e)), idx(number) { } + + RoomEvent* event() const { return evt.get(); } + RoomEvent* operator->() const { return evt.operator->(); } + index_t index() const { return idx; } + + // Used for event redaction + RoomEventPtr replaceEvent(RoomEventPtr&& other); + + private: + RoomEventPtr evt; + index_t idx; + }; + inline QDebug& operator<<(QDebug& d, const TimelineItem& ti) + { + QDebugStateSaver dss(d); + d.nospace() << "(" << ti.index() << "|" << ti->id() << ")"; + return d; + } + + class FileTransferInfo + { + Q_GADGET + Q_PROPERTY(bool active READ active CONSTANT) + Q_PROPERTY(bool completed READ completed CONSTANT) + Q_PROPERTY(bool failed READ failed CONSTANT) + Q_PROPERTY(int progress MEMBER progress CONSTANT) + Q_PROPERTY(int total MEMBER total CONSTANT) + Q_PROPERTY(QUrl localDir MEMBER localDir CONSTANT) + Q_PROPERTY(QUrl localPath MEMBER localPath CONSTANT) + public: + enum Status { None, Started, Completed, Failed }; + Status status = None; + int progress = 0; + int total = -1; + QUrl localDir { }; + QUrl localPath { }; + + bool active() const + { return status == Started || status == Completed; } + bool completed() const { return status == Completed; } + bool failed() const { return status == Failed; } + }; + + class Room: public QObject + { + Q_OBJECT + Q_PROPERTY(Connection* connection READ connection CONSTANT) + Q_PROPERTY(User* localUser READ localUser CONSTANT) + Q_PROPERTY(QString id READ id CONSTANT) + Q_PROPERTY(QString name READ name NOTIFY namesChanged) + Q_PROPERTY(QStringList aliases READ aliases NOTIFY namesChanged) + Q_PROPERTY(QString canonicalAlias READ canonicalAlias NOTIFY namesChanged) + Q_PROPERTY(QString displayName READ displayName NOTIFY namesChanged) + Q_PROPERTY(QString topic READ topic NOTIFY topicChanged) + Q_PROPERTY(QString avatarMediaId READ avatarMediaId NOTIFY avatarChanged STORED false) + Q_PROPERTY(QUrl avatarUrl READ avatarUrl NOTIFY avatarChanged) + Q_PROPERTY(bool usesEncryption READ usesEncryption NOTIFY encryption) + + Q_PROPERTY(int timelineSize READ timelineSize NOTIFY addedMessages) + Q_PROPERTY(QStringList memberNames READ memberNames NOTIFY memberListChanged) + Q_PROPERTY(int memberCount READ memberCount NOTIFY memberListChanged) + + Q_PROPERTY(bool displayed READ displayed WRITE setDisplayed NOTIFY displayedChanged) + Q_PROPERTY(QString firstDisplayedEventId READ firstDisplayedEventId WRITE setFirstDisplayedEventId NOTIFY firstDisplayedEventChanged) + Q_PROPERTY(QString lastDisplayedEventId READ lastDisplayedEventId WRITE setLastDisplayedEventId NOTIFY lastDisplayedEventChanged) + + Q_PROPERTY(QString readMarkerEventId READ readMarkerEventId WRITE markMessagesAsRead NOTIFY readMarkerMoved) + Q_PROPERTY(bool hasUnreadMessages READ hasUnreadMessages NOTIFY unreadMessagesChanged) + Q_PROPERTY(int unreadCount READ unreadCount NOTIFY unreadMessagesChanged) + Q_PROPERTY(QStringList tagNames READ tagNames NOTIFY tagsChanged) + + public: + using Timeline = std::deque; + using rev_iter_t = Timeline::const_reverse_iterator; + using timeline_iter_t = Timeline::const_iterator; + + Room(Connection* connection, QString id, JoinState initialJoinState); + ~Room() override; + + // Property accessors + + Connection* connection() const; + User* localUser() const; + const QString& id() const; + QString name() const; + QStringList aliases() const; + QString canonicalAlias() const; + QString displayName() const; + QString topic() const; + QString avatarMediaId() const; + QUrl avatarUrl() const; + Q_INVOKABLE JoinState joinState() const; + Q_INVOKABLE QList usersTyping() const; + QList membersLeft() const; + + Q_INVOKABLE QList users() const; + QStringList memberNames() const; + int memberCount() const; + int timelineSize() const; + bool usesEncryption() const; + + /** + * Returns a square room avatar with the given size and requests it + * from the network if needed + * @return a pixmap with the avatar or a placeholder if there's none + * available yet + */ + Q_INVOKABLE QImage avatar(int dimension); + /** + * Returns a room avatar with the given dimensions and requests it + * from the network if needed + * @return a pixmap with the avatar or a placeholder if there's none + * available yet + */ + Q_INVOKABLE QImage avatar(int width, int height); + + /** + * @brief Get a user object for a given user id + * This is the recommended way to get a user object in a room + * context. The actual object type may be changed in further + * versions to provide room-specific user information (display name, + * avatar etc.). + * \note The method will return a valid user regardless of + * the membership. + */ + Q_INVOKABLE User* user(const QString& userId) const; + + /** + * \brief Check the join state of a given user in this room + * + * \note Banned and invited users are not tracked for now (Leave + * will be returned for them). + * + * \return either of Join, Leave, depending on the given + * user's state in the room + */ + Q_INVOKABLE JoinState memberJoinState(User* user) const; + + /** + * @brief Produces a disambiguated name for a given user in + * the context of the room + */ + Q_INVOKABLE QString roomMembername(const User* u) const; + /** + * @brief Produces a disambiguated name for a user with this id in + * the context of the room + */ + Q_INVOKABLE QString roomMembername(const QString& userId) const; + + const Timeline& messageEvents() const; + /** + * A convenience method returning the read marker to the before-oldest + * message + */ + rev_iter_t timelineEdge() const; + Q_INVOKABLE TimelineItem::index_t minTimelineIndex() const; + Q_INVOKABLE TimelineItem::index_t maxTimelineIndex() const; + Q_INVOKABLE bool isValidIndex(TimelineItem::index_t timelineIndex) const; + + rev_iter_t findInTimeline(TimelineItem::index_t index) const; + rev_iter_t findInTimeline(const QString& evtId) const; + + bool displayed() const; + void setDisplayed(bool displayed = true); + QString firstDisplayedEventId() const; + rev_iter_t firstDisplayedMarker() const; + void setFirstDisplayedEventId(const QString& eventId); + void setFirstDisplayedEvent(TimelineItem::index_t index); + QString lastDisplayedEventId() const; + rev_iter_t lastDisplayedMarker() const; + void setLastDisplayedEventId(const QString& eventId); + void setLastDisplayedEvent(TimelineItem::index_t index); + + rev_iter_t readMarker(const User* user) const; + rev_iter_t readMarker() const; + QString readMarkerEventId() const; + /** + * @brief Mark the event with uptoEventId as read + * + * Finds in the timeline and marks as read the event with + * the specified id; also posts a read receipt to the server either + * for this message or, if it's from the local user, for + * the nearest non-local message before. uptoEventId must be non-empty. + */ + void markMessagesAsRead(QString uptoEventId); + + /** Check whether there are unread messages in the room */ + bool hasUnreadMessages() const; + + /** Get the number of unread messages in the room + * Depending on the read marker state, this call may return either + * a precise or an estimate number of unread events. Only "notable" + * events (non-redacted message events from users other than local) + * are counted. + * + * In a case when readMarker() == timelineEdge() (the local read + * marker is beyond the local timeline) only the bottom limit of + * the unread messages number can be estimated (and even that may + * be slightly off due to, e.g., redactions of events not loaded + * to the local timeline). + * + * If all messages are read, this function will return -1 (_not_ 0, + * as zero may mean "zero or more unread messages" in a situation + * when the read marker is outside the local timeline. + */ + int unreadCount() const; + + Q_INVOKABLE int notificationCount() const; + Q_INVOKABLE void resetNotificationCount(); + Q_INVOKABLE int highlightCount() const; + Q_INVOKABLE void resetHighlightCount(); + + QStringList tagNames() const; + TagsMap tags() const; + TagRecord tag(const QString& name) const; + + /** Add a new tag to this room + * If this room already has this tag, nothing happens. If it's a new + * tag for the room, the respective tag record is added to the set + * of tags and the new set is sent to the server to update other + * clients. + */ + void addTag(const QString& name, const TagRecord& record = {}); + + /** Remove a tag from the room */ + void removeTag(const QString& name); + + /** Overwrite the room's tags + * This completely replaces the existing room's tags with a set + * of new ones and updates the new set on the server. Unlike + * most other methods in Room, this one sends a signal about changes + * immediately, not waiting for confirmation from the server + * (because tags are saved in account data rather than in shared + * room state). + */ + void setTags(const TagsMap& newTags); + + /** Check whether the list of tags has m.favourite */ + bool isFavourite() const; + /** Check whether the list of tags has m.lowpriority */ + bool isLowPriority() const; + + /** Check whether this room is a direct chat */ + bool isDirectChat() const; + + /** Get the list of users this room is a direct chat with */ + QList directChatUsers() const; + + Q_INVOKABLE QUrl urlToThumbnail(const QString& eventId); + Q_INVOKABLE QUrl urlToDownload(const QString& eventId); + Q_INVOKABLE QString fileNameToDownload(const QString& eventId); + Q_INVOKABLE FileTransferInfo fileTransferInfo(const QString& id) const; + + /** Pretty-prints plain text into HTML + * This includes HTML escaping of <,>,",& and URLs linkification. + */ + QString prettyPrint(const QString& plainText) const; + + MemberSorter memberSorter() const; + + QJsonObject toJson() const; + void updateData(SyncRoomData&& data ); + void setJoinState( JoinState state ); + + public slots: + void postMessage(const QString& plainText, + MessageEventType type = MessageEventType::Text); + void postMessage(const RoomMessageEvent& event); + /** @deprecated If you have a custom event type, construct the event + * and pass it as a whole to postMessage() */ + void postMessage(const QString& type, const QString& plainText); + void setName(const QString& newName); + void setCanonicalAlias(const QString& newAlias); + void setTopic(const QString& newTopic); + + void getPreviousContent(int limit = 10); + + void inviteToRoom(const QString& memberId); + LeaveRoomJob* leaveRoom(); + void kickMember(const QString& memberId, const QString& reason = {}); + void ban(const QString& userId, const QString& reason = {}); + void unban(const QString& userId); + void redactEvent(const QString& eventId, + const QString& reason = {}); + + void uploadFile(const QString& id, const QUrl& localFilename, + const QString& overrideContentType = {}); + // If localFilename is empty a temporary file is created + void downloadFile(const QString& eventId, + const QUrl& localFilename = {}); + void cancelFileTransfer(const QString& id); + + /** Mark all messages in the room as read */ + void markAllMessagesAsRead(); + + signals: + void aboutToAddHistoricalMessages(RoomEventsRange events); + void aboutToAddNewMessages(RoomEventsRange events); + void addedMessages(); + + /** + * @brief The room name, the canonical alias or other aliases changed + * + * Not triggered when displayname changes. + */ + void namesChanged(Room* room); + /** @brief The room displayname changed */ + void displaynameChanged(Room* room); + void topicChanged(); + void avatarChanged(); + void userAdded(User* user); + void userRemoved(User* user); + void memberAboutToRename(User* user, QString newName); + void memberRenamed(User* user); + void memberListChanged(); + void encryption(); + + void joinStateChanged(JoinState oldState, JoinState newState); + void typingChanged(); + + void highlightCountChanged(Room* room); + void notificationCountChanged(Room* room); + + void displayedChanged(bool displayed); + void firstDisplayedEventChanged(); + void lastDisplayedEventChanged(); + void lastReadEventChanged(User* user); + void readMarkerMoved(); + void unreadMessagesChanged(Room* room); + + void tagsChanged(); + + void replacedEvent(const RoomEvent* newEvent, + const RoomEvent* oldEvent); + + void newFileTransfer(QString id, QUrl localFile); + void fileTransferProgress(QString id, qint64 progress, qint64 total); + void fileTransferCompleted(QString id, QUrl localFile, QUrl mxcUrl); + void fileTransferFailed(QString id, QString errorMessage = {}); + void fileTransferCancelled(QString id); + + protected: + virtual void processStateEvents(const RoomEvents& events); + virtual void processEphemeralEvent(EventPtr event); + virtual void processAccountDataEvent(EventPtr event); + virtual void onAddNewTimelineEvents(timeline_iter_t from) { } + virtual void onAddHistoricalTimelineEvents(rev_iter_t from) { } + virtual void onRedaction(const RoomEvent* prevEvent, + const RoomEvent* after) { } + + private: + class Private; + Private* d; + }; + + class MemberSorter + { + public: + explicit MemberSorter(const Room* r) : room(r) { } + + bool operator()(User* u1, User* u2) const; + bool operator()(User* u1, const QString& u2name) const; + + template + typename ContT::size_type lowerBoundIndex(const ContT& c, + const ValT& v) const + { + return std::lower_bound(c.begin(), c.end(), v, *this) - c.begin(); + } + + private: + const Room* room; + }; +} // namespace QMatrixClient +Q_DECLARE_METATYPE(QMatrixClient::FileTransferInfo) diff --git a/lib/settings.cpp b/lib/settings.cpp new file mode 100644 index 00000000..852e19cb --- /dev/null +++ b/lib/settings.cpp @@ -0,0 +1,123 @@ +#include "settings.h" + +#include "logging.h" + +#include + +using namespace QMatrixClient; + +QString Settings::legacyOrganizationName {}; +QString Settings::legacyApplicationName {}; + +void Settings::setLegacyNames(const QString& organizationName, + const QString& applicationName) +{ + legacyOrganizationName = organizationName; + legacyApplicationName = applicationName; +} + +void Settings::setValue(const QString& key, const QVariant& value) +{ +// qCDebug() << "Setting" << key << "to" << value; + QSettings::setValue(key, value); + if (legacySettings.contains(key)) + legacySettings.remove(key); +} + +QVariant Settings::value(const QString& key, const QVariant& defaultValue) const +{ + auto value = QSettings::value(key, legacySettings.value(key, defaultValue)); + // QML's Qt.labs.Settings stores boolean values as strings, which, if loaded + // through the usual QSettings interface, confuses QML + // (QVariant("false") == true in JavaScript). Since we have a mixed + // environment where both QSettings and Qt.labs.Settings may potentially + // work with same settings, better ensure compatibility. + return value.toString() == QStringLiteral("false") ? QVariant(false) : value; +} + +bool Settings::contains(const QString& key) const +{ + return QSettings::contains(key) || legacySettings.contains(key); +} + +QStringList Settings::childGroups() const +{ + auto l = QSettings::childGroups(); + return !l.isEmpty() ? l : legacySettings.childGroups(); +} + +void SettingsGroup::setValue(const QString& key, const QVariant& value) +{ + Settings::setValue(groupPath + '/' + key, value); +} + +bool SettingsGroup::contains(const QString& key) const +{ + return Settings::contains(groupPath + '/' + key); +} + +QVariant SettingsGroup::value(const QString& key, const QVariant& defaultValue) const +{ + return Settings::value(groupPath + '/' + key, defaultValue); +} + +QString SettingsGroup::group() const +{ + return groupPath; +} + +QStringList SettingsGroup::childGroups() const +{ + const_cast(this)->beginGroup(groupPath); + const_cast(legacySettings).beginGroup(groupPath); + QStringList l = Settings::childGroups(); + const_cast(this)->endGroup(); + const_cast(legacySettings).endGroup(); + return l; +} + +void SettingsGroup::remove(const QString& key) +{ + QString fullKey { groupPath }; + if (!key.isEmpty()) + fullKey += "/" + key; + Settings::remove(fullKey); +} + +QMC_DEFINE_SETTING(AccountSettings, QString, deviceId, "device_id", "", setDeviceId) +QMC_DEFINE_SETTING(AccountSettings, QString, deviceName, "device_name", "", setDeviceName) +QMC_DEFINE_SETTING(AccountSettings, bool, keepLoggedIn, "keep_logged_in", false, setKeepLoggedIn) + +QUrl AccountSettings::homeserver() const +{ + return QUrl::fromUserInput(value("homeserver").toString()); +} + +void AccountSettings::setHomeserver(const QUrl& url) +{ + setValue("homeserver", url.toString()); +} + +QString AccountSettings::userId() const +{ + return group().section('/', -1); +} + +QString AccountSettings::accessToken() const +{ + return value("access_token").toString(); +} + +void AccountSettings::setAccessToken(const QString& accessToken) +{ + qCWarning(MAIN) << "Saving access_token to QSettings is insecure." + " Developers, please save access_token separately."; + setValue("access_token", accessToken); +} + +void AccountSettings::clearAccessToken() +{ + legacySettings.remove("access_token"); + legacySettings.remove("device_id"); // Force the server to re-issue it + remove("access_token"); +} diff --git a/lib/settings.h b/lib/settings.h new file mode 100644 index 00000000..27ec9ba5 --- /dev/null +++ b/lib/settings.h @@ -0,0 +1,134 @@ +/****************************************************************************** + * Copyright (C) 2016 Kitsune Ral + * + * 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 + */ + +#pragma once + +#include +#include +#include + +class QVariant; + +namespace QMatrixClient +{ + class Settings: public QSettings + { + Q_OBJECT + public: + /** + * Use this function before creating any Settings objects in order + * to setup a read-only location where configuration has previously + * been stored. This will provide an additional fallback in case of + * renaming the organisation/application. + */ + static void setLegacyNames(const QString& organizationName, + const QString& applicationName = {}); + +#if defined(_MSC_VER) && _MSC_VER < 1900 + // VS 2013 (and probably older) aren't friends with 'using' statements + // that involve private constructors + explicit Settings(QObject* parent = 0) : QSettings(parent) { } +#else + using QSettings::QSettings; +#endif + + Q_INVOKABLE void setValue(const QString &key, + const QVariant &value); + Q_INVOKABLE QVariant value(const QString &key, + const QVariant &defaultValue = {}) const; + Q_INVOKABLE bool contains(const QString& key) const; + Q_INVOKABLE QStringList childGroups() const; + + private: + static QString legacyOrganizationName; + static QString legacyApplicationName; + + protected: + QSettings legacySettings { legacyOrganizationName, + legacyApplicationName }; + }; + + class SettingsGroup: public Settings + { + public: + template + explicit SettingsGroup(const QString& path, ArgTs... qsettingsArgs) + : Settings(qsettingsArgs...) + , groupPath(path) + { } + + Q_INVOKABLE bool contains(const QString& key) const; + Q_INVOKABLE QVariant value(const QString &key, + const QVariant &defaultValue = {}) const; + Q_INVOKABLE QString group() const; + Q_INVOKABLE QStringList childGroups() const; + Q_INVOKABLE void setValue(const QString &key, + const QVariant &value); + + Q_INVOKABLE void remove(const QString& key); + + private: + QString groupPath; + }; + +#define QMC_DECLARE_SETTING(type, propname, setter) \ + Q_PROPERTY(type propname READ propname WRITE setter) \ + public: \ + type propname() const; \ + void setter(type newValue); \ + private: + +#define QMC_DEFINE_SETTING(classname, type, propname, qsettingname, defaultValue, setter) \ +type classname::propname() const \ +{ \ + return value(QStringLiteral(qsettingname), defaultValue).value(); \ +} \ +\ +void classname::setter(type newValue) \ +{ \ + setValue(QStringLiteral(qsettingname), newValue); \ +} \ + + class AccountSettings: public SettingsGroup + { + Q_OBJECT + Q_PROPERTY(QString userId READ userId CONSTANT) + QMC_DECLARE_SETTING(QString, deviceId, setDeviceId) + QMC_DECLARE_SETTING(QString, deviceName, setDeviceName) + QMC_DECLARE_SETTING(bool, keepLoggedIn, setKeepLoggedIn) + /** \deprecated \sa setAccessToken */ + Q_PROPERTY(QString accessToken READ accessToken WRITE setAccessToken) + public: + template + explicit AccountSettings(const QString& accountId, ArgTs... qsettingsArgs) + : SettingsGroup("Accounts/" + accountId, qsettingsArgs...) + { } + + QString userId() const; + + QUrl homeserver() const; + void setHomeserver(const QUrl& url); + + /** \deprecated \sa setToken */ + QString accessToken() const; + /** \deprecated Storing accessToken in QSettings is unsafe, + * see QMatrixClient/Quaternion#181 */ + void setAccessToken(const QString& accessToken); + Q_INVOKABLE void clearAccessToken(); + }; +} // namespace QMatrixClient diff --git a/lib/user.cpp b/lib/user.cpp new file mode 100644 index 00000000..7a6dbc73 --- /dev/null +++ b/lib/user.cpp @@ -0,0 +1,399 @@ +/****************************************************************************** + * Copyright (C) 2015 Felix Rohrbach + * + * 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 + */ + +#include "user.h" + +#include "connection.h" +#include "room.h" +#include "avatar.h" +#include "events/event.h" +#include "events/roommemberevent.h" +#include "jobs/setroomstatejob.h" +#include "jobs/generated/profile.h" +#include "jobs/generated/content-repo.h" + +#include +#include +#include +#include +#include + +#include + +using namespace QMatrixClient; +using namespace std::placeholders; +using std::move; + +class User::Private +{ + public: + static Avatar makeAvatar(QUrl url) + { + static const QIcon icon + { QIcon::fromTheme(QStringLiteral("user-available")) }; + return Avatar(move(url), icon); + } + + Private(QString userId, Connection* connection) + : userId(move(userId)), connection(connection) + { } + + QString userId; + Connection* connection; + + QString bridged; + QString mostUsedName; + QMultiHash otherNames; + Avatar mostUsedAvatar { makeAvatar({}) }; + std::vector otherAvatars; + auto otherAvatar(QUrl url) + { + return std::find_if(otherAvatars.begin(), otherAvatars.end(), + [&url] (const auto& av) { return av.url() == url; }); + } + QMultiHash avatarsToRooms; + + mutable int totalRooms = 0; + + QString nameForRoom(const Room* r, const QString& hint = {}) const; + void setNameForRoom(const Room* r, QString newName, QString oldName); + QUrl avatarUrlForRoom(const Room* r, const QUrl& hint = {}) const; + void setAvatarForRoom(const Room* r, const QUrl& newUrl, + const QUrl& oldUrl); + + void setAvatarOnServer(QString contentUri, User* q); + +}; + + +QString User::Private::nameForRoom(const Room* r, const QString& hint) const +{ + // If the hint is accurate, this function is O(1) instead of O(n) + if (hint == mostUsedName || otherNames.contains(hint, r)) + return hint; + return otherNames.key(r, mostUsedName); +} + +static constexpr int MIN_JOINED_ROOMS_TO_LOG = 20; + +void User::Private::setNameForRoom(const Room* r, QString newName, + QString oldName) +{ + Q_ASSERT(oldName != newName); + Q_ASSERT(oldName == mostUsedName || otherNames.contains(oldName, r)); + if (totalRooms < 2) + { + Q_ASSERT_X(totalRooms > 0 && otherNames.empty(), __FUNCTION__, + "Internal structures inconsistency"); + mostUsedName = move(newName); + return; + } + otherNames.remove(oldName, r); + if (newName != mostUsedName) + { + // Check if the newName is about to become most used. + if (otherNames.count(newName) >= totalRooms - otherNames.size()) + { + Q_ASSERT(totalRooms > 1); + QElapsedTimer et; + if (totalRooms > MIN_JOINED_ROOMS_TO_LOG) + { + qCDebug(MAIN) << "Switching the most used name of user" << userId + << "from" << mostUsedName << "to" << newName; + qCDebug(MAIN) << "The user is in" << totalRooms << "rooms"; + et.start(); + } + + for (auto* r1: connection->roomMap()) + if (nameForRoom(r1) == mostUsedName) + otherNames.insert(mostUsedName, r1); + + mostUsedName = newName; + otherNames.remove(newName); + if (totalRooms > MIN_JOINED_ROOMS_TO_LOG) + qCDebug(PROFILER) << et << "to switch the most used name"; + } + else + otherNames.insert(newName, r); + } +} + +QUrl User::Private::avatarUrlForRoom(const Room* r, const QUrl& hint) const +{ + // If the hint is accurate, this function is O(1) instead of O(n) + if (hint == mostUsedAvatar.url() || avatarsToRooms.contains(hint, r)) + return hint; + auto it = std::find(avatarsToRooms.begin(), avatarsToRooms.end(), r); + return it == avatarsToRooms.end() ? mostUsedAvatar.url() : it.key(); +} + +void User::Private::setAvatarForRoom(const Room* r, const QUrl& newUrl, + const QUrl& oldUrl) +{ + Q_ASSERT(oldUrl != newUrl); + Q_ASSERT(oldUrl == mostUsedAvatar.url() || + avatarsToRooms.contains(oldUrl, r)); + if (totalRooms < 2) + { + Q_ASSERT_X(totalRooms > 0 && otherAvatars.empty(), __FUNCTION__, + "Internal structures inconsistency"); + mostUsedAvatar.updateUrl(newUrl); + return; + } + avatarsToRooms.remove(oldUrl, r); + if (!avatarsToRooms.contains(oldUrl)) + { + auto it = otherAvatar(oldUrl); + if (it != otherAvatars.end()) + otherAvatars.erase(it); + } + if (newUrl != mostUsedAvatar.url()) + { + // Check if the new avatar is about to become most used. + if (avatarsToRooms.count(newUrl) >= totalRooms - avatarsToRooms.size()) + { + QElapsedTimer et; + if (totalRooms > MIN_JOINED_ROOMS_TO_LOG) + { + qCDebug(MAIN) << "Switching the most used avatar of user" << userId + << "from" << mostUsedAvatar.url().toDisplayString() + << "to" << newUrl.toDisplayString(); + et.start(); + } + avatarsToRooms.remove(newUrl); + auto nextMostUsedIt = otherAvatar(newUrl); + Q_ASSERT(nextMostUsedIt != otherAvatars.end()); + std::swap(mostUsedAvatar, *nextMostUsedIt); + for (const auto* r1: connection->roomMap()) + if (avatarUrlForRoom(r1) == nextMostUsedIt->url()) + avatarsToRooms.insert(nextMostUsedIt->url(), r1); + + if (totalRooms > MIN_JOINED_ROOMS_TO_LOG) + qCDebug(PROFILER) << et << "to switch the most used avatar"; + } else { + if (otherAvatar(newUrl) == otherAvatars.end()) + otherAvatars.emplace_back(makeAvatar(newUrl)); + avatarsToRooms.insert(newUrl, r); + } + } +} + +User::User(QString userId, Connection* connection) + : QObject(connection), d(new Private(move(userId), connection)) +{ + setObjectName(userId); +} + +User::~User() = default; + +QString User::id() const +{ + return d->userId; +} + +bool User::isGuest() const +{ + Q_ASSERT(!d->userId.isEmpty() && d->userId.startsWith('@')); + auto it = std::find_if_not(d->userId.begin() + 1, d->userId.end(), + [] (QChar c) { return c.isDigit(); }); + Q_ASSERT(it != d->userId.end()); + return *it == ':'; +} + +QString User::name(const Room* room) const +{ + return d->nameForRoom(room); +} + +void User::updateName(const QString& newName, const Room* room) +{ + updateName(newName, d->nameForRoom(room), room); +} + +void User::updateName(const QString& newName, const QString& oldName, + const Room* room) +{ + Q_ASSERT(oldName == d->mostUsedName || d->otherNames.contains(oldName, room)); + if (newName != oldName) + { + emit nameAboutToChange(newName, oldName, room); + d->setNameForRoom(room, newName, oldName); + setObjectName(displayname()); + emit nameChanged(newName, oldName, room); + } +} + +void User::updateAvatarUrl(const QUrl& newUrl, const QUrl& oldUrl, + const Room* room) +{ + Q_ASSERT(oldUrl == d->mostUsedAvatar.url() || + d->avatarsToRooms.contains(oldUrl, room)); + if (newUrl != oldUrl) + { + d->setAvatarForRoom(room, newUrl, oldUrl); + setObjectName(displayname()); + emit avatarChanged(this, room); + } + +} + +void User::rename(const QString& newName) +{ + auto job = d->connection->callApi(id(), newName); + connect(job, &BaseJob::success, this, [=] { updateName(newName); }); +} + +void User::rename(const QString& newName, const Room* r) +{ + if (!r) + { + qCWarning(MAIN) << "Passing a null room to two-argument User::rename()" + "is incorrect; client developer, please fix it"; + rename(newName); + } + Q_ASSERT_X(r->memberJoinState(this) == JoinState::Join, __FUNCTION__, + "Attempt to rename a user that's not a room member"); + MemberEventContent evtC; + evtC.displayName = newName; + auto job = d->connection->callApi( + r->id(), id(), RoomMemberEvent(move(evtC))); + connect(job, &BaseJob::success, this, [=] { updateName(newName, r); }); +} + +bool User::setAvatar(const QString& fileName) +{ + return avatarObject().upload(d->connection, fileName, + std::bind(&Private::setAvatarOnServer, d.data(), _1, this)); +} + +bool User::setAvatar(QIODevice* source) +{ + return avatarObject().upload(d->connection, source, + std::bind(&Private::setAvatarOnServer, d.data(), _1, this)); +} + +void User::requestDirectChat() +{ + Q_ASSERT(d->connection); + d->connection->requestDirectChat(d->userId); +} + +void User::Private::setAvatarOnServer(QString contentUri, User* q) +{ + auto* j = connection->callApi(userId, contentUri); + connect(j, &BaseJob::success, q, + [=] { q->updateAvatarUrl(contentUri, avatarUrlForRoom(nullptr)); }); +} + +QString User::displayname(const Room* room) const +{ + auto name = d->nameForRoom(room); + return name.isEmpty() ? d->userId : + room ? room->roomMembername(name) : name; +} + +QString User::fullName(const Room* room) const +{ + auto name = d->nameForRoom(room); + return name.isEmpty() ? d->userId : name % " (" % d->userId % ')'; +} + +QString User::bridged() const +{ + return d->bridged; +} + +const Avatar& User::avatarObject(const Room* room) const +{ + auto it = d->otherAvatar(d->avatarUrlForRoom(room)); + return it != d->otherAvatars.end() ? *it : d->mostUsedAvatar; +} + +QImage User::avatar(int dimension, const Room* room) +{ + return avatar(dimension, dimension, room); +} + +QImage User::avatar(int width, int height, const Room* room) +{ + return avatar(width, height, room, []{}); +} + +QImage User::avatar(int width, int height, const Room* room, + Avatar::get_callback_t callback) +{ + return avatarObject(room).get(d->connection, width, height, + [=] { emit avatarChanged(this, room); callback(); }); +} + +QString User::avatarMediaId(const Room* room) const +{ + return avatarObject(room).mediaId(); +} + +QUrl User::avatarUrl(const Room* room) const +{ + return avatarObject(room).url(); +} + +void User::processEvent(RoomMemberEvent* event, const Room* room) +{ + if (event->membership() != MembershipType::Invite && + event->membership() != MembershipType::Join) + return; + + auto aboutToEnter = room->memberJoinState(this) == JoinState::Leave && + (event->membership() == MembershipType::Join || + event->membership() == MembershipType::Invite); + if (aboutToEnter) + ++d->totalRooms; + + auto newName = event->displayName(); + // `bridged` value uses the same notification signal as the name; + // it is assumed that first setting of the bridge occurs together with + // the first setting of the name, and further bridge updates are + // exceptionally rare (the only reasonable case being that the bridge + // changes the naming convention). For the same reason room-specific + // bridge tags are not supported at all. + QRegularExpression reSuffix(" \\((IRC|Gitter|Telegram)\\)$"); + auto match = reSuffix.match(newName); + if (match.hasMatch()) + { + if (d->bridged != match.captured(1)) + { + if (!d->bridged.isEmpty()) + qCWarning(MAIN) << "Bridge for user" << id() << "changed:" + << d->bridged << "->" << match.captured(1); + d->bridged = match.captured(1); + } + newName.truncate(match.capturedStart(0)); + } + if (event->prevContent()) + { + // FIXME: the hint doesn't work for bridged users + auto oldNameHint = + d->nameForRoom(room, event->prevContent()->displayName); + updateName(event->displayName(), oldNameHint, room); + updateAvatarUrl(event->avatarUrl(), + d->avatarUrlForRoom(room, event->prevContent()->avatarUrl), + room); + } else { + updateName(event->displayName(), room); + updateAvatarUrl(event->avatarUrl(), d->avatarUrlForRoom(room), room); + } +} diff --git a/lib/user.h b/lib/user.h new file mode 100644 index 00000000..f76f9e0a --- /dev/null +++ b/lib/user.h @@ -0,0 +1,125 @@ +/****************************************************************************** + * Copyright (C) 2015 Felix Rohrbach + * + * 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 + */ + +#pragma once + +#include +#include +#include "avatar.h" + +namespace QMatrixClient +{ + class Connection; + class Room; + class RoomMemberEvent; + + class User: public QObject + { + Q_OBJECT + Q_PROPERTY(QString id READ id CONSTANT) + Q_PROPERTY(bool isGuest READ isGuest CONSTANT) + Q_PROPERTY(QString name READ name NOTIFY nameChanged) + Q_PROPERTY(QString displayName READ displayname NOTIFY nameChanged STORED false) + Q_PROPERTY(QString fullName READ fullName NOTIFY nameChanged STORED false) + Q_PROPERTY(QString bridgeName READ bridged NOTIFY nameChanged STORED false) + Q_PROPERTY(QString avatarMediaId READ avatarMediaId NOTIFY avatarChanged STORED false) + Q_PROPERTY(QUrl avatarUrl READ avatarUrl NOTIFY avatarChanged) + public: + User(QString userId, Connection* connection); + ~User() override; + + /** Get unique stable user id + * User id is generated by the server and is not changed ever. + */ + QString id() const; + + /** Get the name chosen by the user + * This may be empty if the user didn't choose the name or cleared + * it. + * \sa displayName + */ + QString name(const Room* room = nullptr) const; + + /** Get the displayed user name + * This method returns the result of name() if its non-empty; + * otherwise it returns user id. This is convenient to show a user + * name outside of a room context. In a room context, user names + * should be disambiguated. + * \sa name, id, fullName Room::roomMembername + */ + QString displayname(const Room* room = nullptr) const; + + /** Get user name and id in one string + * The constructed string follows the format 'name (id)' + * used for users disambiguation in a room context and in other + * places. + * \sa displayName, Room::roomMembername + */ + QString fullName(const Room* room = nullptr) const; + + /** + * Returns the name of bridge the user is connected from or empty. + */ + QString bridged() const; + + /** Whether the user is a guest + * As of now, the function relies on the convention used in Synapse + * that guests and only guests have all-numeric IDs. This may or + * may not work with non-Synapse servers. + */ + bool isGuest() const; + + const Avatar& avatarObject(const Room* room = nullptr) const; + Q_INVOKABLE QImage avatar(int dimension, const Room* room = nullptr); + Q_INVOKABLE QImage avatar(int requestedWidth, int requestedHeight, + const Room* room = nullptr); + QImage avatar(int width, int height, const Room* room, + Avatar::get_callback_t callback); + + QString avatarMediaId(const Room* room = nullptr) const; + QUrl avatarUrl(const Room* room = nullptr) const; + + void processEvent(RoomMemberEvent* event, const Room* r = nullptr); + + public slots: + void rename(const QString& newName); + void rename(const QString& newName, const Room* r); + bool setAvatar(const QString& fileName); + bool setAvatar(QIODevice* source); + void requestDirectChat(); + + signals: + void nameAboutToChange(QString newName, QString oldName, + const Room* roomContext); + void nameChanged(QString newName, QString oldName, + const Room* roomContext); + void avatarChanged(User* user, const Room* roomContext); + + private slots: + void updateName(const QString& newName, const Room* room = nullptr); + void updateName(const QString& newName, const QString& oldName, + const Room* room = nullptr); + void updateAvatarUrl(const QUrl& newUrl, const QUrl& oldUrl, + const Room* room = nullptr); + + private: + class Private; + QScopedPointer d; + }; +} +Q_DECLARE_METATYPE(QMatrixClient::User*) diff --git a/lib/util.h b/lib/util.h new file mode 100644 index 00000000..65de0610 --- /dev/null +++ b/lib/util.h @@ -0,0 +1,205 @@ +/****************************************************************************** + * Copyright (C) 2016 Kitsune Ral + * + * 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 + */ + +#pragma once + +#include +#include + +#include + +namespace QMatrixClient +{ + /** + * @brief Lookup a value by a key in a varargs list + * + * This function template takes the value of its first argument (selector) + * as a key and searches for it in the key-value map passed in + * a parameter pack (every next pair of arguments forms a key-value pair). + * If a match is found, the respective value is returned; if no pairs + * matched, the last value (fallback) is returned. + * + * All options should be of the same type or implicitly castable to the + * type of the first option. If you need some specific type to cast to + * you can explicitly provide it as the ValueT template parameter + * (e.g. lookup(parameters...)). Note that pointers + * to methods of different classes and even to functions with different + * signatures are of different types. If their return types are castable + * to some common one, @see dispatch that deals with this by swallowing + * the method invocation. + * + * Below is an example of usage to select a parser depending on contents of + * a JSON object: + * {@code + * auto parser = lookup(obj.value["type"].toString(), + * "type1", fn1, + * "type2", fn2, + * fallbackFn); + * parser(obj); + * } + * + * The implementation is based on tail recursion; every recursion step + * removes 2 arguments (match and value). There's no selector value for the + * fallback option (the last one); therefore, the total number of lookup() + * arguments should be even: selector + n key-value pairs + fallback + * + * @note Beware of calling lookup() with a const char* selector + * (the first parameter) - most likely it won't do what you expect because + * of shallow comparison. + */ + template + ValueT lookup(SelectorT/*unused*/, ValueT&& fallback) + { + return std::forward(fallback); + } + + template + ValueT lookup(SelectorT&& selector, KeyT&& key, ValueT&& value, Ts&&... remainder) + { + if( selector == key ) + return std::forward(value); + + // Drop the failed key-value pair and recurse with 2 arguments less. + return lookup(std::forward(selector), + std::forward(remainder)...); + } + + /** + * A wrapper around lookup() for functions of different types castable + * to a common std::function<> form + * + * This class uses std::function<> magic to first capture arguments of + * a yet-unknown function or function object, and then to coerce types of + * all functions/function objects passed for lookup to the type + * std::function, you would have + * to pass the specific function type to lookup, since your functions have + * different signatures. The type is not always obvious, and the resulting + * construct in client code would almost always be rather cumbersome. + * Dispatch<> deduces the necessary function type (well, almost - you still + * have to specify the result type) and hides the clumsiness. For more + * information on what std::function<> can wrap around, see + * https://cpptruths.blogspot.jp/2015/11/covariance-and-contravariance-in-c.html + * + * The function arguments are captured by value (i.e. copied) to avoid + * hard-to-find issues with dangling references in cases when a Dispatch<> + * object is passed across different contexts (e.g. returned from another + * function). + * + * \tparam ResultT - the desired type of a picked function invocation (mandatory) + * \tparam ArgTs - function argument types (deduced) + */ +#if __GNUC__ < 5 && __GNUC_MINOR__ < 9 + // GCC 4.8 cannot cope with parameter packs inside lambdas; so provide a single + // argument version of Dispatch<> that we only need so far. + template +#else + template +#endif + class Dispatch + { + // The implementation takes a chapter from functional programming: + // Dispatch<> uses a function that in turn accepts a function as its + // argument. The sole purpose of the outer function (initialized by + // a lambda-expression in the constructor) is to store the arguments + // to any of the functions later looked up. The inner function (its + // type is defined by fn_t alias) is the one returned by lookup() + // invocation inside to(). + // + // It's a bit counterintuitive to specify function parameters before + // the list of functions but otherwise it would take several overloads + // here to match all the ways a function-like behaviour can be done: + // reference-to-function, pointer-to-function, function object. This + // probably could be done as well but I preferred a more compact + // solution: you show what you have and if it's possible to bring all + // your functions to the same std::function<> based on what you have + // as parameters, the code will compile. If it's not possible, modern + // compilers are already good enough at pinpointing a specific place + // where types don't match. + public: +#if __GNUC__ < 5 && __GNUC_MINOR__ < 9 + using fn_t = std::function; + explicit Dispatch(ArgT&& arg) + : boundArgs([=](fn_t &&f) { return f(std::move(arg)); }) + { } +#else + using fn_t = std::function; + explicit Dispatch(ArgTs&&... args) + : boundArgs([=](fn_t &&f) { return f(std::move(args)...); }) + { } +#endif + + template + ResultT to(LookupParamTs&&... lookupParams) + { + // Here's the magic, two pieces of it: + // 1. Specifying fn_t in lookup() wraps all functions in + // \p lookupParams into the same std::function<> type. This + // includes conversion of return types from more specific to more + // generic (because std::function is covariant by return types and + // contravariant by argument types (see the link in the Doxygen + // part of the comments). + auto fn = lookup(std::forward(lookupParams)...); + // 2. Passing the result of lookup() to boundArgs() invokes the + // lambda-expression mentioned in the constructor, which simply + // invokes this passed function with a set of arguments captured + // by lambda. + if (fn) + return boundArgs(std::move(fn)); + + // A shortcut to allow passing nullptr for a function; + // a default-constructed ResultT will be returned + // (for pointers, it will be nullptr) + return {}; + } + + private: + std::function boundArgs; + }; + + /** + * Dispatch a set of parameters to one of a set of functions, depending on + * a selector value + * + * Use dispatch(parameters).to(lookup parameters) + * instead of lookup() if you need to pick one of several functions returning + * types castable to the same CommonType. See event.cpp for a typical use case. + * + * \see Dispatch + */ + template + Dispatch dispatch(ArgTs&& ... args) + { + return Dispatch(std::forward(args)...); + } + + // The below enables pretty-printing of enums in logs +#if (QT_VERSION >= QT_VERSION_CHECK(5, 5, 0)) +#define REGISTER_ENUM(EnumName) Q_ENUM(EnumName) +#else + // Thanks to Olivier for spelling it and for making Q_ENUM to replace it: + // https://woboq.com/blog/q_enum.html +#define REGISTER_ENUM(EnumName) \ + Q_ENUMS(EnumName) \ + friend QDebug operator<<(QDebug dbg, EnumName val) \ + { \ + static int enumIdx = staticMetaObject.indexOfEnumerator(#EnumName); \ + return dbg << Event::staticMetaObject.enumerator(enumIdx).valueToKey(int(val)); \ + } +#endif +} // namespace QMatrixClient + diff --git a/libqmatrixclient.pri b/libqmatrixclient.pri index 144c9dbc..646af4b1 100644 --- a/libqmatrixclient.pri +++ b/libqmatrixclient.pri @@ -1,5 +1,5 @@ QT += network -CONFIG += c++14 warn_on rtti_off +CONFIG += c++14 warn_on rtti_off create_prl win32-msvc* { QMAKE_CXXFLAGS_WARN_ON += -wd4100 @@ -7,72 +7,72 @@ win32-msvc* { QMAKE_CXXFLAGS_WARN_ON += -Wno-unused-parameter } -INCLUDEPATH += $$PWD +INCLUDEPATH += $$PWD/lib HEADERS += \ - $$PWD/connectiondata.h \ - $$PWD/connection.h \ - $$PWD/room.h \ - $$PWD/user.h \ - $$PWD/avatar.h \ - $$PWD/util.h \ - $$PWD/events/event.h \ - $$PWD/events/eventcontent.h \ - $$PWD/events/roommessageevent.h \ - $$PWD/events/simplestateevents.h \ - $$PWD/events/roommemberevent.h \ - $$PWD/events/roomavatarevent.h \ - $$PWD/events/typingevent.h \ - $$PWD/events/receiptevent.h \ - $$PWD/events/accountdataevents.h \ - $$PWD/events/directchatevent.h \ - $$PWD/events/redactionevent.h \ - $$PWD/jobs/requestdata.h \ - $$PWD/jobs/basejob.h \ - $$PWD/jobs/checkauthmethods.h \ - $$PWD/jobs/passwordlogin.h \ - $$PWD/jobs/sendeventjob.h \ - $$PWD/jobs/postreceiptjob.h \ - $$PWD/jobs/joinroomjob.h \ - $$PWD/jobs/roommessagesjob.h \ - $$PWD/jobs/syncjob.h \ - $$PWD/jobs/mediathumbnailjob.h \ - $$PWD/jobs/setroomstatejob.h \ - $$files($$PWD/jobs/generated/*.h, false) \ - $$PWD/logging.h \ - $$PWD/settings.h \ - $$PWD/networksettings.h \ - $$PWD/networkaccessmanager.h \ - $$PWD/jobs/downloadfilejob.h \ - $$PWD/jobs/postreadmarkersjob.h + $$PWD/lib/connectiondata.h \ + $$PWD/lib/connection.h \ + $$PWD/lib/room.h \ + $$PWD/lib/user.h \ + $$PWD/lib/avatar.h \ + $$PWD/lib/util.h \ + $$PWD/lib/events/event.h \ + $$PWD/lib/events/eventcontent.h \ + $$PWD/lib/events/roommessageevent.h \ + $$PWD/lib/events/simplestateevents.h \ + $$PWD/lib/events/roommemberevent.h \ + $$PWD/lib/events/roomavatarevent.h \ + $$PWD/lib/events/typingevent.h \ + $$PWD/lib/events/receiptevent.h \ + $$PWD/lib/events/accountdataevents.h \ + $$PWD/lib/events/directchatevent.h \ + $$PWD/lib/events/redactionevent.h \ + $$PWD/lib/jobs/requestdata.h \ + $$PWD/lib/jobs/basejob.h \ + $$PWD/lib/jobs/checkauthmethods.h \ + $$PWD/lib/jobs/passwordlogin.h \ + $$PWD/lib/jobs/sendeventjob.h \ + $$PWD/lib/jobs/postreceiptjob.h \ + $$PWD/lib/jobs/joinroomjob.h \ + $$PWD/lib/jobs/roommessagesjob.h \ + $$PWD/lib/jobs/syncjob.h \ + $$PWD/lib/jobs/mediathumbnailjob.h \ + $$PWD/lib/jobs/setroomstatejob.h \ + $$PWD/lib/jobs/downloadfilejob.h \ + $$PWD/lib/jobs/postreadmarkersjob.h \ + $$files($$PWD/lib/jobs/generated/*.h, false) \ + $$PWD/lib/logging.h \ + $$PWD/lib/settings.h \ + $$PWD/lib/networksettings.h \ + $$PWD/lib/networkaccessmanager.h SOURCES += \ - $$PWD/connectiondata.cpp \ - $$PWD/connection.cpp \ - $$PWD/room.cpp \ - $$PWD/user.cpp \ - $$PWD/avatar.cpp \ - $$PWD/events/event.cpp \ - $$PWD/events/eventcontent.cpp \ - $$PWD/events/roommessageevent.cpp \ - $$PWD/events/roommemberevent.cpp \ - $$PWD/events/typingevent.cpp \ - $$PWD/events/receiptevent.cpp \ - $$PWD/events/directchatevent.cpp \ - $$PWD/jobs/requestdata.cpp \ - $$PWD/jobs/basejob.cpp \ - $$PWD/jobs/checkauthmethods.cpp \ - $$PWD/jobs/passwordlogin.cpp \ - $$PWD/jobs/sendeventjob.cpp \ - $$PWD/jobs/postreceiptjob.cpp \ - $$PWD/jobs/joinroomjob.cpp \ - $$PWD/jobs/roommessagesjob.cpp \ - $$PWD/jobs/syncjob.cpp \ - $$PWD/jobs/mediathumbnailjob.cpp \ - $$PWD/jobs/setroomstatejob.cpp \ - $$files($$PWD/jobs/generated/*.cpp, false) \ - $$PWD/logging.cpp \ - $$PWD/settings.cpp \ - $$PWD/networksettings.cpp \ - $$PWD/networkaccessmanager.cpp \ - $$PWD/jobs/downloadfilejob.cpp + $$PWD/lib/connectiondata.cpp \ + $$PWD/lib/connection.cpp \ + $$PWD/lib/room.cpp \ + $$PWD/lib/user.cpp \ + $$PWD/lib/avatar.cpp \ + $$PWD/lib/events/event.cpp \ + $$PWD/lib/events/eventcontent.cpp \ + $$PWD/lib/events/roommessageevent.cpp \ + $$PWD/lib/events/roommemberevent.cpp \ + $$PWD/lib/events/typingevent.cpp \ + $$PWD/lib/events/receiptevent.cpp \ + $$PWD/lib/events/directchatevent.cpp \ + $$PWD/lib/jobs/requestdata.cpp \ + $$PWD/lib/jobs/basejob.cpp \ + $$PWD/lib/jobs/checkauthmethods.cpp \ + $$PWD/lib/jobs/passwordlogin.cpp \ + $$PWD/lib/jobs/sendeventjob.cpp \ + $$PWD/lib/jobs/postreceiptjob.cpp \ + $$PWD/lib/jobs/joinroomjob.cpp \ + $$PWD/lib/jobs/roommessagesjob.cpp \ + $$PWD/lib/jobs/syncjob.cpp \ + $$PWD/lib/jobs/mediathumbnailjob.cpp \ + $$PWD/lib/jobs/setroomstatejob.cpp \ + $$PWD/lib/jobs/downloadfilejob.cpp \ + $$files($$PWD/lib/jobs/generated/*.cpp, false) \ + $$PWD/lib/logging.cpp \ + $$PWD/lib/settings.cpp \ + $$PWD/lib/networksettings.cpp \ + $$PWD/lib/networkaccessmanager.cpp diff --git a/logging.cpp b/logging.cpp deleted file mode 100644 index 7476781f..00000000 --- a/logging.cpp +++ /dev/null @@ -1,33 +0,0 @@ -/****************************************************************************** - * Copyright (C) 2017 Elvis Angelaccio - * - * 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 - */ - -#include "logging.h" - -#if QT_VERSION >= QT_VERSION_CHECK(5, 5, 0) -#define LOGGING_CATEGORY(Name, Id) Q_LOGGING_CATEGORY((Name), (Id), QtInfoMsg) -#else -#define LOGGING_CATEGORY(Name, Id) Q_LOGGING_CATEGORY((Name), (Id)) -#endif - -// Use LOGGING_CATEGORY instead of Q_LOGGING_CATEGORY in the rest of the code -LOGGING_CATEGORY(MAIN, "libqmatrixclient.main") -LOGGING_CATEGORY(PROFILER, "libqmatrixclient.profiler") -LOGGING_CATEGORY(EVENTS, "libqmatrixclient.events") -LOGGING_CATEGORY(EPHEMERAL, "libqmatrixclient.events.ephemeral") -LOGGING_CATEGORY(JOBS, "libqmatrixclient.jobs") -LOGGING_CATEGORY(SYNCJOB, "libqmatrixclient.jobs.sync") diff --git a/logging.h b/logging.h deleted file mode 100644 index 8dbfdf30..00000000 --- a/logging.h +++ /dev/null @@ -1,78 +0,0 @@ -/****************************************************************************** - * Copyright (C) 2017 Kitsune Ral - * - * 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 - */ - -#pragma once - -#include -#include - -Q_DECLARE_LOGGING_CATEGORY(MAIN) -Q_DECLARE_LOGGING_CATEGORY(PROFILER) -Q_DECLARE_LOGGING_CATEGORY(EVENTS) -Q_DECLARE_LOGGING_CATEGORY(EPHEMERAL) -Q_DECLARE_LOGGING_CATEGORY(JOBS) -Q_DECLARE_LOGGING_CATEGORY(SYNCJOB) - -namespace QMatrixClient -{ - // QDebug manipulators - - using QDebugManip = QDebug (*)(QDebug); - - /** - * @brief QDebug manipulator to setup the stream for JSON output - * - * Originally made to encapsulate the change in QDebug behavior in Qt 5.4 - * and the respective addition of QDebug::noquote(). - * Together with the operator<<() helper, the proposed usage is - * (similar to std:: I/O manipulators): - * - * @example qCDebug() << formatJson << json_object; // (QJsonObject, etc.) - */ - inline QDebug formatJson(QDebug debug_object) - { -#if QT_VERSION < QT_VERSION_CHECK(5, 4, 0) - return debug_object; -#else - return debug_object.noquote(); -#endif - } - - /** - * @brief A helper operator to facilitate usage of formatJson (and possibly - * other manipulators) - * - * @param debug_object to output the json to - * @param qdm a QDebug manipulator - * @return a copy of debug_object that has its mode altered by qdm - */ - inline QDebug operator<< (QDebug debug_object, QDebugManip qdm) - { - return qdm(debug_object); - } -} - -inline QDebug operator<< (QDebug debug_object, const QElapsedTimer& et) -{ - auto val = et.nsecsElapsed() / 1000; - if (val < 1000) - debug_object << val << "µs"; - else - debug_object << val / 1000 << "ms"; - return debug_object; -} diff --git a/networkaccessmanager.cpp b/networkaccessmanager.cpp deleted file mode 100644 index 89967a8a..00000000 --- a/networkaccessmanager.cpp +++ /dev/null @@ -1,75 +0,0 @@ -/****************************************************************************** - * Copyright (C) 2018 Kitsune Ral - * - * 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 - */ - -#include "networkaccessmanager.h" - -#include -#include - -using namespace QMatrixClient; - -class NetworkAccessManager::Private -{ - public: - QList ignoredSslErrors; -}; - -NetworkAccessManager::NetworkAccessManager(QObject* parent) : d(std::make_unique()) -{ } - -QList NetworkAccessManager::ignoredSslErrors() const -{ - return d->ignoredSslErrors; -} - -void NetworkAccessManager::addIgnoredSslError(const QSslError& error) -{ - d->ignoredSslErrors << error; -} - -void NetworkAccessManager::clearIgnoredSslErrors() -{ - d->ignoredSslErrors.clear(); -} - -static NetworkAccessManager* createNam() -{ - auto nam = new NetworkAccessManager(QCoreApplication::instance()); - // See #109. Once Qt bearer management gets better, this workaround - // should become unnecessary. - nam->connect(nam, &QNetworkAccessManager::networkAccessibleChanged, - [nam] { nam->setNetworkAccessible(QNetworkAccessManager::Accessible); }); - return nam; -} - -NetworkAccessManager* NetworkAccessManager::instance() -{ - static auto* nam = createNam(); - return nam; -} - -NetworkAccessManager::~NetworkAccessManager() = default; - -QNetworkReply* NetworkAccessManager::createRequest(Operation op, - const QNetworkRequest& request, QIODevice* outgoingData) -{ - auto reply = - QNetworkAccessManager::createRequest(op, request, outgoingData); - reply->ignoreSslErrors(d->ignoredSslErrors); - return reply; -} diff --git a/networkaccessmanager.h b/networkaccessmanager.h deleted file mode 100644 index ae847582..00000000 --- a/networkaccessmanager.h +++ /dev/null @@ -1,49 +0,0 @@ -/****************************************************************************** - * Copyright (C) 2018 Kitsune Ral - * - * 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 - */ - -#pragma once - -#include - -#include - -namespace QMatrixClient -{ - class NetworkAccessManager : public QNetworkAccessManager - { - Q_OBJECT - public: - NetworkAccessManager(QObject* parent = nullptr); - ~NetworkAccessManager() override; - - QList ignoredSslErrors() const; - void addIgnoredSslError(const QSslError& error); - void clearIgnoredSslErrors(); - - /** Get a pointer to the singleton */ - static NetworkAccessManager* instance(); - - private: - QNetworkReply * createRequest(Operation op, - const QNetworkRequest &request, - QIODevice *outgoingData = Q_NULLPTR) override; - - class Private; - std::unique_ptr d; - }; -} // namespace QMatrixClient diff --git a/networksettings.cpp b/networksettings.cpp deleted file mode 100644 index 48bd09f3..00000000 --- a/networksettings.cpp +++ /dev/null @@ -1,31 +0,0 @@ -/****************************************************************************** - * Copyright (C) 2017 Kitsune Ral - * - * 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 - */ - -#include "networksettings.h" - -using namespace QMatrixClient; - -void NetworkSettings::setupApplicationProxy() const -{ - QNetworkProxy::setApplicationProxy( - { proxyType(), proxyHostName(), proxyPort() }); -} - -QMC_DEFINE_SETTING(NetworkSettings, QNetworkProxy::ProxyType, proxyType, "proxy_type", QNetworkProxy::DefaultProxy, setProxyType) -QMC_DEFINE_SETTING(NetworkSettings, QString, proxyHostName, "proxy_hostname", "", setProxyHostName) -QMC_DEFINE_SETTING(NetworkSettings, quint16, proxyPort, "proxy_port", -1, setProxyPort) diff --git a/networksettings.h b/networksettings.h deleted file mode 100644 index 83613060..00000000 --- a/networksettings.h +++ /dev/null @@ -1,44 +0,0 @@ -/****************************************************************************** - * Copyright (C) 2017 Kitsune Ral - * - * 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 - */ - -#pragma once - -#include "settings.h" - -#include - -Q_DECLARE_METATYPE(QNetworkProxy::ProxyType) - -namespace QMatrixClient { - class NetworkSettings: public SettingsGroup - { - Q_OBJECT - QMC_DECLARE_SETTING(QNetworkProxy::ProxyType, proxyType, setProxyType) - QMC_DECLARE_SETTING(QString, proxyHostName, setProxyHostName) - QMC_DECLARE_SETTING(quint16, proxyPort, setProxyPort) - Q_PROPERTY(QString proxyHost READ proxyHostName WRITE setProxyHostName) - public: - template - explicit NetworkSettings(ArgTs... qsettingsArgs) - : SettingsGroup(QStringLiteral("Network"), qsettingsArgs...) - { } - ~NetworkSettings() override = default; - - Q_INVOKABLE void setupApplicationProxy() const; - }; -} diff --git a/room.cpp b/room.cpp deleted file mode 100644 index 25669889..00000000 --- a/room.cpp +++ /dev/null @@ -1,1851 +0,0 @@ -/****************************************************************************** - * Copyright (C) 2015 Felix Rohrbach - * - * 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 - */ - -#include "room.h" - -#include "jobs/generated/kicking.h" -#include "jobs/generated/inviting.h" -#include "jobs/generated/banning.h" -#include "jobs/generated/leaving.h" -#include "jobs/generated/receipts.h" -#include "jobs/generated/redaction.h" -#include "jobs/generated/account-data.h" -#include "jobs/setroomstatejob.h" -#include "events/simplestateevents.h" -#include "events/roomavatarevent.h" -#include "events/roommemberevent.h" -#include "events/typingevent.h" -#include "events/receiptevent.h" -#include "events/redactionevent.h" -#include "jobs/sendeventjob.h" -#include "jobs/roommessagesjob.h" -#include "jobs/mediathumbnailjob.h" -#include "jobs/downloadfilejob.h" -#include "jobs/postreadmarkersjob.h" -#include "avatar.h" -#include "connection.h" -#include "user.h" - -#include -#include // for efficient string concats (operator%) -#include -#include -#include -#include -#include - -#include -#include -#include - -using namespace QMatrixClient; -using namespace std::placeholders; -#if !(defined __GLIBCXX__ && __GLIBCXX__ <= 20150123) -using std::llround; -#endif - -enum EventsPlacement : int { Older = -1, Newer = 1 }; - -// A workaround for MSVC 2015 that fails with "error C2440: 'return': -// cannot convert from 'initializer list' to 'QMatrixClient::FileTransferInfo'" -#if (defined(_MSC_VER) && _MSC_VER < 1910) || (defined(__GNUC__) && __GNUC__ <= 4) -# define WORKAROUND_EXTENDED_INITIALIZER_LIST -#endif - -class Room::Private -{ - public: - /** Map of user names to users. User names potentially duplicate, hence a multi-hashmap. */ - typedef QMultiHash members_map_t; - - Private(Connection* c, QString id_, JoinState initialJoinState) - : q(nullptr), connection(c), id(std::move(id_)) - , joinState(initialJoinState) - { } - - Room* q; - - // This updates the room displayname field (which is the way a room - // should be shown in the room list) It should be called whenever the - // list of members or the room name (m.room.name) or canonical alias change. - void updateDisplayname(); - - Connection* connection; - Timeline timeline; - QHash eventsIndex; - QString id; - QStringList aliases; - QString canonicalAlias; - QString name; - QString displayname; - QString topic; - QString encryptionAlgorithm; - Avatar avatar; - JoinState joinState; - int highlightCount = 0; - int notificationCount = 0; - members_map_t membersMap; - QList usersTyping; - QList membersLeft; - int unreadMessages = 0; - bool displayed = false; - QString firstDisplayedEventId; - QString lastDisplayedEventId; - QHash lastReadEventIds; - QString serverReadMarker; - TagsMap tags; - QHash accountData; - QString prevBatch; - QPointer roomMessagesJob; - - struct FileTransferPrivateInfo - { -#ifdef WORKAROUND_EXTENDED_INITIALIZER_LIST - FileTransferPrivateInfo() = default; - FileTransferPrivateInfo(BaseJob* j, QString fileName) - : job(j), localFileInfo(fileName) - { } -#endif - QPointer job = nullptr; - QFileInfo localFileInfo { }; - FileTransferInfo::Status status = FileTransferInfo::Started; - qint64 progress = 0; - qint64 total = -1; - - void update(qint64 p, qint64 t) - { - if (t == 0) - { - t = -1; - if (p == 0) - p = -1; - } - if (p != -1) - qCDebug(PROFILER) << "Transfer progress:" << p << "/" << t - << "=" << llround(double(p) / t * 100) << "%"; - progress = p; total = t; - } - }; - void failedTransfer(const QString& tid, const QString& errorMessage = {}) - { - qCWarning(MAIN) << "File transfer failed for id" << tid; - if (!errorMessage.isEmpty()) - qCWarning(MAIN) << "Message:" << errorMessage; - fileTransfers[tid].status = FileTransferInfo::Failed; - emit q->fileTransferFailed(tid, errorMessage); - } - // A map from event/txn ids to information about the long operation; - // used for both download and upload operations - QHash fileTransfers; - - const RoomMessageEvent* getEventWithFile(const QString& eventId) const; - QString fileNameToDownload(const RoomMessageEvent* event) const; - - //void inviteUser(User* u); // We might get it at some point in time. - void insertMemberIntoMap(User* u); - void renameMember(User* u, QString oldName); - void removeMemberFromMap(const QString& username, User* u); - - void getPreviousContent(int limit = 10); - - bool isEventNotable(const TimelineItem& ti) const - { - return !ti->isRedacted() && - ti->senderId() != connection->userId() && - ti->type() == EventType::RoomMessage; - } - - void addNewMessageEvents(RoomEvents&& events); - void addHistoricalMessageEvents(RoomEvents&& events); - - /** - * @brief Move events into the timeline - * - * Insert events into the timeline, either new or historical. - * Pointers in the original container become empty, the ownership - * is passed to the timeline container. - * @param events - the range of events to be inserted - * @param placement - position and direction of insertion: Older for - * historical messages, Newer for new ones - */ - Timeline::size_type insertEvents(RoomEventsRange&& events, - EventsPlacement placement); - - /** - * Removes events from the passed container that are already in the timeline - */ - void dropDuplicateEvents(RoomEvents* events) const; - - void setLastReadEvent(User* u, const QString& eventId); - void updateUnreadCount(rev_iter_t from, rev_iter_t to); - void promoteReadMarker(User* u, rev_iter_t newMarker, - bool force = false); - - void markMessagesAsRead(rev_iter_t upToMarker); - - /** - * @brief Apply redaction to the timeline - * - * Tries to find an event in the timeline and redact it; deletes the - * redaction event whether the redacted event was found or not. - */ - void processRedaction(RoomEventPtr redactionEvent); - - void broadcastTagUpdates() - { - connection->callApi( - connection->userId(), id, TagEvent::typeId(), - TagEvent(tags).toJson()); - emit q->tagsChanged(); - } - - QJsonObject toJson() const; - - private: - QString calculateDisplayname() const; - QString roomNameFromMemberNames(const QList& userlist) const; - - bool isLocalUser(const User* u) const - { - return u == q->localUser(); - } -}; - -RoomEventPtr TimelineItem::replaceEvent(RoomEventPtr&& other) -{ - return std::exchange(evt, std::move(other)); -} - -Room::Room(Connection* connection, QString id, JoinState initialJoinState) - : QObject(connection), d(new Private(connection, id, initialJoinState)) -{ - setObjectName(id); - // See "Accessing the Public Class" section in - // https://marcmutz.wordpress.com/translated-articles/pimp-my-pimpl-%E2%80%94-reloaded/ - d->q = this; - connect(this, &Room::userAdded, this, &Room::memberListChanged); - connect(this, &Room::userRemoved, this, &Room::memberListChanged); - connect(this, &Room::memberRenamed, this, &Room::memberListChanged); - qCDebug(MAIN) << "New" << toCString(initialJoinState) << "Room:" << id; -} - -Room::~Room() -{ - delete d; -} - -const QString& Room::id() const -{ - return d->id; -} - -const Room::Timeline& Room::messageEvents() const -{ - return d->timeline; -} - -QString Room::name() const -{ - return d->name; -} - -QStringList Room::aliases() const -{ - return d->aliases; -} - -QString Room::canonicalAlias() const -{ - return d->canonicalAlias; -} - -QString Room::displayName() const -{ - return d->displayname; -} - -QString Room::topic() const -{ - return d->topic; -} - -QString Room::avatarMediaId() const -{ - return d->avatar.mediaId(); -} - -QUrl Room::avatarUrl() const -{ - return d->avatar.url(); -} - -QImage Room::avatar(int dimension) -{ - return avatar(dimension, dimension); -} - -QImage Room::avatar(int width, int height) -{ - if (!d->avatar.url().isEmpty()) - return d->avatar.get(connection(), width, height, [=] { emit avatarChanged(); }); - - // Use the other side's avatar for 1:1's - if (d->membersMap.size() == 2) - { - auto theOtherOneIt = d->membersMap.begin(); - if (theOtherOneIt.value() == localUser()) - ++theOtherOneIt; - return (*theOtherOneIt)->avatar(width, height, this, - [=] { emit avatarChanged(); }); - } - return {}; -} - -User* Room::user(const QString& userId) const -{ - return connection()->user(userId); -} - -JoinState Room::memberJoinState(User* user) const -{ - return - d->membersMap.contains(user->name(this), user) ? JoinState::Join : - JoinState::Leave; -} - -JoinState Room::joinState() const -{ - return d->joinState; -} - -void Room::setJoinState(JoinState state) -{ - JoinState oldState = d->joinState; - if( state == oldState ) - return; - d->joinState = state; - qCDebug(MAIN) << "Room" << id() << "changed state: " - << int(oldState) << "->" << int(state); - emit joinStateChanged(oldState, state); -} - -void Room::Private::setLastReadEvent(User* u, const QString& eventId) -{ - auto& storedId = lastReadEventIds[u]; - if (storedId == eventId) - return; - storedId = eventId; - emit q->lastReadEventChanged(u); - if (isLocalUser(u)) - { - if (eventId != serverReadMarker) - connection->callApi(id, eventId); - emit q->readMarkerMoved(); - } -} - -void Room::Private::updateUnreadCount(rev_iter_t from, rev_iter_t to) -{ - Q_ASSERT(from >= timeline.crbegin() && from <= timeline.crend()); - Q_ASSERT(to >= from && to <= timeline.crend()); - - // Catch a special case when the last read event id refers to an event - // that has just arrived. In this case we should recalculate - // unreadMessages and might need to promote the read marker further - // over local-origin messages. - const auto readMarker = q->readMarker(); - if (readMarker >= from && readMarker < to) - { - qCDebug(MAIN) << "Discovered last read event in room" << displayname; - promoteReadMarker(q->localUser(), readMarker, true); - return; - } - - Q_ASSERT(to <= readMarker); - - QElapsedTimer et; et.start(); - const auto newUnreadMessages = count_if(from, to, - std::bind(&Room::Private::isEventNotable, this, _1)); - if (et.nsecsElapsed() > 10000) - qCDebug(PROFILER) << "Counting gained unread messages took" << et; - - if(newUnreadMessages > 0) - { - // See https://github.com/QMatrixClient/libqmatrixclient/wiki/unread_count - if (unreadMessages < 0) - unreadMessages = 0; - - unreadMessages += newUnreadMessages; - qCDebug(MAIN) << "Room" << displayname << "has gained" - << newUnreadMessages << "unread message(s)," - << (q->readMarker() == timeline.crend() ? - "in total at least" : "in total") - << unreadMessages << "unread message(s)"; - emit q->unreadMessagesChanged(q); - } -} - -void Room::Private::promoteReadMarker(User* u, rev_iter_t newMarker, bool force) -{ - Q_ASSERT_X(u, __FUNCTION__, "User* should not be nullptr"); - Q_ASSERT(newMarker >= timeline.crbegin() && newMarker <= timeline.crend()); - - const auto prevMarker = q->readMarker(u); - if (!force && prevMarker <= newMarker) // Remember, we deal with reverse iterators - return; - - Q_ASSERT(newMarker < timeline.crend()); - - // Try to auto-promote the read marker over the user's own messages - // (switch to direct iterators for that). - auto eagerMarker = find_if(newMarker.base(), timeline.cend(), - [=](const TimelineItem& ti) { return ti->senderId() != u->id(); }); - - setLastReadEvent(u, (*(eagerMarker - 1))->id()); - if (isLocalUser(u)) - { - const auto oldUnreadCount = unreadMessages; - QElapsedTimer et; et.start(); - unreadMessages = count_if(eagerMarker, timeline.cend(), - std::bind(&Room::Private::isEventNotable, this, _1)); - if (et.nsecsElapsed() > 10000) - qCDebug(PROFILER) << "Recounting unread messages took" << et; - - // See https://github.com/QMatrixClient/libqmatrixclient/wiki/unread_count - if (unreadMessages == 0) - unreadMessages = -1; - - if (force || unreadMessages != oldUnreadCount) - { - if (unreadMessages == -1) - { - qCDebug(MAIN) << "Room" << displayname - << "has no more unread messages"; - } else - qCDebug(MAIN) << "Room" << displayname << "still has" - << unreadMessages << "unread message(s)"; - emit q->unreadMessagesChanged(q); - } - } -} - -void Room::Private::markMessagesAsRead(rev_iter_t upToMarker) -{ - const auto prevMarker = q->readMarker(); - promoteReadMarker(q->localUser(), upToMarker); - if (prevMarker != upToMarker) - qCDebug(MAIN) << "Marked messages as read until" << *q->readMarker(); - - // We shouldn't send read receipts for the local user's own messages - so - // search earlier messages for the latest message not from the local user - // until the previous last-read message, whichever comes first. - for (; upToMarker < prevMarker; ++upToMarker) - { - if ((*upToMarker)->senderId() != q->localUser()->id()) - { - connection->callApi(id, "m.read", - (*upToMarker)->id()); - break; - } - } -} - -void Room::markMessagesAsRead(QString uptoEventId) -{ - d->markMessagesAsRead(findInTimeline(uptoEventId)); -} - -void Room::markAllMessagesAsRead() -{ - if (!d->timeline.empty()) - d->markMessagesAsRead(d->timeline.crbegin()); -} - -bool Room::hasUnreadMessages() const -{ - return unreadCount() >= 0; -} - -int Room::unreadCount() const -{ - return d->unreadMessages; -} - -Room::rev_iter_t Room::timelineEdge() const -{ - return d->timeline.crend(); -} - -TimelineItem::index_t Room::minTimelineIndex() const -{ - return d->timeline.empty() ? 0 : d->timeline.front().index(); -} - -TimelineItem::index_t Room::maxTimelineIndex() const -{ - return d->timeline.empty() ? 0 : d->timeline.back().index(); -} - -bool Room::isValidIndex(TimelineItem::index_t timelineIndex) const -{ - return !d->timeline.empty() && - timelineIndex >= minTimelineIndex() && - timelineIndex <= maxTimelineIndex(); -} - -Room::rev_iter_t Room::findInTimeline(TimelineItem::index_t index) const -{ - return timelineEdge() - - (isValidIndex(index) ? index - minTimelineIndex() + 1 : 0); -} - -Room::rev_iter_t Room::findInTimeline(const QString& evtId) const -{ - if (!d->timeline.empty() && d->eventsIndex.contains(evtId)) - return findInTimeline(d->eventsIndex.value(evtId)); - return timelineEdge(); -} - -bool Room::displayed() const -{ - return d->displayed; -} - -void Room::setDisplayed(bool displayed) -{ - if (d->displayed == displayed) - return; - - d->displayed = displayed; - emit displayedChanged(displayed); - if( displayed ) - { - resetHighlightCount(); - resetNotificationCount(); - } -} - -QString Room::firstDisplayedEventId() const -{ - return d->firstDisplayedEventId; -} - -Room::rev_iter_t Room::firstDisplayedMarker() const -{ - return findInTimeline(firstDisplayedEventId()); -} - -void Room::setFirstDisplayedEventId(const QString& eventId) -{ - if (d->firstDisplayedEventId == eventId) - return; - - d->firstDisplayedEventId = eventId; - emit firstDisplayedEventChanged(); -} - -void Room::setFirstDisplayedEvent(TimelineItem::index_t index) -{ - Q_ASSERT(isValidIndex(index)); - setFirstDisplayedEventId(findInTimeline(index)->event()->id()); -} - -QString Room::lastDisplayedEventId() const -{ - return d->lastDisplayedEventId; -} - -Room::rev_iter_t Room::lastDisplayedMarker() const -{ - return findInTimeline(lastDisplayedEventId()); -} - -void Room::setLastDisplayedEventId(const QString& eventId) -{ - if (d->lastDisplayedEventId == eventId) - return; - - d->lastDisplayedEventId = eventId; - emit lastDisplayedEventChanged(); -} - -void Room::setLastDisplayedEvent(TimelineItem::index_t index) -{ - Q_ASSERT(isValidIndex(index)); - setLastDisplayedEventId(findInTimeline(index)->event()->id()); -} - -Room::rev_iter_t Room::readMarker(const User* user) const -{ - Q_ASSERT(user); - return findInTimeline(d->lastReadEventIds.value(user)); -} - -Room::rev_iter_t Room::readMarker() const -{ - return readMarker(localUser()); -} - -QString Room::readMarkerEventId() const -{ - return d->lastReadEventIds.value(localUser()); -} - -int Room::notificationCount() const -{ - return d->notificationCount; -} - -void Room::resetNotificationCount() -{ - if( d->notificationCount == 0 ) - return; - d->notificationCount = 0; - emit notificationCountChanged(this); -} - -int Room::highlightCount() const -{ - return d->highlightCount; -} - -void Room::resetHighlightCount() -{ - if( d->highlightCount == 0 ) - return; - d->highlightCount = 0; - emit highlightCountChanged(this); -} - -QStringList Room::tagNames() const -{ - return d->tags.keys(); -} - -TagsMap Room::tags() const -{ - return d->tags; -} - -TagRecord Room::tag(const QString& name) const -{ - return d->tags.value(name); -} - -void Room::addTag(const QString& name, const TagRecord& record) -{ - if (d->tags.contains(name)) - return; - - d->tags.insert(name, record); - d->broadcastTagUpdates(); -} - -void Room::removeTag(const QString& name) -{ - if (!d->tags.contains(name)) - return; - - d->tags.remove(name); - d->broadcastTagUpdates(); -} - -void Room::setTags(const TagsMap& newTags) -{ - if (newTags == d->tags) - return; - d->tags = newTags; - d->broadcastTagUpdates(); -} - -bool Room::isFavourite() const -{ - return d->tags.contains(FavouriteTag); -} - -bool Room::isLowPriority() const -{ - return d->tags.contains(LowPriorityTag); -} - -bool Room::isDirectChat() const -{ - return connection()->isDirectChat(id()); -} - -QList Room::directChatUsers() const -{ - return connection()->directChatUsers(this); -} - -const RoomMessageEvent* -Room::Private::getEventWithFile(const QString& eventId) const -{ - auto evtIt = q->findInTimeline(eventId); - if (evtIt != timeline.rend() && - evtIt->event()->type() == EventType::RoomMessage) - { - auto* event = static_cast(evtIt->event()); - if (event->hasFileContent()) - return event; - } - qWarning() << "No files to download in event" << eventId; - return nullptr; -} - -QString Room::Private::fileNameToDownload(const RoomMessageEvent* event) const -{ - Q_ASSERT(event->hasFileContent()); - const auto* fileInfo = event->content()->fileInfo(); - QString fileName; - if (!fileInfo->originalName.isEmpty()) - { - fileName = QFileInfo(fileInfo->originalName).fileName(); - } - else if (!event->plainBody().isEmpty()) - { - // Having no better options, assume that the body has - // the original file URL or at least the file name. - QUrl u { event->plainBody() }; - if (u.isValid()) - fileName = QFileInfo(u.path()).fileName(); - } - // Check the file name for sanity - if (fileName.isEmpty() || !QTemporaryFile(fileName).open()) - return "file." % fileInfo->mimeType.preferredSuffix(); - - if (QSysInfo::productType() == "windows") - { - const auto& suffixes = fileInfo->mimeType.suffixes(); - if (!suffixes.isEmpty() && - std::none_of(suffixes.begin(), suffixes.end(), - [&fileName] (const QString& s) { - return fileName.endsWith(s); })) - return fileName % '.' % fileInfo->mimeType.preferredSuffix(); - } - return fileName; -} - -QUrl Room::urlToThumbnail(const QString& eventId) -{ - if (auto* event = d->getEventWithFile(eventId)) - if (event->hasThumbnail()) - { - auto* thumbnail = event->content()->thumbnailInfo(); - Q_ASSERT(thumbnail != nullptr); - return MediaThumbnailJob::makeRequestUrl(connection()->homeserver(), - thumbnail->url, thumbnail->imageSize); - } - qDebug() << "Event" << eventId << "has no thumbnail"; - return {}; -} - -QUrl Room::urlToDownload(const QString& eventId) -{ - if (auto* event = d->getEventWithFile(eventId)) - { - auto* fileInfo = event->content()->fileInfo(); - Q_ASSERT(fileInfo != nullptr); - return DownloadFileJob::makeRequestUrl(connection()->homeserver(), - fileInfo->url); - } - return {}; -} - -QString Room::fileNameToDownload(const QString& eventId) -{ - if (auto* event = d->getEventWithFile(eventId)) - return d->fileNameToDownload(event); - return {}; -} - -FileTransferInfo Room::fileTransferInfo(const QString& id) const -{ - auto infoIt = d->fileTransfers.find(id); - if (infoIt == d->fileTransfers.end()) - return {}; - - // FIXME: Add lib tests to make sure FileTransferInfo::status stays - // consistent with FileTransferInfo::job - - qint64 progress = infoIt->progress; - qint64 total = infoIt->total; - if (total > INT_MAX) - { - // JavaScript doesn't deal with 64-bit integers; scale down if necessary - progress = llround(double(progress) / total * INT_MAX); - total = INT_MAX; - } - -#ifdef WORKAROUND_EXTENDED_INITIALIZER_LIST - FileTransferInfo fti; - fti.status = infoIt->status; - fti.progress = int(progress); - fti.total = int(total); - fti.localDir = QUrl::fromLocalFile(infoIt->localFileInfo.absolutePath()); - fti.localPath = QUrl::fromLocalFile(infoIt->localFileInfo.absoluteFilePath()); - return fti; -#else - return { infoIt->status, int(progress), int(total), - QUrl::fromLocalFile(infoIt->localFileInfo.absolutePath()), - QUrl::fromLocalFile(infoIt->localFileInfo.absoluteFilePath()) - }; -#endif -} - -static const auto RegExpOptions = - QRegularExpression::CaseInsensitiveOption - | QRegularExpression::OptimizeOnFirstUsageOption - | QRegularExpression::UseUnicodePropertiesOption; - -// regexp is originally taken from Konsole (https://github.com/KDE/konsole) -// full url: -// protocolname:// or www. followed by anything other than whitespaces, -// <, >, ' or ", and ends before whitespaces, <, >, ', ", ], !, ), :, -// comma or dot -// Note: outer parentheses are a part of C++ raw string delimiters, not of -// the regex (see http://en.cppreference.com/w/cpp/language/string_literal). -static const QRegularExpression FullUrlRegExp(QStringLiteral( - R"(((www\.(?!\.)|[a-z][a-z0-9+.-]*://)(&(?![lg]t;)|[^&\s<>'"])+(&(?![lg]t;)|[^&!,.\s<>'"\]):])))" - ), RegExpOptions); -// email address: -// [word chars, dots or dashes]@[word chars, dots or dashes].[word chars] -static const QRegularExpression EmailAddressRegExp(QStringLiteral( - R"((mailto:)?(\b(\w|\.|-)+@(\w|\.|-)+\.\w+\b))" - ), RegExpOptions); - -/** Converts all that looks like a URL into HTML links */ -static void linkifyUrls(QString& htmlEscapedText) -{ - // NOTE: htmlEscapedText is already HTML-escaped (no literal <,>,&)! - - htmlEscapedText.replace(EmailAddressRegExp, - QStringLiteral(R"(\1\2)")); - htmlEscapedText.replace(FullUrlRegExp, - QStringLiteral(R"(\1)")); -} - -QString Room::prettyPrint(const QString& plainText) const -{ - auto pt = QStringLiteral("") + - plainText.toHtmlEscaped() + QStringLiteral(""); - pt.replace('\n', "
"); - - linkifyUrls(pt); - return pt; -} - -QList< User* > Room::usersTyping() const -{ - return d->usersTyping; -} - -QList< User* > Room::membersLeft() const -{ - return d->membersLeft; -} - -QList< User* > Room::users() const -{ - return d->membersMap.values(); -} - -QStringList Room::memberNames() const -{ - QStringList res; - for (auto u : d->membersMap) - res.append( roomMembername(u) ); - - return res; -} - -int Room::memberCount() const -{ - return d->membersMap.size(); -} - -int Room::timelineSize() const -{ - return int(d->timeline.size()); -} - -bool Room::usesEncryption() const -{ - return !d->encryptionAlgorithm.isEmpty(); -} - -void Room::Private::insertMemberIntoMap(User *u) -{ - const auto userName = u->name(q); - // If there is exactly one namesake of the added user, signal member renaming - // for that other one because the two should be disambiguated now. - auto namesakes = membersMap.values(userName); - if (namesakes.size() == 1) - emit q->memberAboutToRename(namesakes.front(), - namesakes.front()->fullName(q)); - membersMap.insert(userName, u); - if (namesakes.size() == 1) - emit q->memberRenamed(namesakes.front()); -} - -void Room::Private::renameMember(User* u, QString oldName) -{ - if (u->name(q) == oldName) - { - qCWarning(MAIN) << "Room::Private::renameMember(): the user " - << u->fullName(q) - << "is already known in the room under a new name."; - } - else if (membersMap.contains(oldName, u)) - { - removeMemberFromMap(oldName, u); - insertMemberIntoMap(u); - } - emit q->memberRenamed(u); -} - -void Room::Private::removeMemberFromMap(const QString& username, User* u) -{ - User* namesake = nullptr; - auto namesakes = membersMap.values(username); - if (namesakes.size() == 2) - { - namesake = namesakes.front() == u ? namesakes.back() : namesakes.front(); - Q_ASSERT_X(namesake != u, __FUNCTION__, "Room members list is broken"); - emit q->memberAboutToRename(namesake, username); - } - membersMap.remove(username, u); - // If there was one namesake besides the removed user, signal member renaming - // for it because it doesn't need to be disambiguated anymore. - // TODO: Think about left users. - if (namesake) - emit q->memberRenamed(namesake); -} - -inline auto makeErrorStr(const Event& e, QByteArray msg) -{ - return msg.append("; event dump follows:\n").append(e.originalJson()); -} - -Room::Timeline::size_type Room::Private::insertEvents(RoomEventsRange&& events, - EventsPlacement placement) -{ - // Historical messages arrive in newest-to-oldest order, so the process for - // them is symmetric to the one for new messages. - auto index = timeline.empty() ? -int(placement) : - placement == Older ? timeline.front().index() : - timeline.back().index(); - auto baseIndex = index; - for (auto&& e: events) - { - const auto eId = e->id(); - Q_ASSERT_X(e, __FUNCTION__, "Attempt to add nullptr to timeline"); - Q_ASSERT_X(!eId.isEmpty(), __FUNCTION__, - makeErrorStr(*e, - "Event with empty id cannot be in the timeline")); - Q_ASSERT_X(!eventsIndex.contains(eId), __FUNCTION__, - makeErrorStr(*e, "Event is already in the timeline; " - "incoming events were not properly deduplicated")); - if (placement == Older) - timeline.emplace_front(move(e), --index); - else - timeline.emplace_back(move(e), ++index); - eventsIndex.insert(eId, index); - Q_ASSERT(q->findInTimeline(eId)->event()->id() == eId); - } - // Pointers in "events" are empty now, but events.size() didn't change - Q_ASSERT(int(events.size()) == (index - baseIndex) * int(placement)); - return events.size(); -} - -QString Room::roomMembername(const User* u) const -{ - // See the CS spec, section 11.2.2.3 - - const auto username = u->name(this); - if (username.isEmpty()) - return u->id(); - - // Get the list of users with the same display name. Most likely, - // there'll be one, but there's a chance there are more. - if (d->membersMap.count(username) == 1) - return username; - - // We expect a user to be a member of the room - but technically it is - // possible to invoke roomMemberName() even for non-members. In such case - // we return the name _with_ id, to stay on a safe side. - // XXX: Causes a storm of false alarms when scrolling through older events - // with left users; commented out until we have a proper backtracking of - // room state ("room time machine"). -// if ( !namesakes.contains(u) ) -// { -// qCWarning() -// << "Room::roomMemberName(): user" << u->id() -// << "is not a member of the room" << id(); -// } - - // In case of more than one namesake, use the full name to disambiguate - return u->fullName(this); -} - -QString Room::roomMembername(const QString& userId) const -{ - return roomMembername(user(userId)); -} - -void Room::updateData(SyncRoomData&& data) -{ - if( d->prevBatch.isEmpty() ) - d->prevBatch = data.timelinePrevBatch; - setJoinState(data.joinState); - - QElapsedTimer et; et.start(); - for (auto&& event: data.accountData) - processAccountDataEvent(move(event)); - - if (!data.state.empty()) - { - et.restart(); - processStateEvents(data.state); - qCDebug(PROFILER) << "*** Room::processStateEvents(state):" - << data.state.size() << "event(s)," << et; - } - if (!data.timeline.empty()) - { - et.restart(); - // State changes can arrive in a timeline event; so check those. - processStateEvents(data.timeline); - qCDebug(PROFILER) << "*** Room::processStateEvents(timeline):" - << data.timeline.size() << "event(s)," << et; - - et.restart(); - d->addNewMessageEvents(move(data.timeline)); - qCDebug(PROFILER) << "*** Room::addNewMessageEvents():" << et; - } - for( auto&& ephemeralEvent: data.ephemeral ) - processEphemeralEvent(move(ephemeralEvent)); - - // See https://github.com/QMatrixClient/libqmatrixclient/wiki/unread_count - if (data.unreadCount != -2 && data.unreadCount != d->unreadMessages) - { - qCDebug(MAIN) << "Setting unread_count to" << data.unreadCount; - d->unreadMessages = data.unreadCount; - emit unreadMessagesChanged(this); - } - - if( data.highlightCount != d->highlightCount ) - { - d->highlightCount = data.highlightCount; - emit highlightCountChanged(this); - } - if( data.notificationCount != d->notificationCount ) - { - d->notificationCount = data.notificationCount; - emit notificationCountChanged(this); - } -} - -void Room::postMessage(const QString& type, const QString& plainText) -{ - postMessage(RoomMessageEvent { plainText, type }); -} - -void Room::postMessage(const QString& plainText, MessageEventType type) -{ - postMessage(RoomMessageEvent { plainText, type }); -} - -void Room::postMessage(const RoomMessageEvent& event) -{ - if (usesEncryption()) - { - qCCritical(MAIN) << "Room" << displayName() - << "enforces encryption; sending encrypted messages is not supported yet"; - } - connection()->callApi(id(), event); -} - -void Room::setName(const QString& newName) -{ - connection()->callApi(id(), RoomNameEvent(newName)); -} - -void Room::setCanonicalAlias(const QString& newAlias) -{ - connection()->callApi(id(), - RoomCanonicalAliasEvent(newAlias)); -} - -void Room::setTopic(const QString& newTopic) -{ - RoomTopicEvent evt(newTopic); - connection()->callApi(id(), evt); -} - -void Room::getPreviousContent(int limit) -{ - d->getPreviousContent(limit); -} - -void Room::Private::getPreviousContent(int limit) -{ - if( !isJobRunning(roomMessagesJob) ) - { - roomMessagesJob = - connection->callApi(id, prevBatch, limit); - connect( roomMessagesJob, &RoomMessagesJob::success, [=] { - prevBatch = roomMessagesJob->end(); - addHistoricalMessageEvents(roomMessagesJob->releaseEvents()); - }); - } -} - -void Room::inviteToRoom(const QString& memberId) -{ - connection()->callApi(id(), memberId); -} - -LeaveRoomJob* Room::leaveRoom() -{ - return connection()->callApi(id()); -} - -void Room::kickMember(const QString& memberId, const QString& reason) -{ - connection()->callApi(id(), memberId, reason); -} - -void Room::ban(const QString& userId, const QString& reason) -{ - connection()->callApi(id(), userId, reason); -} - -void Room::unban(const QString& userId) -{ - connection()->callApi(id(), userId); -} - -void Room::redactEvent(const QString& eventId, const QString& reason) -{ - connection()->callApi( - id(), eventId, connection()->generateTxnId(), reason); -} - -void Room::uploadFile(const QString& id, const QUrl& localFilename, - const QString& overrideContentType) -{ - Q_ASSERT_X(localFilename.isLocalFile(), __FUNCTION__, - "localFilename should point at a local file"); - auto fileName = localFilename.toLocalFile(); - auto job = connection()->uploadFile(fileName, overrideContentType); - if (isJobRunning(job)) - { - d->fileTransfers.insert(id, { job, fileName }); - connect(job, &BaseJob::uploadProgress, this, - [this,id] (qint64 sent, qint64 total) { - d->fileTransfers[id].update(sent, total); - emit fileTransferProgress(id, sent, total); - }); - connect(job, &BaseJob::success, this, [this,id,localFilename,job] { - d->fileTransfers[id].status = FileTransferInfo::Completed; - emit fileTransferCompleted(id, localFilename, job->contentUri()); - }); - connect(job, &BaseJob::failure, this, - std::bind(&Private::failedTransfer, d, id, job->errorString())); - emit newFileTransfer(id, localFilename); - } else - d->failedTransfer(id); -} - -void Room::downloadFile(const QString& eventId, const QUrl& localFilename) -{ - auto ongoingTransfer = d->fileTransfers.find(eventId); - if (ongoingTransfer != d->fileTransfers.end() && - ongoingTransfer->status == FileTransferInfo::Started) - { - qCWarning(MAIN) << "Download for" << eventId - << "already started; to restart, cancel it first"; - return; - } - - Q_ASSERT_X(localFilename.isEmpty() || localFilename.isLocalFile(), - __FUNCTION__, "localFilename should point at a local file"); - const auto* event = d->getEventWithFile(eventId); - if (!event) - { - qCCritical(MAIN) - << eventId << "is not in the local timeline or has no file content"; - Q_ASSERT(false); - return; - } - const auto fileUrl = event->content()->fileInfo()->url; - auto filePath = localFilename.toLocalFile(); - if (filePath.isEmpty()) - { - // Build our own file path, starting with temp directory and eventId. - filePath = eventId; - filePath = QDir::tempPath() % '/' % filePath.replace(':', '_') % - '#' % d->fileNameToDownload(event); - } - auto job = connection()->downloadFile(fileUrl, filePath); - if (isJobRunning(job)) - { - // If there was a previous transfer (completed or failed), remove it. - d->fileTransfers.remove(eventId); - d->fileTransfers.insert(eventId, { job, job->targetFileName() }); - connect(job, &BaseJob::downloadProgress, this, - [this,eventId] (qint64 received, qint64 total) { - d->fileTransfers[eventId].update(received, total); - emit fileTransferProgress(eventId, received, total); - }); - connect(job, &BaseJob::success, this, [this,eventId,fileUrl,job] { - d->fileTransfers[eventId].status = FileTransferInfo::Completed; - emit fileTransferCompleted(eventId, fileUrl, - QUrl::fromLocalFile(job->targetFileName())); - }); - connect(job, &BaseJob::failure, this, - std::bind(&Private::failedTransfer, d, - eventId, job->errorString())); - } else - d->failedTransfer(eventId); -} - -void Room::cancelFileTransfer(const QString& id) -{ - auto it = d->fileTransfers.find(id); - if (it == d->fileTransfers.end()) - { - qCWarning(MAIN) << "No information on file transfer" << id - << "in room" << d->id; - return; - } - if (isJobRunning(it->job)) - it->job->abandon(); - d->fileTransfers.remove(id); - emit fileTransferCancelled(id); -} - -void Room::Private::dropDuplicateEvents(RoomEvents* events) const -{ - if (events->empty()) - return; - - // Multiple-remove (by different criteria), single-erase - // 1. Check for duplicates against the timeline. - auto dupsBegin = remove_if(events->begin(), events->end(), - [&] (const RoomEventPtr& e) - { return eventsIndex.contains(e->id()); }); - - // 2. Check for duplicates within the batch if there are still events. - for (auto eIt = events->begin(); distance(eIt, dupsBegin) > 1; ++eIt) - dupsBegin = remove_if(eIt + 1, dupsBegin, - [&] (const RoomEventPtr& e) - { return e->id() == (*eIt)->id(); }); - if (dupsBegin == events->end()) - return; - - qCDebug(EVENTS) << "Dropping" << distance(dupsBegin, events->end()) - << "duplicate event(s)"; - events->erase(dupsBegin, events->end()); -} - -inline bool isRedaction(const RoomEventPtr& e) -{ - return e->type() == EventType::Redaction; -} - -void Room::Private::processRedaction(RoomEventPtr redactionEvent) -{ - Q_ASSERT(redactionEvent && isRedaction(redactionEvent)); - const auto& redaction = - static_cast(redactionEvent.get()); - - const auto pIdx = eventsIndex.find(redaction->redactedEvent()); - if (pIdx == eventsIndex.end()) - { - qCDebug(MAIN) << "Redaction" << redaction->id() - << "ignored: target event not found"; - return; // If the target events comes later, it comes already redacted. - } - Q_ASSERT(q->isValidIndex(*pIdx)); - - auto& ti = timeline[Timeline::size_type(*pIdx - q->minTimelineIndex())]; - - // Apply the redaction procedure from chapter 6.5 of The Spec - auto originalJson = ti->originalJsonObject(); - if (originalJson.value("unsigned").toObject() - .value("redacted_because").toObject() - .value("event_id") == redaction->id()) - { - qCDebug(MAIN) << "Redaction" << redaction->id() - << "of event" << ti.event()->id() << "already done, skipping"; - return; - } - static const QStringList keepKeys = - { "event_id", "type", "room_id", "sender", "state_key", - "prev_content", "content", "origin_server_ts" }; - static const - std::vector> keepContentKeysMap - { { Event::Type::RoomMember, { "membership" } } - , { Event::Type::RoomCreate, { "creator" } } - , { Event::Type::RoomJoinRules, { "join_rule" } } - , { Event::Type::RoomPowerLevels, - { "ban", "events", "events_default", "kick", "redact", - "state_default", "users", "users_default" } } - , { Event::Type::RoomAliases, { "alias" } } - }; - for (auto it = originalJson.begin(); it != originalJson.end();) - { - if (!keepKeys.contains(it.key())) - it = originalJson.erase(it); // TODO: shred the value - else - ++it; - } - auto keepContentKeys = - find_if(keepContentKeysMap.begin(), keepContentKeysMap.end(), - [&ti](const auto& t) { return ti->type() == t.first; } ); - if (keepContentKeys == keepContentKeysMap.end()) - { - originalJson.remove("content"); - originalJson.remove("prev_content"); - } else { - auto content = originalJson.take("content").toObject(); - for (auto it = content.begin(); it != content.end(); ) - { - if (!keepContentKeys->second.contains(it.key())) - it = content.erase(it); - else - ++it; - } - originalJson.insert("content", content); - } - auto unsignedData = originalJson.take("unsigned").toObject(); - unsignedData["redacted_because"] = redaction->originalJsonObject(); - originalJson.insert("unsigned", unsignedData); - - // Make a new event from the redacted JSON, exchange events, - // notify everyone and delete the old event - RoomEventPtr oldEvent - { ti.replaceEvent(makeEvent(originalJson)) }; - q->onRedaction(oldEvent.get(), ti.event()); - qCDebug(MAIN) << "Redacted" << oldEvent->id() << "with" << redaction->id(); - emit q->replacedEvent(ti.event(), oldEvent.get()); -} - -Connection* Room::connection() const -{ - Q_ASSERT(d->connection); - return d->connection; -} - -User* Room::localUser() const -{ - return connection()->user(); -} - -void Room::Private::addNewMessageEvents(RoomEvents&& events) -{ - auto timelineSize = timeline.size(); - - dropDuplicateEvents(&events); - // We want to process redactions in the order of arrival (covering the - // case of one redaction superseding another one), hence stable partition. - const auto normalsBegin = - stable_partition(events.begin(), events.end(), isRedaction); - RoomEventsRange redactions { events.begin(), normalsBegin }, - normalEvents { normalsBegin, events.end() }; - - if (!normalEvents.empty()) - emit q->aboutToAddNewMessages(normalEvents); - const auto insertedSize = insertEvents(std::move(normalEvents), Newer); - const auto from = timeline.cend() - insertedSize; - if (insertedSize > 0) - { - qCDebug(MAIN) - << "Room" << displayname << "received" << insertedSize - << "new events; the last event is now" << timeline.back(); - q->onAddNewTimelineEvents(from); - } - for (auto&& r: redactions) - processRedaction(move(r)); - if (insertedSize > 0) - { - emit q->addedMessages(); - - // The first event in the just-added batch (referred to by `from`) - // defines whose read marker can possibly be promoted any further over - // the same author's events newly arrived. Others will need explicit - // read receipts from the server (or, for the local user, - // markMessagesAsRead() invocation) to promote their read markers over - // the new message events. - auto firstWriter = q->user((*from)->senderId()); - if (q->readMarker(firstWriter) != timeline.crend()) - { - promoteReadMarker(firstWriter, rev_iter_t(from) - 1); - qCDebug(MAIN) << "Auto-promoted read marker for" << firstWriter->id() - << "to" << *q->readMarker(firstWriter); - } - - updateUnreadCount(timeline.crbegin(), rev_iter_t(from)); - } - - Q_ASSERT(timeline.size() == timelineSize + insertedSize); -} - -void Room::Private::addHistoricalMessageEvents(RoomEvents&& events) -{ - const auto timelineSize = timeline.size(); - - dropDuplicateEvents(&events); - const auto redactionsBegin = - remove_if(events.begin(), events.end(), isRedaction); - RoomEventsRange normalEvents { events.begin(), redactionsBegin }; - if (normalEvents.empty()) - return; - - emit q->aboutToAddHistoricalMessages(normalEvents); - const auto insertedSize = insertEvents(std::move(normalEvents), Older); - const auto from = timeline.crend() - insertedSize; - - qCDebug(MAIN) << "Room" << displayname << "received" << insertedSize - << "past events; the oldest event is now" << timeline.front(); - q->onAddHistoricalTimelineEvents(from); - emit q->addedMessages(); - - if (from <= q->readMarker()) - updateUnreadCount(from, timeline.crend()); - - Q_ASSERT(timeline.size() == timelineSize + insertedSize); -} - -void Room::processStateEvents(const RoomEvents& events) -{ - bool emitNamesChanged = false; - for (const auto& e: events) - { - auto* event = e.get(); - switch (event->type()) - { - case EventType::RoomName: { - auto nameEvent = static_cast(event); - d->name = nameEvent->name(); - qCDebug(MAIN) << "Room name updated:" << d->name; - emitNamesChanged = true; - break; - } - case EventType::RoomAliases: { - auto aliasesEvent = static_cast(event); - d->aliases = aliasesEvent->aliases(); - qCDebug(MAIN) << "Room aliases updated:" << d->aliases; - emitNamesChanged = true; - break; - } - case EventType::RoomCanonicalAlias: { - auto aliasEvent = static_cast(event); - d->canonicalAlias = aliasEvent->alias(); - setObjectName(d->canonicalAlias); - qCDebug(MAIN) << "Room canonical alias updated:" << d->canonicalAlias; - emitNamesChanged = true; - break; - } - case EventType::RoomTopic: { - auto topicEvent = static_cast(event); - d->topic = topicEvent->topic(); - qCDebug(MAIN) << "Room topic updated:" << d->topic; - emit topicChanged(); - break; - } - case EventType::RoomAvatar: { - const auto& avatarEventContent = - static_cast(event)->content(); - if (d->avatar.updateUrl(avatarEventContent.url)) - { - qCDebug(MAIN) << "Room avatar URL updated:" - << avatarEventContent.url.toString(); - emit avatarChanged(); - } - break; - } - case EventType::RoomMember: { - auto memberEvent = static_cast(event); - auto u = user(memberEvent->userId()); - u->processEvent(memberEvent, this); - if (u == localUser() && memberJoinState(u) == JoinState::Invite - && memberEvent->isDirect()) - connection()->addToDirectChats(this, - user(memberEvent->senderId())); - - if( memberEvent->membership() == MembershipType::Join ) - { - if (memberJoinState(u) != JoinState::Join) - { - d->insertMemberIntoMap(u); - connect(u, &User::nameAboutToChange, this, - [=] (QString newName, QString, const Room* context) { - if (context == this) - emit memberAboutToRename(u, newName); - }); - connect(u, &User::nameChanged, this, - [=] (QString, QString oldName, const Room* context) { - if (context == this) - d->renameMember(u, oldName); - }); - emit userAdded(u); - } - } - else if( memberEvent->membership() == MembershipType::Leave ) - { - if (memberJoinState(u) == JoinState::Join) - { - if (!d->membersLeft.contains(u)) - d->membersLeft.append(u); - d->removeMemberFromMap(u->name(this), u); - emit userRemoved(u); - } - } - break; - } - case EventType::RoomEncryption: - { - d->encryptionAlgorithm = - static_cast(event)->algorithm(); - qCDebug(MAIN) << "Encryption switched on in" << displayName(); - emit encryption(); - break; - } - default: /* Ignore events of other types */; - } - } - if (emitNamesChanged) { - emit namesChanged(this); - } - d->updateDisplayname(); -} - -void Room::processEphemeralEvent(EventPtr event) -{ - QElapsedTimer et; et.start(); - switch (event->type()) - { - case EventType::Typing: { - auto typingEvent = static_cast(event.get()); - d->usersTyping.clear(); - for( const QString& userId: typingEvent->users() ) - { - auto u = user(userId); - if (memberJoinState(u) == JoinState::Join) - d->usersTyping.append(u); - } - if (!typingEvent->users().isEmpty()) - qCDebug(PROFILER) << "*** Room::processEphemeralEvent(typing):" - << typingEvent->users().size() << "users," << et; - emit typingChanged(); - break; - } - case EventType::Receipt: { - auto receiptEvent = static_cast(event.get()); - for( const auto &p: receiptEvent->eventsWithReceipts() ) - { - { - if (p.receipts.size() == 1) - qCDebug(EPHEMERAL) << "Marking" << p.evtId - << "as read for" << p.receipts[0].userId; - else - qCDebug(EPHEMERAL) << "Marking" << p.evtId - << "as read for" - << p.receipts.size() << "users"; - } - const auto newMarker = findInTimeline(p.evtId); - if (newMarker != timelineEdge()) - { - for( const Receipt& r: p.receipts ) - { - if (r.userId == connection()->userId()) - continue; // FIXME, #185 - auto u = user(r.userId); - if (memberJoinState(u) == JoinState::Join) - d->promoteReadMarker(u, newMarker); - } - } else - { - qCDebug(EPHEMERAL) << "Event" << p.evtId - << "not found; saving read receipts anyway"; - // If the event is not found (most likely, because it's too old - // and hasn't been fetched from the server yet), but there is - // a previous marker for a user, keep the previous marker. - // Otherwise, blindly store the event id for this user. - for( const Receipt& r: p.receipts ) - { - if (r.userId == connection()->userId()) - continue; // FIXME, #185 - auto u = user(r.userId); - if (memberJoinState(u) == JoinState::Join && - readMarker(u) == timelineEdge()) - d->setLastReadEvent(u, p.evtId); - } - } - } - if (!receiptEvent->eventsWithReceipts().isEmpty()) - qCDebug(PROFILER) << "*** Room::processEphemeralEvent(receipts):" - << receiptEvent->eventsWithReceipts().size() - << "events with receipts," << et; - break; - } - default: - qCWarning(EPHEMERAL) << "Unexpected event type in 'ephemeral' batch:" - << event->jsonType(); - } -} - -void Room::processAccountDataEvent(EventPtr event) -{ - switch (event->type()) - { - case EventType::Tag: - { - auto newTags = static_cast(event.get())->tags(); - if (newTags == d->tags) - break; - d->tags = newTags; - qCDebug(MAIN) << "Room" << id() << "is tagged with:" - << tagNames().join(", "); - emit tagsChanged(); - break; - } - case EventType::ReadMarker: - { - const auto* rmEvent = static_cast(event.get()); - const auto& readEventId = rmEvent->event_id(); - qCDebug(MAIN) << "Server-side read marker at" << readEventId; - d->serverReadMarker = readEventId; - const auto newMarker = findInTimeline(readEventId); - if (newMarker != timelineEdge()) - d->markMessagesAsRead(newMarker); - else { - d->setLastReadEvent(localUser(), readEventId); - } - break; - } - default: - d->accountData[event->jsonType()] = - event->contentJson().toVariantHash(); - } -} - -QString Room::Private::roomNameFromMemberNames(const QList &userlist) const -{ - // This is part 3(i,ii,iii) in the room displayname algorithm described - // in the CS spec (see also Room::Private::updateDisplayname() ). - // The spec requires to sort users lexicographically by state_key (user id) - // and use disambiguated display names of two topmost users excluding - // the current one to render the name of the room. - - // std::array is the leanest C++ container - std::array first_two = { {nullptr, nullptr} }; - std::partial_sort_copy( - userlist.begin(), userlist.end(), - first_two.begin(), first_two.end(), - [this](const User* u1, const User* u2) { - // Filter out the "me" user so that it never hits the room name - return isLocalUser(u2) || (!isLocalUser(u1) && u1->id() < u2->id()); - } - ); - - // Spec extension. A single person in the chat but not the local user - // (the local user is apparently invited). - if (userlist.size() == 1 && !isLocalUser(first_two.front())) - return tr("Invitation from %1") - .arg(q->roomMembername(first_two.front())); - - // i. One-on-one chat. first_two[1] == localUser() in this case. - if (userlist.size() == 2) - return q->roomMembername(first_two[0]); - - // ii. Two users besides the current one. - if (userlist.size() == 3) - return tr("%1 and %2") - .arg(q->roomMembername(first_two[0])) - .arg(q->roomMembername(first_two[1])); - - // iii. More users. - if (userlist.size() > 3) - return tr("%1 and %L2 others") - .arg(q->roomMembername(first_two[0])) - .arg(userlist.size() - 3); - - // userlist.size() < 2 - apparently, there's only current user in the room - return QString(); -} - -QString Room::Private::calculateDisplayname() const -{ - // CS spec, section 11.2.2.5 Calculating the display name for a room - // Numbers below refer to respective parts in the spec. - - // 1. Name (from m.room.name) - if (!name.isEmpty()) { - return name; - } - - // 2. Canonical alias - if (!canonicalAlias.isEmpty()) - return canonicalAlias; - - // 3. Room members - QString topMemberNames = roomNameFromMemberNames(membersMap.values()); - if (!topMemberNames.isEmpty()) - return topMemberNames; - - // 4. Users that previously left the room - topMemberNames = roomNameFromMemberNames(membersLeft); - if (!topMemberNames.isEmpty()) - return tr("Empty room (was: %1)").arg(topMemberNames); - - // 5. Fail miserably - return tr("Empty room (%1)").arg(id); - - // Using m.room.aliases is explicitly discouraged by the spec - //if (!aliases.empty() && !aliases.at(0).isEmpty()) - // displayname = aliases.at(0); -} - -void Room::Private::updateDisplayname() -{ - const QString old_name = displayname; - displayname = calculateDisplayname(); - if (old_name != displayname) - emit q->displaynameChanged(q); -} - -void appendStateEvent(QJsonArray& events, const QString& type, - const QJsonObject& content, const QString& stateKey = {}) -{ - if (!content.isEmpty() || !stateKey.isEmpty()) - events.append(QJsonObject - { { QStringLiteral("type"), type } - , { QStringLiteral("content"), content } - , { QStringLiteral("state_key"), stateKey } - }); -} - -#define ADD_STATE_EVENT(events, type, name, content) \ - appendStateEvent((events), QStringLiteral(type), \ - {{ QStringLiteral(name), content }}); - -void appendEvent(QJsonArray& events, const QString& type, - const QJsonObject& content) -{ - if (!content.isEmpty()) - events.append(QJsonObject - { { QStringLiteral("type"), type } - , { QStringLiteral("content"), content } - }); -} - -template -void appendEvent(QJsonArray& events, const EvtT& event) -{ - appendEvent(events, EvtT::TypeId, event.toJson()); -} - -QJsonObject Room::Private::toJson() const -{ - QElapsedTimer et; et.start(); - QJsonObject result; - { - QJsonArray stateEvents; - - ADD_STATE_EVENT(stateEvents, "m.room.name", "name", name); - ADD_STATE_EVENT(stateEvents, "m.room.topic", "topic", topic); - ADD_STATE_EVENT(stateEvents, "m.room.avatar", "url", - avatar.url().toString()); - ADD_STATE_EVENT(stateEvents, "m.room.aliases", "aliases", - QJsonArray::fromStringList(aliases)); - ADD_STATE_EVENT(stateEvents, "m.room.canonical_alias", "alias", - canonicalAlias); - ADD_STATE_EVENT(stateEvents, "m.room.encryption", "algorithm", - encryptionAlgorithm); - - for (const auto *m : membersMap) - appendStateEvent(stateEvents, QStringLiteral("m.room.member"), - { { QStringLiteral("membership"), QStringLiteral("join") } - , { QStringLiteral("displayname"), m->name(q) } - , { QStringLiteral("avatar_url"), m->avatarUrl(q).toString() } - }, m->id()); - - const auto stateObjName = joinState == JoinState::Invite ? - QStringLiteral("invite_state") : QStringLiteral("state"); - result.insert(stateObjName, - QJsonObject {{ QStringLiteral("events"), stateEvents }}); - } - - QJsonArray accountDataEvents; - if (!tags.empty()) - appendEvent(accountDataEvents, TagEvent(tags)); - - if (!serverReadMarker.isEmpty()) - appendEvent(accountDataEvents, ReadMarkerEvent(serverReadMarker)); - - if (!accountData.empty()) - { - for (auto it = accountData.begin(); it != accountData.end(); ++it) - appendEvent(accountDataEvents, it.key(), - QJsonObject::fromVariantHash(it.value())); - } - result.insert("account_data", QJsonObject {{ "events", accountDataEvents }}); - - QJsonObject unreadNotificationsObj; - - unreadNotificationsObj.insert(SyncRoomData::UnreadCountKey, unreadMessages); - if (highlightCount > 0) - unreadNotificationsObj.insert("highlight_count", highlightCount); - if (notificationCount > 0) - unreadNotificationsObj.insert("notification_count", notificationCount); - - result.insert("unread_notifications", unreadNotificationsObj); - - if (et.elapsed() > 50) - qCDebug(PROFILER) << "Room::toJson() for" << displayname << "took" << et; - - return result; -} - -QJsonObject Room::toJson() const -{ - return d->toJson(); -} - -MemberSorter Room::memberSorter() const -{ - return MemberSorter(this); -} - -bool MemberSorter::operator()(User *u1, User *u2) const -{ - return operator()(u1, room->roomMembername(u2)); -} - -bool MemberSorter::operator ()(User* u1, const QString& u2name) const -{ - auto n1 = room->roomMembername(u1); - if (n1.startsWith('@')) - n1.remove(0, 1); - auto n2 = u2name.midRef(u2name.startsWith('@') ? 1 : 0); - - return n1.localeAwareCompare(n2) < 0; -} diff --git a/room.h b/room.h deleted file mode 100644 index bdef04ee..00000000 --- a/room.h +++ /dev/null @@ -1,424 +0,0 @@ -/****************************************************************************** - * Copyright (C) 2015 Felix Rohrbach - * - * 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 - */ - -#pragma once - -#include "jobs/syncjob.h" -#include "events/roommessageevent.h" -#include "events/accountdataevents.h" -#include "joinstate.h" - -#include - -#include -#include -#include - -namespace QMatrixClient -{ - class Event; - class Connection; - class User; - class MemberSorter; - class LeaveRoomJob; - class RedactEventJob; - - class TimelineItem - { - public: - // For compatibility with Qt containers, even though we use - // a std:: container now for the room timeline - using index_t = int; - - TimelineItem(RoomEventPtr&& e, index_t number) - : evt(move(e)), idx(number) { } - - RoomEvent* event() const { return evt.get(); } - RoomEvent* operator->() const { return evt.operator->(); } - index_t index() const { return idx; } - - // Used for event redaction - RoomEventPtr replaceEvent(RoomEventPtr&& other); - - private: - RoomEventPtr evt; - index_t idx; - }; - inline QDebug& operator<<(QDebug& d, const TimelineItem& ti) - { - QDebugStateSaver dss(d); - d.nospace() << "(" << ti.index() << "|" << ti->id() << ")"; - return d; - } - - class FileTransferInfo - { - Q_GADGET - Q_PROPERTY(bool active READ active CONSTANT) - Q_PROPERTY(bool completed READ completed CONSTANT) - Q_PROPERTY(bool failed READ failed CONSTANT) - Q_PROPERTY(int progress MEMBER progress CONSTANT) - Q_PROPERTY(int total MEMBER total CONSTANT) - Q_PROPERTY(QUrl localDir MEMBER localDir CONSTANT) - Q_PROPERTY(QUrl localPath MEMBER localPath CONSTANT) - public: - enum Status { None, Started, Completed, Failed }; - Status status = None; - int progress = 0; - int total = -1; - QUrl localDir { }; - QUrl localPath { }; - - bool active() const - { return status == Started || status == Completed; } - bool completed() const { return status == Completed; } - bool failed() const { return status == Failed; } - }; - - class Room: public QObject - { - Q_OBJECT - Q_PROPERTY(Connection* connection READ connection CONSTANT) - Q_PROPERTY(User* localUser READ localUser CONSTANT) - Q_PROPERTY(QString id READ id CONSTANT) - Q_PROPERTY(QString name READ name NOTIFY namesChanged) - Q_PROPERTY(QStringList aliases READ aliases NOTIFY namesChanged) - Q_PROPERTY(QString canonicalAlias READ canonicalAlias NOTIFY namesChanged) - Q_PROPERTY(QString displayName READ displayName NOTIFY namesChanged) - Q_PROPERTY(QString topic READ topic NOTIFY topicChanged) - Q_PROPERTY(QString avatarMediaId READ avatarMediaId NOTIFY avatarChanged STORED false) - Q_PROPERTY(QUrl avatarUrl READ avatarUrl NOTIFY avatarChanged) - Q_PROPERTY(bool usesEncryption READ usesEncryption NOTIFY encryption) - - Q_PROPERTY(int timelineSize READ timelineSize NOTIFY addedMessages) - Q_PROPERTY(QStringList memberNames READ memberNames NOTIFY memberListChanged) - Q_PROPERTY(int memberCount READ memberCount NOTIFY memberListChanged) - - Q_PROPERTY(bool displayed READ displayed WRITE setDisplayed NOTIFY displayedChanged) - Q_PROPERTY(QString firstDisplayedEventId READ firstDisplayedEventId WRITE setFirstDisplayedEventId NOTIFY firstDisplayedEventChanged) - Q_PROPERTY(QString lastDisplayedEventId READ lastDisplayedEventId WRITE setLastDisplayedEventId NOTIFY lastDisplayedEventChanged) - - Q_PROPERTY(QString readMarkerEventId READ readMarkerEventId WRITE markMessagesAsRead NOTIFY readMarkerMoved) - Q_PROPERTY(bool hasUnreadMessages READ hasUnreadMessages NOTIFY unreadMessagesChanged) - Q_PROPERTY(int unreadCount READ unreadCount NOTIFY unreadMessagesChanged) - Q_PROPERTY(QStringList tagNames READ tagNames NOTIFY tagsChanged) - - public: - using Timeline = std::deque; - using rev_iter_t = Timeline::const_reverse_iterator; - using timeline_iter_t = Timeline::const_iterator; - - Room(Connection* connection, QString id, JoinState initialJoinState); - ~Room() override; - - // Property accessors - - Connection* connection() const; - User* localUser() const; - const QString& id() const; - QString name() const; - QStringList aliases() const; - QString canonicalAlias() const; - QString displayName() const; - QString topic() const; - QString avatarMediaId() const; - QUrl avatarUrl() const; - Q_INVOKABLE JoinState joinState() const; - Q_INVOKABLE QList usersTyping() const; - QList membersLeft() const; - - Q_INVOKABLE QList users() const; - QStringList memberNames() const; - int memberCount() const; - int timelineSize() const; - bool usesEncryption() const; - - /** - * Returns a square room avatar with the given size and requests it - * from the network if needed - * @return a pixmap with the avatar or a placeholder if there's none - * available yet - */ - Q_INVOKABLE QImage avatar(int dimension); - /** - * Returns a room avatar with the given dimensions and requests it - * from the network if needed - * @return a pixmap with the avatar or a placeholder if there's none - * available yet - */ - Q_INVOKABLE QImage avatar(int width, int height); - - /** - * @brief Get a user object for a given user id - * This is the recommended way to get a user object in a room - * context. The actual object type may be changed in further - * versions to provide room-specific user information (display name, - * avatar etc.). - * \note The method will return a valid user regardless of - * the membership. - */ - Q_INVOKABLE User* user(const QString& userId) const; - - /** - * \brief Check the join state of a given user in this room - * - * \note Banned and invited users are not tracked for now (Leave - * will be returned for them). - * - * \return either of Join, Leave, depending on the given - * user's state in the room - */ - Q_INVOKABLE JoinState memberJoinState(User* user) const; - - /** - * @brief Produces a disambiguated name for a given user in - * the context of the room - */ - Q_INVOKABLE QString roomMembername(const User* u) const; - /** - * @brief Produces a disambiguated name for a user with this id in - * the context of the room - */ - Q_INVOKABLE QString roomMembername(const QString& userId) const; - - const Timeline& messageEvents() const; - /** - * A convenience method returning the read marker to the before-oldest - * message - */ - rev_iter_t timelineEdge() const; - Q_INVOKABLE TimelineItem::index_t minTimelineIndex() const; - Q_INVOKABLE TimelineItem::index_t maxTimelineIndex() const; - Q_INVOKABLE bool isValidIndex(TimelineItem::index_t timelineIndex) const; - - rev_iter_t findInTimeline(TimelineItem::index_t index) const; - rev_iter_t findInTimeline(const QString& evtId) const; - - bool displayed() const; - void setDisplayed(bool displayed = true); - QString firstDisplayedEventId() const; - rev_iter_t firstDisplayedMarker() const; - void setFirstDisplayedEventId(const QString& eventId); - void setFirstDisplayedEvent(TimelineItem::index_t index); - QString lastDisplayedEventId() const; - rev_iter_t lastDisplayedMarker() const; - void setLastDisplayedEventId(const QString& eventId); - void setLastDisplayedEvent(TimelineItem::index_t index); - - rev_iter_t readMarker(const User* user) const; - rev_iter_t readMarker() const; - QString readMarkerEventId() const; - /** - * @brief Mark the event with uptoEventId as read - * - * Finds in the timeline and marks as read the event with - * the specified id; also posts a read receipt to the server either - * for this message or, if it's from the local user, for - * the nearest non-local message before. uptoEventId must be non-empty. - */ - void markMessagesAsRead(QString uptoEventId); - - /** Check whether there are unread messages in the room */ - bool hasUnreadMessages() const; - - /** Get the number of unread messages in the room - * Depending on the read marker state, this call may return either - * a precise or an estimate number of unread events. Only "notable" - * events (non-redacted message events from users other than local) - * are counted. - * - * In a case when readMarker() == timelineEdge() (the local read - * marker is beyond the local timeline) only the bottom limit of - * the unread messages number can be estimated (and even that may - * be slightly off due to, e.g., redactions of events not loaded - * to the local timeline). - * - * If all messages are read, this function will return -1 (_not_ 0, - * as zero may mean "zero or more unread messages" in a situation - * when the read marker is outside the local timeline. - */ - int unreadCount() const; - - Q_INVOKABLE int notificationCount() const; - Q_INVOKABLE void resetNotificationCount(); - Q_INVOKABLE int highlightCount() const; - Q_INVOKABLE void resetHighlightCount(); - - QStringList tagNames() const; - TagsMap tags() const; - TagRecord tag(const QString& name) const; - - /** Add a new tag to this room - * If this room already has this tag, nothing happens. If it's a new - * tag for the room, the respective tag record is added to the set - * of tags and the new set is sent to the server to update other - * clients. - */ - void addTag(const QString& name, const TagRecord& record = {}); - - /** Remove a tag from the room */ - void removeTag(const QString& name); - - /** Overwrite the room's tags - * This completely replaces the existing room's tags with a set - * of new ones and updates the new set on the server. Unlike - * most other methods in Room, this one sends a signal about changes - * immediately, not waiting for confirmation from the server - * (because tags are saved in account data rather than in shared - * room state). - */ - void setTags(const TagsMap& newTags); - - /** Check whether the list of tags has m.favourite */ - bool isFavourite() const; - /** Check whether the list of tags has m.lowpriority */ - bool isLowPriority() const; - - /** Check whether this room is a direct chat */ - bool isDirectChat() const; - - /** Get the list of users this room is a direct chat with */ - QList directChatUsers() const; - - Q_INVOKABLE QUrl urlToThumbnail(const QString& eventId); - Q_INVOKABLE QUrl urlToDownload(const QString& eventId); - Q_INVOKABLE QString fileNameToDownload(const QString& eventId); - Q_INVOKABLE FileTransferInfo fileTransferInfo(const QString& id) const; - - /** Pretty-prints plain text into HTML - * This includes HTML escaping of <,>,",& and URLs linkification. - */ - QString prettyPrint(const QString& plainText) const; - - MemberSorter memberSorter() const; - - QJsonObject toJson() const; - void updateData(SyncRoomData&& data ); - void setJoinState( JoinState state ); - - public slots: - void postMessage(const QString& plainText, - MessageEventType type = MessageEventType::Text); - void postMessage(const RoomMessageEvent& event); - /** @deprecated If you have a custom event type, construct the event - * and pass it as a whole to postMessage() */ - void postMessage(const QString& type, const QString& plainText); - void setName(const QString& newName); - void setCanonicalAlias(const QString& newAlias); - void setTopic(const QString& newTopic); - - void getPreviousContent(int limit = 10); - - void inviteToRoom(const QString& memberId); - LeaveRoomJob* leaveRoom(); - void kickMember(const QString& memberId, const QString& reason = {}); - void ban(const QString& userId, const QString& reason = {}); - void unban(const QString& userId); - void redactEvent(const QString& eventId, - const QString& reason = {}); - - void uploadFile(const QString& id, const QUrl& localFilename, - const QString& overrideContentType = {}); - // If localFilename is empty a temporary file is created - void downloadFile(const QString& eventId, - const QUrl& localFilename = {}); - void cancelFileTransfer(const QString& id); - - /** Mark all messages in the room as read */ - void markAllMessagesAsRead(); - - signals: - void aboutToAddHistoricalMessages(RoomEventsRange events); - void aboutToAddNewMessages(RoomEventsRange events); - void addedMessages(); - - /** - * @brief The room name, the canonical alias or other aliases changed - * - * Not triggered when displayname changes. - */ - void namesChanged(Room* room); - /** @brief The room displayname changed */ - void displaynameChanged(Room* room); - void topicChanged(); - void avatarChanged(); - void userAdded(User* user); - void userRemoved(User* user); - void memberAboutToRename(User* user, QString newName); - void memberRenamed(User* user); - void memberListChanged(); - void encryption(); - - void joinStateChanged(JoinState oldState, JoinState newState); - void typingChanged(); - - void highlightCountChanged(Room* room); - void notificationCountChanged(Room* room); - - void displayedChanged(bool displayed); - void firstDisplayedEventChanged(); - void lastDisplayedEventChanged(); - void lastReadEventChanged(User* user); - void readMarkerMoved(); - void unreadMessagesChanged(Room* room); - - void tagsChanged(); - - void replacedEvent(const RoomEvent* newEvent, - const RoomEvent* oldEvent); - - void newFileTransfer(QString id, QUrl localFile); - void fileTransferProgress(QString id, qint64 progress, qint64 total); - void fileTransferCompleted(QString id, QUrl localFile, QUrl mxcUrl); - void fileTransferFailed(QString id, QString errorMessage = {}); - void fileTransferCancelled(QString id); - - protected: - virtual void processStateEvents(const RoomEvents& events); - virtual void processEphemeralEvent(EventPtr event); - virtual void processAccountDataEvent(EventPtr event); - virtual void onAddNewTimelineEvents(timeline_iter_t from) { } - virtual void onAddHistoricalTimelineEvents(rev_iter_t from) { } - virtual void onRedaction(const RoomEvent* prevEvent, - const RoomEvent* after) { } - - private: - class Private; - Private* d; - }; - - class MemberSorter - { - public: - explicit MemberSorter(const Room* r) : room(r) { } - - bool operator()(User* u1, User* u2) const; - bool operator()(User* u1, const QString& u2name) const; - - template - typename ContT::size_type lowerBoundIndex(const ContT& c, - const ValT& v) const - { - return std::lower_bound(c.begin(), c.end(), v, *this) - c.begin(); - } - - private: - const Room* room; - }; -} // namespace QMatrixClient -Q_DECLARE_METATYPE(QMatrixClient::FileTransferInfo) diff --git a/settings.cpp b/settings.cpp deleted file mode 100644 index bf369c58..00000000 --- a/settings.cpp +++ /dev/null @@ -1,123 +0,0 @@ -#include "settings.h" - -#include "logging.h" - -#include - -using namespace QMatrixClient; - -QString Settings::legacyOrganizationName {}; -QString Settings::legacyApplicationName {}; - -void Settings::setLegacyNames(const QString& organizationName, - const QString& applicationName) -{ - legacyOrganizationName = organizationName; - legacyApplicationName = applicationName; -} - -void Settings::setValue(const QString& key, const QVariant& value) -{ -// qCDebug() << "Setting" << key << "to" << value; - QSettings::setValue(key, value); - if (legacySettings.contains(key)) - legacySettings.remove(key); -} - -QVariant Settings::value(const QString& key, const QVariant& defaultValue) const -{ - auto value = QSettings::value(key, legacySettings.value(key, defaultValue)); - // QML's Qt.labs.Settings stores boolean values as strings, which, if loaded - // through the usual QSettings interface, confuses QML - // (QVariant("false") == true in JavaScript). Since we have a mixed - // environment where both QSettings and Qt.labs.Settings may potentially - // work with same settings, better ensure compatibility. - return value.toString() == QStringLiteral("false") ? QVariant(false) : value; -} - -bool Settings::contains(const QString& key) const -{ - return QSettings::contains(key) || legacySettings.contains(key); -} - -QStringList Settings::childGroups() const -{ - auto l = QSettings::childGroups(); - return !l.isEmpty() ? l : legacySettings.childGroups(); -} - -void SettingsGroup::setValue(const QString& key, const QVariant& value) -{ - Settings::setValue(groupPath + '/' + key, value); -} - -bool SettingsGroup::contains(const QString& key) const -{ - return Settings::contains(groupPath + '/' + key); -} - -QVariant SettingsGroup::value(const QString& key, const QVariant& defaultValue) const -{ - return Settings::value(groupPath + '/' + key, defaultValue); -} - -QString SettingsGroup::group() const -{ - return groupPath; -} - -QStringList SettingsGroup::childGroups() const -{ - const_cast(this)->beginGroup(groupPath); - const_cast(legacySettings).beginGroup(groupPath); - QStringList l = Settings::childGroups(); - const_cast(this)->endGroup(); - const_cast(legacySettings).endGroup(); - return l; -} - -void SettingsGroup::remove(const QString& key) -{ - QString fullKey { groupPath }; - if (!key.isEmpty()) - fullKey += "/" + key; - Settings::remove(fullKey); -} - -QMC_DEFINE_SETTING(AccountSettings, QString, deviceId, "device_id", "", setDeviceId) -QMC_DEFINE_SETTING(AccountSettings, QString, deviceName, "device_name", "", setDeviceName) -QMC_DEFINE_SETTING(AccountSettings, bool, keepLoggedIn, "keep_logged_in", false, setKeepLoggedIn) - -QUrl AccountSettings::homeserver() const -{ - return QUrl::fromUserInput(value("homeserver").toString()); -} - -void AccountSettings::setHomeserver(const QUrl& url) -{ - setValue("homeserver", url.toString()); -} - -QString AccountSettings::userId() const -{ - return group().section('/', -1); -} - -QString AccountSettings::accessToken() const -{ - return value("access_token").toString(); -} - -void AccountSettings::setAccessToken(const QString& accessToken) -{ - qCWarning(MAIN) << "Saving access_token to QSettings is insecure." - " Developers, please save access_token separately."; - setValue("access_token", accessToken); -} - -void AccountSettings::clearAccessToken() -{ - legacySettings.remove("access_token"); - legacySettings.remove("device_id"); // Force the server to re-issue it - remove("access_token"); -} diff --git a/settings.h b/settings.h deleted file mode 100644 index 27ec9ba5..00000000 --- a/settings.h +++ /dev/null @@ -1,134 +0,0 @@ -/****************************************************************************** - * Copyright (C) 2016 Kitsune Ral - * - * 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 - */ - -#pragma once - -#include -#include -#include - -class QVariant; - -namespace QMatrixClient -{ - class Settings: public QSettings - { - Q_OBJECT - public: - /** - * Use this function before creating any Settings objects in order - * to setup a read-only location where configuration has previously - * been stored. This will provide an additional fallback in case of - * renaming the organisation/application. - */ - static void setLegacyNames(const QString& organizationName, - const QString& applicationName = {}); - -#if defined(_MSC_VER) && _MSC_VER < 1900 - // VS 2013 (and probably older) aren't friends with 'using' statements - // that involve private constructors - explicit Settings(QObject* parent = 0) : QSettings(parent) { } -#else - using QSettings::QSettings; -#endif - - Q_INVOKABLE void setValue(const QString &key, - const QVariant &value); - Q_INVOKABLE QVariant value(const QString &key, - const QVariant &defaultValue = {}) const; - Q_INVOKABLE bool contains(const QString& key) const; - Q_INVOKABLE QStringList childGroups() const; - - private: - static QString legacyOrganizationName; - static QString legacyApplicationName; - - protected: - QSettings legacySettings { legacyOrganizationName, - legacyApplicationName }; - }; - - class SettingsGroup: public Settings - { - public: - template - explicit SettingsGroup(const QString& path, ArgTs... qsettingsArgs) - : Settings(qsettingsArgs...) - , groupPath(path) - { } - - Q_INVOKABLE bool contains(const QString& key) const; - Q_INVOKABLE QVariant value(const QString &key, - const QVariant &defaultValue = {}) const; - Q_INVOKABLE QString group() const; - Q_INVOKABLE QStringList childGroups() const; - Q_INVOKABLE void setValue(const QString &key, - const QVariant &value); - - Q_INVOKABLE void remove(const QString& key); - - private: - QString groupPath; - }; - -#define QMC_DECLARE_SETTING(type, propname, setter) \ - Q_PROPERTY(type propname READ propname WRITE setter) \ - public: \ - type propname() const; \ - void setter(type newValue); \ - private: - -#define QMC_DEFINE_SETTING(classname, type, propname, qsettingname, defaultValue, setter) \ -type classname::propname() const \ -{ \ - return value(QStringLiteral(qsettingname), defaultValue).value(); \ -} \ -\ -void classname::setter(type newValue) \ -{ \ - setValue(QStringLiteral(qsettingname), newValue); \ -} \ - - class AccountSettings: public SettingsGroup - { - Q_OBJECT - Q_PROPERTY(QString userId READ userId CONSTANT) - QMC_DECLARE_SETTING(QString, deviceId, setDeviceId) - QMC_DECLARE_SETTING(QString, deviceName, setDeviceName) - QMC_DECLARE_SETTING(bool, keepLoggedIn, setKeepLoggedIn) - /** \deprecated \sa setAccessToken */ - Q_PROPERTY(QString accessToken READ accessToken WRITE setAccessToken) - public: - template - explicit AccountSettings(const QString& accountId, ArgTs... qsettingsArgs) - : SettingsGroup("Accounts/" + accountId, qsettingsArgs...) - { } - - QString userId() const; - - QUrl homeserver() const; - void setHomeserver(const QUrl& url); - - /** \deprecated \sa setToken */ - QString accessToken() const; - /** \deprecated Storing accessToken in QSettings is unsafe, - * see QMatrixClient/Quaternion#181 */ - void setAccessToken(const QString& accessToken); - Q_INVOKABLE void clearAccessToken(); - }; -} // namespace QMatrixClient diff --git a/state.cpp b/state.cpp deleted file mode 100644 index 59a3b007..00000000 --- a/state.cpp +++ /dev/null @@ -1,69 +0,0 @@ -/****************************************************************************** - * Copyright (C) 2015 Felix Rohrbach - * - * 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 - */ - -#include "state.h" - -#include "events/event.h" - -using namespace QMatrixClient; - -class State::Private -{ - public: - Event* event; - QString stateKey; - QString replacesState; -}; - - -State::State(Event* event) - : d(new Private) -{ - d->event = event; -} - -State::~State() -{ - delete d; -} - -Event* State::event() const -{ - return d->event; -} - -QString State::stateKey() const -{ - return d->stateKey; -} - -QString State::replacesState() const -{ - return d->replacesState; -} - -State* State::fromJson(const QJsonObject& obj) -{ - Event* event = Event::fromJson(obj); - if( !event ) - return nullptr; - State* state = new State(event); - state->d->stateKey = obj.value("state_key").toString(); - state->d->replacesState = obj.value("replaces_state").toString(); - return state; -} diff --git a/state.h b/state.h deleted file mode 100644 index b059ea92..00000000 --- a/state.h +++ /dev/null @@ -1,47 +0,0 @@ -/****************************************************************************** - * Copyright (C) 2015 Felix Rohrbach - * - * 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 - */ - -#pragma once - -#include -#include - -namespace QMatrixClient -{ - class Event; - - /** - * Wraps an event that is a state - */ - class State - { - public: - State(Event* event); - virtual ~State(); - - Event* event() const; - QString stateKey() const; - QString replacesState() const; - - static State* fromJson(const QJsonObject& obj); - - private: - class Private; - Private* d; - }; -} diff --git a/user.cpp b/user.cpp deleted file mode 100644 index 7a6dbc73..00000000 --- a/user.cpp +++ /dev/null @@ -1,399 +0,0 @@ -/****************************************************************************** - * Copyright (C) 2015 Felix Rohrbach - * - * 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 - */ - -#include "user.h" - -#include "connection.h" -#include "room.h" -#include "avatar.h" -#include "events/event.h" -#include "events/roommemberevent.h" -#include "jobs/setroomstatejob.h" -#include "jobs/generated/profile.h" -#include "jobs/generated/content-repo.h" - -#include -#include -#include -#include -#include - -#include - -using namespace QMatrixClient; -using namespace std::placeholders; -using std::move; - -class User::Private -{ - public: - static Avatar makeAvatar(QUrl url) - { - static const QIcon icon - { QIcon::fromTheme(QStringLiteral("user-available")) }; - return Avatar(move(url), icon); - } - - Private(QString userId, Connection* connection) - : userId(move(userId)), connection(connection) - { } - - QString userId; - Connection* connection; - - QString bridged; - QString mostUsedName; - QMultiHash otherNames; - Avatar mostUsedAvatar { makeAvatar({}) }; - std::vector otherAvatars; - auto otherAvatar(QUrl url) - { - return std::find_if(otherAvatars.begin(), otherAvatars.end(), - [&url] (const auto& av) { return av.url() == url; }); - } - QMultiHash avatarsToRooms; - - mutable int totalRooms = 0; - - QString nameForRoom(const Room* r, const QString& hint = {}) const; - void setNameForRoom(const Room* r, QString newName, QString oldName); - QUrl avatarUrlForRoom(const Room* r, const QUrl& hint = {}) const; - void setAvatarForRoom(const Room* r, const QUrl& newUrl, - const QUrl& oldUrl); - - void setAvatarOnServer(QString contentUri, User* q); - -}; - - -QString User::Private::nameForRoom(const Room* r, const QString& hint) const -{ - // If the hint is accurate, this function is O(1) instead of O(n) - if (hint == mostUsedName || otherNames.contains(hint, r)) - return hint; - return otherNames.key(r, mostUsedName); -} - -static constexpr int MIN_JOINED_ROOMS_TO_LOG = 20; - -void User::Private::setNameForRoom(const Room* r, QString newName, - QString oldName) -{ - Q_ASSERT(oldName != newName); - Q_ASSERT(oldName == mostUsedName || otherNames.contains(oldName, r)); - if (totalRooms < 2) - { - Q_ASSERT_X(totalRooms > 0 && otherNames.empty(), __FUNCTION__, - "Internal structures inconsistency"); - mostUsedName = move(newName); - return; - } - otherNames.remove(oldName, r); - if (newName != mostUsedName) - { - // Check if the newName is about to become most used. - if (otherNames.count(newName) >= totalRooms - otherNames.size()) - { - Q_ASSERT(totalRooms > 1); - QElapsedTimer et; - if (totalRooms > MIN_JOINED_ROOMS_TO_LOG) - { - qCDebug(MAIN) << "Switching the most used name of user" << userId - << "from" << mostUsedName << "to" << newName; - qCDebug(MAIN) << "The user is in" << totalRooms << "rooms"; - et.start(); - } - - for (auto* r1: connection->roomMap()) - if (nameForRoom(r1) == mostUsedName) - otherNames.insert(mostUsedName, r1); - - mostUsedName = newName; - otherNames.remove(newName); - if (totalRooms > MIN_JOINED_ROOMS_TO_LOG) - qCDebug(PROFILER) << et << "to switch the most used name"; - } - else - otherNames.insert(newName, r); - } -} - -QUrl User::Private::avatarUrlForRoom(const Room* r, const QUrl& hint) const -{ - // If the hint is accurate, this function is O(1) instead of O(n) - if (hint == mostUsedAvatar.url() || avatarsToRooms.contains(hint, r)) - return hint; - auto it = std::find(avatarsToRooms.begin(), avatarsToRooms.end(), r); - return it == avatarsToRooms.end() ? mostUsedAvatar.url() : it.key(); -} - -void User::Private::setAvatarForRoom(const Room* r, const QUrl& newUrl, - const QUrl& oldUrl) -{ - Q_ASSERT(oldUrl != newUrl); - Q_ASSERT(oldUrl == mostUsedAvatar.url() || - avatarsToRooms.contains(oldUrl, r)); - if (totalRooms < 2) - { - Q_ASSERT_X(totalRooms > 0 && otherAvatars.empty(), __FUNCTION__, - "Internal structures inconsistency"); - mostUsedAvatar.updateUrl(newUrl); - return; - } - avatarsToRooms.remove(oldUrl, r); - if (!avatarsToRooms.contains(oldUrl)) - { - auto it = otherAvatar(oldUrl); - if (it != otherAvatars.end()) - otherAvatars.erase(it); - } - if (newUrl != mostUsedAvatar.url()) - { - // Check if the new avatar is about to become most used. - if (avatarsToRooms.count(newUrl) >= totalRooms - avatarsToRooms.size()) - { - QElapsedTimer et; - if (totalRooms > MIN_JOINED_ROOMS_TO_LOG) - { - qCDebug(MAIN) << "Switching the most used avatar of user" << userId - << "from" << mostUsedAvatar.url().toDisplayString() - << "to" << newUrl.toDisplayString(); - et.start(); - } - avatarsToRooms.remove(newUrl); - auto nextMostUsedIt = otherAvatar(newUrl); - Q_ASSERT(nextMostUsedIt != otherAvatars.end()); - std::swap(mostUsedAvatar, *nextMostUsedIt); - for (const auto* r1: connection->roomMap()) - if (avatarUrlForRoom(r1) == nextMostUsedIt->url()) - avatarsToRooms.insert(nextMostUsedIt->url(), r1); - - if (totalRooms > MIN_JOINED_ROOMS_TO_LOG) - qCDebug(PROFILER) << et << "to switch the most used avatar"; - } else { - if (otherAvatar(newUrl) == otherAvatars.end()) - otherAvatars.emplace_back(makeAvatar(newUrl)); - avatarsToRooms.insert(newUrl, r); - } - } -} - -User::User(QString userId, Connection* connection) - : QObject(connection), d(new Private(move(userId), connection)) -{ - setObjectName(userId); -} - -User::~User() = default; - -QString User::id() const -{ - return d->userId; -} - -bool User::isGuest() const -{ - Q_ASSERT(!d->userId.isEmpty() && d->userId.startsWith('@')); - auto it = std::find_if_not(d->userId.begin() + 1, d->userId.end(), - [] (QChar c) { return c.isDigit(); }); - Q_ASSERT(it != d->userId.end()); - return *it == ':'; -} - -QString User::name(const Room* room) const -{ - return d->nameForRoom(room); -} - -void User::updateName(const QString& newName, const Room* room) -{ - updateName(newName, d->nameForRoom(room), room); -} - -void User::updateName(const QString& newName, const QString& oldName, - const Room* room) -{ - Q_ASSERT(oldName == d->mostUsedName || d->otherNames.contains(oldName, room)); - if (newName != oldName) - { - emit nameAboutToChange(newName, oldName, room); - d->setNameForRoom(room, newName, oldName); - setObjectName(displayname()); - emit nameChanged(newName, oldName, room); - } -} - -void User::updateAvatarUrl(const QUrl& newUrl, const QUrl& oldUrl, - const Room* room) -{ - Q_ASSERT(oldUrl == d->mostUsedAvatar.url() || - d->avatarsToRooms.contains(oldUrl, room)); - if (newUrl != oldUrl) - { - d->setAvatarForRoom(room, newUrl, oldUrl); - setObjectName(displayname()); - emit avatarChanged(this, room); - } - -} - -void User::rename(const QString& newName) -{ - auto job = d->connection->callApi(id(), newName); - connect(job, &BaseJob::success, this, [=] { updateName(newName); }); -} - -void User::rename(const QString& newName, const Room* r) -{ - if (!r) - { - qCWarning(MAIN) << "Passing a null room to two-argument User::rename()" - "is incorrect; client developer, please fix it"; - rename(newName); - } - Q_ASSERT_X(r->memberJoinState(this) == JoinState::Join, __FUNCTION__, - "Attempt to rename a user that's not a room member"); - MemberEventContent evtC; - evtC.displayName = newName; - auto job = d->connection->callApi( - r->id(), id(), RoomMemberEvent(move(evtC))); - connect(job, &BaseJob::success, this, [=] { updateName(newName, r); }); -} - -bool User::setAvatar(const QString& fileName) -{ - return avatarObject().upload(d->connection, fileName, - std::bind(&Private::setAvatarOnServer, d.data(), _1, this)); -} - -bool User::setAvatar(QIODevice* source) -{ - return avatarObject().upload(d->connection, source, - std::bind(&Private::setAvatarOnServer, d.data(), _1, this)); -} - -void User::requestDirectChat() -{ - Q_ASSERT(d->connection); - d->connection->requestDirectChat(d->userId); -} - -void User::Private::setAvatarOnServer(QString contentUri, User* q) -{ - auto* j = connection->callApi(userId, contentUri); - connect(j, &BaseJob::success, q, - [=] { q->updateAvatarUrl(contentUri, avatarUrlForRoom(nullptr)); }); -} - -QString User::displayname(const Room* room) const -{ - auto name = d->nameForRoom(room); - return name.isEmpty() ? d->userId : - room ? room->roomMembername(name) : name; -} - -QString User::fullName(const Room* room) const -{ - auto name = d->nameForRoom(room); - return name.isEmpty() ? d->userId : name % " (" % d->userId % ')'; -} - -QString User::bridged() const -{ - return d->bridged; -} - -const Avatar& User::avatarObject(const Room* room) const -{ - auto it = d->otherAvatar(d->avatarUrlForRoom(room)); - return it != d->otherAvatars.end() ? *it : d->mostUsedAvatar; -} - -QImage User::avatar(int dimension, const Room* room) -{ - return avatar(dimension, dimension, room); -} - -QImage User::avatar(int width, int height, const Room* room) -{ - return avatar(width, height, room, []{}); -} - -QImage User::avatar(int width, int height, const Room* room, - Avatar::get_callback_t callback) -{ - return avatarObject(room).get(d->connection, width, height, - [=] { emit avatarChanged(this, room); callback(); }); -} - -QString User::avatarMediaId(const Room* room) const -{ - return avatarObject(room).mediaId(); -} - -QUrl User::avatarUrl(const Room* room) const -{ - return avatarObject(room).url(); -} - -void User::processEvent(RoomMemberEvent* event, const Room* room) -{ - if (event->membership() != MembershipType::Invite && - event->membership() != MembershipType::Join) - return; - - auto aboutToEnter = room->memberJoinState(this) == JoinState::Leave && - (event->membership() == MembershipType::Join || - event->membership() == MembershipType::Invite); - if (aboutToEnter) - ++d->totalRooms; - - auto newName = event->displayName(); - // `bridged` value uses the same notification signal as the name; - // it is assumed that first setting of the bridge occurs together with - // the first setting of the name, and further bridge updates are - // exceptionally rare (the only reasonable case being that the bridge - // changes the naming convention). For the same reason room-specific - // bridge tags are not supported at all. - QRegularExpression reSuffix(" \\((IRC|Gitter|Telegram)\\)$"); - auto match = reSuffix.match(newName); - if (match.hasMatch()) - { - if (d->bridged != match.captured(1)) - { - if (!d->bridged.isEmpty()) - qCWarning(MAIN) << "Bridge for user" << id() << "changed:" - << d->bridged << "->" << match.captured(1); - d->bridged = match.captured(1); - } - newName.truncate(match.capturedStart(0)); - } - if (event->prevContent()) - { - // FIXME: the hint doesn't work for bridged users - auto oldNameHint = - d->nameForRoom(room, event->prevContent()->displayName); - updateName(event->displayName(), oldNameHint, room); - updateAvatarUrl(event->avatarUrl(), - d->avatarUrlForRoom(room, event->prevContent()->avatarUrl), - room); - } else { - updateName(event->displayName(), room); - updateAvatarUrl(event->avatarUrl(), d->avatarUrlForRoom(room), room); - } -} diff --git a/user.h b/user.h deleted file mode 100644 index f76f9e0a..00000000 --- a/user.h +++ /dev/null @@ -1,125 +0,0 @@ -/****************************************************************************** - * Copyright (C) 2015 Felix Rohrbach - * - * 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 - */ - -#pragma once - -#include -#include -#include "avatar.h" - -namespace QMatrixClient -{ - class Connection; - class Room; - class RoomMemberEvent; - - class User: public QObject - { - Q_OBJECT - Q_PROPERTY(QString id READ id CONSTANT) - Q_PROPERTY(bool isGuest READ isGuest CONSTANT) - Q_PROPERTY(QString name READ name NOTIFY nameChanged) - Q_PROPERTY(QString displayName READ displayname NOTIFY nameChanged STORED false) - Q_PROPERTY(QString fullName READ fullName NOTIFY nameChanged STORED false) - Q_PROPERTY(QString bridgeName READ bridged NOTIFY nameChanged STORED false) - Q_PROPERTY(QString avatarMediaId READ avatarMediaId NOTIFY avatarChanged STORED false) - Q_PROPERTY(QUrl avatarUrl READ avatarUrl NOTIFY avatarChanged) - public: - User(QString userId, Connection* connection); - ~User() override; - - /** Get unique stable user id - * User id is generated by the server and is not changed ever. - */ - QString id() const; - - /** Get the name chosen by the user - * This may be empty if the user didn't choose the name or cleared - * it. - * \sa displayName - */ - QString name(const Room* room = nullptr) const; - - /** Get the displayed user name - * This method returns the result of name() if its non-empty; - * otherwise it returns user id. This is convenient to show a user - * name outside of a room context. In a room context, user names - * should be disambiguated. - * \sa name, id, fullName Room::roomMembername - */ - QString displayname(const Room* room = nullptr) const; - - /** Get user name and id in one string - * The constructed string follows the format 'name (id)' - * used for users disambiguation in a room context and in other - * places. - * \sa displayName, Room::roomMembername - */ - QString fullName(const Room* room = nullptr) const; - - /** - * Returns the name of bridge the user is connected from or empty. - */ - QString bridged() const; - - /** Whether the user is a guest - * As of now, the function relies on the convention used in Synapse - * that guests and only guests have all-numeric IDs. This may or - * may not work with non-Synapse servers. - */ - bool isGuest() const; - - const Avatar& avatarObject(const Room* room = nullptr) const; - Q_INVOKABLE QImage avatar(int dimension, const Room* room = nullptr); - Q_INVOKABLE QImage avatar(int requestedWidth, int requestedHeight, - const Room* room = nullptr); - QImage avatar(int width, int height, const Room* room, - Avatar::get_callback_t callback); - - QString avatarMediaId(const Room* room = nullptr) const; - QUrl avatarUrl(const Room* room = nullptr) const; - - void processEvent(RoomMemberEvent* event, const Room* r = nullptr); - - public slots: - void rename(const QString& newName); - void rename(const QString& newName, const Room* r); - bool setAvatar(const QString& fileName); - bool setAvatar(QIODevice* source); - void requestDirectChat(); - - signals: - void nameAboutToChange(QString newName, QString oldName, - const Room* roomContext); - void nameChanged(QString newName, QString oldName, - const Room* roomContext); - void avatarChanged(User* user, const Room* roomContext); - - private slots: - void updateName(const QString& newName, const Room* room = nullptr); - void updateName(const QString& newName, const QString& oldName, - const Room* room = nullptr); - void updateAvatarUrl(const QUrl& newUrl, const QUrl& oldUrl, - const Room* room = nullptr); - - private: - class Private; - QScopedPointer d; - }; -} -Q_DECLARE_METATYPE(QMatrixClient::User*) diff --git a/util.h b/util.h deleted file mode 100644 index 65de0610..00000000 --- a/util.h +++ /dev/null @@ -1,205 +0,0 @@ -/****************************************************************************** - * Copyright (C) 2016 Kitsune Ral - * - * 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 - */ - -#pragma once - -#include -#include - -#include - -namespace QMatrixClient -{ - /** - * @brief Lookup a value by a key in a varargs list - * - * This function template takes the value of its first argument (selector) - * as a key and searches for it in the key-value map passed in - * a parameter pack (every next pair of arguments forms a key-value pair). - * If a match is found, the respective value is returned; if no pairs - * matched, the last value (fallback) is returned. - * - * All options should be of the same type or implicitly castable to the - * type of the first option. If you need some specific type to cast to - * you can explicitly provide it as the ValueT template parameter - * (e.g. lookup(parameters...)). Note that pointers - * to methods of different classes and even to functions with different - * signatures are of different types. If their return types are castable - * to some common one, @see dispatch that deals with this by swallowing - * the method invocation. - * - * Below is an example of usage to select a parser depending on contents of - * a JSON object: - * {@code - * auto parser = lookup(obj.value["type"].toString(), - * "type1", fn1, - * "type2", fn2, - * fallbackFn); - * parser(obj); - * } - * - * The implementation is based on tail recursion; every recursion step - * removes 2 arguments (match and value). There's no selector value for the - * fallback option (the last one); therefore, the total number of lookup() - * arguments should be even: selector + n key-value pairs + fallback - * - * @note Beware of calling lookup() with a const char* selector - * (the first parameter) - most likely it won't do what you expect because - * of shallow comparison. - */ - template - ValueT lookup(SelectorT/*unused*/, ValueT&& fallback) - { - return std::forward(fallback); - } - - template - ValueT lookup(SelectorT&& selector, KeyT&& key, ValueT&& value, Ts&&... remainder) - { - if( selector == key ) - return std::forward(value); - - // Drop the failed key-value pair and recurse with 2 arguments less. - return lookup(std::forward(selector), - std::forward(remainder)...); - } - - /** - * A wrapper around lookup() for functions of different types castable - * to a common std::function<> form - * - * This class uses std::function<> magic to first capture arguments of - * a yet-unknown function or function object, and then to coerce types of - * all functions/function objects passed for lookup to the type - * std::function, you would have - * to pass the specific function type to lookup, since your functions have - * different signatures. The type is not always obvious, and the resulting - * construct in client code would almost always be rather cumbersome. - * Dispatch<> deduces the necessary function type (well, almost - you still - * have to specify the result type) and hides the clumsiness. For more - * information on what std::function<> can wrap around, see - * https://cpptruths.blogspot.jp/2015/11/covariance-and-contravariance-in-c.html - * - * The function arguments are captured by value (i.e. copied) to avoid - * hard-to-find issues with dangling references in cases when a Dispatch<> - * object is passed across different contexts (e.g. returned from another - * function). - * - * \tparam ResultT - the desired type of a picked function invocation (mandatory) - * \tparam ArgTs - function argument types (deduced) - */ -#if __GNUC__ < 5 && __GNUC_MINOR__ < 9 - // GCC 4.8 cannot cope with parameter packs inside lambdas; so provide a single - // argument version of Dispatch<> that we only need so far. - template -#else - template -#endif - class Dispatch - { - // The implementation takes a chapter from functional programming: - // Dispatch<> uses a function that in turn accepts a function as its - // argument. The sole purpose of the outer function (initialized by - // a lambda-expression in the constructor) is to store the arguments - // to any of the functions later looked up. The inner function (its - // type is defined by fn_t alias) is the one returned by lookup() - // invocation inside to(). - // - // It's a bit counterintuitive to specify function parameters before - // the list of functions but otherwise it would take several overloads - // here to match all the ways a function-like behaviour can be done: - // reference-to-function, pointer-to-function, function object. This - // probably could be done as well but I preferred a more compact - // solution: you show what you have and if it's possible to bring all - // your functions to the same std::function<> based on what you have - // as parameters, the code will compile. If it's not possible, modern - // compilers are already good enough at pinpointing a specific place - // where types don't match. - public: -#if __GNUC__ < 5 && __GNUC_MINOR__ < 9 - using fn_t = std::function; - explicit Dispatch(ArgT&& arg) - : boundArgs([=](fn_t &&f) { return f(std::move(arg)); }) - { } -#else - using fn_t = std::function; - explicit Dispatch(ArgTs&&... args) - : boundArgs([=](fn_t &&f) { return f(std::move(args)...); }) - { } -#endif - - template - ResultT to(LookupParamTs&&... lookupParams) - { - // Here's the magic, two pieces of it: - // 1. Specifying fn_t in lookup() wraps all functions in - // \p lookupParams into the same std::function<> type. This - // includes conversion of return types from more specific to more - // generic (because std::function is covariant by return types and - // contravariant by argument types (see the link in the Doxygen - // part of the comments). - auto fn = lookup(std::forward(lookupParams)...); - // 2. Passing the result of lookup() to boundArgs() invokes the - // lambda-expression mentioned in the constructor, which simply - // invokes this passed function with a set of arguments captured - // by lambda. - if (fn) - return boundArgs(std::move(fn)); - - // A shortcut to allow passing nullptr for a function; - // a default-constructed ResultT will be returned - // (for pointers, it will be nullptr) - return {}; - } - - private: - std::function boundArgs; - }; - - /** - * Dispatch a set of parameters to one of a set of functions, depending on - * a selector value - * - * Use dispatch(parameters).to(lookup parameters) - * instead of lookup() if you need to pick one of several functions returning - * types castable to the same CommonType. See event.cpp for a typical use case. - * - * \see Dispatch - */ - template - Dispatch dispatch(ArgTs&& ... args) - { - return Dispatch(std::forward(args)...); - } - - // The below enables pretty-printing of enums in logs -#if (QT_VERSION >= QT_VERSION_CHECK(5, 5, 0)) -#define REGISTER_ENUM(EnumName) Q_ENUM(EnumName) -#else - // Thanks to Olivier for spelling it and for making Q_ENUM to replace it: - // https://woboq.com/blog/q_enum.html -#define REGISTER_ENUM(EnumName) \ - Q_ENUMS(EnumName) \ - friend QDebug operator<<(QDebug dbg, EnumName val) \ - { \ - static int enumIdx = staticMetaObject.indexOfEnumerator(#EnumName); \ - return dbg << Event::staticMetaObject.enumerator(enumIdx).valueToKey(int(val)); \ - } -#endif -} // namespace QMatrixClient - -- cgit v1.2.3 From 9222abb1ae7e40cdf1d1eb62f6d38d2168f1c133 Mon Sep 17 00:00:00 2001 From: Kitsune Ral Date: Sat, 31 Mar 2018 14:55:07 +0900 Subject: Install CMake files to the proper place --- CMakeLists.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index c5511935..aca82c49 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -121,7 +121,7 @@ install (TARGETS qmc-example RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR}) #export(TARGETS qmatrixclient FILE QMatrixClientConfig.cmake EXPORT_LINK_INTERFACE_LIBRARIES) export(EXPORT QMatrixClient) export(PACKAGE QMatrixClient) -install (EXPORT QMatrixClient DESTINATION ${CMAKE_INSTALL_LIBDIR}/cmake/qmatrixclient) +install (EXPORT QMatrixClient DESTINATION ${CMAKE_INSTALL_LIBDIR}/cmake) if (WIN32) install (FILES mime/packages/freedesktop.org.xml -- cgit v1.2.3 From 9d8900197e69e9c0ffaaff6f63a40cb80cf08fb1 Mon Sep 17 00:00:00 2001 From: Kitsune Ral Date: Sat, 31 Mar 2018 15:41:16 +0900 Subject: BaseJob::gotReply(): Minor fix in logging --- jobs/basejob.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/jobs/basejob.cpp b/jobs/basejob.cpp index 3cde7c50..a23f43b3 100644 --- a/jobs/basejob.cpp +++ b/jobs/basejob.cpp @@ -285,7 +285,7 @@ void BaseJob::gotReply() // Shortcut to retry instead of executing finishJob() stop(); qCWarning(d->logCat) - << this << "will retry in" << retryInterval; + << this << "will retry in" << retryInterval << "ms"; d->retryTimer.start(retryInterval); emit retryScheduled(d->retriesTaken, retryInterval); return; -- cgit v1.2.3 From 6a61d3a127db1e253821bfb2ebb7f433bd534c4a Mon Sep 17 00:00:00 2001 From: Kitsune Ral Date: Sat, 31 Mar 2018 14:55:41 +0900 Subject: Make and install CMake config package; provide examples/CMakeLists.txt using it --- .travis.yml | 13 ++-- CMakeLists.txt | 73 ++++++++++++++-------- cmake/QMatrixClientConfig.cmake | 1 + examples/CMakeLists.txt | 69 +++++++++++++++++++++ libqmatrixclient.pri | 131 ++++++++++++++++++++-------------------- 5 files changed, 192 insertions(+), 95 deletions(-) create mode 100644 cmake/QMatrixClientConfig.cmake create mode 100644 examples/CMakeLists.txt diff --git a/.travis.yml b/.travis.yml index c0008af0..45db175a 100644 --- a/.travis.yml +++ b/.travis.yml @@ -25,15 +25,20 @@ before_install: - if [ "$TRAVIS_OS_NAME" = "linux" ]; then VALGRIND="valgrind $VALGRIND_OPTIONS"; . /opt/qt56/bin/qt56-env.sh; fi script: -- mkdir build && cd build +# Build and install with CMake +- mkdir build && pushd build - cmake -DCMAKE_INSTALL_PREFIX=../install .. - cmake --build . --target all - cmake --build . --target install -- cd ../examples -- $CC qmc-example.cpp -I../install/include -l../install/lib/libqmatrixclient.a -o qmc-example-lib-installed -- cd .. +# Build qmc-example with the installed library +- popd && mkdir build-example && pushd build-example +- cmake -DCMAKE_PREFIX_PATH=../install ../examples +- cmake --build . --target all +- popd +# Build and install with qmake - qmake qmc-example.pro "CONFIG += debug" "CONFIG -= app_bundle" "QMAKE_CC = $CC" "QMAKE_CXX = $CXX" - make all +# Run the qmake-compiled qmc-example under valgrind - if [ "$QMC_TEST_USER" != "" ]; then $VALGRIND ./qmc-example "$QMC_TEST_USER" "$QMC_TEST_PWD" qmc-example-travis '#qmc-test:matrix.org' "Travis CI job $TRAVIS_JOB_NUMBER"; fi notifications: diff --git a/CMakeLists.txt b/CMakeLists.txt index aca82c49..956c9a1f 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -7,8 +7,6 @@ if (NOT WIN32) include(GNUInstallDirs) endif(NOT WIN32) -# Find includes in corresponding build directories -#set(CMAKE_INCLUDE_CURRENT_DIR ON) # Instruct CMake to run moc automatically when needed. set(CMAKE_AUTOMOC ON) @@ -92,38 +90,61 @@ aux_source_directory(lib/jobs/generated libqmatrixclient_job_SRCS) set(example_SRCS examples/qmc-example.cpp) -add_library(qmatrixclient ${libqmatrixclient_SRCS} ${libqmatrixclient_job_SRCS}) -set_property(TARGET qmatrixclient PROPERTY VERSION "0.2.0") -set_property(TARGET qmatrixclient PROPERTY SOVERSION 0 ) - -target_include_directories(qmatrixclient PUBLIC - $ - $ +add_library(QMatrixClient ${libqmatrixclient_SRCS} ${libqmatrixclient_job_SRCS}) +set(API_VERSION 2) +set_property(TARGET QMatrixClient PROPERTY VERSION "0.2.0") +set_property(TARGET QMatrixClient PROPERTY SOVERSION 0 ) +set_property(TARGET QMatrixClient PROPERTY + INTERFACE_QMatrixClient_MAJOR_VERSION ${API_VERSION}) +set_property(TARGET QMatrixClient APPEND PROPERTY + COMPATIBLE_INTERFACE_STRING QMatrixClient_MAJOR_VERSION) + +target_include_directories(QMatrixClient PUBLIC + $ + $ ) -target_include_directories(qmatrixclient PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/lib) -target_link_libraries(qmatrixclient Qt5::Core Qt5::Network Qt5::Gui) +target_link_libraries(QMatrixClient Qt5::Core Qt5::Network Qt5::Gui) add_executable(qmc-example ${example_SRCS}) -target_include_directories(qmc-example PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/lib) -target_link_libraries(qmc-example Qt5::Core qmatrixclient) +target_link_libraries(qmc-example Qt5::Core QMatrixClient) # Installation -install (TARGETS qmatrixclient EXPORT QMatrixClient - ARCHIVE DESTINATION ${CMAKE_INSTALL_LIBDIR} - LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR} - INCLUDES DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}) -install (DIRECTORY lib/ DESTINATION ${CMAKE_INSTALL_INCLUDEDIR} - FILES_MATCHING PATTERN "*.h") - -install (TARGETS qmc-example RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR}) +install(TARGETS QMatrixClient EXPORT QMatrixClientTargets + ARCHIVE DESTINATION ${CMAKE_INSTALL_LIBDIR} + LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR} + INCLUDES DESTINATION ${CMAKE_INSTALL_INCLUDEDIR} +) +install(DIRECTORY lib/ DESTINATION ${CMAKE_INSTALL_INCLUDEDIR} + FILES_MATCHING PATTERN "*.h") + +include(CMakePackageConfigHelpers) +write_basic_package_version_file( + "${CMAKE_CURRENT_BINARY_DIR}/QMatrixClient/QMatrixClientConfigVersion.cmake" + VERSION ${API_VERSION} + COMPATIBILITY AnyNewerVersion +) -#export(TARGETS qmatrixclient FILE QMatrixClientConfig.cmake EXPORT_LINK_INTERFACE_LIBRARIES) -export(EXPORT QMatrixClient) export(PACKAGE QMatrixClient) -install (EXPORT QMatrixClient DESTINATION ${CMAKE_INSTALL_LIBDIR}/cmake) +export(EXPORT QMatrixClientTargets + FILE "${CMAKE_CURRENT_BINARY_DIR}/QMatrixClient/QMatrixClientTargets.cmake") +configure_file(cmake/QMatrixClientConfig.cmake + "${CMAKE_CURRENT_BINARY_DIR}/QMatrixClient/QMatrixClientConfig.cmake" + COPYONLY +) + +set(ConfigFilesLocation "${CMAKE_INSTALL_LIBDIR}/cmake/QMatrixClient") +install(EXPORT QMatrixClientTargets + FILE QMatrixClientTargets.cmake DESTINATION ${ConfigFilesLocation}) + +install(FILES cmake/QMatrixClientConfig.cmake + "${CMAKE_CURRENT_BINARY_DIR}/QMatrixClient/QMatrixClientConfigVersion.cmake" + DESTINATION ${ConfigFilesLocation} +) +install(EXPORT_ANDROID_MK QMatrixClientTargets DESTINATION share/ndk-modules) if (WIN32) - install (FILES mime/packages/freedesktop.org.xml - DESTINATION mime/packages) + install(FILES mime/packages/freedesktop.org.xml DESTINATION mime/packages) endif (WIN32) + +install(TARGETS qmc-example RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR}) diff --git a/cmake/QMatrixClientConfig.cmake b/cmake/QMatrixClientConfig.cmake new file mode 100644 index 00000000..900038a5 --- /dev/null +++ b/cmake/QMatrixClientConfig.cmake @@ -0,0 +1 @@ +include("${CMAKE_CURRENT_LIST_DIR}/QMatrixClientTargets.cmake") diff --git a/examples/CMakeLists.txt b/examples/CMakeLists.txt new file mode 100644 index 00000000..49e0089a --- /dev/null +++ b/examples/CMakeLists.txt @@ -0,0 +1,69 @@ +cmake_minimum_required(VERSION 3.1) + +# This CMakeLists file assumes that the library is installed to CMAKE_INSTALL_PREFIX +# and ignores the in-tree library code. You can use this to start work on your own client. + +project(qmc-example CXX) + +include(CheckCXXCompilerFlag) +if (NOT WIN32) + include(GNUInstallDirs) +endif(NOT WIN32) + +# Find includes in corresponding build directories +set(CMAKE_INCLUDE_CURRENT_DIR ON) +# Instruct CMake to run moc automatically when needed. +set(CMAKE_AUTOMOC ON) + +# Set a default build type if none was specified +if(NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES) + message(STATUS "Setting build type to 'Debug' as none was specified") + set(CMAKE_BUILD_TYPE Debug CACHE STRING "Choose the type of build" FORCE) + # Set the possible values of build type for cmake-gui + set_property(CACHE CMAKE_BUILD_TYPE PROPERTY STRINGS "Debug" "Release" + "MinSizeRel" "RelWithDebInfo") +endif() + +if (NOT CMAKE_INSTALL_LIBDIR) + set(CMAKE_INSTALL_LIBDIR ".") +endif() + +if (NOT CMAKE_INSTALL_BINDIR) + set(CMAKE_INSTALL_BINDIR ".") +endif() + +if (NOT CMAKE_INSTALL_INCLUDEDIR) + set(CMAKE_INSTALL_INCLUDEDIR "include") +endif() + +set(CMAKE_CXX_STANDARD 14) + +foreach (FLAG all "" pedantic extra error=return-type no-unused-parameter no-gnu-zero-variadic-macro-arguments) + CHECK_CXX_COMPILER_FLAG("-W${FLAG}" WARN_${FLAG}_SUPPORTED) + if ( WARN_${FLAG}_SUPPORTED AND NOT CMAKE_CXX_FLAGS MATCHES "(^| )-W?${FLAG}($| )") + set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -W${FLAG}") + endif () +endforeach () + +find_package(Qt5 5.6 REQUIRED Network Gui) +get_filename_component(Qt5_Prefix "${Qt5_DIR}/../../../.." ABSOLUTE) + +find_package(QMatrixClient REQUIRED) +get_filename_component(QMC_Prefix "${QMatrixClient_DIR}/../.." ABSOLUTE) + +message( STATUS "qmc-example configuration:" ) +if (CMAKE_BUILD_TYPE) + message( STATUS " Build type: ${CMAKE_BUILD_TYPE}") +endif(CMAKE_BUILD_TYPE) +message( STATUS " Compiler: ${CMAKE_CXX_COMPILER_ID} ${CMAKE_CXX_COMPILER_VERSION}" ) +message( STATUS " Qt: ${Qt5_VERSION} at ${Qt5_Prefix}" ) +message( STATUS " QMatrixClient: ${QMatrixClient_VERSION} at ${QMC_Prefix}" ) + +set(example_SRCS qmc-example.cpp) + +add_executable(qmc-example ${example_SRCS}) +target_link_libraries(qmc-example Qt5::Core QMatrixClient) + +# Installation + +install (TARGETS qmc-example RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR}) diff --git a/libqmatrixclient.pri b/libqmatrixclient.pri index 646af4b1..edba623e 100644 --- a/libqmatrixclient.pri +++ b/libqmatrixclient.pri @@ -7,72 +7,73 @@ win32-msvc* { QMAKE_CXXFLAGS_WARN_ON += -Wno-unused-parameter } -INCLUDEPATH += $$PWD/lib +SRCPATH = $$PWD/lib +INCLUDEPATH += $$SRCPATH HEADERS += \ - $$PWD/lib/connectiondata.h \ - $$PWD/lib/connection.h \ - $$PWD/lib/room.h \ - $$PWD/lib/user.h \ - $$PWD/lib/avatar.h \ - $$PWD/lib/util.h \ - $$PWD/lib/events/event.h \ - $$PWD/lib/events/eventcontent.h \ - $$PWD/lib/events/roommessageevent.h \ - $$PWD/lib/events/simplestateevents.h \ - $$PWD/lib/events/roommemberevent.h \ - $$PWD/lib/events/roomavatarevent.h \ - $$PWD/lib/events/typingevent.h \ - $$PWD/lib/events/receiptevent.h \ - $$PWD/lib/events/accountdataevents.h \ - $$PWD/lib/events/directchatevent.h \ - $$PWD/lib/events/redactionevent.h \ - $$PWD/lib/jobs/requestdata.h \ - $$PWD/lib/jobs/basejob.h \ - $$PWD/lib/jobs/checkauthmethods.h \ - $$PWD/lib/jobs/passwordlogin.h \ - $$PWD/lib/jobs/sendeventjob.h \ - $$PWD/lib/jobs/postreceiptjob.h \ - $$PWD/lib/jobs/joinroomjob.h \ - $$PWD/lib/jobs/roommessagesjob.h \ - $$PWD/lib/jobs/syncjob.h \ - $$PWD/lib/jobs/mediathumbnailjob.h \ - $$PWD/lib/jobs/setroomstatejob.h \ - $$PWD/lib/jobs/downloadfilejob.h \ - $$PWD/lib/jobs/postreadmarkersjob.h \ - $$files($$PWD/lib/jobs/generated/*.h, false) \ - $$PWD/lib/logging.h \ - $$PWD/lib/settings.h \ - $$PWD/lib/networksettings.h \ - $$PWD/lib/networkaccessmanager.h + $$SRCPATH/connectiondata.h \ + $$SRCPATH/connection.h \ + $$SRCPATH/room.h \ + $$SRCPATH/user.h \ + $$SRCPATH/avatar.h \ + $$SRCPATH/util.h \ + $$SRCPATH/events/event.h \ + $$SRCPATH/events/eventcontent.h \ + $$SRCPATH/events/roommessageevent.h \ + $$SRCPATH/events/simplestateevents.h \ + $$SRCPATH/events/roommemberevent.h \ + $$SRCPATH/events/roomavatarevent.h \ + $$SRCPATH/events/typingevent.h \ + $$SRCPATH/events/receiptevent.h \ + $$SRCPATH/events/accountdataevents.h \ + $$SRCPATH/events/directchatevent.h \ + $$SRCPATH/events/redactionevent.h \ + $$SRCPATH/jobs/requestdata.h \ + $$SRCPATH/jobs/basejob.h \ + $$SRCPATH/jobs/checkauthmethods.h \ + $$SRCPATH/jobs/passwordlogin.h \ + $$SRCPATH/jobs/sendeventjob.h \ + $$SRCPATH/jobs/postreceiptjob.h \ + $$SRCPATH/jobs/joinroomjob.h \ + $$SRCPATH/jobs/roommessagesjob.h \ + $$SRCPATH/jobs/syncjob.h \ + $$SRCPATH/jobs/mediathumbnailjob.h \ + $$SRCPATH/jobs/setroomstatejob.h \ + $$SRCPATH/jobs/downloadfilejob.h \ + $$SRCPATH/jobs/postreadmarkersjob.h \ + $$files($$SRCPATH/jobs/generated/*.h, false) \ + $$SRCPATH/logging.h \ + $$SRCPATH/settings.h \ + $$SRCPATH/networksettings.h \ + $$SRCPATH/networkaccessmanager.h SOURCES += \ - $$PWD/lib/connectiondata.cpp \ - $$PWD/lib/connection.cpp \ - $$PWD/lib/room.cpp \ - $$PWD/lib/user.cpp \ - $$PWD/lib/avatar.cpp \ - $$PWD/lib/events/event.cpp \ - $$PWD/lib/events/eventcontent.cpp \ - $$PWD/lib/events/roommessageevent.cpp \ - $$PWD/lib/events/roommemberevent.cpp \ - $$PWD/lib/events/typingevent.cpp \ - $$PWD/lib/events/receiptevent.cpp \ - $$PWD/lib/events/directchatevent.cpp \ - $$PWD/lib/jobs/requestdata.cpp \ - $$PWD/lib/jobs/basejob.cpp \ - $$PWD/lib/jobs/checkauthmethods.cpp \ - $$PWD/lib/jobs/passwordlogin.cpp \ - $$PWD/lib/jobs/sendeventjob.cpp \ - $$PWD/lib/jobs/postreceiptjob.cpp \ - $$PWD/lib/jobs/joinroomjob.cpp \ - $$PWD/lib/jobs/roommessagesjob.cpp \ - $$PWD/lib/jobs/syncjob.cpp \ - $$PWD/lib/jobs/mediathumbnailjob.cpp \ - $$PWD/lib/jobs/setroomstatejob.cpp \ - $$PWD/lib/jobs/downloadfilejob.cpp \ - $$files($$PWD/lib/jobs/generated/*.cpp, false) \ - $$PWD/lib/logging.cpp \ - $$PWD/lib/settings.cpp \ - $$PWD/lib/networksettings.cpp \ - $$PWD/lib/networkaccessmanager.cpp + $$SRCPATH/connectiondata.cpp \ + $$SRCPATH/connection.cpp \ + $$SRCPATH/room.cpp \ + $$SRCPATH/user.cpp \ + $$SRCPATH/avatar.cpp \ + $$SRCPATH/events/event.cpp \ + $$SRCPATH/events/eventcontent.cpp \ + $$SRCPATH/events/roommessageevent.cpp \ + $$SRCPATH/events/roommemberevent.cpp \ + $$SRCPATH/events/typingevent.cpp \ + $$SRCPATH/events/receiptevent.cpp \ + $$SRCPATH/events/directchatevent.cpp \ + $$SRCPATH/jobs/requestdata.cpp \ + $$SRCPATH/jobs/basejob.cpp \ + $$SRCPATH/jobs/checkauthmethods.cpp \ + $$SRCPATH/jobs/passwordlogin.cpp \ + $$SRCPATH/jobs/sendeventjob.cpp \ + $$SRCPATH/jobs/postreceiptjob.cpp \ + $$SRCPATH/jobs/joinroomjob.cpp \ + $$SRCPATH/jobs/roommessagesjob.cpp \ + $$SRCPATH/jobs/syncjob.cpp \ + $$SRCPATH/jobs/mediathumbnailjob.cpp \ + $$SRCPATH/jobs/setroomstatejob.cpp \ + $$SRCPATH/jobs/downloadfilejob.cpp \ + $$files($$SRCPATH/jobs/generated/*.cpp, false) \ + $$SRCPATH/logging.cpp \ + $$SRCPATH/settings.cpp \ + $$SRCPATH/networksettings.cpp \ + $$SRCPATH/networkaccessmanager.cpp -- cgit v1.2.3 From 62dc885e46838368f5bba3f3e2fd0985dda36af1 Mon Sep 17 00:00:00 2001 From: Kitsune Ral Date: Sun, 1 Apr 2018 14:01:14 +0900 Subject: BaseJob: use QDebugStateSaver ...instead of copying the whole QDebug object. --- lib/jobs/basejob.h | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/jobs/basejob.h b/lib/jobs/basejob.h index ed630a67..252b8ea0 100644 --- a/lib/jobs/basejob.h +++ b/lib/jobs/basejob.h @@ -101,9 +101,9 @@ namespace QMatrixClient bool good() const { return code < ErrorLevel; } friend QDebug operator<<(QDebug dbg, const Status& s) { - QDebug(dbg).noquote().nospace() + QDebugStateSaver _s(dbg); + return dbg.noquote().nospace() << s.code << ": " << s.message; - return dbg; } int code; -- cgit v1.2.3 From 9b856f28f2745a6d1f0425b8e7ac9c91119f3f36 Mon Sep 17 00:00:00 2001 From: Kitsune Ral Date: Sun, 1 Apr 2018 14:19:56 +0900 Subject: Pass actual changes with Connection::directChatsListChanged() Also: provide Connection::directChats() to get the whole direct chats map. --- examples/qmc-example.cpp | 19 +++++++++--- lib/connection.cpp | 81 ++++++++++++++++++++++++++++++++++-------------- lib/connection.h | 11 +++++-- 3 files changed, 80 insertions(+), 31 deletions(-) diff --git a/examples/qmc-example.cpp b/examples/qmc-example.cpp index 23a1bff1..7226c060 100644 --- a/examples/qmc-example.cpp +++ b/examples/qmc-example.cpp @@ -25,9 +25,11 @@ class QMCTest : public QObject void doTests(); void addAndRemoveTag(); void sendAndRedact(); - void checkRedactionOutcome(QString evtIdToRedact, RoomEventsRange events); + void checkRedactionOutcome(QString evtIdToRedact, + RoomEventsRange events); void markDirectChat(); - void checkDirectChatOutcome(); + void checkDirectChatOutcome( + const Connection::DirectChatsMap& added); void finalize(); private: @@ -211,11 +213,11 @@ void QMCTest::checkRedactionOutcome(QString evtIdToRedact, void QMCTest::markDirectChat() { - if (c->isDirectChat(targetRoom->id())) + if (targetRoom->directChatUsers().contains(c->user())) { cout << "Warning: the room is already a direct chat," " only unmarking will be tested" << endl; - checkDirectChatOutcome(); + checkDirectChatOutcome({{ c->user(), targetRoom->id() }}); } // Connect first because the signal is emitted synchronously. connect(c.data(), &Connection::directChatsListChanged, @@ -224,11 +226,18 @@ void QMCTest::markDirectChat() c->addToDirectChats(targetRoom, c->user()); } -void QMCTest::checkDirectChatOutcome() +void QMCTest::checkDirectChatOutcome(const Connection::DirectChatsMap& added) { disconnect(c.data(), &Connection::directChatsListChanged, nullptr, nullptr); if (!c->isDirectChat(targetRoom->id())) { + cout << "The room has not been marked as a direct chat" << endl; + QMC_CHECK("Direct chat test", false); + return; + } + if (!added.contains(c->user(), targetRoom->id())) + { + cout << "The room has not been listed in new direct chats" << endl; QMC_CHECK("Direct chat test", false); return; } diff --git a/lib/connection.cpp b/lib/connection.cpp index 2d7235b9..a5c74d88 100644 --- a/lib/connection.cpp +++ b/lib/connection.cpp @@ -47,7 +47,22 @@ using namespace QMatrixClient; -using DirectChatsMap = QMultiHash; +// This is very much Qt-specific; STL iterators don't have key() and value() +template +HashT erase_if(HashT& hashMap, Pred pred) +{ + HashT removals; + for (auto it = hashMap.begin(); it != hashMap.end();) + { + if (pred(it)) + { + removals.insert(it.key(), it.value()); + it = hashMap.erase(it); + } else + ++it; + } + return removals; +} class Connection::Private { @@ -80,7 +95,8 @@ class Connection::Private void connectWithToken(const QString& user, const QString& accessToken, const QString& deviceId); - void broadcastDirectChatUpdates(); + void broadcastDirectChatUpdates(const DirectChatsMap& additions, + const DirectChatsMap& removals); }; Connection::Connection(const QUrl& server, QObject* parent) @@ -290,20 +306,32 @@ void Connection::onSyncSuccess(SyncData &&data) { { if (accountEvent->type() == EventType::DirectChat) { - DirectChatsMap newDirectChats; const auto* event = static_cast(accountEvent.get()); auto usersToDCs = event->usersToDirectChats(); + DirectChatsMap removals = + erase_if(d->directChats, [&usersToDCs] (auto it) { + return !usersToDCs.contains(it.key()->id(), it.value()); + }); + if (MAIN().isDebugEnabled()) + for (auto it = removals.begin(); it != removals.end(); ++it) + qCDebug(MAIN) << it.value() + << "is no more a direct chat with" << it.key()->id(); + + DirectChatsMap additions; for (auto it = usersToDCs.begin(); it != usersToDCs.end(); ++it) { - newDirectChats.insert(user(it.key()), it.value()); - qCDebug(MAIN) << "Marked room" << it.value() - << "as a direct chat with" << it.key(); - } - if (newDirectChats != d->directChats) - { - d->directChats = newDirectChats; - emit directChatsListChanged(); + const auto* u = user(it.key()); + if (!d->directChats.contains(u, it.value())) + { + additions.insert(u, it.value()); + d->directChats.insert(u, it.value()); + qCDebug(MAIN) << "Marked room" << it.value() + << "as a direct chat with" << u->id(); + } } + if (!additions.isEmpty() || !removals.isEmpty()) + emit directChatsListChanged(additions, removals); + continue; } d->accountData[accountEvent->jsonType()] = @@ -654,7 +682,12 @@ QVector Connection::roomsWithTag(const QString& tagName) const return rooms; } -QJsonObject toJson(const DirectChatsMap& directChats) +Connection::DirectChatsMap Connection::directChats() const +{ + return d->directChats; +} + +QJsonObject toJson(const Connection::DirectChatsMap& directChats) { QJsonObject json; for (auto it = directChats.keyBegin(); it != directChats.keyEnd(); ++it) @@ -662,11 +695,12 @@ QJsonObject toJson(const DirectChatsMap& directChats) return json; } -void Connection::Private::broadcastDirectChatUpdates() +void Connection::Private::broadcastDirectChatUpdates(const DirectChatsMap& additions, + const DirectChatsMap& removals) { q->callApi(userId, QStringLiteral("m.direct"), toJson(directChats)); - emit q->directChatsListChanged(); + emit q->directChatsListChanged(additions, removals); } void Connection::addToDirectChats(const Room* room, const User* user) @@ -675,7 +709,8 @@ void Connection::addToDirectChats(const Room* room, const User* user) if (d->directChats.contains(user, room->id())) return; d->directChats.insert(user, room->id()); - d->broadcastDirectChatUpdates(); + DirectChatsMap additions { { user, room->id() } }; + d->broadcastDirectChatUpdates(additions, {}); } void Connection::removeFromDirectChats(const QString& roomId, const User* user) @@ -684,17 +719,17 @@ void Connection::removeFromDirectChats(const QString& roomId, const User* user) if ((user != nullptr && !d->directChats.contains(user, roomId)) || d->directChats.key(roomId) == nullptr) return; + + DirectChatsMap removals; if (user != nullptr) + { + removals.insert(user, roomId); d->directChats.remove(user, roomId); + } else - for (auto it = d->directChats.begin(); it != d->directChats.end();) - { - if (it.value() == roomId) - it = d->directChats.erase(it); - else - ++it; - } - d->broadcastDirectChatUpdates(); + removals = erase_if(d->directChats, + [&roomId] (auto it) { return it.value() == roomId; }); + d->broadcastDirectChatUpdates({}, removals); } bool Connection::isDirectChat(const QString& roomId) const diff --git a/lib/connection.h b/lib/connection.h index c6d543ec..016c7e3c 100644 --- a/lib/connection.h +++ b/lib/connection.h @@ -64,6 +64,8 @@ namespace QMatrixClient using user_factory_t = std::function; + using DirectChatsMap = QMultiHash; + enum RoomVisibility { PublishRoom, UnpublishRoom }; // FIXME: Should go inside CreateRoomJob explicit Connection(QObject* parent = nullptr); @@ -114,16 +116,18 @@ namespace QMatrixClient /** Check whether the room id corresponds to a direct chat */ bool isDirectChat(const QString& roomId) const; + /** Get the whole map from users to direct chat rooms */ + DirectChatsMap directChats() const; + /** Retrieve the list of users the room is a direct chat with * @return The list of users for which this room is marked as * a direct chat; an empty list if the room is not a direct chat */ QList directChatUsers(const Room* room) const; + /** Get the full list of users known to this account */ QMap users() const; - // FIXME: Convert Q_INVOKABLEs to Q_PROPERTIES - // (breaks back-compatibility) QUrl homeserver() const; Q_INVOKABLE Room* room(const QString& roomId, JoinStates states = JoinState::Invite|JoinState::Join) const; @@ -421,7 +425,8 @@ namespace QMatrixClient * to direct chat rooms is changed (because of either local updates * or a different list arrived from the server). */ - void directChatsListChanged(); + void directChatsListChanged(DirectChatsMap additions, + DirectChatsMap removals); void cacheStateChanged(); -- cgit v1.2.3 From e003ffd274127a9f734f18c736d28be936a0dc89 Mon Sep 17 00:00:00 2001 From: Kitsune Ral Date: Mon, 2 Apr 2018 14:08:53 +0900 Subject: Room/Connection: accountData()/accountDataChanged() Generic account data are at least readable now (you can get, say, pushers from the library without it even being aware of what a pusher is). --- lib/connection.cpp | 11 +++++++++++ lib/connection.h | 15 +++++++++++++++ lib/room.cpp | 11 +++++++++++ lib/room.h | 13 +++++++++++++ 4 files changed, 50 insertions(+) diff --git a/lib/connection.cpp b/lib/connection.cpp index a5c74d88..2bdbb2e5 100644 --- a/lib/connection.cpp +++ b/lib/connection.cpp @@ -336,6 +336,7 @@ void Connection::onSyncSuccess(SyncData &&data) { } d->accountData[accountEvent->jsonType()] = accountEvent->contentJson().toVariantHash(); + emit accountDataChanged(accountEvent->jsonType()); } } @@ -647,6 +648,16 @@ QHash< QPair, Room* > Connection::roomMap() const return roomMap; } +bool Connection::hasAccountData(const QString& type) const +{ + return d->accountData.contains(type); +} + +QVariantHash Connection::accountData(const QString& type) const +{ + return d->accountData.value(type); +} + QHash> Connection::tagsToRooms() const { QHash> result; diff --git a/lib/connection.h b/lib/connection.h index 016c7e3c..5a2df012 100644 --- a/lib/connection.h +++ b/lib/connection.h @@ -78,6 +78,18 @@ namespace QMatrixClient */ QHash, Room*> roomMap() const; + /** Check whether the account has data of the given type + * Direct chats map is not supported by this method _yet_. + */ + bool hasAccountData(const QString& type) const; + + /** Get a generic account data event of the given type + * This returns a generic hashmap for any account data event + * stored on the server. Direct chats map cannot be retrieved + * using this method _yet_; use directChats() instead. + */ + QVariantHash accountData(const QString& type) const; + /** Get all Invited and Joined rooms grouped by tag * \return a hashmap from tag name to a vector of room pointers, * sorted by their order in the tag - details are at @@ -414,6 +426,9 @@ namespace QMatrixClient */ void createdRoom(Room* room); + /** Account data (except direct chats) have changed */ + void accountDataChanged(QString type); + /** The direct chat room is ready for using * This signal is emitted upon any successful outcome from * requestDirectChat. diff --git a/lib/room.cpp b/lib/room.cpp index 25669889..edbc9266 100644 --- a/lib/room.cpp +++ b/lib/room.cpp @@ -632,6 +632,16 @@ void Room::resetHighlightCount() emit highlightCountChanged(this); } +bool Room::hasAccountData(const QString& type) const +{ + return d->accountData.contains(type); +} + +QVariantHash Room::accountData(const QString& type) const +{ + return d->accountData.value(type); +} + QStringList Room::tagNames() const { return d->tags.keys(); @@ -1644,6 +1654,7 @@ void Room::processAccountDataEvent(EventPtr event) default: d->accountData[event->jsonType()] = event->contentJson().toVariantHash(); + emit accountDataChanged(event->jsonType()); } } diff --git a/lib/room.h b/lib/room.h index bdef04ee..39dee8f5 100644 --- a/lib/room.h +++ b/lib/room.h @@ -259,6 +259,18 @@ namespace QMatrixClient Q_INVOKABLE int highlightCount() const; Q_INVOKABLE void resetHighlightCount(); + /** Check whether the room has account data of the given type + * Tags and read markers are not supported by this method _yet_. + */ + bool hasAccountData(const QString& type) const; + + /** Get a generic account data event of the given type + * This returns a generic hashmap for any room account data event + * stored on the server. Tags and read markers cannot be retrieved + * using this method _yet_. + */ + QVariantHash accountData(const QString& type) const; + QStringList tagNames() const; TagsMap tags() const; TagRecord tag(const QString& name) const; @@ -377,6 +389,7 @@ namespace QMatrixClient void readMarkerMoved(); void unreadMessagesChanged(Room* room); + void accountDataChanged(QString type); void tagsChanged(); void replacedEvent(const RoomEvent* newEvent, -- cgit v1.2.3 From d7725b45bfd33163840536f975853837aa8e4763 Mon Sep 17 00:00:00 2001 From: Kitsune Ral Date: Mon, 2 Apr 2018 14:13:11 +0900 Subject: lookup()/dispatch() removed; unique_ptr_cast() and qAsConst() introduced qAsConst() is a copy-paste from Qt code and is only supplied by QMatrixClient if Qt is below 5.7. unique_ptr_cast<> is similar to static_cast<> of pointers but deals with unique_ptr's, passing ownership to the newly made pointer. --- lib/util.h | 178 ++++++------------------------------------------------------- 1 file changed, 16 insertions(+), 162 deletions(-) diff --git a/lib/util.h b/lib/util.h index 65de0610..92198b0b 100644 --- a/lib/util.h +++ b/lib/util.h @@ -22,171 +22,10 @@ #include #include +#include namespace QMatrixClient { - /** - * @brief Lookup a value by a key in a varargs list - * - * This function template takes the value of its first argument (selector) - * as a key and searches for it in the key-value map passed in - * a parameter pack (every next pair of arguments forms a key-value pair). - * If a match is found, the respective value is returned; if no pairs - * matched, the last value (fallback) is returned. - * - * All options should be of the same type or implicitly castable to the - * type of the first option. If you need some specific type to cast to - * you can explicitly provide it as the ValueT template parameter - * (e.g. lookup(parameters...)). Note that pointers - * to methods of different classes and even to functions with different - * signatures are of different types. If their return types are castable - * to some common one, @see dispatch that deals with this by swallowing - * the method invocation. - * - * Below is an example of usage to select a parser depending on contents of - * a JSON object: - * {@code - * auto parser = lookup(obj.value["type"].toString(), - * "type1", fn1, - * "type2", fn2, - * fallbackFn); - * parser(obj); - * } - * - * The implementation is based on tail recursion; every recursion step - * removes 2 arguments (match and value). There's no selector value for the - * fallback option (the last one); therefore, the total number of lookup() - * arguments should be even: selector + n key-value pairs + fallback - * - * @note Beware of calling lookup() with a const char* selector - * (the first parameter) - most likely it won't do what you expect because - * of shallow comparison. - */ - template - ValueT lookup(SelectorT/*unused*/, ValueT&& fallback) - { - return std::forward(fallback); - } - - template - ValueT lookup(SelectorT&& selector, KeyT&& key, ValueT&& value, Ts&&... remainder) - { - if( selector == key ) - return std::forward(value); - - // Drop the failed key-value pair and recurse with 2 arguments less. - return lookup(std::forward(selector), - std::forward(remainder)...); - } - - /** - * A wrapper around lookup() for functions of different types castable - * to a common std::function<> form - * - * This class uses std::function<> magic to first capture arguments of - * a yet-unknown function or function object, and then to coerce types of - * all functions/function objects passed for lookup to the type - * std::function, you would have - * to pass the specific function type to lookup, since your functions have - * different signatures. The type is not always obvious, and the resulting - * construct in client code would almost always be rather cumbersome. - * Dispatch<> deduces the necessary function type (well, almost - you still - * have to specify the result type) and hides the clumsiness. For more - * information on what std::function<> can wrap around, see - * https://cpptruths.blogspot.jp/2015/11/covariance-and-contravariance-in-c.html - * - * The function arguments are captured by value (i.e. copied) to avoid - * hard-to-find issues with dangling references in cases when a Dispatch<> - * object is passed across different contexts (e.g. returned from another - * function). - * - * \tparam ResultT - the desired type of a picked function invocation (mandatory) - * \tparam ArgTs - function argument types (deduced) - */ -#if __GNUC__ < 5 && __GNUC_MINOR__ < 9 - // GCC 4.8 cannot cope with parameter packs inside lambdas; so provide a single - // argument version of Dispatch<> that we only need so far. - template -#else - template -#endif - class Dispatch - { - // The implementation takes a chapter from functional programming: - // Dispatch<> uses a function that in turn accepts a function as its - // argument. The sole purpose of the outer function (initialized by - // a lambda-expression in the constructor) is to store the arguments - // to any of the functions later looked up. The inner function (its - // type is defined by fn_t alias) is the one returned by lookup() - // invocation inside to(). - // - // It's a bit counterintuitive to specify function parameters before - // the list of functions but otherwise it would take several overloads - // here to match all the ways a function-like behaviour can be done: - // reference-to-function, pointer-to-function, function object. This - // probably could be done as well but I preferred a more compact - // solution: you show what you have and if it's possible to bring all - // your functions to the same std::function<> based on what you have - // as parameters, the code will compile. If it's not possible, modern - // compilers are already good enough at pinpointing a specific place - // where types don't match. - public: -#if __GNUC__ < 5 && __GNUC_MINOR__ < 9 - using fn_t = std::function; - explicit Dispatch(ArgT&& arg) - : boundArgs([=](fn_t &&f) { return f(std::move(arg)); }) - { } -#else - using fn_t = std::function; - explicit Dispatch(ArgTs&&... args) - : boundArgs([=](fn_t &&f) { return f(std::move(args)...); }) - { } -#endif - - template - ResultT to(LookupParamTs&&... lookupParams) - { - // Here's the magic, two pieces of it: - // 1. Specifying fn_t in lookup() wraps all functions in - // \p lookupParams into the same std::function<> type. This - // includes conversion of return types from more specific to more - // generic (because std::function is covariant by return types and - // contravariant by argument types (see the link in the Doxygen - // part of the comments). - auto fn = lookup(std::forward(lookupParams)...); - // 2. Passing the result of lookup() to boundArgs() invokes the - // lambda-expression mentioned in the constructor, which simply - // invokes this passed function with a set of arguments captured - // by lambda. - if (fn) - return boundArgs(std::move(fn)); - - // A shortcut to allow passing nullptr for a function; - // a default-constructed ResultT will be returned - // (for pointers, it will be nullptr) - return {}; - } - - private: - std::function boundArgs; - }; - - /** - * Dispatch a set of parameters to one of a set of functions, depending on - * a selector value - * - * Use dispatch(parameters).to(lookup parameters) - * instead of lookup() if you need to pick one of several functions returning - * types castable to the same CommonType. See event.cpp for a typical use case. - * - * \see Dispatch - */ - template - Dispatch dispatch(ArgTs&& ... args) - { - return Dispatch(std::forward(args)...); - } - // The below enables pretty-printing of enums in logs #if (QT_VERSION >= QT_VERSION_CHECK(5, 5, 0)) #define REGISTER_ENUM(EnumName) Q_ENUM(EnumName) @@ -201,5 +40,20 @@ namespace QMatrixClient return dbg << Event::staticMetaObject.enumerator(enumIdx).valueToKey(int(val)); \ } #endif + + template + inline auto unique_ptr_cast(PtrT2&& p) + { + return std::unique_ptr(static_cast(p.release())); + } + +#if QT_VERSION < QT_VERSION_CHECK(5, 7, 0) + // Copy-pasted from Qt 5.10 + template + Q_DECL_CONSTEXPR typename std::add_const::type &qAsConst(T &t) Q_DECL_NOTHROW { return t; } + // prevent rvalue arguments: + template + static void qAsConst(const T &&) Q_DECL_EQ_DELETE; +#endif } // namespace QMatrixClient -- cgit v1.2.3 From c36f899c4485ebf602fcaf225f41c64fbea773c3 Mon Sep 17 00:00:00 2001 From: Kitsune Ral Date: Mon, 2 Apr 2018 14:15:57 +0900 Subject: Be more cautious with rvalues Clang on FreeBSD doesn't seem to be playing nice in Release configuration. See the comment in the committed code. --- lib/connection.cpp | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/lib/connection.cpp b/lib/connection.cpp index 2bdbb2e5..a2b1b94e 100644 --- a/lib/connection.cpp +++ b/lib/connection.cpp @@ -281,7 +281,8 @@ void Connection::sync(int timeout) void Connection::onSyncSuccess(SyncData &&data) { d->data->setLastEvent(data.nextBatch()); - for (auto&& roomData: data.takeRoomData()) + auto allRoomData = data.takeRoomData(); + for (auto&& roomData: allRoomData) { const auto forgetIdx = d->roomIdsToForget.indexOf(roomData.roomId); if (forgetIdx != -1) @@ -302,7 +303,12 @@ void Connection::onSyncSuccess(SyncData &&data) { r->updateData(std::move(roomData)); QCoreApplication::processEvents(); } - for (auto&& accountEvent: data.takeAccountData()) + // `for (auto&& accountEvent: data.takeAccountData())` doesn't work well + // with Clang on FreeBSD in Release configuration; it seems that + // the lifetime of the returned rvalue does not extend (violating the + // standard) and accountEvent refers to garbage in the loop body as a result. + auto allAccountData = data.takeAccountData(); + for (auto&& accountEvent: allAccountData) { if (accountEvent->type() == EventType::DirectChat) { -- cgit v1.2.3 From 7ef14023ab337972530bb3f4ad35f8daa326233b Mon Sep 17 00:00:00 2001 From: Kitsune Ral Date: Mon, 2 Apr 2018 14:40:05 +0900 Subject: Fixes according to results of static analysis Many thanks to clang-tidy and clazy authors for the tools, and to Qt Creator developers for making them easily available in the IDE. --- lib/connection.cpp | 22 +++++++++++++--------- lib/connection.h | 2 +- lib/events/roommessageevent.cpp | 2 +- lib/user.cpp | 1 + 4 files changed, 16 insertions(+), 11 deletions(-) diff --git a/lib/connection.cpp b/lib/connection.cpp index a2b1b94e..9d0a34f3 100644 --- a/lib/connection.cpp +++ b/lib/connection.cpp @@ -148,7 +148,7 @@ void Connection::resolveServer(const QString& mxidOrDomain) auto domain = maybeBaseUrl.host(); qCDebug(MAIN) << "Finding the server" << domain; // Check if the Matrix server has a dedicated service record. - QDnsLookup* dns = new QDnsLookup(); + auto* dns = new QDnsLookup(); dns->setType(QDnsLookup::SRV); dns->setName("_matrix._tcp." + domain); @@ -264,13 +264,13 @@ void Connection::sync(int timeout) const QString filter { R"({"room": { "timeline": { "limit": 100 } } })" }; auto job = d->syncJob = callApi(d->data->lastEvent(), filter, timeout); - connect( job, &SyncJob::success, [this, job] { + connect( job, &SyncJob::success, this, [this, job] { onSyncSuccess(job->takeData()); d->syncJob = nullptr; emit syncDone(); }); connect( job, &SyncJob::retryScheduled, this, &Connection::networkError); - connect( job, &SyncJob::failure, [this, job] { + connect( job, &SyncJob::failure, this, [this, job] { d->syncJob = nullptr; if (job->error() == BaseJob::ContentAccessError) emit loginError(job->errorString()); @@ -312,8 +312,9 @@ void Connection::onSyncSuccess(SyncData &&data) { { if (accountEvent->type() == EventType::DirectChat) { - const auto* event = static_cast(accountEvent.get()); - auto usersToDCs = event->usersToDirectChats(); + const auto usersToDCs = + unique_ptr_cast(accountEvent) + ->usersToDirectChats(); DirectChatsMap removals = erase_if(d->directChats, [&usersToDCs] (auto it) { return !usersToDCs.contains(it.key()->id(), it.value()); @@ -457,7 +458,7 @@ CreateRoomJob* Connection::createRoom(RoomVisibility visibility, bool isDirect, bool guestsCanJoin, const QVector& initialState, const QVector& invite3pids, - const QJsonObject creationContent) + const QJsonObject& creationContent) { auto job = callApi( visibility == PublishRoom ? "public" : "private", alias, name, @@ -479,8 +480,11 @@ void Connection::doInDirectChat(const QString& userId, { // There can be more than one DC; find the first valid, and delete invalid // (left/forgotten) ones along the way. - for (auto roomId: d->directChats.values(user(userId))) + const auto* u = user(userId); + for (auto it = d->directChats.find(u); + it != d->directChats.end() && it.key() == u; ++it) { + const auto& roomId = *it; if (auto r = room(roomId, JoinState::Join)) { Q_ASSERT(r->id() == roomId); @@ -667,7 +671,7 @@ QVariantHash Connection::accountData(const QString& type) const QHash> Connection::tagsToRooms() const { QHash> result; - for (auto* r: d->roomMap) + for (auto* r: qAsConst(d->roomMap)) { for (const auto& tagName: r->tagNames()) result[tagName].push_back(r); @@ -683,7 +687,7 @@ QHash> Connection::tagsToRooms() const QStringList Connection::tagNames() const { QStringList tags ({FavouriteTag}); - for (auto* r: d->roomMap) + for (auto* r: qAsConst(d->roomMap)) for (const auto& tag: r->tagNames()) if (tag != LowPriorityTag && !tags.contains(tag)) tags.push_back(tag); diff --git a/lib/connection.h b/lib/connection.h index 5a2df012..be414931 100644 --- a/lib/connection.h +++ b/lib/connection.h @@ -272,7 +272,7 @@ namespace QMatrixClient bool guestsCanJoin = false, const QVector& initialState = {}, const QVector& invite3pids = {}, - const QJsonObject creationContent = {}); + const QJsonObject& creationContent = {}); /** Get a direct chat with a single user * This method may return synchronously or asynchoronously depending diff --git a/lib/events/roommessageevent.cpp b/lib/events/roommessageevent.cpp index dec0ca50..1a4e74bf 100644 --- a/lib/events/roommessageevent.cpp +++ b/lib/events/roommessageevent.cpp @@ -87,7 +87,7 @@ RoomMessageEvent::RoomMessageEvent(const QJsonObject& obj) _plainBody = content["body"].toString(); _msgtype = content["msgtype"].toString(); - for (auto mt: msgTypes) + for (const auto& mt: msgTypes) if (mt.jsonType == _msgtype) _content.reset(mt.maker(content)); diff --git a/lib/user.cpp b/lib/user.cpp index 7a6dbc73..6143a061 100644 --- a/lib/user.cpp +++ b/lib/user.cpp @@ -265,6 +265,7 @@ void User::rename(const QString& newName, const Room* r) qCWarning(MAIN) << "Passing a null room to two-argument User::rename()" "is incorrect; client developer, please fix it"; rename(newName); + return; } Q_ASSERT_X(r->memberJoinState(this) == JoinState::Join, __FUNCTION__, "Attempt to rename a user that's not a room member"); -- cgit v1.2.3 From a584bfaaa663347270782bccf9309ff016eea530 Mon Sep 17 00:00:00 2001 From: Kitsune Ral Date: Mon, 2 Apr 2018 15:21:10 +0900 Subject: BaseJob: set the status upon headers arrival So that DownloadFileJob could figure if it should save the incoming payload to the file or it's the JSON details about the error. --- lib/jobs/basejob.cpp | 24 +++++++++++++++--------- lib/jobs/basejob.h | 3 ++- lib/jobs/downloadfilejob.cpp | 20 +++++++++++--------- 3 files changed, 28 insertions(+), 19 deletions(-) diff --git a/lib/jobs/basejob.cpp b/lib/jobs/basejob.cpp index a23f43b3..4e35cb01 100644 --- a/lib/jobs/basejob.cpp +++ b/lib/jobs/basejob.cpp @@ -242,6 +242,8 @@ void BaseJob::sendRequest() connect( d->reply.data(), &QNetworkReply::finished, this, &BaseJob::gotReply ); if (d->reply->isRunning()) { + connect( d->reply.data(), &QNetworkReply::metaDataChanged, + this, &BaseJob::checkReply); connect( d->reply.data(), &QNetworkReply::uploadProgress, this, &BaseJob::uploadProgress); connect( d->reply.data(), &QNetworkReply::downloadProgress, @@ -254,13 +256,22 @@ void BaseJob::sendRequest() qCWarning(d->logCat) << this << "request could not start"; } +void BaseJob::checkReply() +{ + setStatus(doCheckReply(d->reply.data())); +} + void BaseJob::gotReply() { - setStatus(checkReply(d->reply.data())); + checkReply(); + qCDebug(d->logCat).nospace().noquote() << this << " returned HTTP code " + << d->reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt() + << ": " << (d->reply->error() == QNetworkReply::NoError ? + "Success" : d->reply->errorString()) + << " (URL: " << d->reply->url().toDisplayString() << ")"; if (status().good()) setStatus(parseReply(d->reply.data())); - else - { + else { const auto body = d->reply->readAll(); if (!body.isEmpty()) { @@ -324,15 +335,10 @@ bool checkContentType(const QByteArray& type, const QByteArrayList& patterns) return false; } -BaseJob::Status BaseJob::checkReply(QNetworkReply* reply) const +BaseJob::Status BaseJob::doCheckReply(QNetworkReply* reply) const { const auto httpCode = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt(); - qCDebug(d->logCat).nospace().noquote() << this << " returned HTTP code " - << httpCode << ": " - << (reply->error() == QNetworkReply::NoError ? - "Success" : reply->errorString()) - << " (URL: " << reply->url().toDisplayString() << ")"; if (httpCode == 429) // Qt doesn't know about it yet return { TooManyRequestsError, tr("Too many requests") }; diff --git a/lib/jobs/basejob.h b/lib/jobs/basejob.h index 252b8ea0..f243066f 100644 --- a/lib/jobs/basejob.h +++ b/lib/jobs/basejob.h @@ -248,7 +248,7 @@ namespace QMatrixClient * * @see gotReply */ - virtual Status checkReply(QNetworkReply* reply) const; + virtual Status doCheckReply(QNetworkReply* reply) const; /** * Processes the reply. By default, parses the reply into @@ -286,6 +286,7 @@ namespace QMatrixClient private slots: void sendRequest(); + void checkReply(); void gotReply(); private: diff --git a/lib/jobs/downloadfilejob.cpp b/lib/jobs/downloadfilejob.cpp index 6a3d8483..2bf9dd8f 100644 --- a/lib/jobs/downloadfilejob.cpp +++ b/lib/jobs/downloadfilejob.cpp @@ -62,6 +62,8 @@ void DownloadFileJob::beforeStart(const ConnectionData*) void DownloadFileJob::afterStart(const ConnectionData*, QNetworkReply* reply) { connect(reply, &QNetworkReply::metaDataChanged, this, [this,reply] { + if (!status().good()) + return; auto sizeHeader = reply->header(QNetworkRequest::ContentLengthHeader); if (sizeHeader.isValid()) { @@ -77,15 +79,15 @@ void DownloadFileJob::afterStart(const ConnectionData*, QNetworkReply* reply) } }); connect(reply, &QIODevice::readyRead, this, [this,reply] { - auto bytes = reply->read(reply->bytesAvailable()); - if (bytes.isEmpty()) - { - qCWarning(JOBS) - << "Unexpected empty chunk when downloading from" - << reply->url() << "to" << d->tempFile->fileName(); - } else { - d->tempFile->write(bytes); - } + if (!status().good()) + return; + auto bytes = reply->read(reply->bytesAvailable()); + if (!bytes.isEmpty()) + d->tempFile->write(bytes); + else + qCWarning(JOBS) + << "Unexpected empty chunk when downloading from" + << reply->url() << "to" << d->tempFile->fileName(); }); } -- cgit v1.2.3 From 1555dfd596dcdf7e774dd3582802a6f15e6e6cbb Mon Sep 17 00:00:00 2001 From: Kitsune Ral Date: Mon, 2 Apr 2018 15:22:04 +0900 Subject: Updated documentation [skip ci] --- CONTRIBUTING.md | 32 ++++++++++++++++++-------------- README.md | 40 +++++++++++++++++++++++++--------------- 2 files changed, 43 insertions(+), 29 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index e576b886..6ad968ec 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -45,7 +45,7 @@ We use GitHub to track all changes via its [issue tracker](https://github.com/QMatrixClient/libqmatrixclient/issues) and [pull requests](https://github.com/QMatrixClient/libqmatrixclient/pulls). Specific changes are proposed using those mechanisms. -Issues are assigned to an individual, who works it and then marks it complete. +Issues are assigned to an individual who works on it and then marks it complete. If there are questions or objections, the conversation area of that issue or pull request is used to resolve it. @@ -126,25 +126,29 @@ This is our primary language. We don't have a particular code style _as of yet_ * Please don't make hypocritical structs with protected or private members. Just make them classes instead. * Qt containers are generally preferred to STL containers; however, there are notable exceptions, and libqmatrixclient already uses them: * `std::array` and `std::deque` have no direct counterparts in Qt. - * Because of COW semantics, Qt containers cannot hold uncopyable classes. Classes without a default constructor are a problem too. An example of that is `SyncRoomData` and `EventsBatch<>`. Use standard containers for those but see the next point and also consider if you can supply a reasonable copy/default constructor. - * Standard containers can be used in code internal to a translation unit (i.e., in a certain .cpp file) _as long as it's not exposed in the interface_. It's ok to use, e.g., `std::vector` instead of `QVector` in tighter code, or when dealing with uncopyable (see the previous point) elements. However, exposing standard containers in the API that might be used by QML will not work at all and will never be accepted. -* Use `QVector` instead of `QList` where possible - see a [great article of Marc Mutz on Qt containers](https://marcmutz.wordpress.com/effective-qt/containers/) for details. + * Because of COW semantics, Qt containers cannot hold uncopyable classes. Classes without a default constructor are a problem too. Examples of that are `SyncRoomData` and `EventsBatch<>`. Use STL containers for those but see the next point and also consider if you can supply a reasonable copy/default constructor. + * STL containers can be freely used in code internal to a translation unit (i.e., in a certain .cpp file) _as long as that is not exposed in the API_. It's ok to use, e.g., `std::vector` instead of `QVector` to tighten up code where you don't need COW, or when dealing with uncopyable data structures (see the previous point). However, exposing STL containers in the API is not encouraged (except where absolutely necessary, e.g. we use `std::deque` for a timeline). Exposing STL containers or iterators in API intended for usage by QML code (e.g. in `Q_PROPERTY`) will not work at all and will never be accepted. +* Use `QVector` instead of `QList` where possible - see a [great article by Marc Mutz on Qt containers](https://marcmutz.wordpress.com/effective-qt/containers/) for details. ### Automated tests -There's no testing framework as of now; either Catch or QTest or both will be used eventually. However, as a stopgap measure, qmc-example is used for end-to-end testing; so please add another private slot and call it from `QMCTest::doTests()` whenever you add new function worth it. PRs to set up a proper testing appliance instead are very welcome (make sure to migrate tests from qmc-example though). +There's no testing framework as of now; either Catch or Qt Test or both will be used eventually. However, as a stopgap measure, qmc-example is used for automated end-to-end testing. + +Any significant addition to the library API should be accompanied by a respective test in qmc-example. To add a test you should write a new private slot with the test logic to the `QMCTest` class and call it from `QMCTest::doTests()`. `QMCTest` sets up some basic test fixture to help you with testing; notably by the moment `doTests()` is invoked you can rely on having a working connection in `c` member variable and a test room in `targetRoom` member variable. PRs to introduce a proper testing framework are very welcome (make sure to migrate tests from qmc-example though); shifting qmc-example to use Qt Test seems to be a particularly low-hanging fruit. ### Security, privacy, and performance -Pay attention to security, and work *with* (not against) the usual security hardening mechanisms (however few in C++). `char *` and similar unchecked C-style read/write arrays are forbidden - use Qt containers or at the very least `std::array<>` instead. +Pay attention to security, and work *with* (not against) the usual security hardening mechanisms (however few in C++). + +`char *` and similar unchecked C-style read/write arrays are forbidden - use Qt containers or at the very least `std::array<>` instead. Where you see fit (usually with data structures), try to use smart pointers, especially `std::unique_ptr<>` or `QScopedPointer` instead of bare pointers. When dealing with `QObject`s, use the parent-child ownership semantics exercised by Qt. Shared pointers are not used in the code so far; but if you find a particular use case where strict semantics of unique pointers doesn't help and a shared pointer is necessary, feel free to step up with the working code and it will be considered for inclusion. Exercise the [principle of least privilege](https://en.wikipedia.org/wiki/Principle_of_least_privilege) where reasonable and appropriate. Prefer less-coupled cohesive code. Protect private information, in particular passwords and email addresses. Do not forget about local access to data (in particular, be very careful when storing something in temporary files, let alone permanent configuration or state). Avoid mechanisms that could be used for tracking where possible (we do need to verify people are logged in but that's pretty much it), and ensure that third parties can't use interactions for tracking. -We want the software to have decent performance for typical users. At the same time we keep libqmatrixclient single-threaded as much as possible, to keep the code simple. That means being cautious about operation complexity (read about big-O notation if you need a kickstart on the topic). This especially refers to operations on the whole timeline - it can easily be several thousands elements long so even operations with linear complexity, if heavy enough, can produce noticeable GUI freezing. +We want the software to have decent performance for typical users. At the same time we keep libqmatrixclient single-threaded as much as possible, to keep the code simple. That means being cautious about operation complexity (read about big-O notation if you need a kickstart on the topic). This especially refers to operations on the whole timeline and the list of users - each of these can have tens of thousands of elements so even operations with linear complexity, if heavy enough, can produce noticeable GUI freezing. A solution to such cases is to embed `processEvents()` invocations in heavy loops (see `Connection::saveState()` to get the idea). -Having said that, there's always a trade-off between various attributes; in particular, readability and maintainability of the code is more important than squeezing every bit out of that clumsy algorithm. +Having said that, there's always a trade-off between various attributes; in particular, readability and maintainability of the code is more important than squeezing every bit out of that clumsy algorithm. Beware of premature optimization and have profiling information around if you are about to go into some hardcore optimization. ## Documentation changes @@ -166,11 +170,11 @@ Do not use trailing two spaces for line breaks, since these cannot be seen and m ## How to check proposed changes before submitting them -Checking the code on at least one configuration is essential; if you only have a hasty fix that doesn't even compile, better make an issue and put a link to your commit into it (with an explanation what it is about and why). +Checking the code on at least one configuration is essential; if you only have a hasty fix that doesn't even compile, better make an issue and put a link to your commit into it (with an explanation what it is about and why). The best check is a continuous integration pipeline automatically triggered when you submit a PR. ### Standard checks -`-Wall -pedantic` is used with GCC and Clang. We don't turn those warnings to errors but please treat them as such. +The following warnings configuration is applied with GCC and Clang when using CMake: `-W -Wall -Wextra -pedantic -Werror=return-type -Wno-unused-parameter -Wno-gnu-zero-variadic-macro-arguments` (the last one is to mute a warning triggered by Qt code for debug logging). We don't turn most of the warnings to errors but please treat them as such. In Qt Creator, the following line can be used with the Clang code model (you should explicitly enable the code model plugin): `-Weverything -Werror=return-type -Wno-c++98-compat -Wno-c++98-compat-pedantic -Wno-unused-macros -Wno-newline-eof -Wno-exit-time-destructors -Wno-global-constructors -Wno-gnu-zero-variadic-macro-arguments -Wno-documentation -Wno-missing-prototypes -Wno-shadow-field-in-constructor -Wno-padded -Wno-weak-vtables` ### Continuous Integration @@ -178,7 +182,7 @@ We use Travis CI to check buildability on Linux (GCC, Clang) and MacOS (Clang), ### Other tools -If you know how to use clang-tidy, here's a list of checks we do and do not use (a leading hyphen means a disabled check, an asterisk is a wildcard): `*,cert-env33-c,-cppcoreguidelines-pro-bounds-array-to-pointer-decay,-cppcoreguidelines-pro-bounds-constant-array-index,-cppcoreguidelines-pro-bounds-pointer-arithmetic,-cppcoreguidelines-pro-type-const-cast,-cppcoreguidelines-pro-type-union-access,-cppcoreguidelines-special-member-functions,-google-build-using-namespace,-google-readability-braces-around-statements,-hicpp-*,-llvm-*,-misc-unused-parameters,-misc-noexcept-moveconstructor,-modernize-use-using,-readability-braces-around-statements,readability-identifier-naming,-readability-implicit-bool-cast,-clang-diagnostic-*,-clang-analyzer-*`. If you're on CLion (which makes clang-tidy usage a no-brainer), you can simple copy-paste the above list into the Clang-Tidy inspection configuration. +If you know how to use clang-tidy, here's a list of checks we do and do not use (a leading hyphen means a disabled check, an asterisk is a wildcard): `*,cert-env33-c,-cppcoreguidelines-pro-bounds-array-to-pointer-decay,-cppcoreguidelines-pro-bounds-constant-array-index,-cppcoreguidelines-pro-bounds-pointer-arithmetic,-cppcoreguidelines-pro-type-const-cast,-cppcoreguidelines-pro-type-union-access,-cppcoreguidelines-special-member-functions,-google-build-using-namespace,-google-readability-braces-around-statements,-hicpp-*,-llvm-*,-misc-unused-parameters,-misc-noexcept-moveconstructor,-modernize-use-using,-readability-braces-around-statements,readability-identifier-naming,-readability-implicit-bool-cast,-clang-diagnostic-*,-clang-analyzer-*`. If you're on CLion, you can simply copy-paste the above list into the Clang-Tidy inspection configuration. In Qt Creator 4.6, one can enable clang-tidy and clazy (clazy level 1 eats away CPU but produces some very relevant and unobvious notices, such as possible unintended copying of a Qt container, or unguarded null pointers). ## Git commit messages @@ -196,13 +200,13 @@ When writing git commit messages, try to follow the guidelines in ## Reuse (libraries, frameworks, etc.) -C++ is unfortunately not very coherent about SDK/package management, and we try to keep building the library as easy as possible. Because of that we are very (and it means _very_) conservative about adding dependencies to libqmatrixclient. That relates to both additional Qt components and even more to other libraries. +C++ is unfortunately not very coherent about SDK/package management, and we try to keep building the library as easy as possible. Because of that we are very (and it means _very_) conservative about adding dependencies to libqmatrixclient. That relates to both additional Qt components and even more to other libraries. Fortunately, even the Qt components now in use (Qt Core and Network) are very feature-rich and provide plenty of ready-made stuff. -Regardless of the above paragraph (and as mentioned earlier in the text), we're now looking at possible options for automated testing, so PRs onboarding a test framework will be considered with much gratitude. Just don't forget to bootstrap with at least some tests on the existing codebase, rather than just throw in a Git submodule. +Regardless of the above paragraph (and as mentioned earlier in the text), we're now looking at possible options for automated testing, so PRs onboarding a test framework will be considered with much gratitude. Some cases need additional explanation: * Before rolling out your own super-optimised container or algorithm written from scratch, take a good long look through documentation on Qt and C++ standard library (including the experimental/future sections). Please try to reuse the existing facilities as much as possible. -* You should have a good reason (or better several ones) to add a component from KDE Frameworks. We don't rule this out and there's no prejudice against KDE; it just so happened that KDE Frameworks is one of most obvious reuse candidates for us but so far none of these components survived as libqmatrixclient deps. +* You should have a good reason (or better several ones) to add a component from KDE Frameworks. We don't rule this out and there's no prejudice against KDE; it just so happened that KDE Frameworks is one of most obvious reuse candidates but so far none of these components survived as libqmatrixclient deps. So we are cautious. * Never forget that libqmatrixclient is aimed to be a non-visual library; QtGui in dependencies is only driven by (entirely offscreen) dealing with QPixmaps. While there's a bunch of visual code (in C++ and QML) shared between libqmatrixclient-enabled _applications_, this is likely to end up in a separate (libqmatrixclient-enabled) library, rather than libqmatrixclient. ## Attribution diff --git a/README.md b/README.md index dc6b22ca..471e5f5a 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# Libqmatrixclient +# libQMatrixClient [![license](https://img.shields.io/github/license/QMatrixClient/libqmatrixclient.svg)](https://github.com/QMatrixClient/libqmatrixclient/blob/master/COPYING) ![status](https://img.shields.io/badge/status-beta-yellow.svg) @@ -6,35 +6,37 @@ [![CII Best Practices](https://bestpractices.coreinfrastructure.org/projects/1023/badge)](https://bestpractices.coreinfrastructure.org/projects/1023) [![PRs Welcome](https://img.shields.io/badge/PRs-welcome-brightgreen.svg?style=flat-square)](http://makeapullrequest.com) -libqmatrixclient is a Qt5-based library to make IM clients for the [Matrix](https://matrix.org) protocol. It is the backbone of [Quaternion](https://github.com/QMatrixClient/Quaternion), [Tensor](https://matrix.org/docs/projects/client/tensor.html) and some other projects. +libQMatrixClient is a Qt5-based library to make IM clients for the [Matrix](https://matrix.org) protocol. It is the backbone of [Quaternion](https://github.com/QMatrixClient/Quaternion), [Tensor](https://matrix.org/docs/projects/client/tensor.html) and some other projects. ## Contacts -You can find authors of libqmatrixclient in the Matrix room: [#qmatrixclient:matrix.org](https://matrix.to/#/#qmatrixclient:matrix.org). +You can find authors of libQMatrixClient in the Matrix room: [#qmatrixclient:matrix.org](https://matrix.to/#/#qmatrixclient:matrix.org). You can also file issues at [the project's issue tracker](https://github.com/QMatrixClient/libqmatrixclient/issues). If you have what looks like a security issue, please see respective instructions in CONTRIBUTING.md. ## Building and usage -So far the library is typically used as a git submodule of another project (such as Quaternion); however it can be built separately (either as a static or as a dynamic library). There is no installation sequence outside of other projects so far (PRs are most welcome). +So far the library is typically used as a git submodule of another project (such as Quaternion); however it can be built separately (either as a static or as a dynamic library). As of version 0.2, the library can be installed and CMake package config files are provided; projects can use `find_package(QMatrixClient)` to setup their code with the installed library files. PRs to enable the same for qmake are most welcome. -The source code is hosted at GitHub: https://github.com/QMatrixClient/libqmatrixclient - checking out a certain commit or tag from GitHub (rather than downloading the archive) is the recommended way for one-off building. If you want to hack on the library as a part of another project (e.g. Quaternion), you're advised to make a recursive check out of that project and update the library submodule to its master branch. +The source code is hosted at GitHub: https://github.com/QMatrixClient/libqmatrixclient - checking out a certain commit or tag from GitHub (rather than downloading the archive) is the recommended way for one-off building. If you want to hack on the library as a part of another project (e.g. you are working on Quaternion but need to do some changes to the library code), you're advised to make a recursive check out of that project (in this case, Quaternion) and update the library submodule to its master branch. -There are very few tags so far; there will be more, as new versions are released. +Tags starting with `v` represent released versions; `rc` mark release candidates. ## Pre-requisites -- a Linux, OSX or Windows system (desktop versions tried; mobile Linux/Windows might work too) +- a Linux, OSX or Windows system (desktop versions tried; Ubuntu Touch is known to work; mobile Windows and iOS might work too but never tried) - For Ubuntu flavours - zesty or later (or a derivative) is good enough out of the box; older ones will need PPAs at least for a newer Qt; in particular, if you have xenial you're advised to add Kubuntu Backports PPA for it - a Git client to check out this repo -- CMake (from your package management system or [the official website](https://cmake.org/download/)) - Qt 5 (either Open Source or Commercial), version 5.6 or higher +- a build configuration tool: + - CMake (from your package management system or [the official website](https://cmake.org/download/)) + - or qmake (comes with Qt) - a C++ toolchain supported by your version of Qt (see a link for your platform at [the Qt's platform requirements page](http://doc.qt.io/qt-5/gettingstarted.html#platform-requirements)) - - GCC 5 (Windows, Linux, OSX), Clang 5 (Linux), Apple Clang 8.1 (OSX) and Visual C++ 2015 (Windows) are the oldest officially supported - - any build system that works with CMake should be fine: GNU Make, ninja (any platform), NMake, jom (Windows) are known to work. + - GCC 5 (Windows, Linux, OSX), Clang 5 (Linux), Apple Clang 8.1 (OSX) and Visual C++ 2015 (Windows) are the oldest officially supported; Clang 3.8 and GCC 4.9.2 are known to still work, maintenance patches for them are accepted + - any build system that works with CMake and/or qmake should be fine: GNU Make, ninja (any platform), NMake, jom (Windows) are known to work. #### Linux -Just install things from the list above using your preferred package manager. If your Qt package base is fine-grained you might want to take a look at `CMakeLists.txt` to figure out which specific libraries libqmatrixclient uses (or blindly run cmake and look at error messages). The library is entirely offscreen (Qt::Core and Qt::Network are essential) but it also depends on Qt::Gui in order to operate with avatar thumbnails. +Just install things from the list above using your preferred package manager. If your Qt package base is fine-grained you might want to run cmake/qmake and look at error messages. The library is entirely offscreen (QtCore and QtNetwork are essential) but it also depends on QtGui in order to handle avatar thumbnails. #### OS X -`brew install qt5` should get you Qt5. If you plan to use CMake, you may need to tell it about the path to Qt by passing `-DCMAKE_PREFIX_PATH=` +`brew install qt5` should get you a recent Qt5. If you plan to use CMake, you may need to tell it about the path to Qt by passing `-DCMAKE_PREFIX_PATH=` #### Windows 1. Install Qt5, using their official installer. @@ -51,15 +53,23 @@ cd build_dir cmake .. # Pass -DCMAKE_PREFIX_PATH and -DCMAKE_INSTALL_PREFIX here if needed cmake --build . --target all ``` -This will get you the compiled library in `build_dir` inside your project sources. Only static builds of libqmatrixclient are tested at the moment; experiments with dynamic builds are welcome. The two known projects to link with libqmatrixclient are Tensor and Quaternion; you should take a look at their source code before doing anything with libqmatrixclient on your own. +This will get you the compiled library in `build_dir` inside your project sources. Only static builds of libqmatrixclient are tested at the moment; experiments with dynamic builds are welcome. The three known projects to link with libqmatrixclient are Quaternion, uMatriks and Tensor; you should take a look at their source code before doing something big with libqmatrixclient on your own. For smaller things, taking a look at qmc-example should give you a basic idea of using the library. + +You can install the library with CMake: +``` +cmake --build . --target install +``` +This will also install cmake package config files; once this is done, you can use `examples/CMakeLists.txt` to compile the example with the _installed_ library. This file is a good starting point for your own CMake-based project using libQMatrixClient. ### qmake-based -The library only provides a .pri file with an intention to be included from a bigger project's .pro file. As a starting point you can use `qmc-example.pro` that will build a minimal example of library usage for you. In the root directory of the project sources: +The library provides a .pri file with an intention to be included from a bigger project's .pro file. As a starting point you can use `qmc-example.pro` that will build a minimal example of library usage for you. In the root directory of the project sources: ``` qmake qmc-example.pro make all ``` -This will get you `debug/qmc-example` and `release/qmc-example` console executables that login to the Matrix server at matrix.org with credentials of your choosing (pass the username and password as arguments) and run a sync long-polling loop, showing some information about received events. +This will get you `debug/qmc-example` and `release/qmc-example` console executables that login to the Matrix server at matrix.org with credentials of your choosing (pass the username and password as arguments), run a sync long-polling loop and do some tests of the library API. + +Installing the library with qmake is not possible; similarly, a .prl file is not provided. A PR to fix this is welcome. ## Troubleshooting -- cgit v1.2.3 From eef87062763a22ff14f829808435d9acc0a411e4 Mon Sep 17 00:00:00 2001 From: Kitsune Ral Date: Tue, 3 Apr 2018 18:02:35 +0900 Subject: CMakeLists.txt: Specify the API version correctly It's not version 2, it's still version 0.2. --- CMakeLists.txt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 956c9a1f..50f04a91 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -91,8 +91,8 @@ aux_source_directory(lib/jobs/generated libqmatrixclient_job_SRCS) set(example_SRCS examples/qmc-example.cpp) add_library(QMatrixClient ${libqmatrixclient_SRCS} ${libqmatrixclient_job_SRCS}) -set(API_VERSION 2) -set_property(TARGET QMatrixClient PROPERTY VERSION "0.2.0") +set(API_VERSION "0.2") +set_property(TARGET QMatrixClient PROPERTY VERSION "${API_VERSION}.0") set_property(TARGET QMatrixClient PROPERTY SOVERSION 0 ) set_property(TARGET QMatrixClient PROPERTY INTERFACE_QMatrixClient_MAJOR_VERSION ${API_VERSION}) -- cgit v1.2.3 From d9f09be82b716c5261a36feb6006c4978d989d9e Mon Sep 17 00:00:00 2001 From: Kitsune Ral Date: Tue, 3 Apr 2018 18:03:55 +0900 Subject: More fixes from static analysis --- lib/events/event.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/events/event.h b/lib/events/event.h index eccfec41..d614115a 100644 --- a/lib/events/event.h +++ b/lib/events/event.h @@ -141,7 +141,7 @@ namespace QMatrixClient // The below line accommodates the difference in size types of // STL and Qt containers. this->reserve(static_cast(objs.size())); - for (auto objValue: objs) + for (const auto& objValue: objs) this->emplace_back(makeEvent(objValue.toObject())); } }; -- cgit v1.2.3 From d76b2fbe30a503009d33c4df06848d7356fc39a6 Mon Sep 17 00:00:00 2001 From: Kitsune Ral Date: Tue, 3 Apr 2018 18:32:14 +0900 Subject: DirectChatEvent: be careful with range-for over temporaries ...because temporaries returned by temporaries tend to disappear before you enter the loop body (see the bottom of http://en.cppreference.com/w/cpp/language/range-for#Explanation). --- lib/connection.cpp | 10 ++-------- lib/events/directchatevent.cpp | 11 +++++++++-- 2 files changed, 11 insertions(+), 10 deletions(-) diff --git a/lib/connection.cpp b/lib/connection.cpp index 9d0a34f3..71ada8f7 100644 --- a/lib/connection.cpp +++ b/lib/connection.cpp @@ -281,8 +281,7 @@ void Connection::sync(int timeout) void Connection::onSyncSuccess(SyncData &&data) { d->data->setLastEvent(data.nextBatch()); - auto allRoomData = data.takeRoomData(); - for (auto&& roomData: allRoomData) + for (auto&& roomData: data.takeRoomData()) { const auto forgetIdx = d->roomIdsToForget.indexOf(roomData.roomId); if (forgetIdx != -1) @@ -303,12 +302,7 @@ void Connection::onSyncSuccess(SyncData &&data) { r->updateData(std::move(roomData)); QCoreApplication::processEvents(); } - // `for (auto&& accountEvent: data.takeAccountData())` doesn't work well - // with Clang on FreeBSD in Release configuration; it seems that - // the lifetime of the returned rvalue does not extend (violating the - // standard) and accountEvent refers to garbage in the loop body as a result. - auto allAccountData = data.takeAccountData(); - for (auto&& accountEvent: allAccountData) + for (auto&& accountEvent: data.takeAccountData()) { if (accountEvent->type() == EventType::DirectChat) { diff --git a/lib/events/directchatevent.cpp b/lib/events/directchatevent.cpp index 7049d967..63d638a3 100644 --- a/lib/events/directchatevent.cpp +++ b/lib/events/directchatevent.cpp @@ -29,8 +29,15 @@ DirectChatEvent::DirectChatEvent(const QJsonObject& obj) QMultiHash DirectChatEvent::usersToDirectChats() const { QMultiHash result; - for (auto it = contentJson().begin(); it != contentJson().end(); ++it) - for (auto roomIdValue: it.value().toArray()) + const auto json = contentJson(); + for (auto it = json.begin(); it != json.end(); ++it) + { + // Beware of range-for's over temporary returned from temporary + // (see the bottom of + // http://en.cppreference.com/w/cpp/language/range-for#Explanation) + const auto roomIds = it.value().toArray(); + for (const auto& roomIdValue: roomIds) result.insert(it.key(), roomIdValue.toString()); + } return result; } -- cgit v1.2.3 From b8244eadff1dbe151fe15998cf60cb9b82e684e6 Mon Sep 17 00:00:00 2001 From: Kitsune Ral Date: Thu, 5 Apr 2018 19:25:40 +0900 Subject: Unofficially relax Qt requirement to 5.5.1 This is to support uMatriks that still has to compile on xenial codebase. --- CMakeLists.txt | 2 +- lib/connection.cpp | 10 ++++++++-- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 50f04a91..8d8de0ae 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -40,7 +40,7 @@ foreach (FLAG all "" pedantic extra error=return-type no-unused-parameter no-gnu endif () endforeach () -find_package(Qt5 5.6 REQUIRED Network Gui) +find_package(Qt5 5.5.1 REQUIRED Network Gui) get_filename_component(Qt5_Prefix "${Qt5_DIR}/../../../.." ABSOLUTE) message( STATUS ) diff --git a/lib/connection.cpp b/lib/connection.cpp index 71ada8f7..600ab396 100644 --- a/lib/connection.cpp +++ b/lib/connection.cpp @@ -705,8 +705,14 @@ Connection::DirectChatsMap Connection::directChats() const QJsonObject toJson(const Connection::DirectChatsMap& directChats) { QJsonObject json; - for (auto it = directChats.keyBegin(); it != directChats.keyEnd(); ++it) - json.insert((*it)->id(), toJson(directChats.values(*it))); + for (auto it = directChats.begin(); it != directChats.end();) + { + QJsonArray roomIds; + const auto* user = it.key(); + for (; it != directChats.end() && it.key() == user; ++it) + roomIds.append(*it); + json.insert(user->id(), roomIds); + } return json; } -- cgit v1.2.3 From cf5b5e74b20de2a0579de6a176d76f96bbe87603 Mon Sep 17 00:00:00 2001 From: Kitsune Ral Date: Thu, 5 Apr 2018 20:41:19 +0900 Subject: qmc-example: fix false negatives due to a missing return --- examples/qmc-example.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/examples/qmc-example.cpp b/examples/qmc-example.cpp index 7226c060..e2ec1d12 100644 --- a/examples/qmc-example.cpp +++ b/examples/qmc-example.cpp @@ -218,6 +218,7 @@ void QMCTest::markDirectChat() cout << "Warning: the room is already a direct chat," " only unmarking will be tested" << endl; checkDirectChatOutcome({{ c->user(), targetRoom->id() }}); + return; } // Connect first because the signal is emitted synchronously. connect(c.data(), &Connection::directChatsListChanged, -- cgit v1.2.3 From 7ca442a432994a19a86d43c917a1f537bcebb387 Mon Sep 17 00:00:00 2001 From: Kitsune Ral Date: Wed, 11 Apr 2018 18:51:48 +0900 Subject: Comment out install(EXPORT_ANDROID_MK) It's only available in CMake 3.7 and later and is not needed for any known project anyway. --- CMakeLists.txt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 8d8de0ae..dae8b287 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -141,7 +141,8 @@ install(FILES cmake/QMatrixClientConfig.cmake "${CMAKE_CURRENT_BINARY_DIR}/QMatrixClient/QMatrixClientConfigVersion.cmake" DESTINATION ${ConfigFilesLocation} ) -install(EXPORT_ANDROID_MK QMatrixClientTargets DESTINATION share/ndk-modules) +# Only available from CMake 3.7; reserved for future use +#install(EXPORT_ANDROID_MK QMatrixClientTargets DESTINATION share/ndk-modules) if (WIN32) install(FILES mime/packages/freedesktop.org.xml DESTINATION mime/packages) -- cgit v1.2.3